From 5c247b107dbb3d83748e6313c0f9fdc7fa6d4c4e Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Tue, 13 Jun 2023 23:31:19 -0400 Subject: [PATCH] feat: email tui --- attachments.go | 45 ++++++++ go.mod | 28 +++++ go.sum | 53 ++++++++++ go.work | 5 + go.work.sum | 4 + keymap.go | 67 ++++++++++++ main.go | 29 +++++ model.go | 282 +++++++++++++++++++++++++++++++++++++++++++++++++ style.go | 20 ++++ 9 files changed, 533 insertions(+) create mode 100644 attachments.go create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 keymap.go create mode 100644 main.go create mode 100644 model.go create mode 100644 style.go diff --git a/attachments.go b/attachments.go new file mode 100644 index 0000000..be134da --- /dev/null +++ b/attachments.go @@ -0,0 +1,45 @@ +package main + +import ( + "io" + "path/filepath" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type attachment string + +func (a attachment) FilterValue() string { + return string(a) +} + +type attachmentDelegate struct { + focused bool +} + +func (d attachmentDelegate) Height() int { + return 1 +} + +func (d attachmentDelegate) Spacing() int { + return 0 +} + +func (d attachmentDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { + path := filepath.Base(item.(attachment).FilterValue()) + style := textStyle + if m.Index() == index && d.focused { + style = activeTextStyle + } + + if m.Index() == index { + _, _ = w.Write([]byte(style.Render("• " + path))) + } else { + _, _ = w.Write([]byte(style.Render(" " + path))) + } +} + +func (d attachmentDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { + return nil +} diff --git a/go.mod b/go.mod index aee0770..a13c5ad 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,31 @@ module github.com/maaslalani/email go 1.19 + +require ( + github.com/charmbracelet/bubbles v0.16.1 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba // indirect + github.com/charmbracelet/lipgloss v0.7.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum index e69de29..ac37cc1 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.1 h1:LpdYfnu+Qc6XtvMz6d/6rRY71yttHTP5HtrjMgWvixc= +github.com/charmbracelet/bubbletea v0.24.1/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg= +github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba h1:hWjlqzPiG3dWN5KZUAKKnqz/R8BlYqX1Aw+nk7riQss= +github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..6da4469 --- /dev/null +++ b/go.work @@ -0,0 +1,5 @@ +go 1.19 + +use ( + . +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..56271a9 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,4 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= diff --git a/keymap.go b/keymap.go new file mode 100644 index 0000000..931a86f --- /dev/null +++ b/keymap.go @@ -0,0 +1,67 @@ +package main + +import "github.com/charmbracelet/bubbles/key" + +// KeyMap represents the key bindings for the application. +type KeyMap struct { + NextInput key.Binding + PrevInput key.Binding + Send key.Binding + Attach key.Binding + Unattach key.Binding + Quit key.Binding +} + +func DefaultKeybinds() KeyMap { + return KeyMap{ + NextInput: key.NewBinding( + key.WithKeys("tab", "ctrl+n"), + key.WithHelp("tab", "next"), + ), + PrevInput: key.NewBinding( + key.WithKeys("shift+tab", "ctrl+p"), + ), + Send: key.NewBinding( + key.WithKeys("ctrl+d", "enter"), + key.WithHelp("enter", "send"), + key.WithDisabled(), + ), + Attach: key.NewBinding( + key.WithKeys("a"), + key.WithHelp("a", "attach file"), + key.WithDisabled(), + ), + Unattach: key.NewBinding( + key.WithKeys("x"), + key.WithHelp("x", "remove"), + key.WithDisabled(), + ), + Quit: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "quit"), + ), + } +} + +func (k KeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + k.NextInput, + k.Quit, + k.Attach, + k.Unattach, + k.Send, + } +} + +func (k KeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.NextInput, k.Send, k.Attach, k.Unattach, k.Quit}, + } +} + +func (m *Model) updateKeymap() { + m.keymap.Attach.SetEnabled(m.state == editingAttachments) + canSend := m.From.Value() != "" && m.To.Value() != "" && m.Subject.Value() != "" && m.Body.Value() != "" + m.keymap.Send.SetEnabled(canSend && m.state != editingBody) + m.keymap.Unattach.SetEnabled(m.state == editingAttachments && len(m.Attachments.Items()) > 0) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a6fa6f5 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "email", + Short: "email is a command line interface for sending emails.", + Long: `email is a command line interface for sending emails.`, + RunE: func(cmd *cobra.Command, args []string) error { + p := tea.NewProgram(NewModel()) + _, err := p.Run() + if err != nil { + return err + } + return nil + }, +} + +func main() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..f55c14a --- /dev/null +++ b/model.go @@ -0,0 +1,282 @@ +package main + +import ( + "os" + "strings" + + "github.com/charmbracelet/bubbles/filepicker" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type State int + +const ( + editingFrom State = iota + editingTo + editingSubject + editingBody + editingAttachments + pickingFile + sendingEmail +) + +type Model struct { + // state represents the current state of the application. + state State + + // From represents the sender's email address. + From textinput.Model + // To represents the recipient's email address. + // This can be a comma-separated list of addresses. + To textinput.Model + // Subject represents the email's subject. + Subject textinput.Model + // Body represents the email's body. + // This can be written in markdown and will be converted to HTML. + Body textarea.Model + // Attachments represents the email's attachments. + // This is a list of file paths which are picked with a filepicker. + Attachments list.Model + + // filepicker is used to pick file attachments. + filepicker filepicker.Model + help help.Model + keymap KeyMap + quitting bool +} + +func NewModel() Model { + from := textinput.New() + from.Prompt = "From " + from.Placeholder = "me@example.com" + from.PromptStyle = labelStyle.Copy() + from.PromptStyle = activeLabelStyle + from.TextStyle = activeTextStyle + from.Cursor.Style = cursorStyle + from.PlaceholderStyle = placeholderStyle + from.Focus() + + to := textinput.New() + to.Prompt = "To " + to.PromptStyle = labelStyle.Copy() + to.Cursor.Style = cursorStyle + to.PlaceholderStyle = placeholderStyle + to.Placeholder = "you@example.com" + + subject := textinput.New() + subject.Prompt = "Subject " + subject.PromptStyle = labelStyle.Copy() + subject.Cursor.Style = cursorStyle + subject.PlaceholderStyle = placeholderStyle + subject.Placeholder = "Hello!" + + body := textarea.New() + body.Placeholder = "# Hi" + body.ShowLineNumbers = false + body.FocusedStyle.CursorLine = activeTextStyle + body.FocusedStyle.Prompt = activeLabelStyle + body.FocusedStyle.Text = activeTextStyle + body.FocusedStyle.Placeholder = placeholderStyle + body.BlurredStyle.CursorLine = textStyle + body.BlurredStyle.Prompt = labelStyle + body.BlurredStyle.Text = textStyle + body.BlurredStyle.Placeholder = placeholderStyle + body.Cursor.Style = cursorStyle + body.Blur() + + attachments := list.New([]list.Item{}, attachmentDelegate{}, 0, 3) + attachments.DisableQuitKeybindings() + attachments.SetShowTitle(true) + attachments.Title = "Attachments" + attachments.Styles.Title = labelStyle + attachments.Styles.TitleBar = labelStyle + attachments.SetShowHelp(false) + attachments.SetShowStatusBar(false) + attachments.SetStatusBarItemName("attachment", "attachments") + attachments.SetShowPagination(false) + + picker := filepicker.New() + picker.CurrentDirectory, _ = os.UserHomeDir() + + return Model{ + state: editingFrom, + From: from, + To: to, + Subject: subject, + Body: body, + Attachments: attachments, + filepicker: picker, + help: help.New(), + keymap: DefaultKeybinds(), + } +} + +func (m Model) Init() tea.Cmd { + return tea.Batch( + m.From.Cursor.BlinkCmd(), + m.filepicker.Init(), + ) +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.From.Width = msg.Width - 2 + m.To.Width = msg.Width - 2 + m.Subject.Width = msg.Width - 2 + m.Body.SetWidth(msg.Width - 2) + m.Attachments.SetWidth(msg.Width - 2) + + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keymap.NextInput): + m.blurInputs() + switch m.state { + case editingFrom: + m.state = editingTo + m.To.Focus() + case editingTo: + m.state = editingSubject + case editingSubject: + m.state = editingBody + case editingBody: + m.state = editingAttachments + case editingAttachments: + m.state = editingFrom + } + m.focusActiveInput() + + case key.Matches(msg, m.keymap.PrevInput): + m.blurInputs() + switch m.state { + case editingFrom: + m.state = editingAttachments + case editingTo: + m.state = editingFrom + case editingSubject: + m.state = editingTo + case editingBody: + m.state = editingSubject + case editingAttachments: + m.state = editingBody + } + m.focusActiveInput() + + case key.Matches(msg, m.keymap.Send): + case key.Matches(msg, m.keymap.Attach): + m.state = pickingFile + case key.Matches(msg, m.keymap.Unattach): + m.Attachments.RemoveItem(m.Attachments.Index()) + m.Attachments.SetHeight(max(len(m.Attachments.Items()), 1) + 2) + case key.Matches(msg, m.keymap.Quit): + m.quitting = true + return m, tea.Quit + } + } + + m.updateKeymap() + + var cmds []tea.Cmd + var cmd tea.Cmd + m.From, cmd = m.From.Update(msg) + cmds = append(cmds, cmd) + m.To, cmd = m.To.Update(msg) + cmds = append(cmds, cmd) + m.Subject, cmd = m.Subject.Update(msg) + cmds = append(cmds, cmd) + m.Body, cmd = m.Body.Update(msg) + cmds = append(cmds, cmd) + m.filepicker, cmd = m.filepicker.Update(msg) + cmds = append(cmds, cmd) + + if m.state == pickingFile { + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { + m.Attachments.InsertItem(0, attachment(path)) + m.Attachments.SetHeight(len(m.Attachments.Items()) + 2) + m.state = editingAttachments + m.updateKeymap() + } + } + + if m.state == editingAttachments { + m.Attachments, cmd = m.Attachments.Update(msg) + cmds = append(cmds, cmd) + } + m.help, cmd = m.help.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m *Model) blurInputs() { + m.From.Blur() + m.To.Blur() + m.Subject.Blur() + m.Body.Blur() + m.From.PromptStyle = labelStyle + m.To.PromptStyle = labelStyle + m.Subject.PromptStyle = labelStyle + m.From.TextStyle = textStyle + m.To.TextStyle = textStyle + m.Subject.TextStyle = textStyle + m.Attachments.Styles.Title = labelStyle + m.Attachments.SetDelegate(attachmentDelegate{false}) +} + +func (m *Model) focusActiveInput() { + switch m.state { + case editingFrom: + m.From.PromptStyle = activeLabelStyle + m.From.TextStyle = activeTextStyle + m.From.Focus() + m.From.CursorEnd() + case editingTo: + m.To.PromptStyle = activeLabelStyle + m.To.TextStyle = activeTextStyle + m.To.Focus() + m.To.CursorEnd() + case editingSubject: + m.Subject.PromptStyle = activeLabelStyle + m.Subject.TextStyle = activeTextStyle + m.Subject.Focus() + m.Subject.CursorEnd() + case editingBody: + m.Body.Focus() + m.Body.CursorEnd() + case editingAttachments: + m.Attachments.Styles.Title = activeLabelStyle + m.Attachments.SetDelegate(attachmentDelegate{true}) + } +} + +func (m Model) View() string { + if m.quitting { + return "" + } + + if m.state == pickingFile { + return "\n" + activeLabelStyle.Render("Attachments") + + "\n\n" + m.filepicker.View() + } + + var s strings.Builder + + s.WriteString(m.From.View()) + s.WriteString("\n") + s.WriteString(m.To.View()) + s.WriteString("\n") + s.WriteString(m.Subject.View()) + s.WriteString("\n\n") + s.WriteString(m.Body.View()) + s.WriteString("\n\n") + s.WriteString(m.Attachments.View()) + s.WriteString("\n") + s.WriteString(m.help.View(m.keymap)) + + return s.String() +} diff --git a/style.go b/style.go new file mode 100644 index 0000000..c924afc --- /dev/null +++ b/style.go @@ -0,0 +1,20 @@ +package main + +import "github.com/charmbracelet/lipgloss" + +const accentColor = lipgloss.Color("99") +const whiteColor = lipgloss.Color("255") +const grayColor = lipgloss.Color("241") +const darkGrayColor = lipgloss.Color("236") +const lightGrayColor = lipgloss.Color("247") + +var ( + activeTextStyle = lipgloss.NewStyle().Foreground(whiteColor) + textStyle = lipgloss.NewStyle().Foreground(lightGrayColor) + + activeLabelStyle = lipgloss.NewStyle().Foreground(accentColor) + labelStyle = lipgloss.NewStyle().Foreground(grayColor) + + placeholderStyle = lipgloss.NewStyle().Foreground(darkGrayColor) + cursorStyle = lipgloss.NewStyle().Foreground(whiteColor) +)