Files
MujicaUI/agent/command_execution_card.go
T

131 lines
3.9 KiB
Go

package agent
import (
"fmt"
"log/slog"
"strings"
"time"
"github.com/egoist/mygo/ui"
"github.com/ZacharyZhang-NY/MujicaUI/core"
"github.com/ZacharyZhang-NY/MujicaUI/icons"
"github.com/ZacharyZhang-NY/MujicaUI/input"
"github.com/ZacharyZhang-NY/MujicaUI/internal"
"github.com/ZacharyZhang-NY/MujicaUI/theme"
)
// CommandRun is a command run; the caller appends Output.
type CommandRun struct {
Command string
Dir string
// Output is the raw terminal output; ANSI colors are kept.
Output string
Running bool
ExitCode int
Duration time.Duration
}
// CommandExecutionCardOptions configure a CommandExecutionCard.
type CommandExecutionCardOptions struct {
// Height of the output well; 240 DIPs when zero.
Height float32
Disabled bool
Still bool
}
// commandOutput keeps parsed lines; appends parse only new lines.
type commandOutput struct {
src string
lines [][]internal.AnsiSpan
style internal.AnsiStyle
list ui.ListState
}
func (o *commandOutput) update(out string) {
if out == o.src {
return
}
if !strings.HasPrefix(out, o.src) || o.src != "" && !strings.HasSuffix(o.src, "\n") {
o.lines, o.style = nil, internal.AnsiStyle{}
o.src = ""
}
for _, line := range internal.SplitLines(out[len(o.src):]) {
var spans []internal.AnsiSpan
spans, o.style = internal.ParseAnsi(line, o.style)
o.lines = append(o.lines, spans)
}
o.src = out
}
// CommandExecutionCard shows a command run; Changed requests an abort.
func CommandExecutionCard(c *ui.Context, run CommandRun, opts CommandExecutionCardOptions) core.ChoiceResult {
if run.Command == "" {
panic("mujica: CommandExecutionCard needs a Command")
}
if opts.Height < 0 {
panic("mujica: CommandExecutionCard Height is negative")
}
h := opts.Height
if h == 0 {
h = 240
}
k := core.Tokens(c)
u := core.Density(c).Unit()
var r core.ChoiceResult
r.Element = AgentCard(c).Disabled(opts.Disabled).Label(run.Command)
out := ui.Local(r.Element, "mujica.cmd.output", func() *commandOutput { return &commandOutput{} })
o := *out
o.list.FollowEnd = true
o.update(run.Output)
r.Element.Children(func() {
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize).Children(func() {
ui.Text(c, "$").Font(theme.MonoFont).TextColor(k.TextMuted).Shrink(0)
ui.Text(c, run.Command).Font(theme.MonoFont).FontWeight(600).Grow(1).Shrink(1).Selectable()
if run.Running {
agentMark(c, AgentRunning, 16, opts.Still)
AgentCaption(c, core.Msg(c, "agent_status.running"))
locked := r.Element.IsDisabled()
if input.Button(c, core.Msg(c, "command_execution_card.abort"), input.ButtonOptions{Variant: input.Outline, Disabled: locked}).Clicked() && !locked {
r = core.NewChoiceResult(r.Element, true)
}
return
}
commandExit(c, run.ExitCode)
AgentCaption(c, AgentDuration(run.Duration))
})
if run.Dir != "" {
ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Children(func() {
ui.Icon(c, icons.Must("folder")).Size(14, 14).TextColor(k.TextMuted)
AgentCaption(c, run.Dir).Font(theme.MonoFont)
})
}
well := ui.List(c, &o.list, len(o.lines), func(i int) {
ui.RichText(c, agentAnsiSpans(k, o.lines[i])...)
})
agentWell(c, well, h).Height(h).Label(core.Msg(c, "command_execution_card.output")).Children(func() {
if len(o.lines) == 0 {
AgentCaption(c, core.Msg(c, "command_execution_card.no_output"))
}
})
})
if r.Changed() {
slog.Debug("mujica: command_execution_card abort", "command", run.Command)
}
return r
}
// commandExit shows the exit code by icon and word.
func commandExit(c *ui.Context, code int) {
k := core.Tokens(c)
tone, icon := k.Success, "circle-check"
if code != 0 {
tone, icon = k.Danger, "circle-x"
}
ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() {
ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(tone)
ui.Text(c, fmt.Sprintf(core.Msg(c, "command_execution_card.exit"), code)).TextColor(tone).
FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine()
})
}