Skip to content

Adapt CLI colors to the terminal background (light/dark)#893

Merged
phinze merged 1 commit into
mainfrom
phinze/mir-1312-cli-colors-are-hard-to-read-on-light-background-terminals
Jul 7, 2026
Merged

Adapt CLI colors to the terminal background (light/dark)#893
phinze merged 1 commit into
mainfrom
phinze/mir-1312-cli-colors-are-hard-to-read-on-light-background-terminals

Conversation

@phinze

@phinze phinze commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

A community member on a light-background terminal reported the miren CLI was hard to read. The palette was tuned for dark terminals and hard-coded as bright 256-color ANSI indices spread across about twenty files, so on white the yellow section headers and table headings washed out and the Faint(true) secondary text dropped below readable contrast. There was no central theme, and nothing ever told lipgloss the background color, so even the lone AdaptiveColor already in the tree silently always picked its dark variant.

This adds a pkg/theme package that owns the palette. It exposes semantic roles (Header, Success, Warning, Error, Info, Muted, Highlight) instead of raw indices, and resolves the right variant once at startup. Detection follows the usual conventions: NO_COLOR/CLICOLOR=0 turn color off, FORCE_COLOR/CLICOLOR_FORCE turn it on, a MIREN_THEME env var or a theme field in clientconfig.yaml overrides everything, and otherwise it auto-detects from the terminal background (a COLORFGBG hint, then an OSC 11 query, then dark as the default). All the scattered call sites move onto the semantic roles, and Faint(true) becomes a real muted foreground, which is the biggest readability win on light backgrounds.

The roles are CompleteAdaptiveColor, so each carries explicit values per color profile: truecolor terminals get saturated jewel tones, and 256-color terminals get values pinned to xterm anchor indices so two distinct roles never quantize into the same bucket. Interactive terminals whose TERM this termenv build doesn't recognize (Ghostty over SSH, for one) floor to ANSI256 rather than rendering monochrome.

There's also a miren debug colors command that prints the resolved theme, detected background, active profile, and a swatch of every role, so a misdetection report comes with paste-able diagnostics.

Closes MIR-1312

@phinze phinze requested a review from a team as a code owner July 7, 2026 19:22
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42c68b36-ef2d-4798-abca-0e940e8ba009

📥 Commits

Reviewing files that changed from the base of the PR and between 0558ffa and d819d04.

📒 Files selected for processing (27)
  • cli/commands/admin.go
  • cli/commands/app.go
  • cli/commands/app_history.go
  • cli/commands/app_status.go
  • cli/commands/cluster.go
  • cli/commands/color.go
  • cli/commands/config_bind_helpers.go
  • cli/commands/deploy.go
  • cli/commands/deploy_ui.go
  • cli/commands/doctor.go
  • cli/commands/env.go
  • cli/commands/global.go
  • cli/commands/help_render.go
  • cli/commands/init.go
  • cli/commands/tea_utils.go
  • clientconfig/clientconfig.go
  • pkg/progress/progressui/display.go
  • pkg/theme/theme.go
  • pkg/theme/theme_test.go
  • pkg/ui/definition_list.go
  • pkg/ui/hint.go
  • pkg/ui/named_value.go
  • pkg/ui/picker.go
  • pkg/ui/prompt.go
  • pkg/ui/status.go
  • pkg/ui/table.go
  • pkg/ui/terminal.go
✅ Files skipped from review due to trivial changes (10)
  • cli/commands/cluster.go
  • pkg/ui/table.go
  • cli/commands/doctor.go
  • pkg/progress/progressui/display.go
  • cli/commands/app_history.go
  • cli/commands/app.go
  • cli/commands/env.go
  • pkg/ui/prompt.go
  • pkg/ui/picker.go
  • cli/commands/admin.go
