Skip to content

feat(tui): add adaptive theme support#9

Merged
bnema merged 5 commits into
mainfrom
feature/theme-modes
Jun 17, 2026
Merged

feat(tui): add adaptive theme support#9
bnema merged 5 commits into
mainfrom
feature/theme-modes

Conversation

@bnema

@bnema bnema commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add dark/light/auto theme mode selection via CLI, config, and live Viper reloads.
  • Introduce explicit dark and light palettes for TUI chrome, markdown/code rendering, diff rows, and syntax highlighting.
  • Detect terminal background preference in auto mode and update rendering live while keeping caches consistent.

Test Plan

  • rtk go test ./...
  • rtk go test -race ./internal/adapters/in/tui ./internal/adapters/in/tui/render ./internal/app
  • wrap-up review workflow run and addressed must-fix findings

Summary by CodeRabbit

Release Notes

  • New Features
    • Added TUI theming with auto/light/dark modes, including live switching in auto mode based on system theme updates.
    • Added --theme and --config CLI options to control theme mode and configuration loading.
  • Bug Fixes
    • Improved theme-consistent styling across status indicators, comments, syntax highlighting, markdown rendering (including light code blocks), and TUI backgrounds.
    • Ensured rendered line styling updates correctly when theme appearance changes and caches are invalidated.
  • Chores
    • Updated dependency declarations.
  • Tests
    • Added/expanded coverage for theme parsing, auto/forced behavior, live updates, and light-theme rendering.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d5eaac5f-b858-40a5-9eeb-9ae9153024e2

📥 Commits

Reviewing files that changed from the base of the PR and between d7d5f01 and 6b4862e.

📒 Files selected for processing (3)
  • internal/adapters/in/tui/model_theme_test.go
  • internal/adapters/in/tui/theme/styles.go
  • internal/core/theme.go

📝 Walkthrough

Walkthrough

Introduces a complete light/dark/auto theme system. Core types (ThemeMode, ThemeAppearance, SystemThemePreference) are added with resolution logic. A dual-palette theme package replaces hardcoded dark constants with thread-safe switching. New --theme/--config CLI flags, a config-file loader, and an fsnotify-based watcher push theme changes to the TUI model. The model reacts to explicit config changes, terminal background-color signals (in auto mode), and generation-checked periodic re-checks. System theme detection via XDG Desktop Portal DBus adapter supports auto mode. All TUI renderers—including syntax highlighter, markdown, inline comments, status bar, and review pane—now derive colors and styles dynamically from the active palette, with cache invalidation when theme changes.

Changes

Dynamic Light/Dark Theme System

