From 90f67fa589170f6e2f4820778a3c5a9cd2c6ff82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Moroz?= Date: Thu, 23 Jul 2026 22:57:57 +0200 Subject: [PATCH] feat(tui): add light color scheme with auto-detect and JSON config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TUI shipped a single hardcoded, dark-only "Blade Runner amber" theme that was unreadable on light/white terminal backgrounds. Add a second, light-optimized scheme while keeping the dark one byte-for-byte unchanged. - Convert the col* palette to lipgloss.AdaptiveColor{Dark, Light}; Dark keeps the original hex, Light is a warm palette tuned for white backgrounds. Fold the duplicated inline #1a1305 into colOnAccent / colSurface0 so badge text stays legible in both modes. - Route the resolved dark/light flag (internal/tui/theme.go: SetTheme) to the three subsystems that don't read the palette: glamour markdown (dark/light standard style), chroma syntax highlighting (dark vs light style list), and the spinner color. - Resolve the scheme once at startup, before Bubble Tea takes the screen, and freeze it with lipgloss.SetHasDarkBackground — the "auto" path detects the terminal background there to avoid the AltScreen OSC-query deadlock documented in markdown.go; dark is the fallback. - Config: add PRCHECK_THEME (auto|dark|light, default auto) plus an optional JSON config file at os.UserConfigDir()/prcheck/config.json, parsed with stdlib encoding/json. Env overrides the file; a missing or malformed file is non-fatal. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 23 +++++++ cmd/prcheck/main.go | 25 ++++++++ internal/config/env.go | 38 +++++++++++ internal/config/file.go | 52 +++++++++++++++ internal/config/file_test.go | 121 +++++++++++++++++++++++++++++++++++ internal/tui/diffcolor.go | 33 ++++++---- internal/tui/markdown.go | 27 +++++--- internal/tui/model.go | 2 +- internal/tui/theme.go | 20 ++++++ internal/tui/view.go | 58 ++++++++++------- 10 files changed, 353 insertions(+), 46 deletions(-) create mode 100644 internal/config/file.go create mode 100644 internal/config/file_test.go create mode 100644 internal/tui/theme.go diff --git a/README.md b/README.md index 62092cc..de6fb58 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Nothing is required. Everything is optional: | Variable | Effect | |----------|--------| +| `PRCHECK_THEME` | Color scheme: `auto` (default — detect the terminal background at startup, dark if unsure), `dark`, or `light`. | | `PRCHECK_AGENT` | Which agent to start with: `claude` (default) or `cursor`. You can also switch in the app with `A`. | | `PRCHECK_FOCUS` | Review aspects to emphasize, comma-separated, e.g. `security,performance,requirements,tests`. Empty means a balanced review. | | `PRCHECK_NOTIFY=1` | Send a desktop notification when a review finishes. | @@ -94,6 +95,28 @@ Nothing is required. Everything is optional: | `PRCHECK_DEBUG=1` | Send subprocess (`gh`/agent) output to the log for troubleshooting. | | `PRCHECK_CACHE_DIR` | Where snapshots, bookmarks, and history are written. Defaults to the OS cache dir (see below). | +**Config file (optional).** Settings can also live in a JSON file so you don't have to +export env vars. prcheck looks for it at (via `os.UserConfigDir()`): + +- Linux: `~/.config/prcheck/config.json` +- macOS: `~/Library/Application Support/prcheck/config.json` +- Windows: `%AppData%\prcheck\config.json` + +```json +{ + "theme": "light" +} +``` + +Environment variables **override** the file (e.g. `PRCHECK_THEME=dark` wins over +`"theme": "light"`). A missing file is fine; a malformed one is ignored with a warning. +Currently only `theme` is read from the file — the other settings remain env-only for now. + +**Color scheme.** `theme` (or `PRCHECK_THEME`) is `auto`, `dark`, or `light`. `auto` checks +the terminal's background color once at startup and picks the matching scheme, falling back +to `dark` if the terminal doesn't report one. The default `dark` scheme is tuned for dark +terminals; `light` is tuned for white/light backgrounds. + **Watch mode** (`w` key) auto-refreshes all tabs every `PRCHECK_WATCH` minutes (default 5). When combined with `PRCHECK_NOTIFY=1`, it sends desktop notifications: "Review requested" when a PR (re-)appears in the Review tab, "New PR" for new PRs elsewhere, and "Updated" diff --git a/cmd/prcheck/main.go b/cmd/prcheck/main.go index 83185ec..536595c 100644 --- a/cmd/prcheck/main.go +++ b/cmd/prcheck/main.go @@ -9,6 +9,7 @@ import ( "runtime" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/KamilSupera/github-pullrequests-checkecker/internal/claude" "github.com/KamilSupera/github-pullrequests-checkecker/internal/config" @@ -46,6 +47,15 @@ func run() error { return err } + // Resolve the color scheme once, up front. This must happen BEFORE + // tea.NewProgram takes over stdin in AltScreen mode: the "auto" path + // queries the terminal background, which would deadlock mid-run (see + // internal/tui/markdown.go). Freezing the result with + // SetHasDarkBackground also prevents lipgloss's own lazy query later. + dark := resolveDarkBackground(cfg.Theme) + lipgloss.SetHasDarkBackground(dark) + tui.SetTheme(dark) + if err := checkBinary("gh"); err != nil { return err } @@ -103,6 +113,21 @@ func run() error { return err } +// resolveDarkBackground maps the configured theme to a dark/light choice. +// "dark"/"light" force the scheme; "auto" (and anything else) detects the +// terminal background, falling back to dark when the terminal doesn't +// answer (termenv reports a dark background for a non-TTY or no reply). +func resolveDarkBackground(theme string) bool { + switch theme { + case "light": + return false + case "dark": + return true + default: // "auto" + return lipgloss.HasDarkBackground() + } +} + type claudeAdapter struct{} func (claudeAdapter) Invoke(ctx context.Context, prompt string) (*claude.Review, error) { diff --git a/internal/config/env.go b/internal/config/env.go index fa8eaac..177fc6a 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "strconv" "strings" @@ -20,12 +21,25 @@ type Config struct { // WatchMinutes is the auto-refresh interval for Watch mode, in // minutes. Read from PRCHECK_WATCH; defaults to 5, clamped to >=1. WatchMinutes int + + // Theme selects the color scheme: "auto" (detect the terminal + // background at startup, dark on no reply), "dark", or "light". + // Read from the config file then PRCHECK_THEME (env overrides file); + // unknown/empty values fall back to "auto". + Theme string } // Load reads the runtime configuration from environment variables. // All Jira access goes through the Atlassian MCP server reachable via // `claude`, so no Jira credentials are required. func Load() (*Config, error) { + // Config file first; env vars below override it. A malformed file is + // non-fatal — warn and carry on with defaults + environment. + fc, ferr := loadFile() + if ferr != nil { + fmt.Fprintln(os.Stderr, "prcheck: ignoring config file:", ferr) + } + cfg := &Config{ Debug: os.Getenv("PRCHECK_DEBUG") == "1", Agent: strings.TrimSpace(os.Getenv("PRCHECK_AGENT")), @@ -43,5 +57,29 @@ func Load() (*Config, error) { cfg.WatchMinutes = n } } + + // Theme: config file value, then PRCHECK_THEME override, then validate. + theme := "" + if fc.Theme != nil { + theme = *fc.Theme + } + if e := strings.TrimSpace(os.Getenv("PRCHECK_THEME")); e != "" { + theme = e + } + cfg.Theme = normalizeTheme(theme) + return cfg, nil } + +// normalizeTheme trims/lowercases the theme string and validates it. +// Unknown or empty values fall back to "auto". +func normalizeTheme(s string) string { + switch strings.ToLower(strings.TrimSpace(s)) { + case "dark": + return "dark" + case "light": + return "light" + default: + return "auto" + } +} diff --git a/internal/config/file.go b/internal/config/file.go new file mode 100644 index 0000000..d1843de --- /dev/null +++ b/internal/config/file.go @@ -0,0 +1,52 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" +) + +// fileConfig mirrors the on-disk JSON config file. Every field is a +// pointer so an omitted key stays distinguishable from an explicit zero +// value and never clobbers a setting supplied via the environment. Only +// Theme is wired up today; other fields can be added here as the +// PRCHECK_* env vars migrate into the file. +type fileConfig struct { + Theme *string `json:"theme"` +} + +// configPath returns the path to prcheck's JSON config file, or "" when +// the user config dir cannot be determined. It uses os.UserConfigDir() +// — the config-dir analog of the cache dir used elsewhere — which is +// ~/.config/prcheck on Linux and ~/Library/Application Support/prcheck on +// macOS. +func configPath() string { + dir, err := os.UserConfigDir() + if err != nil { + return "" + } + return filepath.Join(dir, "prcheck", "config.json") +} + +// loadFile reads and decodes the JSON config file. A missing file is not +// an error — it returns a zero fileConfig. Malformed JSON returns an +// error so the caller can warn the user; callers must treat it as +// non-fatal and fall back to defaults + environment. +func loadFile() (fileConfig, error) { + var fc fileConfig + path := configPath() + if path == "" { + return fc, nil + } + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return fc, nil + } + return fc, err + } + if err := json.Unmarshal(data, &fc); err != nil { + return fc, err + } + return fc, nil +} diff --git a/internal/config/file_test.go b/internal/config/file_test.go new file mode 100644 index 0000000..06e34c1 --- /dev/null +++ b/internal/config/file_test.go @@ -0,0 +1,121 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +// isolateConfigDir points os.UserConfigDir() at a fresh temp directory so +// the test never reads the developer's real ~/.config/prcheck/config.json. +// It sets both HOME (macOS) and XDG_CONFIG_HOME (Linux) to cover either +// platform's UserConfigDir resolution. +func isolateConfigDir(t *testing.T) { + t.Helper() + dir := t.TempDir() + t.Setenv("HOME", dir) + t.Setenv("XDG_CONFIG_HOME", filepath.Join(dir, ".config")) + // Env vars must not leak in from the developer's shell. + t.Setenv("PRCHECK_THEME", "") +} + +// writeConfig writes raw bytes to the resolved config path, creating the +// parent directory. Empty content means "no file". +func writeConfig(t *testing.T, content string) { + t.Helper() + if content == "" { + return + } + path := configPath() + if path == "" { + t.Fatal("configPath() returned empty") + } + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } +} + +func TestLoad_ThemeDefaultsAuto(t *testing.T) { + isolateConfigDir(t) + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() err: %v", err) + } + if cfg.Theme != "auto" { + t.Errorf("Theme = %q, want default \"auto\"", cfg.Theme) + } +} + +func TestLoad_ThemeFromFile(t *testing.T) { + isolateConfigDir(t) + writeConfig(t, `{"theme": "light"}`) + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() err: %v", err) + } + if cfg.Theme != "light" { + t.Errorf("Theme = %q, want \"light\" from file", cfg.Theme) + } +} + +func TestLoad_ThemeEnvOverridesFile(t *testing.T) { + isolateConfigDir(t) + writeConfig(t, `{"theme": "light"}`) + t.Setenv("PRCHECK_THEME", "dark") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() err: %v", err) + } + if cfg.Theme != "dark" { + t.Errorf("Theme = %q, want env value \"dark\" to win over file", cfg.Theme) + } +} + +func TestLoad_ThemeUnknownFallsBackAuto(t *testing.T) { + isolateConfigDir(t) + t.Setenv("PRCHECK_THEME", "psychedelic") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() err: %v", err) + } + if cfg.Theme != "auto" { + t.Errorf("Theme = %q, want \"auto\" for unknown value", cfg.Theme) + } +} + +func TestLoad_MalformedFileIsNonFatal(t *testing.T) { + isolateConfigDir(t) + writeConfig(t, `{ this is not valid json `) + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() should not fail on malformed file: %v", err) + } + if cfg.Theme != "auto" { + t.Errorf("Theme = %q, want \"auto\" when file is malformed", cfg.Theme) + } +} + +func TestNormalizeTheme(t *testing.T) { + cases := map[string]string{ + "dark": "dark", + "DARK": "dark", + " light": "light", + "Light ": "light", + "auto": "auto", + "": "auto", + "nonsense": "auto", + } + for in, want := range cases { + if got := normalizeTheme(in); got != want { + t.Errorf("normalizeTheme(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/internal/tui/diffcolor.go b/internal/tui/diffcolor.go index 2f94e83..f4d44ad 100644 --- a/internal/tui/diffcolor.go +++ b/internal/tui/diffcolor.go @@ -12,13 +12,18 @@ import ( "github.com/charmbracelet/lipgloss" ) +// Diff line colors adapt to the terminal background: the Dark variants +// are the original ANSI-256 tones; the Light variants are darkened so +// they stay legible on a white background. var ( - diffAddLine = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) // green - diffDelLine = lipgloss.NewStyle().Foreground(lipgloss.Color("203")) // red - diffHunkLine = lipgloss.NewStyle().Foreground(lipgloss.Color("39")) // blue - diffFileLine = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("214")) - - chromaStyle = pickChromaStyle() + diffAddLine = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: "42", Light: "#2e7d32"}) // green + diffDelLine = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: "203", Light: "#c62828"}) // red + diffHunkLine = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: "39", Light: "#1565c0"}) // blue + diffFileLine = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.AdaptiveColor{Dark: "214", Light: "#b25e00"}) + + // chromaStyle defaults to the dark syntax theme; SetTheme() swaps it + // to a light one when the terminal background is light. + chromaStyle = pickChromaStyle(true) chromaFormatter chroma.Formatter ) @@ -29,11 +34,17 @@ func init() { } } -// pickChromaStyle prefers a warm/amber-leaning style for the Blade -// Runner theme. Falls back through alternatives, then to a default -// style if none of the preferred names exist in the installed chroma. -func pickChromaStyle() *chroma.Style { - for _, name := range []string{"gruvbox", "tango", "solarized-dark", "monokai"} { +// pickChromaStyle returns the syntax-highlight style for the active +// theme. For dark it prefers a warm/amber-leaning style matching the +// Blade Runner palette; for light it prefers styles designed for a white +// background. Falls through the preference list, then to chroma's default +// if none of the preferred names exist in the installed version. +func pickChromaStyle(dark bool) *chroma.Style { + prefs := []string{"gruvbox", "tango", "solarized-dark", "monokai"} + if !dark { + prefs = []string{"github", "solarized-light", "friendly", "tango"} + } + for _, name := range prefs { if s := styles.Get(name); s != nil && s.Name != "" { return s } diff --git a/internal/tui/markdown.go b/internal/tui/markdown.go index 109d008..4c42a90 100644 --- a/internal/tui/markdown.go +++ b/internal/tui/markdown.go @@ -6,16 +6,19 @@ import ( "github.com/charmbracelet/glamour" ) -// renderMarkdown converts a markdown body to colored terminal text -// using a "dark" style. Width sets the wrap column; lower bound 40. -// Falls back to the plain stripped body when glamour fails. +// renderMarkdown converts a markdown body to colored terminal text using +// glamour's built-in "dark" or "light" standard style, chosen from the +// theme resolved at startup (see internal/tui/theme.go). Width sets the +// wrap column; lower bound 40. Falls back to the plain stripped body when +// glamour fails. // -// We use WithStandardStyle("dark") rather than WithAutoStyle(). AutoStyle -// queries the terminal for its background color via an OSC escape and -// reads the response from the TTY — but bubbletea has already taken over -// stdin in AltScreen mode, so the response never reaches termenv and the -// call blocks forever, freezing the entire UI on the first markdown -// render (e.g. when Space loads PR detail). +// We pass the style explicitly rather than using WithAutoStyle(). +// AutoStyle queries the terminal for its background color via an OSC +// escape and reads the response from the TTY — but bubbletea has already +// taken over stdin in AltScreen mode, so the response never reaches +// termenv and the call blocks forever, freezing the entire UI on the +// first markdown render (e.g. when Space loads PR detail). SetTheme is +// therefore called once before the program starts, never mid-run. func renderMarkdown(s string, width int) string { if strings.TrimSpace(s) == "" { return "" @@ -23,8 +26,12 @@ func renderMarkdown(s string, width int) string { if width < 40 { width = 40 } + style := "dark" + if !useDarkTheme { + style = "light" + } r, err := glamour.NewTermRenderer( - glamour.WithStandardStyle("dark"), + glamour.WithStandardStyle(style), glamour.WithWordWrap(width), ) if err != nil { diff --git a/internal/tui/model.go b/internal/tui/model.go index ef04537..37f244a 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -136,7 +136,7 @@ type Model struct { func NewModel(loader loaderFn, df detailFn, dfn diffFn, cf checksFn, qr quickReviewFn, rp runPipelineFn, open openFn, watchMin int) Model { sp := spinner.New() sp.Spinner = spinner.MiniDot - sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffb000")) + sp.Style = lipgloss.NewStyle().Foreground(colMauve) ctx, cancel := context.WithCancel(context.Background()) prsByTab := map[Tab][]github.PR{} diff --git a/internal/tui/theme.go b/internal/tui/theme.go new file mode 100644 index 0000000..c9cbfad --- /dev/null +++ b/internal/tui/theme.go @@ -0,0 +1,20 @@ +package tui + +// useDarkTheme mirrors the resolved dark/light choice for the two +// subsystems that do NOT read the lipgloss col* palette: glamour markdown +// (markdown.go) and chroma syntax highlighting (diffcolor.go). The +// palette itself adapts automatically via lipgloss.AdaptiveColor and the +// renderer's HasDarkBackground flag, which main() sets alongside SetTheme. +// +// It defaults to true (dark) so any code path or test that never calls +// SetTheme keeps the original dark behavior. +var useDarkTheme = true + +// SetTheme records the resolved dark/light choice and refreshes the +// chroma syntax-highlight style to match. Call it once at startup, before +// the Bubble Tea program runs and before anything renders — never mid-run +// (see the AltScreen note in markdown.go). +func SetTheme(dark bool) { + useDarkTheme = dark + chromaStyle = pickChromaStyle(dark) +} diff --git a/internal/tui/view.go b/internal/tui/view.go index a8b3407..e26fa4b 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -20,26 +20,36 @@ var ( reBlankLines = regexp.MustCompile(`\n{3,}`) ) -// Theme — Blade Runner amber. Warm yellows and oranges against a near- +// Theme — "Blade Runner amber." Warm yellows and oranges against a near- // black backdrop, with a single cool neon-cyan accent for contrast. +// +// Each color is an AdaptiveColor: the Dark variant is the original amber +// scheme (unchanged, shown on dark terminals); the Light variant is a +// darkened warm palette tuned for legibility on white/light backgrounds. +// lipgloss picks the variant from the renderer's HasDarkBackground flag, +// which main() sets once at startup (see cmd/prcheck + internal/tui/theme.go). // Hex colors degrade gracefully to 256-color when truecolor is off. var ( - colFG = lipgloss.Color("#f5e6c8") // warm off-white - colSubtext = lipgloss.Color("#b89e5a") // muted amber - colOverlay = lipgloss.Color("#6b5a2a") // dim amber - colMauve = lipgloss.Color("#ffb000") // PRIMARY: amber yellow - colBlue = lipgloss.Color("#00bfff") // neon cyan accent - colTeal = lipgloss.Color("#ff8800") // deep orange (branch refs) - colGreen = lipgloss.Color("#a0d060") // muted neon green - colYellow = lipgloss.Color("#ffc107") // brighter gold (counts) - colPeach = lipgloss.Color("#ff6f00") // burnt orange (author) - colRed = lipgloss.Color("#ff4444") // alarm red - colPink = lipgloss.Color("#e5a100") // dark amber (inline path) - colSurface0 = lipgloss.Color("#1a1305") // near-black warm brown + colFG = lipgloss.AdaptiveColor{Dark: "#f5e6c8", Light: "#2e2410"} // body text + colSubtext = lipgloss.AdaptiveColor{Dark: "#b89e5a", Light: "#7a5c12"} // secondary text + colOverlay = lipgloss.AdaptiveColor{Dark: "#6b5a2a", Light: "#a98f4e"} // borders / dim + colMauve = lipgloss.AdaptiveColor{Dark: "#ffb000", Light: "#b25e00"} // PRIMARY amber + colBlue = lipgloss.AdaptiveColor{Dark: "#00bfff", Light: "#0e6fa8"} // cyan accent + colTeal = lipgloss.AdaptiveColor{Dark: "#ff8800", Light: "#c05e00"} // deep orange (branch refs) + colGreen = lipgloss.AdaptiveColor{Dark: "#a0d060", Light: "#3f8f2e"} // green (pass/approve) + colYellow = lipgloss.AdaptiveColor{Dark: "#ffc107", Light: "#b07d00"} // gold (counts) + colPeach = lipgloss.AdaptiveColor{Dark: "#ff6f00", Light: "#c25400"} // burnt orange (author) + colRed = lipgloss.AdaptiveColor{Dark: "#ff4444", Light: "#c62828"} // alarm red + colPink = lipgloss.AdaptiveColor{Dark: "#e5a100", Light: "#9c6b00"} // dark amber (inline path) + colSurface0 = lipgloss.AdaptiveColor{Dark: "#1a1305", Light: "#ece0c4"} // recessed surface (pills) + // colOnAccent is text drawn on top of a filled accent color (tabs, + // badges): near-black on the bright dark-mode fills, near-white on the + // darker light-mode fills. + colOnAccent = lipgloss.AdaptiveColor{Dark: "#1a1305", Light: "#fbf4e6"} tabActive = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color("#1a1305")). // dark text on amber bar + Foreground(colOnAccent). // contrasting text on amber bar Background(colMauve). Padding(0, 1) tabInactive = lipgloss.NewStyle(). @@ -68,12 +78,12 @@ var ( // Status badges (filled pills) — dark text on neon. badgeApproved = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color("#1a1305")). + Foreground(colOnAccent). Background(colGreen). Padding(0, 1) badgeChanges = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color("#1a1305")). + Foreground(colOnAccent). Background(colRed). Padding(0, 1) badgeCommented = lipgloss.NewStyle(). @@ -100,7 +110,7 @@ func paneInnerSize(termW, termH int) (w, h int) { // borderFor returns the border color for a box: bright when it holds the // current focus, dim otherwise. -func (m Model) borderFor(area focusArea) lipgloss.Color { +func (m Model) borderFor(area focusArea) lipgloss.TerminalColor { if m.focus == area { return borderActiveColor } @@ -378,7 +388,7 @@ func (m Model) renderTabs() string { // pops against the amber bar. activeCount := lipgloss.NewStyle(). Foreground(colMauve). - Background(lipgloss.Color("#1a1305")). + Background(colSurface0). Bold(true). Padding(0, 1) for i := Tab(0); i < 3; i++ { @@ -410,7 +420,7 @@ func (m Model) renderTabs() string { // renderLegend is the one-line key for the list gutter glyphs. func (m Model) renderLegend() string { - c := func(col lipgloss.Color, s string) string { + c := func(col lipgloss.TerminalColor, s string) string { return lipgloss.NewStyle().Foreground(col).Render(s) } return " " + strings.Join([]string{ @@ -956,11 +966,11 @@ func renderStatusLine(d *github.PRDetail) string { var parts []string switch d.State { case "OPEN": - parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#1a1305")).Background(colGreen).Padding(0, 1).Render("OPEN")) + parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(colOnAccent).Background(colGreen).Padding(0, 1).Render("OPEN")) case "CLOSED": - parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#1a1305")).Background(colRed).Padding(0, 1).Render("CLOSED")) + parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(colOnAccent).Background(colRed).Padding(0, 1).Render("CLOSED")) case "MERGED": - parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#1a1305")).Background(colMauve).Padding(0, 1).Render("MERGED")) + parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(colOnAccent).Background(colMauve).Padding(0, 1).Render("MERGED")) } if d.IsDraft { parts = append(parts, badgeCommented.Render("DRAFT")) @@ -971,7 +981,7 @@ func renderStatusLine(d *github.PRDetail) string { case "CHANGES_REQUESTED": parts = append(parts, badgeChanges.Render("CHANGES")) case "REVIEW_REQUIRED": - parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#1a1305")).Background(colYellow).Padding(0, 1).Render("REVIEW REQ")) + parts = append(parts, lipgloss.NewStyle().Bold(true).Foreground(colOnAccent).Background(colYellow).Padding(0, 1).Render("REVIEW REQ")) } switch d.Mergeable { case "CONFLICTING": @@ -1277,7 +1287,7 @@ func aspectLine(a claude.Aspect, w int) string { } func severityBadge(sev string) string { - style := lipgloss.NewStyle().Bold(true).Padding(0, 1).Foreground(lipgloss.Color("#1a1305")) + style := lipgloss.NewStyle().Bold(true).Padding(0, 1).Foreground(colOnAccent) switch sev { case "blocker": return style.Background(colRed).Render("BLOCKER")