Files
beads/internal/ui/styles.go
Ryan a11b20960a fix(doctor): UX improvements for diagnostics and daemon (#687)
* fix(doctor): UX improvements for diagnostics and daemon

- Add Repo Fingerprint check to detect when database belongs to a
  different repository (copied .beads dir or git remote URL change)
- Add interactive fix for repo fingerprint with options: update repo ID,
  reinitialize database, or skip
- Add visible warning when daemon takes >5s to start, recommending
  'bd doctor' for diagnosis
- Detect install method (Homebrew vs script) and show only relevant
  upgrade command
- Improve WARNINGS section:
  - Add icons (⚠ or ✖) next to each item
  - Color numbers by severity (yellow for warnings, red for errors)
  - Render entire error lines in red
  - Sort by severity (errors first)
  - Fix alignment with checkmarks above
- Use heavier fail icon (✖) for better visibility
- Add integration and validation tests for doctor fixes

* fix(lint): address errcheck and gosec warnings

- mol_bond.go: explicitly ignore ephStore.Close() error
- beads.go: add nosec for .gitignore file permissions (0644 is standard)
2025-12-22 01:25:23 -08:00

362 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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",
}
// === 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: "",
}
// === 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)
)
// 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)
)
// 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 "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)
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)
}