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)
47
48func ReportInfo(info string) tea.Cmd {
49 return CmdHandler(InfoMsg{
50 Type: InfoTypeInfo,
51 Msg: info,
52 })
53}
54
55func ReportWarn(warn string) tea.Cmd {
56 return CmdHandler(InfoMsg{
57 Type: InfoTypeWarn,
58 Msg: warn,
59 })
60}
61
62type (
63 InfoMsg struct {
64 Type InfoType
65 Msg string
66 TTL time.Duration
67 }
68 ClearStatusMsg struct{}
69)
70
71// shellCommand wraps a shell interpreter to implement tea.ExecCommand.
72type shellCommand struct {
73 ctx context.Context
74 file *syntax.File
75 stdin io.Reader
76 stdout io.Writer
77 stderr io.Writer
78}
79
80func (s *shellCommand) SetStdin(r io.Reader) {
81 s.stdin = r
82}
83
84func (s *shellCommand) SetStdout(w io.Writer) {
85 s.stdout = w
86}
87
88func (s *shellCommand) SetStderr(w io.Writer) {
89 s.stderr = w
90}
91
92func (s *shellCommand) Run() error {
93 runner, err := interp.New(
94 interp.StdIO(s.stdin, s.stdout, s.stderr),
95 )
96 if err != nil {
97 return err
98 }
99 return runner.Run(s.ctx, s.file)
100}
101
102// ExecShell executes a shell command string using tea.Exec.
103// The command is parsed and executed via mvdan.cc/sh/v3/interp, allowing
104// proper handling of shell syntax like quotes and arguments.
105func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
106 parsed, err := syntax.NewParser().Parse(strings.NewReader(cmdStr), "")
107 if err != nil {
108 return ReportError(err)
109 }
110
111 cmd := &shellCommand{
112 ctx: ctx,
113 file: parsed,
114 }
115
116 return tea.Exec(cmd, callback)
117}