pills.go

  1package chat
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"charm.land/lipgloss/v2"
  8	"github.com/charmbracelet/crush/internal/session"
  9	"github.com/charmbracelet/crush/internal/tui/components/chat/todos"
 10	"github.com/charmbracelet/crush/internal/tui/styles"
 11)
 12
 13func hasIncompleteTodos(todos []session.Todo) bool {
 14	for _, todo := range todos {
 15		if todo.Status != session.TodoStatusCompleted {
 16			return true
 17		}
 18	}
 19	return false
 20}
 21
 22const (
 23	pillHeightWithBorder  = 3
 24	maxTaskDisplayLength  = 40
 25	maxQueueDisplayLength = 60
 26)
 27
 28func queuePill(queue int, focused, pillsPanelFocused bool, t *styles.Theme) string {
 29	if queue <= 0 {
 30		return ""
 31	}
 32	triangles := styles.ForegroundGrad("▶▶▶▶▶▶▶▶▶", false, t.RedDark, t.Accent)
 33	if queue < 10 {
 34		triangles = triangles[:queue]
 35	}
 36
 37	content := fmt.Sprintf("%s %d Queued", strings.Join(triangles, ""), queue)
 38
 39	style := t.S().Base.PaddingLeft(1).PaddingRight(1)
 40	if !pillsPanelFocused || focused {
 41		style = style.BorderStyle(lipgloss.RoundedBorder()).BorderForeground(t.BgOverlay)
 42	} else {
 43		style = style.BorderStyle(lipgloss.HiddenBorder())
 44	}
 45	return style.Render(content)
 46}
 47
 48func todoPill(todos []session.Todo, spinnerView string, focused, pillsPanelFocused bool, t *styles.Theme) string {
 49	if !hasIncompleteTodos(todos) {
 50		return ""
 51	}
 52
 53	completed := 0
 54	var currentTodo *session.Todo
 55	for i := range todos {
 56		switch todos[i].Status {
 57		case session.TodoStatusCompleted:
 58			completed++
 59		case session.TodoStatusInProgress:
 60			if currentTodo == nil {
 61				currentTodo = &todos[i]
 62			}
 63		}
 64	}
 65
 66	total := len(todos)
 67
 68	label := "To-Do"
 69	progress := t.S().Base.Foreground(t.FgMuted).Render(fmt.Sprintf("%d/%d", completed, total))
 70
 71	var content string
 72	if pillsPanelFocused {
 73		content = fmt.Sprintf("%s %s", label, progress)
 74	} else if currentTodo != nil {
 75		taskText := currentTodo.Content
 76		if currentTodo.ActiveForm != "" {
 77			taskText = currentTodo.ActiveForm
 78		}
 79		if len(taskText) > maxTaskDisplayLength {
 80			taskText = taskText[:maxTaskDisplayLength-1] + "…"
 81		}
 82		task := t.S().Base.Foreground(t.FgSubtle).Render(taskText)
 83		content = fmt.Sprintf("%s %s %s  %s", spinnerView, label, progress, task)
 84	} else {
 85		content = fmt.Sprintf("%s %s", label, progress)
 86	}
 87
 88	style := t.S().Base.PaddingLeft(1).PaddingRight(1)
 89	if !pillsPanelFocused || focused {
 90		style = style.BorderStyle(lipgloss.RoundedBorder()).BorderForeground(t.BgOverlay)
 91	} else {
 92		style = style.BorderStyle(lipgloss.HiddenBorder())
 93	}
 94	return style.Render(content)
 95}
 96
 97func todoList(sessionTodos []session.Todo, spinnerView string, t *styles.Theme, width int) string {
 98	return todos.FormatTodosList(sessionTodos, spinnerView, t, width)
 99}
100
101func queueList(queueItems []string, t *styles.Theme) string {
102	if len(queueItems) == 0 {
103		return ""
104	}
105
106	var lines []string
107	for _, item := range queueItems {
108		text := item
109		if len(text) > maxQueueDisplayLength {
110			text = text[:maxQueueDisplayLength-1] + "…"
111		}
112		prefix := t.S().Base.Foreground(t.FgMuted).Render("  •") + " "
113		lines = append(lines, prefix+t.S().Base.Foreground(t.FgMuted).Render(text))
114	}
115
116	return strings.Join(lines, "\n")
117}
118
119func sectionLine(availableWidth int, t *styles.Theme) string {
120	if availableWidth <= 0 {
121		return ""
122	}
123	line := strings.Repeat("─", availableWidth)
124	return t.S().Base.Foreground(t.Border).Render(line)
125}