Layer / File(s) Summary
Core theme types and resolution logic
internal/core/theme.go, internal/core/theme_test.go
Defines ThemeMode, SystemThemePreference, ThemeAppearance types and constants. Implements ParseThemeMode, SystemThemePreferenceFromDarkBackground, and ResolveThemeAppearance with precedence rules. Full table-driven unit tests.
Dual-palette theme package with thread-safe switching
internal/adapters/in/tui/theme/styles.go, internal/adapters/in/tui/theme/styles_test.go
Refactors from fixed dark constants into Palette/Styles structs with darkPalette/lightPalette. Adds RWMutex-protected state, ApplyAppearance, CurrentPalette, PaletteForAppearance, StylesForAppearance. Implements applyPalette (global mutation) and stylesForPalette (compute-only). Tests validate dark default and light palette switch.
CLI flags, config file loading, and fsnotify watcher
internal/adapters/in/cli/root.go, internal/adapters/in/cli/root_test.go, internal/app/config.go, internal/app/app_test.go, go.mod
Adds --theme/--config persistent flags bound to Viper. loadRuntimeConfig resolves config file path with fallback defaults and reads config. watchThemeConfigChanges starts Viper file watcher and sends parsed theme mode on buffered channel (non-blocking). Promotes fsnotify v1.9.0 to direct dependency. Integration tests validate flag binding and config-file theme propagation.
System theme detection via DBus portal
internal/ports/theme.go, internal/adapters/in/systemtheme/portal.go
Defines SystemThemeReader port interface. Implements PortalReader to connect to XDG Desktop Portal via DBus, call color-scheme setting with ReadOne fallback to Read, convert returned uint32 variant to SystemThemePreference, and handle DBus UnknownMethod errors.
App startup: configuration, theme initialization, and TUI wiring
internal/app/app.go, internal/adapters/in/tui/startup_prompt.go
app.go loads config and starts theme-change watcher at startup. Computes initialThemeMode from config, optionally detects initialSystemTheme when mode is auto, derives initialThemeAppearance. Introduces themedStartupPrompt interface and applies WithAppearance to prompt. Passes ModelConfig with theme state to NewModelWithActiveProviderContextConfig. StartupPrompt gains appearance field, WithAppearance method, and derives View styles from StylesForAppearance.
TUI Model theme state and update loop
internal/adapters/in/tui/model.go, internal/adapters/in/tui/model_theme_test.go
Adds ModelConfig struct, themeConfigChangedMsg/themeDetectionTickMsg message types. Model stores theme state (mode, appearance, system preference), watches ThemeModeChanges channel, and uses generation counter for stale tick detection. Init watches theme-mode channel and schedules periodic background-color checks in auto mode. Update applies theme changes, reacts to tea.BackgroundColorMsg in auto mode, and ignores stale ticks. applyThemeAppearance updates appearance, clears caches, and syncs viewport. View reapplies appearance and sets BackgroundColor. Six tests cover auto/forced/stale/live-change scenarios.
Palette-driven rendering: syntax, markdown, comments, and status
internal/adapters/in/tui/render/line.go, internal/adapters/in/tui/render/line_test.go, internal/adapters/in/tui/render/review_row.go, internal/adapters/in/tui/markdown_renderer.go, internal/adapters/in/tui/markdown_renderer_test.go, internal/adapters/in/tui/pr_sheet.go, internal/adapters/in/tui/comment_editor.go, internal/adapters/in/tui/component/statusbar.go
render/line.go replaces fixed github-dark Chroma with chromaStyle() from palette. review_row.go converts exported style globals to dynamicStyle computed via theme helpers; splits comment ID display from rendering. Markdown renderer adds Clear(), markdownThemeForAppearance(), and palette-driven code theme. pr_sheet.go converts renderPRSheetMarkdown to Model method using m.themeAppearance. Comment editor and statusbar replace hardcoded colors with palette values. Tests verify light Chroma/markdown styles.
Review pane gutter width and styled row width handling
internal/adapters/in/tui/review_pane.go, internal/adapters/in/tui/review_pane_test.go
Refactors renderRow to compute gutter width explicitly, derive capped content width, truncate rendered content, then apply conditional width-aware styling when background is present. Adds test for styled row width verification and updates renderer style logic to return styled backgrounds for cursor rows.
Line cache theme appearance invalidation
internal/adapters/in/tui/render/line_cache.go, internal/adapters/in/tui/render/line_cache_test.go
Adds Appearance field to reviewLineCacheKey so cached lines are segregated by current theme appearance. Test verifies rendered output invalidates and recomputes with different ANSI colors when theme switches.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI flags<br/>--theme light
  participant Root as cli/root.go
  participant App as app.go
  participant Config as config.go<br/>+ Viper
  participant SystemTheme as SystemThemeReader<br/>DBus Portal
  participant Prompt as StartupPrompt
  participant Model as tui.Model
  participant Theme as theme.ApplyAppearance

  CLI->>Root: parse --theme light --config path
  Root->>Viper: BindPFlag(theme, light)
  Root->>Viper: BindPFlag(config, ./ero.toml)
  App->>Config: loadRuntimeConfig(cfg)
  App->>Config: watchThemeConfigChanges(cfg) → themeModeChanges
  alt --theme not set or auto
    App->>SystemTheme: CurrentPreference()
    SystemTheme-->>App: SystemThemePreference
  end
  App->>App: compute initialThemeAppearance
  App->>Prompt: WithAppearance(initialThemeAppearance)
  Prompt-->>App: themed StartupPrompt
  App->>Model: NewModelWithActiveProviderContextConfig(ModelConfig)
  Model->>Model: Init() → watchThemeModeChanges cmd
  Config-->>Model: themeConfigChangedMsg (file change)
  Model->>Theme: ApplyAppearance(ResolveThemeAppearance(...))
  Model->>Model: MarkdownRenderer.Clear()
  Note over Model: tea.BackgroundColorMsg (auto mode)
  Model->>Theme: ApplyAppearance(from terminal background)
  Model->>Model: scheduleThemeDetectionTick (generation++)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop, hop — the palette blooms!
No more dark-only terminal glooms,
Light or dark, the rabbit picks,
Watching configs with fsnotify tricks.
Auto mode sniffs the background hue —
Now themes dance in every view! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(tui): add adaptive theme support' clearly and concisely describes the main objective of the changeset - adding theme support with auto-detection to the TUI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/theme-modes

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

