diff --git a/go.mod b/go.mod index 74b03e3bc82add9c1d4ba05348159f0fce60452e..5cf43373dd12beea2b369ae03867ade7e9d7c2ac 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/andrinoff/email-cli go 1.24.5 require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/bubbletea v1.3.6 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.9.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -24,7 +25,7 @@ require ( github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect ) diff --git a/main.go b/main.go index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f83a2e237edc8a106bf26347697c164dc9ffb83b 100644 --- a/main.go +++ b/main.go @@ -0,0 +1,234 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// --- STYLES --- +// Define styles for different parts of the UI using lipgloss. +var ( + // Style for the main container/dialog box. + dialogBoxStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#874BFD")). + Padding(1, 0). + BorderTop(true). + BorderLeft(true). + BorderRight(true). + BorderBottom(true) + + // Style for the text labels (e.g., "To:", "Subject:"). + labelStyle = lipgloss.NewStyle().Padding(0, 1) + + // Style for the help text at the bottom. + helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + + // Style for success messages. + successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true) + + // Style for error/sending messages. + infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true) +) + +// --- MODEL --- +// The model represents the state of our TUI application. +type model struct { + // Inputs for the email fields. + recipientInput textinput.Model + subjectInput textinput.Model + bodyArea textarea.Model + + // index tracks which input is currently focused. + // 0: recipient, 1: subject, 2: body + index int + + // State flags + sending bool // Is the email currently being "sent"? + sent bool // Has the email been "sent"? + err error // Any error that occurred. +} + +// initialModel creates the starting state of the application. +func initialModel() model { + // --- Recipient Input --- + ti := textinput.New() + ti.Placeholder = "recipient@example.com" + ti.Focus() // Start with the recipient field focused. + ti.CharLimit = 156 + ti.Width = 50 + ti.Prompt = "To: " + + // --- Subject Input --- + si := textinput.New() + si.Placeholder = "Hello there!" + si.CharLimit = 156 + si.Width = 50 + si.Prompt = "Subject: " + + // --- Body Text Area --- + ta := textarea.New() + ta.Placeholder = "Dear friend, ..." + ta.SetWidth(50) + ta.SetHeight(10) + + return model{ + recipientInput: ti, + subjectInput: si, + bodyArea: ta, + index: 0, + err: nil, + } +} + +// --- BUBBLETEA METHODS --- + +// Init is the first function that will be called. It returns a command. +// We'll use it to make the cursor blink. +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +// Update handles all user input and updates the model accordingly. +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + + switch msg := msg.(type) { + // Handle key presses + case tea.KeyMsg: + switch msg.Type { + // Quit the application + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + + // Trigger the "send" action + case tea.KeyCtrlS: + // If we are not already sending, start the sending process. + if !m.sending { + m.sending = true + // In a real app, this is where you'd send the email. + // We'll return a command that simulates a network operation. + // For now, we'll just pretend it was successful after a short delay. + return m, func() tea.Msg { + // time.Sleep(time.Second * 2) // Simulate network delay + return "sent" // Send a message back on success + } + } + + // Handle Tab and Shift+Tab to navigate between fields + case tea.KeyTab, tea.KeyShiftTab: + if msg.Type == tea.KeyTab { + m.index = (m.index + 1) % 3 + } else { + m.index-- + if m.index < 0 { + m.index = 2 + } + } + + // Blur all inputs + m.recipientInput.Blur() + m.subjectInput.Blur() + m.bodyArea.Blur() + + // Focus the correct input based on the index + switch m.index { + case 0: + m.recipientInput.Focus() + case 1: + m.subjectInput.Focus() + case 2: + m.bodyArea.Focus() + } + return m, nil + } + + // Handle the "sent" message from our simulated send command + case string: + if msg == "sent" { + m.sending = false + m.sent = true + // Quit after a short delay to show the success message. + return m, tea.Sequence( + func() tea.Msg { + // time.Sleep(time.Second) + return nil + }, + tea.Quit, + ) + } + } + + // Pass the message to the focused input field for handling. + switch m.index { + case 0: + m.recipientInput, cmd = m.recipientInput.Update(msg) + case 1: + m.subjectInput, cmd = m.subjectInput.Update(msg) + case 2: + m.bodyArea, cmd = m.bodyArea.Update(msg) + } + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +// View renders the UI based on the current model state. +func (m model) View() string { + // If the email was sent, just show a success message. + if m.sent { + return successStyle.Render("\n ✓ Email sent successfully! \n\n") + } + + // If we are "sending", show an info message. + if m.sending { + return infoStyle.Render("\n Sending email... \n\n") + } + + // Otherwise, render the form. + var s string + + s += "\n" + s += m.recipientInput.View() + "\n" + s += m.subjectInput.View() + "\n" + s += m.bodyArea.View() + + help := fmt.Sprintf("\n\n %s | %s | %s | %s \n", + "Tab: Next Field", "Esc: Quit", "Ctrl+S: Send", helpStyle.Render("Focus: "+m.focusString())) + s += helpStyle.Render(help) + + return dialogBoxStyle.Render(s) +} + +// Helper function to show which field is focused. +func (m model) focusString() string { + switch m.index { + case 0: + return "Recipient" + case 1: + return "Subject" + case 2: + return "Body" + default: + return "" + } +} + +// --- MAIN FUNCTION --- +func main() { + // Initialize the Bubble Tea program. + p := tea.NewProgram(initialModel()) + + // Run the program and handle any errors. + if _, err := p.Run(); err != nil { + log.Fatalf("Alas, there's been an error: %v", err) + os.Exit(1) + } +}