Skip to content

Commit 88d6d3d

Browse files
authored
feat: support for external editors (#411)
## What? <!-- Describe what this PR changes. Keep it concise — what code was added, removed, or modified? --> Adds external editor support (`$EDITOR`) to the email composer via `ctrl+e`. ## Why? <!-- Explain the motivation behind this change. What problem does it solve, or what addition does it enable? Link related issues if applicable. --> Closes #398 --------- Signed-off-by: drew <me@andrinoff.com>
1 parent 649019b commit 88d6d3d

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

main.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,23 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
839839
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
840840
return m, m.current.Init()
841841

842+
case tui.OpenEditorMsg:
843+
composer, ok := m.current.(*tui.Composer)
844+
if !ok {
845+
return m, nil
846+
}
847+
return m, openExternalEditor(composer.GetBody())
848+
849+
case tui.EditorFinishedMsg:
850+
if msg.Err != nil {
851+
log.Printf("Editor error: %v", msg.Err)
852+
return m, nil
853+
}
854+
if composer, ok := m.current.(*tui.Composer); ok {
855+
composer.SetBody(msg.Body)
856+
}
857+
return m, nil
858+
842859
case tui.GoToFilePickerMsg:
843860
m.previousModel = m.current
844861
wd, _ := os.Getwd()
@@ -1559,6 +1576,51 @@ func archiveEmailCmd(account *config.Account, uid uint32, accountID string, mail
15591576
}
15601577
}
15611578

1579+
// --- External editor command ---
1580+
1581+
// openExternalEditor writes the body to a temp file, opens $EDITOR, and reads back the result.
1582+
func openExternalEditor(body string) tea.Cmd {
1583+
editor := os.Getenv("EDITOR")
1584+
if editor == "" {
1585+
editor = os.Getenv("VISUAL")
1586+
}
1587+
if editor == "" {
1588+
editor = "vi"
1589+
}
1590+
1591+
tmpFile, err := os.CreateTemp("", "matcha-*.md")
1592+
if err != nil {
1593+
return func() tea.Msg {
1594+
return tui.EditorFinishedMsg{Err: fmt.Errorf("creating temp file: %w", err)}
1595+
}
1596+
}
1597+
tmpPath := tmpFile.Name()
1598+
1599+
if _, err := tmpFile.WriteString(body); err != nil {
1600+
tmpFile.Close()
1601+
os.Remove(tmpPath)
1602+
return func() tea.Msg {
1603+
return tui.EditorFinishedMsg{Err: fmt.Errorf("writing temp file: %w", err)}
1604+
}
1605+
}
1606+
tmpFile.Close()
1607+
1608+
parts := strings.Fields(editor)
1609+
args := append(parts[1:], tmpPath)
1610+
c := exec.Command(parts[0], args...)
1611+
return tea.ExecProcess(c, func(err error) tea.Msg {
1612+
defer os.Remove(tmpPath)
1613+
if err != nil {
1614+
return tui.EditorFinishedMsg{Err: err}
1615+
}
1616+
content, readErr := os.ReadFile(tmpPath)
1617+
if readErr != nil {
1618+
return tui.EditorFinishedMsg{Err: readErr}
1619+
}
1620+
return tui.EditorFinishedMsg{Body: string(content)}
1621+
})
1622+
}
1623+
15621624
// --- IDLE command ---
15631625

15641626
// listenForIdleUpdates blocks until an IDLE update arrives, then returns it as a tea.Msg.

tui/composer.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
331331
switch msg.String() {
332332
case "ctrl+c":
333333
return m, tea.Quit
334+
case "ctrl+e":
335+
return m, func() tea.Msg { return OpenEditorMsg{} }
334336
case "esc":
335337
m.confirmingExit = true
336338
return m, nil
@@ -598,7 +600,7 @@ func (m *Composer) View() tea.View {
598600
}
599601

600602
mainContent := lipgloss.JoinVertical(lipgloss.Left, composerViewElements...)
601-
helpText := "Markdown/HTML • tab/shift+tab: navigate • esc: save draft & exit"
603+
helpText := "Markdown/HTML • tab/shift+tab: navigate • ctrl+e: $EDITOR • esc: save draft & exit"
602604
if m.pluginStatus != "" {
603605
helpText += " • " + m.pluginStatus
604606
}
@@ -706,6 +708,11 @@ func (m *Composer) GetBody() string {
706708
return m.bodyInput.Value()
707709
}
708710

711+
// SetBody updates the Body field with new content.
712+
func (m *Composer) SetBody(body string) {
713+
m.bodyInput.SetValue(body)
714+
}
715+
709716
// GetAttachmentPaths returns the current attachment paths.
710717
func (m *Composer) GetAttachmentPaths() []string {
711718
return m.attachmentPaths

tui/messages.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,17 @@ type FetchFolderMoreEmailsMsg struct {
391391
Limit uint32
392392
}
393393

394+
// --- External Editor Messages ---
395+
396+
// OpenEditorMsg signals that the composer body should be opened in $EDITOR.
397+
type OpenEditorMsg struct{}
398+
399+
// EditorFinishedMsg signals that the external editor has closed.
400+
type EditorFinishedMsg struct {
401+
Body string
402+
Err error
403+
}
404+
394405
// --- IDLE Messages ---
395406

396407
// IdleNewMailMsg signals that IMAP IDLE detected new mail for an account/folder.

0 commit comments

Comments
 (0)