@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.

Actionable comments posted: 4

🤖 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.

Inline comments:
In `@internal/adapters/in/tui/model.go`:
- Around line 265-280: The issue is in the applyThemeMode function where
themeDetectionGeneration is incremented whenever the mode is auto (checking
previousMode == core.ThemeModeAuto || m.themeMode == core.ThemeModeAuto on line
270), but a new theme detection tick is only scheduled when transitioning from
non-auto to auto (checking previousMode != core.ThemeModeAuto on line 278). When
the mode stays as auto, this increments the generation counter and invalidates
the queued tick without scheduling a replacement, breaking the periodic
detection loop. Change the condition on line 270 to only increment
themeDetectionGeneration when transitioning to auto mode, meaning it should
check that previousMode is not auto AND the new themeMode is auto, ensuring the
generation bump only happens alongside the new tick scheduling logic.

In `@internal/adapters/in/tui/render/line_test.go`:
- Around line 74-75: The test cleanup function hardcodes theme.ApplyAppearance
to core.ThemeAppearanceDark, which can leak global state across tests if the
theme had a different appearance before. Save the current theme appearance
before calling theme.ApplyAppearance(core.ThemeAppearanceLight), then restore
that saved original appearance in the t.Cleanup callback instead of hardcoding
the dark appearance.

In `@internal/adapters/in/tui/theme/styles_test.go`:
- Around line 11-13: The test TestApplyAppearanceKeepsDarkPaletteAsDefault
assumes dark mode is already active before asserting that applying dark again
results in no change, which causes flakiness with global state and shuffled test
execution. Add an initial call to ApplyAppearance(core.ThemeAppearanceDark)
before line 12 to establish dark mode as the baseline state, ensuring the
subsequent ApplyAppearance call on line 12 is testing the deterministic no-op
behavior of applying dark when dark is already active.

In `@internal/app/app.go`:
- Around line 35-38: The `themedStartupPrompt` interface declared at line 35 is
not being used anywhere, causing a lint failure. At line 104, the same interface
contract is being redefined inline in a type assertion. To fix this, replace the
inline interface definition at line 104 with a reference to the already-declared
`themedStartupPrompt` interface. This will eliminate the unused declaration and
resolve the CI blocker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7a525086-6ef7-40bd-890f-abcc1161f70e

📥 Commits

Reviewing files that changed from the base of the PR and between 5aed73d and babafce.

📒 Files selected for processing (21)
  • go.mod
  • internal/adapters/in/cli/root.go
  • internal/adapters/in/cli/root_test.go
  • internal/adapters/in/tui/comment_editor.go
  • internal/adapters/in/tui/component/statusbar.go
  • internal/adapters/in/tui/markdown_renderer.go
  • internal/adapters/in/tui/markdown_renderer_test.go
  • internal/adapters/in/tui/model.go
  • internal/adapters/in/tui/model_theme_test.go
  • internal/adapters/in/tui/pr_sheet.go
  • internal/adapters/in/tui/render/line.go
  • internal/adapters/in/tui/render/line_test.go
  • internal/adapters/in/tui/render/review_row.go
  • internal/adapters/in/tui/startup_prompt.go
  • internal/adapters/in/tui/theme/styles.go
  • internal/adapters/in/tui/theme/styles_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/app/config.go
  • internal/core/theme.go
  • internal/core/theme_test.go

Comment thread internal/adapters/in/tui/model.go
Comment thread internal/adapters/in/tui/render/line_test.go Outdated
Comment thread internal/adapters/in/tui/theme/styles_test.go
Comment thread internal/app/app.go

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/adapters/in/tui/theme/styles.go (1)

362-362: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

MutedStyle uses FileRuleFg instead of ColorMutedText, causing poor contrast in light mode.

In the light palette, FileRuleFg is #d0d7de while ColorMutedText is #57606a. Using #d0d7de for muted text on a white background results in a contrast ratio of approximately 1.3:1, well below WCAG minimum (4.5:1). This would render help text and subtitles nearly invisible in light mode.

