admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

489 lines
15 KiB
Go

package tui
import (
"os"
"strings"
"charm.land/lipgloss/v2"
)
// noColor detects NO_COLOR environment variable.
var noColor = os.Getenv("NO_COLOR") != ""
// Nord Color Palette (https://www.nordtheme.com/)
// Nord Dark (Polar Night + Frost)
var (
// Polar Night (dark theme background/text)
nord0 = "#2E3440" // base background
nord1 = "#3B4252" // lighter background
nord2 = "#434C5E" // selection/background elements
nord3 = "#4C566A" // comments/borders
// Frost (dark theme foreground/text)
nord4 = "#D8DEE9" // primary text
nord5 = "#E5E9F0" // secondary text
nord6 = "#ECEFF4" // emphasized text
// Aurora (dark theme accents)
nord7 = "#BF616A" // red (errors/warnings)
nord8 = "#D08770" // orange (warnings)
nord9 = "#EBCB8B" // yellow (warnings/highlights)
nord10 = "#A3BE8C" // green (success)
nord11 = "#B48EAD" // purple (special)
nord12 = "#88C0D0" // cyan (primary accent)
nord13 = "#81A1C1" // blue (secondary accent)
nord14 = "#5E81AC" // dark blue (links/details)
)
// Nord Light (Aurora variant for light theme)
var (
// Light background
nordLight0 = "#FFFFFF" // base background
nordLight1 = "#ECEFF4" // lighter background
nordLight2 = "#E5E9F0" // selection
nordLight3 = "#D8DEE9" // borders
// Light text
nordLight4 = "#4C566A" // primary text
nordLight5 = "#3B4252" // secondary text
nordLight6 = "#2E3440" // emphasized text
// Aurora accents (same as dark, work well on light)
nordLight7 = "#BF616A" // red
nordLight8 = "#D08770" // orange
nordLight9 = "#EBCB8B" // yellow
nordLight10 = "#A3BE8C" // green
nordLight11 = "#B48EAD" // purple
nordLight12 = "#88C0D0" // cyan
nordLight13 = "#81A1C1" // blue
nordLight14 = "#5E81AC" // dark blue
)
// Styles holds all pre-built lipgloss styles.
type Styles struct {
// Header
HeaderTitle lipgloss.Style
HeaderInfo lipgloss.Style
HeaderRule lipgloss.Style
// Messages
UserLabel lipgloss.Style
UserContent lipgloss.Style
AsstLabel lipgloss.Style
AsstContent lipgloss.Style
RoleRule lipgloss.Style
StreamCursor lipgloss.Style
// Tools
ToolCallIcon lipgloss.Style
ToolCallText lipgloss.Style
ToolResultIcon lipgloss.Style
ToolResultText lipgloss.Style
ToolErrorIcon lipgloss.Style
ToolErrorText lipgloss.Style
ToolDoneIcon lipgloss.Style
ToolDoneText lipgloss.Style
ToolRunningText lipgloss.Style
ToolDetailText lipgloss.Style
// Footer
Divider lipgloss.Style
StatusDot lipgloss.Style
StatusText lipgloss.Style
StatusCheck lipgloss.Style
StatusError lipgloss.Style
ApprovalPrompt lipgloss.Style
StreamHint lipgloss.Style
ErrorText lipgloss.Style
Dimmed lipgloss.Style
// System messages
SystemText lipgloss.Style
WelcomeHint lipgloss.Style
// Completion popup
CompletionBorder lipgloss.Style
CompletionSelected lipgloss.Style
// Completion modal
CompletionFilter lipgloss.Style
CompletionCursor lipgloss.Style
CompletionCategory lipgloss.Style
CompletionFooter lipgloss.Style
CompletionSearching lipgloss.Style
// Startup progress
StartupCheck lipgloss.Style
StartupFail lipgloss.Style
StartupLabel lipgloss.Style
StartupDetail lipgloss.Style
StartupSpin lipgloss.Style
// Mode badges
ModeAsk lipgloss.Style
ModePlan lipgloss.Style
ModeBuild lipgloss.Style
// Context percentage fuel gauge
ContextPctLow lipgloss.Style
ContextPctMid lipgloss.Style
ContextPctHigh lipgloss.Style
// Tool type rendering
ToolBashCmd lipgloss.Style
// Diff view
DiffAdded lipgloss.Style
DiffRemoved lipgloss.Style
DiffContext lipgloss.Style
DiffHeader lipgloss.Style
// Thinking display
ThinkingHeader lipgloss.Style
ThinkingContent lipgloss.Style
ThinkingBorder lipgloss.Style
// Shared overlay styles (used by help, model picker, sessions, plan form, completion)
OverlayTitle lipgloss.Style
OverlayBorder string
OverlayAccent lipgloss.Style
OverlayDim lipgloss.Style
// Focus indicators
FocusIndicator lipgloss.Style
}
// NewStyles creates a Styles set based on the background color.
func NewStyles(isDark bool) Styles {
if noColor {
return plainStyles()
}
return adaptiveStyles(isDark)
}
func adaptiveStyles(isDark bool) Styles {
// Select Nord palette based on theme
var (
colorDim string
colorMuted string
colorText string
colorAccent string
colorAccent2 string
colorError string
colorSuccess string
colorSpecial string
colorBorder string
)
if isDark {
// Nord Dark Theme (Polar Night + Frost + Aurora)
colorDim = nord3 // #4C566A - comments/borders
colorMuted = nord4 // #D8DEE9 - primary text (muted)
colorText = nord5 // #E5E9F0 - secondary text
colorAccent = nord12 // #88C0D0 - cyan (primary accent)
colorAccent2 = nord13 // #81A1C1 - blue (secondary accent)
colorError = nord7 // #BF616A - red
colorSuccess = nord10 // #A3BE8C - green
colorSpecial = nord11 // #B48EAD - purple
colorBorder = nord3
} else {
// Nord Light Theme (Aurora)
colorDim = nordLight3 // #D8DEE9 - borders
colorMuted = nordLight4 // #4C566A - primary text (muted)
colorText = nordLight5 // #3B4252 - secondary text
colorAccent = nordLight12 // #88C0D0 - cyan
colorAccent2 = nordLight13 // #81A1C1 - blue
colorError = nordLight7 // #BF616A - red
colorSuccess = nordLight10 // #A3BE8C - green
colorSpecial = nordLight11 // #B48EAD - purple
colorBorder = nordLight3
}
// Helper for theme-specific colors
nordColor := func(dark, light string) string {
if isDark {
return dark
}
return light
}
return Styles{
HeaderTitle: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorAccent)).
PaddingLeft(1),
HeaderInfo: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
PaddingRight(1),
HeaderRule: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
UserLabel: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorAccent2)).
PaddingLeft(2),
UserContent: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorText)).
PaddingLeft(2),
AsstLabel: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorSuccess)).
PaddingLeft(2),
AsstContent: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorText)).
PaddingLeft(4),
RoleRule: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
StreamCursor: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
Bold(true),
ToolCallIcon: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSpecial)).
PaddingLeft(4),
ToolCallText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSpecial)),
ToolResultIcon: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
PaddingLeft(4),
ToolResultText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
ToolErrorIcon: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)).
PaddingLeft(4),
ToolErrorText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)),
ToolDoneIcon: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSuccess)).
PaddingLeft(4),
ToolDoneText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
ToolRunningText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)),
ToolDetailText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorMuted)),
Divider: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
StatusDot: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
PaddingLeft(1),
StatusText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
StatusCheck: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSuccess)).
PaddingLeft(1),
StatusError: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)).
PaddingLeft(1),
ApprovalPrompt: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
Bold(true),
StreamHint: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
Italic(true),
ErrorText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)).
Bold(true).
PaddingLeft(2),
Dimmed: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
SystemText: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorText)).
Italic(true).
PaddingLeft(2),
WelcomeHint: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent2)).
Bold(true),
CompletionBorder: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
CompletionSelected: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
Bold(true),
CompletionFilter: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorText)),
CompletionCursor: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
Bold(true),
CompletionCategory: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
CompletionFooter: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
Italic(true),
CompletionSearching: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSpecial)).
Italic(true),
StartupCheck: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSuccess)),
StartupFail: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)),
StartupLabel: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorText)),
StartupDetail: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
StartupSpin: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)),
ModeAsk: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorAccent2)),
ModePlan: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(nordColor(nord9, nordLight9))), // yellow
ModeBuild: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorSuccess)),
ContextPctLow: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSuccess)),
ContextPctMid: lipgloss.NewStyle().
Foreground(lipgloss.Color(nordColor(nord9, nordLight9))),
ContextPctHigh: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)),
ToolBashCmd: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
Italic(true),
DiffAdded: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSuccess)).
PaddingLeft(6),
DiffRemoved: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorError)).
PaddingLeft(6),
DiffContext: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
PaddingLeft(6),
DiffHeader: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
PaddingLeft(6),
ThinkingHeader: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorSpecial)).
Italic(true),
ThinkingContent: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)).
PaddingLeft(4),
ThinkingBorder: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
OverlayTitle: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorAccent)),
OverlayBorder: colorBorder,
OverlayAccent: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent2)).
Bold(true),
OverlayDim: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorDim)),
FocusIndicator: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorAccent)).
Bold(true),
}
}
func plainStyles() Styles {
p := lipgloss.NewStyle()
b := lipgloss.NewStyle().Bold(true)
pl2 := lipgloss.NewStyle().PaddingLeft(2)
pl4 := lipgloss.NewStyle().PaddingLeft(4)
return Styles{
HeaderTitle: b.PaddingLeft(1),
HeaderInfo: p.PaddingRight(1),
HeaderRule: p,
UserLabel: b.PaddingLeft(2),
UserContent: pl2,
AsstLabel: b.PaddingLeft(2),
AsstContent: pl2,
RoleRule: p,
StreamCursor: b,
ToolCallIcon: pl4,
ToolCallText: p,
ToolResultIcon: pl4,
ToolResultText: p,
ToolErrorIcon: pl4,
ToolErrorText: b,
ToolDoneIcon: pl4,
ToolDoneText: p,
ToolRunningText: p,
ToolDetailText: p,
Divider: p,
StatusDot: p.PaddingLeft(1),
StatusText: p,
StatusCheck: p.PaddingLeft(1),
StatusError: p.PaddingLeft(1),
ApprovalPrompt: b,
StreamHint: p.Italic(true),
ErrorText: b.PaddingLeft(2),
Dimmed: p,
SystemText: p.PaddingLeft(2).Italic(true),
WelcomeHint: b,
CompletionBorder: p,
CompletionSelected: b,
CompletionFilter: p,
CompletionCursor: b,
CompletionCategory: p,
CompletionFooter: p.Italic(true),
CompletionSearching: p.Italic(true),
StartupCheck: p,
StartupFail: b,
StartupLabel: p,
StartupDetail: p,
StartupSpin: p,
ModeAsk: b,
ModePlan: b,
ModeBuild: b,
ContextPctLow: p,
ContextPctMid: p,
ContextPctHigh: p,
ToolBashCmd: p.Italic(true),
DiffAdded: pl4,
DiffRemoved: pl4,
DiffContext: pl4,
DiffHeader: pl4,
ThinkingHeader: p.Italic(true),
ThinkingContent: pl4,
ThinkingBorder: p,
OverlayTitle: b,
OverlayBorder: "",
OverlayAccent: b,
OverlayDim: p,
FocusIndicator: b,
}
}
// rule generates a horizontal line of the given width using a thin character.
func rule(width int) string {
if width < 1 {
return ""
}
return strings.Repeat("─", width)
}
// thickRule generates a horizontal line using a thick character.
func thickRule(width int) string {
if width < 1 {
return ""
}
return strings.Repeat("━", width)
}