refactor(tui): move ExecShell to uiutil package

Ayman Bagabas created

Change summary

internal/tui/util/shell.go | 15 ++-------------
internal/uiutil/uiutil.go  | 20 ++++++++++++++++++++
2 files changed, 22 insertions(+), 13 deletions(-)

Detailed changes

internal/tui/util/shell.go 🔗

@@ -2,25 +2,14 @@ package util
 
 import (
 	"context"
-	"errors"
-	"os/exec"
 
 	tea "charm.land/bubbletea/v2"
-	"mvdan.cc/sh/v3/shell"
+	"github.com/charmbracelet/crush/internal/uiutil"
 )
 
 // 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)
+	return uiutil.ExecShell(ctx, cmdStr, callback)
 }

internal/uiutil/uiutil.go 🔗

@@ -4,10 +4,14 @@
 package uiutil
 
 import (
+	"context"
+	"errors"
 	"log/slog"
+	"os/exec"
 	"time"
 
 	tea "charm.land/bubbletea/v2"
+	"mvdan.cc/sh/v3/shell"
 )
 
 type Cursor interface {
@@ -60,3 +64,19 @@ type (
 	}
 	ClearStatusMsg struct{}
 )
+
+// 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)
+}