1package main
2
3import (
4 "fmt"
5 "log"
6 "os"
7
8 "github.com/charmbracelet/bubbles/textarea"
9 "github.com/charmbracelet/bubbles/textinput"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12)
13
14// --- STYLES ---
15// Define styles for different parts of the UI using lipgloss.
16var (
17 // Style for the main container/dialog box.
18 dialogBoxStyle = lipgloss.NewStyle().
19 Border(lipgloss.RoundedBorder()).
20 BorderForeground(lipgloss.Color("#874BFD")).
21 Padding(1, 0).
22 BorderTop(true).
23 BorderLeft(true).
24 BorderRight(true).
25 BorderBottom(true)
26
27 // Style for the text labels (e.g., "To:", "Subject:").
28 labelStyle = lipgloss.NewStyle().Padding(0, 1)
29
30 // Style for the help text at the bottom.
31 helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
32
33 // Style for success messages.
34 successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
35
36 // Style for error/sending messages.
37 infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
38)
39
40// --- MODEL ---
41// The model represents the state of our TUI application.
42type model struct {
43 // Inputs for the email fields.
44 recipientInput textinput.Model
45 subjectInput textinput.Model
46 bodyArea textarea.Model
47
48 // index tracks which input is currently focused.
49 // 0: recipient, 1: subject, 2: body
50 index int
51
52 // State flags
53 sending bool // Is the email currently being "sent"?
54 sent bool // Has the email been "sent"?
55 err error // Any error that occurred.
56}
57
58// initialModel creates the starting state of the application.
59func initialModel() model {
60 // --- Recipient Input ---
61 ti := textinput.New()
62 ti.Placeholder = "recipient@example.com"
63 ti.Focus() // Start with the recipient field focused.
64 ti.CharLimit = 156
65 ti.Width = 50
66 ti.Prompt = "To: "
67
68 // --- Subject Input ---
69 si := textinput.New()
70 si.Placeholder = "Hello there!"
71 si.CharLimit = 156
72 si.Width = 50
73 si.Prompt = "Subject: "
74
75 // --- Body Text Area ---
76 ta := textarea.New()
77 ta.Placeholder = "Dear friend, ..."
78 ta.SetWidth(50)
79 ta.SetHeight(10)
80
81 return model{
82 recipientInput: ti,
83 subjectInput: si,
84 bodyArea: ta,
85 index: 0,
86 err: nil,
87 }
88}
89
90// --- BUBBLETEA METHODS ---
91
92// Init is the first function that will be called. It returns a command.
93// We'll use it to make the cursor blink.
94func (m model) Init() tea.Cmd {
95 return textinput.Blink
96}
97
98// Update handles all user input and updates the model accordingly.
99func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
100 var cmd tea.Cmd
101 var cmds []tea.Cmd
102
103 switch msg := msg.(type) {
104 // Handle key presses
105 case tea.KeyMsg:
106 switch msg.Type {
107 // Quit the application
108 case tea.KeyCtrlC, tea.KeyEsc:
109 return m, tea.Quit
110
111 // Trigger the "send" action
112 case tea.KeyCtrlS:
113 // If we are not already sending, start the sending process.
114 if !m.sending {
115 m.sending = true
116 // In a real app, this is where you'd send the email.
117 // We'll return a command that simulates a network operation.
118 // For now, we'll just pretend it was successful after a short delay.
119 return m, func() tea.Msg {
120 // time.Sleep(time.Second * 2) // Simulate network delay
121 return "sent" // Send a message back on success
122 }
123 }
124
125 // Handle Tab and Shift+Tab to navigate between fields
126 case tea.KeyTab, tea.KeyShiftTab:
127 if msg.Type == tea.KeyTab {
128 m.index = (m.index + 1) % 3
129 } else {
130 m.index--
131 if m.index < 0 {
132 m.index = 2
133 }
134 }
135
136 // Blur all inputs
137 m.recipientInput.Blur()
138 m.subjectInput.Blur()
139 m.bodyArea.Blur()
140
141 // Focus the correct input based on the index
142 switch m.index {
143 case 0:
144 m.recipientInput.Focus()
145 case 1:
146 m.subjectInput.Focus()
147 case 2:
148 m.bodyArea.Focus()
149 }
150 return m, nil
151 }
152
153 // Handle the "sent" message from our simulated send command
154 case string:
155 if msg == "sent" {
156 m.sending = false
157 m.sent = true
158 // Quit after a short delay to show the success message.
159 return m, tea.Sequence(
160 func() tea.Msg {
161 // time.Sleep(time.Second)
162 return nil
163 },
164 tea.Quit,
165 )
166 }
167 }
168
169 // Pass the message to the focused input field for handling.
170 switch m.index {
171 case 0:
172 m.recipientInput, cmd = m.recipientInput.Update(msg)
173 case 1:
174 m.subjectInput, cmd = m.subjectInput.Update(msg)
175 case 2:
176 m.bodyArea, cmd = m.bodyArea.Update(msg)
177 }
178 cmds = append(cmds, cmd)
179
180 return m, tea.Batch(cmds...)
181}
182
183// View renders the UI based on the current model state.
184func (m model) View() string {
185 // If the email was sent, just show a success message.
186 if m.sent {
187 return successStyle.Render("\n ✓ Email sent successfully! \n\n")
188 }
189
190 // If we are "sending", show an info message.
191 if m.sending {
192 return infoStyle.Render("\n Sending email... \n\n")
193 }
194
195 // Otherwise, render the form.
196 var s string
197
198 s += "\n"
199 s += m.recipientInput.View() + "\n"
200 s += m.subjectInput.View() + "\n"
201 s += m.bodyArea.View()
202
203 help := fmt.Sprintf("\n\n %s | %s | %s | %s \n",
204 "Tab: Next Field", "Esc: Quit", "Ctrl+S: Send", helpStyle.Render("Focus: "+m.focusString()))
205 s += helpStyle.Render(help)
206
207 return dialogBoxStyle.Render(s)
208}
209
210// Helper function to show which field is focused.
211func (m model) focusString() string {
212 switch m.index {
213 case 0:
214 return "Recipient"
215 case 1:
216 return "Subject"
217 case 2:
218 return "Body"
219 default:
220 return ""
221 }
222}
223
224// --- MAIN FUNCTION ---
225func main() {
226 // Initialize the Bubble Tea program.
227 p := tea.NewProgram(initialModel())
228
229 // Run the program and handle any errors.
230 if _, err := p.Run(); err != nil {
231 log.Fatalf("Alas, there's been an error: %v", err)
232 os.Exit(1)
233 }
234}