Proposed fix
-		MutedStyle:             lipgloss.NewStyle().Foreground(lipgloss.Color(p.FileRuleFg)),
+		MutedStyle:             lipgloss.NewStyle().Foreground(lipgloss.Color(p.ColorMutedText)),
🤖 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 `@internal/adapters/in/tui/theme/styles.go` at line 362, The MutedStyle
definition in the styles.go file is using FileRuleFg color which results in poor
contrast for text elements in light mode. Change the MutedStyle assignment to
use ColorMutedText instead of FileRuleFg as the foreground color. This will
ensure that muted text elements like help text and subtitles maintain proper
contrast ratios and remain visible in light mode.
🤖 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.

Outside diff comments:
In `@internal/adapters/in/tui/theme/styles.go`:
- Line 362: The MutedStyle definition in the styles.go file is using FileRuleFg
color which results in poor contrast for text elements in light mode. Change the
MutedStyle assignment to use ColorMutedText instead of FileRuleFg as the
foreground color. This will ensure that muted text elements like help text and
subtitles maintain proper contrast ratios and remain visible in light mode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 41378ae4-54cf-4ce2-81ff-940350464588

📥 Commits

Reviewing files that changed from the base of the PR and between a413152 and e514425.

📒 Files selected for processing (7)
  • internal/adapters/in/tui/model.go
  • internal/adapters/in/tui/model_theme_test.go
  • internal/adapters/in/tui/review_pane.go
  • internal/adapters/in/tui/review_pane_test.go
  • internal/adapters/in/tui/theme/styles.go
  • internal/adapters/in/tui/theme/styles_test.go
  • internal/app/app.go

@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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/adapters/in/tui/model_theme_test.go (1)

106-107: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Hard-coded color sequences are somewhat brittle.

Checking for the absence of specific ANSI color codes ("48;5;236", "48;2;31;42;68") will pass even if the dark palette changes to use different codes in the future. A more robust approach would assert the presence of expected light background colors rather than the absence of known dark codes.

🤖 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 `@internal/adapters/in/tui/model_theme_test.go` around lines 106 - 107, The
test assertions in model_theme_test.go are checking for the absence of dark
palette color codes using NotContains, which is fragile because changes to the
dark palette that use different codes would still pass the test. Replace the two
NotContains assertions (checking for "48;5;236" and "48;2;31;42;68") with
positive assertions that verify the presence of expected light background color
codes instead, ensuring the test will catch any unintended palette changes in
the future.
🤖 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.

Inline comments:
In `@internal/adapters/in/tui/model_theme_test.go`:
- Around line 16-34: Add a cleanup handler to the
TestModelAutoUsesInitialSystemThemePreference test function for consistency with
other tests in the file. Insert a t.Cleanup() call at the beginning of the
subtest within the t.Run closure to ensure proper state cleanup after each test
iteration, even though the test doesn't appear to mutate global theme state.

In `@internal/core/theme.go`:
- Around line 57-69: The ParseSystemThemePreference function uses casts of iota
constants (SystemThemePreferDark and SystemThemePreferLight) in the switch
cases, creating implicit coupling between the constant ordering and the XDG
Desktop Portal protocol values. Replace the switch cases that cast these
constants with explicit literal values corresponding to the portal spec: use
case 1 for prefer-dark, case 2 for prefer-light, and ensure the default case
handles case 0 for no preference. This makes the wire-protocol contract explicit
and decouples the function from future reordering of the iota constants.

---

Outside diff comments:
In `@internal/adapters/in/tui/model_theme_test.go`:
- Around line 106-107: The test assertions in model_theme_test.go are checking
for the absence of dark palette color codes using NotContains, which is fragile
because changes to the dark palette that use different codes would still pass
the test. Replace the two NotContains assertions (checking for "48;5;236" and
"48;2;31;42;68") with positive assertions that verify the presence of expected
light background color codes instead, ensuring the test will catch any
unintended palette changes in the future.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d952056-e034-428c-bed2-f1faea3b11ae

📥 Commits

Reviewing files that changed from the base of the PR and between e514425 and d7d5f01.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • go.mod
  • internal/adapters/in/systemtheme/portal.go
  • internal/adapters/in/tui/model.go
  • internal/adapters/in/tui/model_theme_test.go
  • internal/adapters/in/tui/render/line_cache.go
  • internal/adapters/in/tui/render/line_cache_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/core/theme.go
  • internal/core/theme_test.go
  • internal/ports/theme.go

Comment thread internal/adapters/in/tui/model_theme_test.go
Comment thread internal/core/theme.go
@bnema bnema merged commit d56beb4 into main Jun 17, 2026
4 checks passed
@bnema bnema deleted the feature/theme-modes branch June 17, 2026 21:02
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.

1 participant