Change summary
internal/tui/components/chat/editor/editor.go | 8 +----
internal/tui/util/shell.go | 26 +++++++++++++++++++++
2 files changed, 28 insertions(+), 6 deletions(-)
Detailed changes
@@ -6,7 +6,6 @@ import (
"math/rand"
"net/http"
"os"
- "os/exec"
"path/filepath"
"runtime"
"slices"
@@ -112,11 +111,8 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
if _, err := tmpfile.WriteString(value); err != nil {
return util.ReportError(err)
}
- c := exec.CommandContext(context.TODO(), editor, tmpfile.Name())
- c.Stdin = os.Stdin
- c.Stdout = os.Stdout
- c.Stderr = os.Stderr
- return tea.ExecProcess(c, func(err error) tea.Msg {
+ cmdStr := editor + " " + tmpfile.Name()
+ return util.ExecShell(context.TODO(), cmdStr, func(err error) tea.Msg {
if err != nil {
return util.ReportError(err)
}
@@ -0,0 +1,26 @@
+package util
+
+import (
+ "context"
+ "errors"
+ "os/exec"
+
+ tea "charm.land/bubbletea/v2"
+ "mvdan.cc/sh/v3/shell"
+)
+
+// ExecShell parses a shell command string and executes it with exec.Command.
+// Uses shell.Fields for proper handling of shell syntax like quotes and
+// arguments while preserving TTY handling for terminal editors.
+func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
+ fields, err := shell.Fields(cmdStr, nil)
+ if err != nil {
+ return ReportError(err)
+ }
+ if len(fields) == 0 {
+ return ReportError(errors.New("empty command"))
+ }
+
+ cmd := exec.CommandContext(ctx, fields[0], fields[1:]...)
+ return tea.ExecProcess(cmd, callback)
+}