/editor: non-successful editor exits should keep the text in the textarea

# Conflicts:
#	packages/tui/internal/tui/tui.go
This commit is contained in:
Gal Schlezinger 2025-07-04 23:41:29 +03:00
parent 969ad80ed2
commit a1161fd0e8
2 changed files with 30 additions and 5 deletions

View file

@ -27,6 +27,7 @@ type EditorComponent interface {
Content(width int) string Content(width int) string
Lines() int Lines() int
Value() string Value() string
SetValue(value string)
Focused() bool Focused() bool
Focus() (tea.Model, tea.Cmd) Focus() (tea.Model, tea.Cmd)
Blur() Blur()
@ -230,6 +231,10 @@ func (m *editorComponent) Value() string {
return m.textarea.Value() return m.textarea.Value()
} }
func (m *editorComponent) SetValue(value string) {
m.textarea.SetValue(value)
}
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
value := strings.TrimSpace(m.Value()) value := strings.TrimSpace(m.Value())
if value == "" { if value == "" {

View file

@ -32,6 +32,11 @@ import (
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires // InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
type InterruptDebounceTimeoutMsg struct{} type InterruptDebounceTimeoutMsg struct{}
// EditorRestoreMsg is sent when the external editor fails and we need to restore the original content
type EditorRestoreMsg struct {
Content string
}
// InterruptKeyState tracks the state of interrupt key presses for debouncing // InterruptKeyState tracks the state of interrupt key presses for debouncing
type InterruptKeyState int type InterruptKeyState int
@ -486,6 +491,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Reset interrupt key state after timeout // Reset interrupt key state after timeout
a.interruptKeyState = InterruptKeyIdle a.interruptKeyState = InterruptKeyIdle
a.editor.SetInterruptKeyInDebounce(false) a.editor.SetInterruptKeyInDebounce(false)
case EditorRestoreMsg:
// Restore the original content to the textarea when editor fails
a.editor.SetValue(msg.Content)
case dialog.FindSelectedMsg: case dialog.FindSelectedMsg:
return a.openFile(msg.FilePath) return a.openFile(msg.FilePath)
} }
@ -789,20 +797,32 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
c.Stdout = os.Stdout c.Stdout = os.Stdout
c.Stderr = os.Stderr c.Stderr = os.Stderr
cmd = tea.ExecProcess(c, func(err error) tea.Msg { cmd = tea.ExecProcess(c, func(err error) tea.Msg {
defer os.Remove(tmpfile.Name())
// Check if the process exited with non-zero status
if err != nil { if err != nil {
slog.Error("Failed to open editor", "error", err) if exitError, ok := err.(*exec.ExitError); ok {
return nil slog.Warn("Editor exited with non-zero status", "exitCode", exitError.ExitCode())
// Restore original content when editor exits unsuccessfully
return EditorRestoreMsg{Content: value}
} else {
slog.Error("Failed to open editor", "error", err)
// Restore original content when editor fails to start
return EditorRestoreMsg{Content: value}
}
} }
content, err := os.ReadFile(tmpfile.Name()) content, err := os.ReadFile(tmpfile.Name())
if err != nil { if err != nil {
slog.Error("Failed to read file", "error", err) slog.Error("Failed to read file", "error", err)
return nil // Restore original content when file read fails
return EditorRestoreMsg{Content: value}
} }
if len(content) == 0 { if len(content) == 0 {
slog.Warn("Message is empty") slog.Warn("Message is empty")
return nil // Restore original content when message is empty
return EditorRestoreMsg{Content: value}
} }
os.Remove(tmpfile.Name())
return app.SendMsg{ return app.SendMsg{
Text: string(content), Text: string(content),
} }