composer.go

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