util.go

 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}