composer.go

  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}