util.go

  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}