1package tui
2
3import (
4 "fmt"
5
6 "github.com/charmbracelet/bubbles/textarea"
7 "github.com/charmbracelet/bubbles/textinput"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10)
11
12// Styles for the UI
13var (
14 focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
15 blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
16 cursorStyle = focusedStyle.Copy()
17 noStyle = lipgloss.NewStyle()
18 focusedButton = focusedStyle.Copy().Render("[ Send ]")
19 blurredButton = blurredStyle.Copy().Render("[ Send ]")
20 emailRecipientStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
21 attachmentStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("240"))
22)
23
24// Composer model holds the state of the email composition UI.
25type Composer struct {
26 focusIndex int
27 toInput textinput.Model
28 subjectInput textinput.Model
29 bodyInput textarea.Model
30 attachmentPath string
31 fromAddr string
32}
33
34// NewComposer initializes a new composer model.
35func NewComposer(from, to, subject, body string) *Composer {
36 m := &Composer{fromAddr: from}
37
38 m.toInput = textinput.New()
39 m.toInput.Cursor.Style = cursorStyle
40 m.toInput.Placeholder = "To"
41 m.toInput.SetValue(to)
42 m.toInput.Focus()
43 m.toInput.Prompt = "> "
44 m.toInput.CharLimit = 256
45
46 m.subjectInput = textinput.New()
47 m.subjectInput.Cursor.Style = cursorStyle
48 m.subjectInput.Placeholder = "Subject"
49 m.subjectInput.SetValue(subject)
50 m.subjectInput.Prompt = "> "
51 m.subjectInput.CharLimit = 256
52
53 m.bodyInput = textarea.New()
54 m.bodyInput.Cursor.Style = cursorStyle
55 m.bodyInput.Placeholder = "Body (Markdown supported)..."
56 m.bodyInput.SetValue(body)
57 m.bodyInput.Prompt = "> "
58 m.bodyInput.SetHeight(10)
59 m.bodyInput.SetCursor(0)
60
61 return m
62}
63
64func (m *Composer) Init() tea.Cmd {
65 return textinput.Blink
66}
67
68func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
69 var cmds []tea.Cmd
70 var cmd tea.Cmd
71
72 switch msg := msg.(type) {
73 case tea.WindowSizeMsg:
74 inputWidth := msg.Width - 6
75 m.toInput.Width = inputWidth
76 m.subjectInput.Width = inputWidth
77 m.bodyInput.SetWidth(inputWidth)
78
79 case SetComposerCursorToStartMsg:
80 m.bodyInput.SetCursor(0)
81 return m, nil
82
83 case FileSelectedMsg:
84 m.attachmentPath = msg.Path
85 return m, nil
86
87 case tea.KeyMsg:
88 switch msg.Type {
89 case tea.KeyCtrlC:
90 return m, tea.Quit
91
92 case tea.KeyTab, tea.KeyShiftTab:
93 if msg.Type == tea.KeyShiftTab {
94 m.focusIndex--
95 } else {
96 m.focusIndex++
97 }
98
99 if m.focusIndex > 4 { // 4 is the Send button
100 m.focusIndex = 0
101 } else if m.focusIndex < 0 {
102 m.focusIndex = 4
103 }
104
105 m.toInput.Blur()
106 m.subjectInput.Blur()
107 m.bodyInput.Blur()
108
109 switch m.focusIndex {
110 case 0:
111 cmds = append(cmds, m.toInput.Focus())
112 case 1:
113 cmds = append(cmds, m.subjectInput.Focus())
114 case 2:
115 cmds = append(cmds, m.bodyInput.Focus())
116 cmds = append(cmds, func() tea.Msg { return SetComposerCursorToStartMsg{} })
117 }
118 return m, tea.Batch(cmds...)
119
120 case tea.KeyEnter:
121 if m.focusIndex == 3 { // Attachment field focused
122 return m, func() tea.Msg { return GoToFilePickerMsg{} }
123 }
124 if m.focusIndex == 4 { // Send button focused
125 return m, func() tea.Msg {
126 return SendEmailMsg{
127 To: m.toInput.Value(),
128 Subject: m.subjectInput.Value(),
129 Body: m.bodyInput.Value(),
130 AttachmentPath: m.attachmentPath,
131 }
132 }
133 }
134 }
135 }
136
137 switch m.focusIndex {
138 case 0:
139 m.toInput, cmd = m.toInput.Update(msg)
140 cmds = append(cmds, cmd)
141 case 1:
142 m.subjectInput, cmd = m.subjectInput.Update(msg)
143 cmds = append(cmds, cmd)
144 case 2:
145 m.bodyInput, cmd = m.bodyInput.Update(msg)
146 cmds = append(cmds, cmd)
147 }
148
149 return m, tea.Batch(cmds...)
150}
151
152func (m *Composer) View() string {
153 var button string
154 if m.focusIndex == 4 { // Send button
155 button = focusedButton
156 } else {
157 button = blurredButton
158 }
159
160 var attachmentField string
161 attachmentText := "None (Press Enter to select)"
162 if m.attachmentPath != "" {
163 attachmentText = m.attachmentPath
164 }
165
166 if m.focusIndex == 3 { // Attachment field
167 attachmentField = focusedStyle.Render(fmt.Sprintf("> Attachment: %s", attachmentText))
168 } else {
169 attachmentField = blurredStyle.Render(fmt.Sprintf(" Attachment: %s", attachmentText))
170 }
171
172 return lipgloss.JoinVertical(lipgloss.Left,
173 "Compose New Email",
174 "From: "+emailRecipientStyle.Render(m.fromAddr),
175 m.toInput.View(),
176 m.subjectInput.View(),
177 m.bodyInput.View(),
178 attachmentStyle.Render(attachmentField),
179 button,
180 helpStyle.Render("Markdown/HTML • tab: next field • esc: back to menu"),
181 )
182}