🚧 Files skipped from review as they are similar to previous changes (16)
  • cli/commands/color.go
  • cli/commands/app_status.go
  • cli/commands/tea_utils.go
  • cli/commands/global.go
  • pkg/ui/hint.go
  • pkg/ui/status.go
  • pkg/theme/theme_test.go
  • clientconfig/clientconfig.go
  • pkg/ui/definition_list.go
  • cli/commands/help_render.go
  • pkg/theme/theme.go
  • cli/commands/config_bind_helpers.go
  • pkg/ui/named_value.go
  • cli/commands/deploy_ui.go
  • pkg/ui/terminal.go
  • cli/commands/deploy.go

📝 Walkthrough

Walkthrough

This PR adds a shared pkg/theme package with semantic color roles and theme resolution, persists a theme setting in client config, and initializes the theme during CLI startup. It also rewrites the colors debug command, updates CLI commands and shared UI components to use theme colors, and changes terminal hyperlink rendering to accept terminal colors.

Changes

Related issues: None
Related PRs: None identified
Suggested labels: enhancement, cli, ui
Suggested reviewers: None identified

Sequence Diagram(s)

sequenceDiagram
  participant global.go setup
  participant ClientConfig
  participant theme package
  participant Colors command
  global.go setup->>ClientConfig: Theme()
  ClientConfig-->>global.go setup: configured theme string
  global.go setup->>theme package: Init(configuredTheme)
  Colors command->>theme package: Current()
  Colors command->>theme package: DetectedBackground()
  Colors command->>theme package: Roles()
Loading

Poem:
A theme now guides the CLI display,
With colors tuned the runtime way.
From config to screen,
The palette stays clean,
And links and tables match the day.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
cli/commands/color.go (1)

14-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider --format json support for the roles/diagnostic output.

As per coding guidelines, cli/commands/**/*.go commands that output lists should support --format json via the FormatOptions pattern. Colors() prints a list of semantic roles alongside resolved theme/background/profile without this option. Given the ANSI swatches are inherently visual, JSON support here has lower payoff than for typical tabular commands — worth a look if this command is expected to be scripted/parsed, otherwise low priority.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/color.go` around lines 14 - 37, `Colors()` in
cli/commands/color.go currently prints a list of semantic roles and diagnostics
as plain text only; add `FormatOptions` handling so it can honor `--format json`
like other list-output commands. Update the `Colors` command signature and
output path to branch on the requested format, preserving the existing
human-readable ANSI swatches for default output and emitting structured JSON for
parsed use cases. Use the existing `FormatOptions` pattern and keep
`theme.Roles()`, `color.Background()`, and
`profileName(lipgloss.ColorProfile())` as the key data sources.

Source: Coding guidelines

cli/commands/app.go (1)

609-639: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inconsistent footer coloring between stack and horizontal modes.

The horizontal-mode footer (Line 636) now explicitly applies theme.Muted, but the stack-mode footer just above (Lines 616-619) renders the "Updated:" text with plain lipgloss.NewStyle() and no color. This produces visually inconsistent output depending on which view mode is active. Also, since faint (Line 382) is already lipgloss.NewStyle().Foreground(theme.Muted), the horizontal-mode footer could reuse it instead of duplicating Foreground(theme.Muted).

♻️ Proposed fix
 		footer =
-			lipgloss.NewStyle().Width(m.width).Align(lipgloss.Right).Render(
+			faint.Width(m.width).Align(lipgloss.Right).Render(
 				fmt.Sprintf("Updated: %s", time.Now().Format(Stamp)),
 			)
 		footer =
-			lipgloss.NewStyle().Width(m.width - 3).Align(lipgloss.Right).Foreground(theme.Muted).Render(
+			faint.Width(m.width - 3).Align(lipgloss.Right).Render(
 				fmt.Sprintf("Updated: %s", time.Now().Format(Stamp)),
 			)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/app.go` around lines 609 - 639, The footer styling in the render
