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