347 lines
10 KiB
Go
347 lines
10 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"charm.land/bubbles/v2/spinner"
|
|
"charm.land/lipgloss/v2"
|
|
)
|
|
|
|
// ToolCardKind represents the type of tool operation.
|
|
type ToolCardKind int
|
|
|
|
const (
|
|
ToolCardFile ToolCardKind = iota
|
|
ToolCardBash
|
|
ToolCardSearch
|
|
ToolCardGit
|
|
ToolCardGeneric
|
|
)
|
|
|
|
// ToolCardState represents the execution state.
|
|
type ToolCardState int
|
|
|
|
const (
|
|
ToolCardRunning ToolCardState = iota
|
|
ToolCardSuccess
|
|
ToolCardError
|
|
)
|
|
|
|
// ToolCard is a fancy tool execution display component.
|
|
type ToolCard struct {
|
|
Name string
|
|
Kind ToolCardKind
|
|
State ToolCardState
|
|
Args string
|
|
Result string
|
|
StartTime time.Time
|
|
Duration time.Duration
|
|
Expanded bool
|
|
Spinner spinner.Model
|
|
ElapsedTimer *time.Timer
|
|
Elapsed time.Duration
|
|
IsDark bool
|
|
Styles ToolCardStyles
|
|
}
|
|
|
|
// ToolCardStyles holds styles for the tool card.
|
|
type ToolCardStyles struct {
|
|
BorderRunning lipgloss.Style
|
|
BorderSuccess lipgloss.Style
|
|
BorderError lipgloss.Style
|
|
TitleRunning lipgloss.Style
|
|
TitleSuccess lipgloss.Style
|
|
TitleError lipgloss.Style
|
|
Args lipgloss.Style
|
|
Result lipgloss.Style
|
|
Error lipgloss.Style
|
|
Dimmed lipgloss.Style
|
|
Elapsed lipgloss.Style
|
|
}
|
|
|
|
// NewToolCardStyles creates styles based on theme.
|
|
func NewToolCardStyles(isDark bool) ToolCardStyles {
|
|
if isDark {
|
|
return ToolCardStyles{
|
|
BorderRunning: lipgloss.NewStyle().Foreground(lipgloss.Color("#81a1c1")),
|
|
BorderSuccess: lipgloss.NewStyle().Foreground(lipgloss.Color("#a3be8c")),
|
|
BorderError: lipgloss.NewStyle().Foreground(lipgloss.Color("#bf616a")),
|
|
TitleRunning: lipgloss.NewStyle().Foreground(lipgloss.Color("#88c0d0")).Bold(true),
|
|
TitleSuccess: lipgloss.NewStyle().Foreground(lipgloss.Color("#a3be8c")).Bold(true),
|
|
TitleError: lipgloss.NewStyle().Foreground(lipgloss.Color("#bf616a")).Bold(true),
|
|
Args: lipgloss.NewStyle().Foreground(lipgloss.Color("#d8dee9")),
|
|
Result: lipgloss.NewStyle().Foreground(lipgloss.Color("#d8dee9")),
|
|
Error: lipgloss.NewStyle().Foreground(lipgloss.Color("#bf616a")),
|
|
Dimmed: lipgloss.NewStyle().Foreground(lipgloss.Color("#4c566a")),
|
|
Elapsed: lipgloss.NewStyle().Foreground(lipgloss.Color("#81a1c1")),
|
|
}
|
|
}
|
|
return ToolCardStyles{
|
|
BorderRunning: lipgloss.NewStyle().Foreground(lipgloss.Color("#5e81ac")),
|
|
BorderSuccess: lipgloss.NewStyle().Foreground(lipgloss.Color("#4f8f38")),
|
|
BorderError: lipgloss.NewStyle().Foreground(lipgloss.Color("#c94f4f")),
|
|
TitleRunning: lipgloss.NewStyle().Foreground(lipgloss.Color("#4f8f8f")).Bold(true),
|
|
TitleSuccess: lipgloss.NewStyle().Foreground(lipgloss.Color("#4f8f38")).Bold(true),
|
|
TitleError: lipgloss.NewStyle().Foreground(lipgloss.Color("#c94f4f")).Bold(true),
|
|
Args: lipgloss.NewStyle().Foreground(lipgloss.Color("#4c566a")),
|
|
Result: lipgloss.NewStyle().Foreground(lipgloss.Color("#4c566a")),
|
|
Error: lipgloss.NewStyle().Foreground(lipgloss.Color("#c94f4f")),
|
|
Dimmed: lipgloss.NewStyle().Foreground(lipgloss.Color("#9ca0a8")),
|
|
Elapsed: lipgloss.NewStyle().Foreground(lipgloss.Color("#5e81ac")),
|
|
}
|
|
}
|
|
|
|
// NewToolCard creates a new tool card.
|
|
func NewToolCard(name string, kind ToolCardKind, isDark bool) ToolCard {
|
|
s := spinner.New(
|
|
spinner.WithSpinner(spinner.MiniDot),
|
|
spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#88c0d0"))),
|
|
)
|
|
return ToolCard{
|
|
Name: name,
|
|
Kind: kind,
|
|
State: ToolCardRunning,
|
|
Spinner: s,
|
|
IsDark: isDark,
|
|
Styles: NewToolCardStyles(isDark),
|
|
}
|
|
}
|
|
|
|
// SetDark updates the theme.
|
|
func (c *ToolCard) SetDark(isDark bool) {
|
|
c.IsDark = isDark
|
|
c.Styles = NewToolCardStyles(isDark)
|
|
}
|
|
|
|
// Tick advances the spinner animation.
|
|
func (c *ToolCard) Tick() {
|
|
c.Spinner.Tick()
|
|
}
|
|
|
|
// UpdateElapsed updates the elapsed time counter.
|
|
func (c *ToolCard) UpdateElapsed() {
|
|
if c.State == ToolCardRunning {
|
|
c.Elapsed = time.Since(c.StartTime)
|
|
}
|
|
}
|
|
|
|
// getIcon returns the appropriate icon for the tool kind and state.
|
|
func (c *ToolCard) getIcon() string {
|
|
switch c.Kind {
|
|
case ToolCardFile:
|
|
if c.State == ToolCardRunning {
|
|
return "📄"
|
|
}
|
|
if c.State == ToolCardSuccess {
|
|
return "✓"
|
|
}
|
|
return "✗"
|
|
case ToolCardBash:
|
|
if c.State == ToolCardRunning {
|
|
return "💻"
|
|
}
|
|
if c.State == ToolCardSuccess {
|
|
return "✓"
|
|
}
|
|
return "✗"
|
|
case ToolCardSearch:
|
|
if c.State == ToolCardRunning {
|
|
return "🔍"
|
|
}
|
|
if c.State == ToolCardSuccess {
|
|
return "✓"
|
|
}
|
|
return "✗"
|
|
case ToolCardGit:
|
|
if c.State == ToolCardRunning {
|
|
return "🌿"
|
|
}
|
|
if c.State == ToolCardSuccess {
|
|
return "✓"
|
|
}
|
|
return "✗"
|
|
default:
|
|
if c.State == ToolCardRunning {
|
|
return "◌"
|
|
}
|
|
if c.State == ToolCardSuccess {
|
|
return "✓"
|
|
}
|
|
return "✗"
|
|
}
|
|
}
|
|
|
|
// getStatusText returns the status text based on state.
|
|
func (c *ToolCard) getStatusText() string {
|
|
switch c.State {
|
|
case ToolCardRunning:
|
|
return "running..."
|
|
case ToolCardSuccess:
|
|
return fmt.Sprintf("(%s)", formatDuration(c.Duration))
|
|
case ToolCardError:
|
|
return fmt.Sprintf("error (%s)", formatDuration(c.Duration))
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// getBorderStyle returns the appropriate border style.
|
|
func (c *ToolCard) getBorderStyle() lipgloss.Style {
|
|
switch c.State {
|
|
case ToolCardRunning:
|
|
return c.Styles.BorderRunning
|
|
case ToolCardSuccess:
|
|
return c.Styles.BorderSuccess
|
|
case ToolCardError:
|
|
return c.Styles.BorderError
|
|
default:
|
|
return c.Styles.BorderRunning
|
|
}
|
|
}
|
|
|
|
// getTitleStyle returns the appropriate title style.
|
|
func (c *ToolCard) getTitleStyle() lipgloss.Style {
|
|
switch c.State {
|
|
case ToolCardRunning:
|
|
return c.Styles.TitleRunning
|
|
case ToolCardSuccess:
|
|
return c.Styles.TitleSuccess
|
|
case ToolCardError:
|
|
return c.Styles.TitleError
|
|
default:
|
|
return c.Styles.TitleRunning
|
|
}
|
|
}
|
|
|
|
// View renders the tool card.
|
|
func (c *ToolCard) View(width int) string {
|
|
// Update elapsed time for running tools
|
|
c.UpdateElapsed()
|
|
|
|
// Build title line
|
|
icon := c.getIcon()
|
|
statusText := c.getStatusText()
|
|
|
|
var titleParts []string
|
|
titleParts = append(titleParts, icon)
|
|
titleParts = append(titleParts, c.Name)
|
|
|
|
if c.State == ToolCardRunning {
|
|
titleParts = append(titleParts, c.Spinner.View())
|
|
titleParts = append(titleParts, statusText)
|
|
// Show elapsed time for running tools
|
|
elapsedStr := fmt.Sprintf("%.1fs", c.Elapsed.Seconds())
|
|
titleParts = append(titleParts, c.Styles.Elapsed.Render(elapsedStr))
|
|
} else {
|
|
titleParts = append(titleParts, statusText)
|
|
}
|
|
|
|
title := strings.Join(titleParts, " ")
|
|
titleStyle := c.getTitleStyle()
|
|
|
|
// Create bordered box
|
|
content := titleStyle.Render(title)
|
|
|
|
if c.Expanded && c.State != ToolCardRunning {
|
|
// Show args and result when expanded
|
|
var details strings.Builder
|
|
|
|
if c.Args != "" {
|
|
args := truncate(c.Args, 80)
|
|
details.WriteString(c.Styles.Args.Render(" args: " + args))
|
|
details.WriteString("\n")
|
|
}
|
|
|
|
if c.Result != "" {
|
|
if c.State == ToolCardError {
|
|
details.WriteString(c.Styles.Error.Render(" " + truncate(c.Result, 200)))
|
|
} else {
|
|
details.WriteString(c.Styles.Result.Render(" " + truncate(c.Result, 200)))
|
|
}
|
|
details.WriteString("\n")
|
|
}
|
|
|
|
content = lipgloss.JoinVertical(lipgloss.Left, content, details.String())
|
|
}
|
|
|
|
// Apply border
|
|
borderStyle := c.getBorderStyle()
|
|
box := lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(borderStyle.GetForeground()).
|
|
Padding(0, 1)
|
|
|
|
return box.Render(content)
|
|
}
|
|
|
|
// ToolCardManager manages multiple tool cards with synchronized animations.
|
|
type ToolCardManager struct {
|
|
Cards []ToolCard
|
|
IsDark bool
|
|
}
|
|
|
|
// NewToolCardManager creates a new manager.
|
|
func NewToolCardManager(isDark bool) ToolCardManager {
|
|
return ToolCardManager{
|
|
Cards: []ToolCard{},
|
|
IsDark: isDark,
|
|
}
|
|
}
|
|
|
|
// AddCard adds a new tool card.
|
|
func (m *ToolCardManager) AddCard(name string, kind ToolCardKind, startTime time.Time) {
|
|
card := NewToolCard(name, kind, m.IsDark)
|
|
card.StartTime = startTime
|
|
m.Cards = append(m.Cards, card)
|
|
}
|
|
|
|
// UpdateCard updates an existing card by name.
|
|
func (m *ToolCardManager) UpdateCard(name string, state ToolCardState, result string, duration time.Duration) {
|
|
for i := range m.Cards {
|
|
if m.Cards[i].Name == name && m.Cards[i].State == ToolCardRunning {
|
|
m.Cards[i].State = state
|
|
m.Cards[i].Result = result
|
|
m.Cards[i].Duration = duration
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetExpanded sets a card's expanded state.
|
|
func (m *ToolCardManager) SetExpanded(name string, expanded bool) {
|
|
for i := range m.Cards {
|
|
if m.Cards[i].Name == name {
|
|
m.Cards[i].Expanded = expanded
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tick advances all running card spinners.
|
|
func (m *ToolCardManager) Tick() {
|
|
for i := range m.Cards {
|
|
if m.Cards[i].State == ToolCardRunning {
|
|
m.Cards[i].Tick()
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetDark updates theme for all cards.
|
|
func (m *ToolCardManager) SetDark(isDark bool) {
|
|
m.IsDark = isDark
|
|
for i := range m.Cards {
|
|
m.Cards[i].SetDark(isDark)
|
|
}
|
|
}
|
|
|
|
// View renders all cards.
|
|
func (m *ToolCardManager) View(width int) string {
|
|
var lines []string
|
|
for i := range m.Cards {
|
|
lines = append(lines, m.Cards[i].View(width))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|