on main help sign in
fix(editor): fix opening `$EDITOR` when it contains arguments (#1481)
commit77da29d673bd2d01a80849821db06f0e6444010d authorAmolith <amolith@secluded.site> dateWed 26 Nov 2025, 10:34 (1 year ago) parent68bed39
2 files changed+64−7
internal/tui/components/chat/editor/editor.go+6−7 internal/tui/util/shell.go+58
internal/tui/components/chat/editor/editor.go+6−7
@@ -6,7 +6,6 @@ import (
66 "math/rand"
77 "net/http"
88 "os"
9− "os/exec"
109 "path/filepath"
1110 "runtime"
1211 "slices"
@@ -96,7 +95,7 @@ type OpenEditorMsg struct {
9695func (m *editorCmp) openEditor(value string) tea.Cmd {
9796 editor := os.Getenv("EDITOR")
9897 if editor == "" {
99− // Use platform-appropriate default editor
98+ // Use platform-appropriate default editor.
10099 if runtime.GOOS == "windows" {
101100 editor = "notepad"
102101 } else {
@@ -112,11 +111,11 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
112111 if _, err := tmpfile.WriteString(value); err != nil {
113112 return util.ReportError(err)
114113 }
115− c := exec.CommandContext(context.TODO(), editor, tmpfile.Name())
116− c.Stdin = os.Stdin
117− c.Stdout = os.Stdout
118− c.Stderr = os.Stderr
119− return tea.ExecProcess(c, func(err error) tea.Msg {
114+
115+ // Build the full shell command with the file argument.
116+ cmdStr := fmt.Sprintf("%s %s", editor, tmpfile.Name())
117+
118+ return util.ExecShell(context.TODO(), cmdStr, func(err error) tea.Msg {
120119 if err != nil {
121120 return util.ReportError(err)
122121 }
internal/tui/util/shell.gonew file+58
@@ -0,0 +1,58 @@
1+package util
2+
3+import (
4+ "context"
5+ "io"
6+ "strings"
7+
8+ tea "charm.land/bubbletea/v2"
9+ "mvdan.cc/sh/v3/interp"
10+ "mvdan.cc/sh/v3/syntax"
11+)
12+
13+// shellCommand wraps a shell interpreter to implement tea.ExecCommand.
14+type shellCommand struct {
15+ ctx context.Context
16+ file *syntax.File
17+ stdin io.Reader
18+ stdout io.Writer
19+ stderr io.Writer
20+}
21+
22+func (s *shellCommand) SetStdin(r io.Reader) {
23+ s.stdin = r
24+}
25+
26+func (s *shellCommand) SetStdout(w io.Writer) {
27+ s.stdout = w
28+}
29+
30+func (s *shellCommand) SetStderr(w io.Writer) {
31+ s.stderr = w
32+}
33+
34+func (s *shellCommand) Run() error {
35+ runner, err := interp.New(
36+ interp.StdIO(s.stdin, s.stdout, s.stderr),
37+ )
38+ if err != nil {
39+ return err
40+ }
41+ return runner.Run(s.ctx, s.file)
42+}
43+
44+// ExecShell executes a shell command string using tea.Exec.
45+// The command is parsed and executed via mvdan.cc/sh/v3/interp, allowing
46+// proper handling of shell syntax like quotes and arguments.
47+func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
48+ parsed, err := syntax.NewParser().Parse(strings.NewReader(cmdStr), "")
49+ if err != nil {
50+ return ReportError(err)
51+ }
52+
53+ cmd := &shellCommand{
54+ ctx: ctx,
55+ file: parsed,
56+ }
57+ return tea.Exec(cmd, callback)
58+}