@@ -839,6 +839,23 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
return m, m.current.Init()
+ case tui.OpenEditorMsg:
+ composer, ok := m.current.(*tui.Composer)
+ if !ok {
+ return m, nil
+ }
+ return m, openExternalEditor(composer.GetBody())
+
+ case tui.EditorFinishedMsg:
+ if msg.Err != nil {
+ log.Printf("Editor error: %v", msg.Err)
+ return m, nil
+ }
+ if composer, ok := m.current.(*tui.Composer); ok {
+ composer.SetBody(msg.Body)
+ }
+ return m, nil
+
case tui.GoToFilePickerMsg:
m.previousModel = m.current
wd, _ := os.Getwd()
@@ -1559,6 +1576,51 @@ func archiveEmailCmd(account *config.Account, uid uint32, accountID string, mail
}
}
+// --- External editor command ---
+
+// openExternalEditor writes the body to a temp file, opens $EDITOR, and reads back the result.
+func openExternalEditor(body string) tea.Cmd {
+ editor := os.Getenv("EDITOR")
+ if editor == "" {
+ editor = os.Getenv("VISUAL")
+ }
+ if editor == "" {
+ editor = "vi"
+ }
+
+ tmpFile, err := os.CreateTemp("", "matcha-*.md")
+ if err != nil {
+ return func() tea.Msg {
+ return tui.EditorFinishedMsg{Err: fmt.Errorf("creating temp file: %w", err)}
+ }
+ }
+ tmpPath := tmpFile.Name()
+
+ if _, err := tmpFile.WriteString(body); err != nil {
+ tmpFile.Close()
+ os.Remove(tmpPath)
+ return func() tea.Msg {
+ return tui.EditorFinishedMsg{Err: fmt.Errorf("writing temp file: %w", err)}
+ }
+ }
+ tmpFile.Close()
+
+ parts := strings.Fields(editor)
+ args := append(parts[1:], tmpPath)
+ c := exec.Command(parts[0], args...)
+ return tea.ExecProcess(c, func(err error) tea.Msg {
+ defer os.Remove(tmpPath)
+ if err != nil {
+ return tui.EditorFinishedMsg{Err: err}
+ }
+ content, readErr := os.ReadFile(tmpPath)
+ if readErr != nil {
+ return tui.EditorFinishedMsg{Err: readErr}
+ }
+ return tui.EditorFinishedMsg{Body: string(content)}
+ })
+}
+
// --- IDLE command ---
// listenForIdleUpdates blocks until an IDLE update arrives, then returns it as a tea.Msg.
@@ -331,6 +331,8 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
+ case "ctrl+e":
+ return m, func() tea.Msg { return OpenEditorMsg{} }
case "esc":
m.confirmingExit = true
return m, nil
@@ -598,7 +600,7 @@ func (m *Composer) View() tea.View {
}
mainContent := lipgloss.JoinVertical(lipgloss.Left, composerViewElements...)
- helpText := "Markdown/HTML • tab/shift+tab: navigate • esc: save draft & exit"
+ helpText := "Markdown/HTML • tab/shift+tab: navigate • ctrl+e: $EDITOR • esc: save draft & exit"
if m.pluginStatus != "" {
helpText += " • " + m.pluginStatus
}
@@ -706,6 +708,11 @@ func (m *Composer) GetBody() string {
return m.bodyInput.Value()
}
+// SetBody updates the Body field with new content.
+func (m *Composer) SetBody(body string) {
+ m.bodyInput.SetValue(body)
+}
+
// GetAttachmentPaths returns the current attachment paths.
func (m *Composer) GetAttachmentPaths() []string {
return m.attachmentPaths
@@ -391,6 +391,17 @@ type FetchFolderMoreEmailsMsg struct {
Limit uint32
}
+// --- External Editor Messages ---
+
+// OpenEditorMsg signals that the composer body should be opened in $EDITOR.
+type OpenEditorMsg struct{}
+
+// EditorFinishedMsg signals that the external editor has closed.
+type EditorFinishedMsg struct {
+ Body string
+ Err error
+}
+
// --- IDLE Messages ---
// IdleNewMailMsg signals that IMAP IDLE detected new mail for an account/folder.