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(NewErrorMsg(err))
 30}
 31
 32type InfoType int
 33
 34const (
 35	InfoTypeInfo InfoType = iota
 36	InfoTypeSuccess
 37	InfoTypeWarn
 38	InfoTypeError
 39	InfoTypeUpdate
 40)
 41
 42func NewInfoMsg(info string) InfoMsg {
 43	return InfoMsg{
 44		Type: InfoTypeInfo,
 45		Msg:  info,
 46	}
 47}
 48
 49func NewWarnMsg(warn string) InfoMsg {
 50	return InfoMsg{
 51		Type: InfoTypeWarn,
 52		Msg:  warn,
 53	}
 54}
 55
 56func NewErrorMsg(err error) InfoMsg {
 57	return InfoMsg{
 58		Type: InfoTypeError,
 59		Msg:  err.Error(),
 60	}
 61}
 62
 63func ReportInfo(info string) tea.Cmd {
 64	return CmdHandler(NewInfoMsg(info))
 65}
 66
 67func ReportWarn(warn string) tea.Cmd {
 68	return CmdHandler(NewWarnMsg(warn))
 69}
 70
 71type (
 72	InfoMsg struct {
 73		Type InfoType
 74		Msg  string
 75		TTL  time.Duration
 76	}
 77	ClearStatusMsg struct{}
 78)
 79
 80// IsEmpty checks if the [InfoMsg] is empty.
 81func (m InfoMsg) IsEmpty() bool {
 82	var zero InfoMsg
 83	return m == zero
 84}
 85
 86// ExecShell parses a shell command string and executes it with exec.Command.
 87// Uses shell.Fields for proper handling of shell syntax like quotes and
 88// arguments while preserving TTY handling for terminal editors.
 89func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
 90	fields, err := shell.Fields(cmdStr, nil)
 91	if err != nil {
 92		return ReportError(err)
 93	}
 94	if len(fields) == 0 {
 95		return ReportError(errors.New("empty command"))
 96	}
 97
 98	cmd := exec.CommandContext(ctx, fields[0], fields[1:]...)
 99	return tea.ExecProcess(cmd, callback)
100}