uiutil.go

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