1// Package util provides utility functions for UI message handling.
2package util
3
4import (
5 "context"
6 "errors"
7 "log/slog"
8 "os/exec"
9 "time"
10
11 tea "charm.land/bubbletea/v2"
12 "mvdan.cc/sh/v3/shell"
13)
14
15type Cursor interface {
16 Cursor() *tea.Cursor
17}
18
19func CmdHandler(msg tea.Msg) tea.Cmd {
20 return func() tea.Msg {
21 return msg
22 }
23}
24
25func ReportError(err error) tea.Cmd {
26 slog.Error("Error reported", "error", err)
27 return CmdHandler(NewErrorMsg(err))
28}
29
30type InfoType int
31
32const (
33 InfoTypeInfo InfoType = iota
34 InfoTypeSuccess
35 InfoTypeWarn
36 InfoTypeError
37 InfoTypeUpdate
38)
39
40func NewInfoMsg(info string) InfoMsg {
41 return InfoMsg{
42 Type: InfoTypeInfo,
43 Msg: info,
44 }
45}
46
47func NewWarnMsg(warn string) InfoMsg {
48 return InfoMsg{
49 Type: InfoTypeWarn,
50 Msg: warn,
51 }
52}
53
54func NewErrorMsg(err error) InfoMsg {
55 return InfoMsg{
56 Type: InfoTypeError,
57 Msg: err.Error(),
58 }
59}
60
61func ReportInfo(info string) tea.Cmd {
62 return CmdHandler(NewInfoMsg(info))
63}
64
65func ReportWarn(warn string) tea.Cmd {
66 return CmdHandler(NewWarnMsg(warn))
67}
68
69type (
70 InfoMsg struct {
71 Type InfoType
72 Msg string
73 TTL time.Duration
74 }
75 ClearStatusMsg struct{}
76)
77
78// IsEmpty checks if the [InfoMsg] is empty.
79func (m InfoMsg) IsEmpty() bool {
80 var zero InfoMsg
81 return m == zero
82}
83
84// ExecShell parses a shell command string and executes it with exec.Command.
85// Uses shell.Fields for proper handling of shell syntax like quotes and
86// arguments while preserving TTY handling for terminal editors.
87func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
88 fields, err := shell.Fields(cmdStr, nil)
89 if err != nil {
90 return ReportError(err)
91 }
92 if len(fields) == 0 {
93 return ReportError(errors.New("empty command"))
94 }
95
96 cmd := exec.CommandContext(ctx, fields[0], fields[1:]...)
97 return tea.ExecProcess(cmd, callback)
98}