1package util
2
3import (
4 "context"
5 "io"
6 "log/slog"
7 "strings"
8 "time"
9
10 tea "charm.land/bubbletea/v2"
11 "mvdan.cc/sh/v3/interp"
12 "mvdan.cc/sh/v3/syntax"
13)
14
15type Cursor interface {
16 Cursor() *tea.Cursor
17}
18
19type Model interface {
20 Init() tea.Cmd
21 Update(tea.Msg) (Model, tea.Cmd)
22 View() string
23}
24
25func CmdHandler(msg tea.Msg) tea.Cmd {
26 return func() tea.Msg {
27 return msg
28 }
29}
30
31func ReportError(err error) tea.Cmd {
32 slog.Error("Error reported", "error", err)
33 return CmdHandler(InfoMsg{
34 Type: InfoTypeError,
35 Msg: err.Error(),
36 })
37}
38
39type InfoType int
40
41const (
42 InfoTypeInfo InfoType = iota
43 InfoTypeSuccess
44 InfoTypeWarn
45 InfoTypeError
46 InfoTypeUpdate
47)
48
49func ReportInfo(info string) tea.Cmd {
50 return CmdHandler(InfoMsg{
51 Type: InfoTypeInfo,
52 Msg: info,
53 })
54}
55
56func ReportWarn(warn string) tea.Cmd {
57 return CmdHandler(InfoMsg{
58 Type: InfoTypeWarn,
59 Msg: warn,
60 })
61}
62
63type (
64 InfoMsg struct {
65 Type InfoType
66 Msg string
67 TTL time.Duration
68 }
69 ClearStatusMsg struct{}
70)
71
72// shellCommand wraps a shell interpreter to implement tea.ExecCommand.
73type shellCommand struct {
74 ctx context.Context
75 file *syntax.File
76 stdin io.Reader
77 stdout io.Writer
78 stderr io.Writer
79}
80
81func (s *shellCommand) SetStdin(r io.Reader) {
82 s.stdin = r
83}
84
85func (s *shellCommand) SetStdout(w io.Writer) {
86 s.stdout = w
87}
88
89func (s *shellCommand) SetStderr(w io.Writer) {
90 s.stderr = w
91}
92
93func (s *shellCommand) Run() error {
94 runner, err := interp.New(
95 interp.StdIO(s.stdin, s.stdout, s.stderr),
96 )
97 if err != nil {
98 return err
99 }
100 return runner.Run(s.ctx, s.file)
101}
102
103// ExecShell executes a shell command string using tea.Exec.
104// The command is parsed and executed via mvdan.cc/sh/v3/interp, allowing
105// proper handling of shell syntax like quotes and arguments.
106func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
107 parsed, err := syntax.NewParser().Parse(strings.NewReader(cmdStr), "")
108 if err != nil {
109 return ReportError(err)
110 }
111
112 cmd := &shellCommand{
113 ctx: ctx,
114 file: parsed,
115 }
116
117 return tea.Exec(cmd, callback)
118}