path is inconsistent between the m.stack and horizontal branches in app.go.
Update the stack-mode footer to use the same muted styling as the horizontal
footer, and reuse the existing faint style instead of duplicating theme.Muted
styling in the horizontal branch. Keep the Updated: render logic aligned across
both branches so footer appearance is consistent regardless of layout.
cli/commands/deploy.go (3)

665-669: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant local faintStyle shadows the one already declared at Line 287.

Inside the same Deploy function, faintStyle is already declared with the identical theme.Muted foreground at Line 287. Re-declaring it here is unnecessary duplication.

♻️ Reuse the existing style
 	if useExplainMode {
 		if useOptimized && cachedFiles > 0 {
-			faintStyle := lipgloss.NewStyle().Foreground(theme.Muted)
 			ctx.Printf("  %s\n", faintStyle.Render(fmt.Sprintf("Reused %d/%d files from previous deploy, uploading %d", cachedFiles, totalFiles, len(neededPaths))))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/deploy.go` around lines 665 - 669, The Deploy function
redundantly re-declares faintStyle inside the useExplainMode/useOptimized block
even though the same theme.Muted style already exists earlier in Deploy. Remove
the local faintStyle declaration here and reuse the existing variable when
rendering the cached-files message so the styling stays consistent and the code
avoids duplication.

1373-1403: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Badge colors for distinct event kinds now collapse to the same theme role.

badgeFile/badgeDir both render with theme.Info, and badgeConfig/badgeScript both render with theme.Warning — the stale // blue/// cyan and // yellow/// orange comments no longer describe distinct colors. Previously-distinguishable event kinds in the "Detection" output will now look visually identical. Consider using Bold/Italic or another differentiator to preserve visual distinction, or update/remove the now-inaccurate comments.

Also applies to: 1406-1424

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/deploy.go` around lines 1373 - 1403, The badge styles in the
Detection output are no longer visually distinct because `badgeFile` and
`badgeDir` both use `theme.Info`, and `badgeConfig` and `badgeScript` both use
`theme.Warning`. Update the style definitions near
`analyzeTitleStyle`/`badgeFile`/`badgePackage`/`badgeFramework`/`badgeConfig`/`badgeDir`/`badgeScript`
so each event kind remains distinguishable, either by assigning different theme
roles or by adding another visual cue like `Bold`/`Italic`; also remove or
correct the stale color comments that no longer match the actual styles.

1245-1250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the warning theme role for markdown links
ui.RenderMarkdownLink(link, 208) still hardcodes the old warning ANSI index. Thread the warning role through instead so the link color follows the light/dark theme like the rest of the warning block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/deploy.go` around lines 1245 - 1250, The warning block in the
deploy output still hardcodes the old ANSI index when rendering the markdown
link. Update the link rendering in the fields["link"] branch to thread the
warning theme role through the existing ui.RenderMarkdownLink call instead of
using the fixed 208 value, so it matches the rest of the warning styling. Use
the surrounding detail/link printing logic in deploy and the
ui.RenderMarkdownLink helper to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cli/commands/app.go`:
- Around line 609-639: The footer styling in the render path is inconsistent
between the m.stack and horizontal branches in app.go. Update the stack-mode
footer to use the same muted styling as the horizontal footer, and reuse the
existing faint style instead of duplicating theme.Muted styling in the
horizontal branch. Keep the Updated: render logic aligned across both branches
so footer appearance is consistent regardless of layout.

In `@cli/commands/color.go`:
- Around line 14-37: `Colors()` in cli/commands/color.go currently prints a list
of semantic roles and diagnostics as plain text only; add `FormatOptions`
handling so it can honor `--format json` like other list-output commands. Update
the `Colors` command signature and output path to branch on the requested
format, preserving the existing human-readable ANSI swatches for default output
and emitting structured JSON for parsed use cases. Use the existing
`FormatOptions` pattern and keep `theme.Roles()`, `color.Background()`, and
`profileName(lipgloss.ColorProfile())` as the key data sources.

In `@cli/commands/deploy.go`:
- Around line 665-669: The Deploy function redundantly re-declares faintStyle
inside the useExplainMode/useOptimized block even though the same theme.Muted
style already exists earlier in Deploy. Remove the local faintStyle declaration
here and reuse the existing variable when rendering the cached-files message so
the styling stays consistent and the code avoids duplication.
- Around line 1373-1403: The badge styles in the Detection output are no longer
visually distinct because `badgeFile` and `badgeDir` both use `theme.Info`, and
`badgeConfig` and `badgeScript` both use `theme.Warning`. Update the style
definitions near
`analyzeTitleStyle`/`badgeFile`/`badgePackage`/`badgeFramework`/`badgeConfig`/`badgeDir`/`badgeScript`
so each event kind remains distinguishable, either by assigning different theme
roles or by adding another visual cue like `Bold`/`Italic`; also remove or
correct the stale color comments that no longer match the actual styles.
- Around line 1245-1250: The warning block in the deploy output still hardcodes
the old ANSI index when rendering the markdown link. Update the link rendering
in the fields["link"] branch to thread the warning theme role through the
existing ui.RenderMarkdownLink call instead of using the fixed 208 value, so it
matches the rest of the warning styling. Use the surrounding detail/link
printing logic in deploy and the ui.RenderMarkdownLink helper to locate the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da1e1346-c01e-4e95-8ec0-e75efa61a7bb

📥 Commits

Reviewing files that changed from the base of the PR and between 9d0af1c and e6a9769.

📒 Files selected for processing (26)
  • cli/commands/admin.go
  • cli/commands/app.go
  • cli/commands/app_history.go
  • cli/commands/app_status.go
  • cli/commands/cluster.go
  • cli/commands/color.go
  • cli/commands/config_bind_helpers.go
  • cli/commands/deploy.go
  • cli/commands/deploy_ui.go
  • cli/commands/doctor.go
  • cli/commands/env.go
  • cli/commands/global.go
  • cli/commands/help_render.go
  • cli/commands/init.go
  • cli/commands/tea_utils.go
  • clientconfig/clientconfig.go
  • pkg/progress/progressui/display.go
  • pkg/theme/theme.go
  • pkg/theme/theme_test.go
  • pkg/ui/definition_list.go
  • pkg/ui/hint.go
  • pkg/ui/named_value.go
  • pkg/ui/picker.go
  • pkg/ui/prompt.go
  • pkg/ui/status.go
  • pkg/ui/table.go

@phinze phinze force-pushed the phinze/mir-1312-cli-colors-are-hard-to-read-on-light-background-terminals branch from e6a9769 to 0558ffa Compare July 7, 2026 20:50
The miren CLI's palette was tuned for dark terminals and hard-coded as
bright 256-color ANSI indices scattered across ~20 files. On a light
background the yellow headers washed out and the Faint(true) secondary
text dropped below readable contrast.

Introduce a central pkg/theme package that exposes semantic color roles
(Header, Success, Warning, ...) as profile-aware CompleteAdaptiveColor
values, resolved once at startup from the terminal background. Detection
honors NO_COLOR / FORCE_COLOR / CLICOLOR, a MIREN_THEME env var, and a
`theme` client-config field, falling back to an OSC 11 probe and finally
to dark. The scattered color sites move onto the semantic roles, and
Faint(true) becomes a real muted foreground.

Each role carries per-profile values: truecolor terminals get jewel
tones, 256-color terminals get xterm anchor indices so distinct roles
don't collapse into one muddy bucket, and interactive terminals with a
TERM termenv doesn't recognize floor to ANSI256 instead of going
monochrome.
@phinze phinze force-pushed the phinze/mir-1312-cli-colors-are-hard-to-read-on-light-background-terminals branch from 0558ffa to d819d04 Compare July 7, 2026 21:32
@phinze phinze merged commit dac0117 into main Jul 7, 2026
19 checks passed
@phinze phinze deleted the phinze/mir-1312-cli-colors-are-hard-to-read-on-light-background-terminals branch July 7, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants