Separates semantics of 'pinned' (identity records) from work-on-hook: - 'pinned' = domain table / identity record (agents, roles) - non-blocking - 'hooked' = work on agent's hook (GUPP-driven) - blocks dependents Changes: - Add StatusHooked constant to types.go - Update all blocking queries to include 'hooked' status - Add cyan styling for 'hooked' in UI output - Create migration 032 to convert pinned work items to hooked Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
383 lines
11 KiB
Go
383 lines
11 KiB
Go
// Package ui provides terminal styling for beads CLI output.
|
||
// Uses the Ayu color theme with adaptive light/dark mode support.
|
||
package ui
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// Ayu theme color palette
|
||
// Dark: https://terminalcolors.com/themes/ayu/dark/
|
||
// Light: https://terminalcolors.com/themes/ayu/light/
|
||
// Source: https://github.com/ayu-theme/ayu-colors
|
||
var (
|
||
// Core semantic colors (Ayu theme - adaptive light/dark)
|
||
ColorPass = lipgloss.AdaptiveColor{
|
||
Light: "#86b300", // ayu light bright green
|
||
Dark: "#c2d94c", // ayu dark bright green
|
||
}
|
||
ColorWarn = lipgloss.AdaptiveColor{
|
||
Light: "#f2ae49", // ayu light bright yellow
|
||
Dark: "#ffb454", // ayu dark bright yellow
|
||
}
|
||
ColorFail = lipgloss.AdaptiveColor{
|
||
Light: "#f07171", // ayu light bright red
|
||
Dark: "#f07178", // ayu dark bright red
|
||
}
|
||
ColorMuted = lipgloss.AdaptiveColor{
|
||
Light: "#828c99", // ayu light muted
|
||
Dark: "#6c7680", // ayu dark muted
|
||
}
|
||
ColorAccent = lipgloss.AdaptiveColor{
|
||
Light: "#399ee6", // ayu light bright blue
|
||
Dark: "#59c2ff", // ayu dark bright blue
|
||
}
|
||
|
||
// === Workflow Status Colors ===
|
||
// Only actionable states get color - open/closed match standard text
|
||
ColorStatusOpen = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
ColorStatusInProgress = lipgloss.AdaptiveColor{
|
||
Light: "#f2ae49", // yellow - active work, very visible
|
||
Dark: "#ffb454",
|
||
}
|
||
ColorStatusClosed = lipgloss.AdaptiveColor{
|
||
Light: "#9099a1", // slightly dimmed - visually shows "done"
|
||
Dark: "#8090a0",
|
||
}
|
||
ColorStatusBlocked = lipgloss.AdaptiveColor{
|
||
Light: "#f07171", // red - needs attention
|
||
Dark: "#f26d78",
|
||
}
|
||
ColorStatusPinned = lipgloss.AdaptiveColor{
|
||
Light: "#d2a6ff", // purple - special/elevated
|
||
Dark: "#d2a6ff",
|
||
}
|
||
ColorStatusHooked = lipgloss.AdaptiveColor{
|
||
Light: "#59c2ff", // cyan - actively worked by agent (GUPP)
|
||
Dark: "#59c2ff",
|
||
}
|
||
|
||
// === Priority Colors ===
|
||
// Only P0/P1 get color - P2/P3/P4 match standard text
|
||
ColorPriorityP0 = lipgloss.AdaptiveColor{
|
||
Light: "#f07171", // bright red - critical
|
||
Dark: "#f07178",
|
||
}
|
||
ColorPriorityP1 = lipgloss.AdaptiveColor{
|
||
Light: "#ff8f40", // orange - high urgency
|
||
Dark: "#ff8f40",
|
||
}
|
||
ColorPriorityP2 = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
ColorPriorityP3 = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
ColorPriorityP4 = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
|
||
// === Issue Type Colors ===
|
||
// Bugs and epics get color - they need attention
|
||
// All other types use standard text
|
||
ColorTypeBug = lipgloss.AdaptiveColor{
|
||
Light: "#f07171", // bright red - bugs are problems
|
||
Dark: "#f26d78",
|
||
}
|
||
ColorTypeFeature = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
ColorTypeTask = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
ColorTypeEpic = lipgloss.AdaptiveColor{
|
||
Light: "#d2a6ff", // purple - larger scope work
|
||
Dark: "#d2a6ff",
|
||
}
|
||
ColorTypeChore = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
ColorTypeAgent = lipgloss.AdaptiveColor{
|
||
Light: "#59c2ff", // cyan - agent identity
|
||
Dark: "#59c2ff",
|
||
}
|
||
ColorTypeRole = lipgloss.AdaptiveColor{
|
||
Light: "#7fd962", // green - role definition
|
||
Dark: "#7fd962",
|
||
}
|
||
|
||
// === Issue ID Color ===
|
||
// IDs use standard text color - subtle, not attention-grabbing
|
||
ColorID = lipgloss.AdaptiveColor{
|
||
Light: "", // standard text color
|
||
Dark: "",
|
||
}
|
||
)
|
||
|
||
// Core styles - consistent across all commands
|
||
var (
|
||
PassStyle = lipgloss.NewStyle().Foreground(ColorPass)
|
||
WarnStyle = lipgloss.NewStyle().Foreground(ColorWarn)
|
||
FailStyle = lipgloss.NewStyle().Foreground(ColorFail)
|
||
MutedStyle = lipgloss.NewStyle().Foreground(ColorMuted)
|
||
AccentStyle = lipgloss.NewStyle().Foreground(ColorAccent)
|
||
)
|
||
|
||
// Issue ID style
|
||
var IDStyle = lipgloss.NewStyle().Foreground(ColorID)
|
||
|
||
// Status styles for workflow states
|
||
var (
|
||
StatusOpenStyle = lipgloss.NewStyle().Foreground(ColorStatusOpen)
|
||
StatusInProgressStyle = lipgloss.NewStyle().Foreground(ColorStatusInProgress)
|
||
StatusClosedStyle = lipgloss.NewStyle().Foreground(ColorStatusClosed)
|
||
StatusBlockedStyle = lipgloss.NewStyle().Foreground(ColorStatusBlocked)
|
||
StatusPinnedStyle = lipgloss.NewStyle().Foreground(ColorStatusPinned)
|
||
StatusHookedStyle = lipgloss.NewStyle().Foreground(ColorStatusHooked)
|
||
)
|
||
|
||
// Priority styles
|
||
var (
|
||
PriorityP0Style = lipgloss.NewStyle().Foreground(ColorPriorityP0).Bold(true)
|
||
PriorityP1Style = lipgloss.NewStyle().Foreground(ColorPriorityP1)
|
||
PriorityP2Style = lipgloss.NewStyle().Foreground(ColorPriorityP2)
|
||
PriorityP3Style = lipgloss.NewStyle().Foreground(ColorPriorityP3)
|
||
PriorityP4Style = lipgloss.NewStyle().Foreground(ColorPriorityP4)
|
||
)
|
||
|
||
// Type styles for issue categories
|
||
var (
|
||
TypeBugStyle = lipgloss.NewStyle().Foreground(ColorTypeBug)
|
||
TypeFeatureStyle = lipgloss.NewStyle().Foreground(ColorTypeFeature)
|
||
TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask)
|
||
TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic)
|
||
TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore)
|
||
TypeAgentStyle = lipgloss.NewStyle().Foreground(ColorTypeAgent)
|
||
TypeRoleStyle = lipgloss.NewStyle().Foreground(ColorTypeRole)
|
||
)
|
||
|
||
// CategoryStyle for section headers - bold with accent color
|
||
var CategoryStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent)
|
||
|
||
// Status icons - consistent semantic indicators
|
||
const (
|
||
IconPass = "✓"
|
||
IconWarn = "⚠"
|
||
IconFail = "✖"
|
||
IconSkip = "-"
|
||
IconInfo = "ℹ"
|
||
)
|
||
|
||
// Tree characters for hierarchical display
|
||
const (
|
||
TreeChild = "⎿ " // child indicator
|
||
TreeLast = "└─ " // last child / detail line
|
||
TreeIndent = " " // 2-space indent per level
|
||
)
|
||
|
||
// Separators
|
||
const (
|
||
SeparatorLight = "──────────────────────────────────────────"
|
||
SeparatorHeavy = "══════════════════════════════════════════"
|
||
)
|
||
|
||
// RenderPass renders text with pass (green) styling
|
||
func RenderPass(s string) string {
|
||
return PassStyle.Render(s)
|
||
}
|
||
|
||
// RenderWarn renders text with warning (yellow) styling
|
||
func RenderWarn(s string) string {
|
||
return WarnStyle.Render(s)
|
||
}
|
||
|
||
// RenderFail renders text with fail (red) styling
|
||
func RenderFail(s string) string {
|
||
return FailStyle.Render(s)
|
||
}
|
||
|
||
// RenderMuted renders text with muted (gray) styling
|
||
func RenderMuted(s string) string {
|
||
return MutedStyle.Render(s)
|
||
}
|
||
|
||
// RenderAccent renders text with accent (blue) styling
|
||
func RenderAccent(s string) string {
|
||
return AccentStyle.Render(s)
|
||
}
|
||
|
||
// RenderCategory renders a category header in uppercase with accent color
|
||
func RenderCategory(s string) string {
|
||
return CategoryStyle.Render(strings.ToUpper(s))
|
||
}
|
||
|
||
// RenderSeparator renders the light separator line in muted color
|
||
func RenderSeparator() string {
|
||
return MutedStyle.Render(SeparatorLight)
|
||
}
|
||
|
||
// RenderPassIcon renders the pass icon with styling
|
||
func RenderPassIcon() string {
|
||
return PassStyle.Render(IconPass)
|
||
}
|
||
|
||
// RenderWarnIcon renders the warning icon with styling
|
||
func RenderWarnIcon() string {
|
||
return WarnStyle.Render(IconWarn)
|
||
}
|
||
|
||
// RenderFailIcon renders the fail icon with styling
|
||
func RenderFailIcon() string {
|
||
return FailStyle.Render(IconFail)
|
||
}
|
||
|
||
// RenderSkipIcon renders the skip icon with styling
|
||
func RenderSkipIcon() string {
|
||
return MutedStyle.Render(IconSkip)
|
||
}
|
||
|
||
// RenderInfoIcon renders the info icon with styling
|
||
func RenderInfoIcon() string {
|
||
return AccentStyle.Render(IconInfo)
|
||
}
|
||
|
||
// === Issue Component Renderers ===
|
||
|
||
// RenderID renders an issue ID with semantic styling
|
||
func RenderID(id string) string {
|
||
return IDStyle.Render(id)
|
||
}
|
||
|
||
// RenderStatus renders a status with semantic styling
|
||
// in_progress/blocked/pinned get color; open/closed use standard text
|
||
func RenderStatus(status string) string {
|
||
switch status {
|
||
case "in_progress":
|
||
return StatusInProgressStyle.Render(status)
|
||
case "blocked":
|
||
return StatusBlockedStyle.Render(status)
|
||
case "pinned":
|
||
return StatusPinnedStyle.Render(status)
|
||
case "hooked":
|
||
return StatusHookedStyle.Render(status)
|
||
case "closed":
|
||
return StatusClosedStyle.Render(status)
|
||
default: // open and others
|
||
return StatusOpenStyle.Render(status)
|
||
}
|
||
}
|
||
|
||
// RenderPriority renders a priority level with semantic styling
|
||
// P0/P1 get color; P2/P3/P4 use standard text
|
||
func RenderPriority(priority int) string {
|
||
label := fmt.Sprintf("P%d", priority)
|
||
switch priority {
|
||
case 0:
|
||
return PriorityP0Style.Render(label)
|
||
case 1:
|
||
return PriorityP1Style.Render(label)
|
||
case 2:
|
||
return PriorityP2Style.Render(label)
|
||
case 3:
|
||
return PriorityP3Style.Render(label)
|
||
case 4:
|
||
return PriorityP4Style.Render(label)
|
||
default:
|
||
return label
|
||
}
|
||
}
|
||
|
||
// RenderType renders an issue type with semantic styling
|
||
// bugs get color; all other types use standard text
|
||
func RenderType(issueType string) string {
|
||
switch issueType {
|
||
case "bug":
|
||
return TypeBugStyle.Render(issueType)
|
||
case "feature":
|
||
return TypeFeatureStyle.Render(issueType)
|
||
case "task":
|
||
return TypeTaskStyle.Render(issueType)
|
||
case "epic":
|
||
return TypeEpicStyle.Render(issueType)
|
||
case "chore":
|
||
return TypeChoreStyle.Render(issueType)
|
||
case "agent":
|
||
return TypeAgentStyle.Render(issueType)
|
||
case "role":
|
||
return TypeRoleStyle.Render(issueType)
|
||
default:
|
||
return issueType
|
||
}
|
||
}
|
||
|
||
// RenderIssueCompact renders a compact one-line issue summary
|
||
// Format: ID [Priority] [Type] Status - Title
|
||
// When status is "closed", the entire line is dimmed to show it's done
|
||
func RenderIssueCompact(id string, priority int, issueType, status, title string) string {
|
||
line := fmt.Sprintf("%s [P%d] [%s] %s - %s",
|
||
id, priority, issueType, status, title)
|
||
if status == "closed" {
|
||
// Entire line is dimmed - visually shows "done"
|
||
return StatusClosedStyle.Render(line)
|
||
}
|
||
return fmt.Sprintf("%s [%s] [%s] %s - %s",
|
||
RenderID(id),
|
||
RenderPriority(priority),
|
||
RenderType(issueType),
|
||
RenderStatus(status),
|
||
title,
|
||
)
|
||
}
|
||
|
||
// RenderPriorityForStatus renders priority with color only if not closed
|
||
func RenderPriorityForStatus(priority int, status string) string {
|
||
if status == "closed" {
|
||
return fmt.Sprintf("P%d", priority)
|
||
}
|
||
return RenderPriority(priority)
|
||
}
|
||
|
||
// RenderTypeForStatus renders type with color only if not closed
|
||
func RenderTypeForStatus(issueType, status string) string {
|
||
if status == "closed" {
|
||
return issueType
|
||
}
|
||
return RenderType(issueType)
|
||
}
|
||
|
||
// RenderClosedLine renders an entire line in the closed/dimmed style
|
||
func RenderClosedLine(line string) string {
|
||
return StatusClosedStyle.Render(line)
|
||
}
|
||
|
||
// BoldStyle for emphasis
|
||
var BoldStyle = lipgloss.NewStyle().Bold(true)
|
||
|
||
// CommandStyle for command names - subtle contrast, not attention-grabbing
|
||
var CommandStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
|
||
Light: "#5c6166", // slightly darker than standard
|
||
Dark: "#bfbdb6", // slightly brighter than standard
|
||
})
|
||
|
||
// RenderBold renders text in bold
|
||
func RenderBold(s string) string {
|
||
return BoldStyle.Render(s)
|
||
}
|
||
|
||
// RenderCommand renders a command name with subtle styling
|
||
func RenderCommand(s string) string {
|
||
return CommandStyle.Render(s)
|
||
}
|