Skip to content

Add accessible theme modes and custom palette support#30

Merged
harumiWeb merged 2 commits intomainfrom
feat/color-mode
Apr 6, 2026
Merged

Add accessible theme modes and custom palette support#30
harumiWeb merged 2 commits intomainfrom
feat/color-mode

Conversation

@harumiWeb
Copy link
Copy Markdown
Owner

@harumiWeb harumiWeb commented Apr 6, 2026

Summary

  • add default, no_color, neon, and custom theme modes with config-backed palette support
  • update the home settings UI and non-color affordances so selection and error states stay readable without color
  • preserve the legacy default theme colors and omit unset theme_palette slots when saving config

Testing

  • go test ./...
  • golangci-lint run

Closes #28


Open with Devin

Summary by CodeRabbit

  • New Features

    • Theme mode selection with presets (default, no_color, neon, custom)
    • Switch theme mode from the home settings overlay
    • Support for custom color palettes via config.toml
  • Improvements

    • Error lines prefixed with "error:" for clearer status
    • Answer-mode tabs shown with brackets for clearer selection
    • Improved settings cursor and selection visualization
  • Documentation

    • README, spec, and examples updated; English and Japanese locales added

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds configurable display themes (default, no_color, neon, custom) with palette support, wiring through config parsing/validation, TUI style construction, settings overlay controls, localization, docs/specs, and tests.

Changes

Cohort / File(s) Summary
Configuration & Persistence
internal/config/config.go, internal/config/config_test.go
Add ThemeMode and ThemePalette to Settings; parse/normalize/validate theme_mode and theme_palette (#RRGGBB), normalize colors (trim+uppercase), and omit empty palette slots when saving.
TUI Styling System
internal/tui/styles.go, internal/tui/styles_test.go
Refactor NewStylesNewStyles(theme Theme); add Theme/ThemePalette types and mode constants; implement palette resolution, no-color and preset palettes, and palette application helpers.
Application State & UI Integration
internal/app/model.go, internal/app/update.go, internal/app/view.go
Introduce settingsThemeMode, theme settings row, cycle theme modes with ←/→, construct styles from settings via themeFromSettings, render theme row and conditional custom-note, change error prefix to error: and mark selected answer modes with brackets.
Application Tests
internal/app/update_test.go, internal/app/view_test.go
Add tests for theme-mode cycling, saving applies theme styles, settings overlay rendering for themes, error-line prefix, and bracketed selected answer modes.
TUI Tests
internal/tui/styles_test.go
New tests validating palette resolution and style construction across ThemeDefault, ThemeNoColor, ThemeNeon, and ThemeCustom (merge/fallback semantics).
Localization
assets/locale/en.toml, assets/locale/ja.toml, internal/i18n/keys.go, internal/i18n/i18n_test.go
Add theme-related i18n keys and translations; extend i18n tests to include new keys.
Documentation & Specs
README.en.md, README.md, docs/specs/theme-settings.md, tasks/feature_spec.md, tasks/lessons.md, tasks/todo.md
Document theme modes, examples for minimal and custom config.toml including [theme_palette], add spec for allowed modes, palette format/rules, serialization behavior, and operational notes.
Locale & README updates
README.en.md, README.md, assets/locale/*
Add "Display Themes" section and localization strings; clarify that overlay switches mode only and config.toml edits custom colors.

Sequence Diagram(s)

sequenceDiagram
  participant User as "User / TUI"
  participant TUI as "Settings Overlay"
  participant Model as "RootModel"
  participant Config as "Config (file)"
  participant Styles as "Styles Builder"

  User->>TUI: open settings
  TUI->>Model: set settingsThemeMode (←/→)
  Model->>Styles: NewStyles(themeFromSettings(settingsThemeMode, themePalette))
  Styles-->>Model: Styles (applied immediately)
  User->>TUI: Save (Enter)
  TUI->>Model: produce draft (draft.ThemeMode = settingsThemeMode)
  Model->>Config: Save(draft)
  Config-->>Model: persisted
  Model->>Styles: NewStyles(themeFromSettings(persisted.ThemeMode, persisted.ThemePalette))
  Styles-->>Model: Styles (persisted & applied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 I hopped through settings, palette bright,
No-color whispers, neon lights,
Brackets blink where choices play,
Custom hues saved for another day,
A rabbit cheers: themes done right! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately summarizes the main changes: adding four accessible theme modes (default, no_color, neon, custom) with custom palette support, directly matching the PR's core objectives.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #28: implements no-color mode plus three additional themes with configuration support, ensures UI readability without color through non-color affordances (brackets, prefixes, status vs error labels), provides home settings theme switching, and documents configuration in README and specs.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #28 objectives: theme configuration system (config.go), TUI styling refactor (styles.go), home settings UI integration (view.go, model.go, update.go), i18n keys, comprehensive tests, and documentation updates. No extraneous modifications detected.

✏️ 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 feat/color-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 6, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 147 complexity · 24 duplication

Metric Results
Complexity 147
Duplication 24

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
internal/app/model.go (1)

305-316: Minor: Redundant normalization in settingsThemeModeLabel.

m.settingsThemeMode is already normalized when the settings overlay is opened (line 241). The additional config.NormalizeThemeMode() call on line 306 is defensive but unnecessary.

This is a minor observation and doesn't affect correctness—the defensive call ensures safety if the field is ever modified without normalization elsewhere.

♻️ Optional: remove redundant normalization
 func (m RootModel) settingsThemeModeLabel() string {
-	switch config.NormalizeThemeMode(m.settingsThemeMode) {
+	switch m.settingsThemeMode {
 	case config.ThemeModeNoColor:
 		return i18n.T(i18n.SettingsThemeNoColor)
 	case config.ThemeModeNeon:
 		return i18n.T(i18n.SettingsThemeNeon)
 	case config.ThemeModeCustom:
 		return i18n.T(i18n.SettingsThemeCustom)
 	default:
 		return i18n.T(i18n.SettingsThemeDefault)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/app/model.go` around lines 305 - 316, The settingsThemeModeLabel
function redundantly calls config.NormalizeThemeMode on m.settingsThemeMode;
remove that defensive normalization by switching directly on m.settingsThemeMode
inside settingsThemeModeLabel (function name: settingsThemeModeLabel) so the
switch becomes switch m.settingsThemeMode { ... } returning the same i18n.T
branches for config.ThemeModeNoColor, config.ThemeModeNeon,
config.ThemeModeCustom, and default; no behavior changes required beyond
removing the extra NormalizeThemeMode invocation.
internal/app/update_test.go (1)

674-676: Consider direct string comparison for lipgloss.Color.

lipgloss.Color is a string type alias, so reflect.DeepEqual works but a direct comparison is clearer and avoids the reflection overhead:

if final.styles.Title.GetForeground() != lipgloss.Color("#A6FF00") {

However, the current approach is functionally correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/app/update_test.go` around lines 674 - 676, The test uses
reflect.DeepEqual to compare lipgloss.Color values
(final.styles.Title.GetForeground() and lipgloss.Color("#A6FF00")); replace the
reflect.DeepEqual call with a direct string equality check
(final.styles.Title.GetForeground() != lipgloss.Color("#A6FF00")) to be clearer
and avoid reflection while keeping the same failure message via t.Fatalf.
internal/tui/styles.go (2)

175-187: Parameter name hex is misleading for ANSI color codes.

These helper functions accept both hex colors (#A6FF00) and ANSI color codes (81, 243). Consider renaming the parameter to color or colorSpec for clarity.

💡 Suggested rename
-func foregroundStyle(style lipgloss.Style, hex string) lipgloss.Style {
-	if hex == "" {
+func foregroundStyle(style lipgloss.Style, color string) lipgloss.Style {
+	if color == "" {
 		return style
 	}
-	return style.Foreground(lipgloss.Color(hex))
+	return style.Foreground(lipgloss.Color(color))
 }

-func borderStyle(style lipgloss.Style, hex string) lipgloss.Style {
-	if hex == "" {
+func borderStyle(style lipgloss.Style, color string) lipgloss.Style {
+	if color == "" {
 		return style
 	}
-	return style.BorderForeground(lipgloss.Color(hex))
+	return style.BorderForeground(lipgloss.Color(color))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/styles.go` around lines 175 - 187, The parameter name `hex` in
helper functions foregroundStyle and borderStyle is misleading because they
accept hex or ANSI color codes; rename the parameter to a clearer name like
`color` (or `colorSpec`) and update the function signatures and their internal
uses (e.g., lipgloss.Color(hex) -> lipgloss.Color(color)) as well as all callers
to pass the renamed parameter to avoid compile errors.

61-70: Clarify: ResolvePalette returns defaultPalette() for ThemeNoColor.

This may be intentional (e.g., for configuration display purposes), but it's worth documenting that ResolvePalette(ThemeNoColor) does not return an empty palette—it returns the default palette values even though NewStyles(ThemeNoColor) doesn't use them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/styles.go` around lines 61 - 70, ResolvePalette currently
returns defaultPalette() when given a Theme with Mode normalized to
ThemeNoColor, which can be confusing because NewStyles(ThemeNoColor) ignores
palettes; add a brief comment above ResolvePalette stating that
ResolvePalette(ThemeNoColor) intentionally returns the default palette (not an
empty palette) for purposes like configuration display, and note that NewStyles
ignores the palette for ThemeNoColor so the palette is unused in rendering;
reference the ResolvePalette function and the ThemeNoColor constant and
NewStyles behavior in the comment to make the distinction clear.
internal/tui/styles_test.go (1)

11-25: Consider adding test coverage for ResolvePalette with ThemeNoColor.

Tests cover ThemeDefault, ThemeNeon, and ThemeCustom, but there's no explicit test for ResolvePalette(Theme{Mode: ThemeNoColor}). While the implementation likely falls through to the default case, an explicit test would document the expected behavior.

💡 Suggested test addition
func TestResolvePaletteNoColor(t *testing.T) {
	t.Parallel()

	palette := ResolvePalette(Theme{Mode: ThemeNoColor})
	// NoColor mode falls back to default palette (or returns empty - verify expected behavior)
	want := ThemePalette{
		Accent:  "81",
		Success: "42",
		Danger:  "203",
		Muted:   "245",
		Border:  "",
	}
	if palette != want {
		t.Fatalf("palette = %+v, want %+v", palette, want)
	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/styles_test.go` around lines 11 - 25, Add a unit test named
TestResolvePaletteNoColor that calls ResolvePalette(Theme{Mode: ThemeNoColor})
and asserts it returns the same ThemePalette as the default case; use the same
expected ThemePalette values as TestResolvePaletteDefault (Accent "81", Success
"42", Danger "203", Muted "245", Border ""), and fail the test with t.Fatalf if
the returned palette differs; reference ResolvePalette, Theme, ThemeNoColor and
ThemePalette to locate where to add the test.
tasks/feature_spec.md (1)

437-437: Clarify: docs/specs/ mentioned in scope vs. earlier "don't add" policy.

Line 124 (from 2026-03-29 spec) states docs/specs/ は増やさず (don't add to docs/specs), but this spec includes docs/specs/ の恒久仕様 in scope. Please clarify if the team has revised the documentation policy, or if this should reference a different location (e.g., README or ADR).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tasks/feature_spec.md` at line 437, Resolve the contradiction between the
mention of `docs/specs/` in scope and the earlier "don't add to docs/specs"
policy by choosing one of two fixes: either remove `docs/specs/` from the scope
list and replace it with the intended location (e.g., README or ADR), or amend
the earlier policy text to explicitly allow permanent specs in `docs/specs/` and
describe any conditions; update the spec text where `docs/specs/` is referenced
and the earlier "don't add to docs/specs" statement so they consistently
reference the same canonical storage location and rationale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/app/model.go`:
- Around line 305-316: The settingsThemeModeLabel function redundantly calls
config.NormalizeThemeMode on m.settingsThemeMode; remove that defensive
normalization by switching directly on m.settingsThemeMode inside
settingsThemeModeLabel (function name: settingsThemeModeLabel) so the switch
becomes switch m.settingsThemeMode { ... } returning the same i18n.T branches
for config.ThemeModeNoColor, config.ThemeModeNeon, config.ThemeModeCustom, and
default; no behavior changes required beyond removing the extra
NormalizeThemeMode invocation.

In `@internal/app/update_test.go`:
- Around line 674-676: The test uses reflect.DeepEqual to compare lipgloss.Color
values (final.styles.Title.GetForeground() and lipgloss.Color("#A6FF00"));
replace the reflect.DeepEqual call with a direct string equality check
(final.styles.Title.GetForeground() != lipgloss.Color("#A6FF00")) to be clearer
and avoid reflection while keeping the same failure message via t.Fatalf.

In `@internal/tui/styles_test.go`:
- Around line 11-25: Add a unit test named TestResolvePaletteNoColor that calls
ResolvePalette(Theme{Mode: ThemeNoColor}) and asserts it returns the same
ThemePalette as the default case; use the same expected ThemePalette values as
TestResolvePaletteDefault (Accent "81", Success "42", Danger "203", Muted "245",
Border ""), and fail the test with t.Fatalf if the returned palette differs;
reference ResolvePalette, Theme, ThemeNoColor and ThemePalette to locate where
to add the test.

In `@internal/tui/styles.go`:
- Around line 175-187: The parameter name `hex` in helper functions
foregroundStyle and borderStyle is misleading because they accept hex or ANSI
color codes; rename the parameter to a clearer name like `color` (or
`colorSpec`) and update the function signatures and their internal uses (e.g.,
lipgloss.Color(hex) -> lipgloss.Color(color)) as well as all callers to pass the
renamed parameter to avoid compile errors.
- Around line 61-70: ResolvePalette currently returns defaultPalette() when
given a Theme with Mode normalized to ThemeNoColor, which can be confusing
because NewStyles(ThemeNoColor) ignores palettes; add a brief comment above
ResolvePalette stating that ResolvePalette(ThemeNoColor) intentionally returns
the default palette (not an empty palette) for purposes like configuration
display, and note that NewStyles ignores the palette for ThemeNoColor so the
palette is unused in rendering; reference the ResolvePalette function and the
ThemeNoColor constant and NewStyles behavior in the comment to make the
distinction clear.

In `@tasks/feature_spec.md`:
- Line 437: Resolve the contradiction between the mention of `docs/specs/` in
scope and the earlier "don't add to docs/specs" policy by choosing one of two
fixes: either remove `docs/specs/` from the scope list and replace it with the
intended location (e.g., README or ADR), or amend the earlier policy text to
explicitly allow permanent specs in `docs/specs/` and describe any conditions;
update the spec text where `docs/specs/` is referenced and the earlier "don't
add to docs/specs" statement so they consistently reference the same canonical
storage location and rationale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab5f451d-c028-43f7-be85-6ef6829f6a35

📥 Commits

Reviewing files that changed from the base of the PR and between e90b9c6 and 5eebcda.

📒 Files selected for processing (19)
  • README.en.md
  • README.md
  • assets/locale/en.toml
  • assets/locale/ja.toml
  • docs/specs/theme-settings.md
  • internal/app/model.go
  • internal/app/update.go
  • internal/app/update_test.go
  • internal/app/view.go
  • internal/app/view_test.go
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/i18n/i18n_test.go
  • internal/i18n/keys.go
  • internal/tui/styles.go
  • internal/tui/styles_test.go
  • tasks/feature_spec.md
  • tasks/lessons.md
  • tasks/todo.md

This comment was marked as resolved.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/tui/styles.go (1)

184-195: Consider extracting shared normalization logic to reduce duplication.

NormalizeThemeMode duplicates config.NormalizeThemeMode (both lower+trim and switch on the same values). While this keeps packages independent, you could extract the shared logic into a minimal shared package (e.g., internal/theme/mode.go) if the duplication becomes a maintenance burden.

For now this works correctly since both use identical string values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/styles.go` around lines 184 - 195, The NormalizeThemeMode
function duplicates logic already present in config.NormalizeThemeMode; extract
the shared normalization (trim+lowercase and the Theme* switch mapping) into a
small internal package (e.g., internal/theme/mode.go) that exposes a single
NormalizeThemeMode and the Theme constants, then update internal/tui/styles.go
to call that shared NormalizeThemeMode instead of duplicating the switch logic;
ensure you keep the same returned constants (ThemeNoColor, ThemeNeon,
ThemeCustom, ThemeDefault) and update imports accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/tui/styles.go`:
- Around line 184-195: The NormalizeThemeMode function duplicates logic already
present in config.NormalizeThemeMode; extract the shared normalization
(trim+lowercase and the Theme* switch mapping) into a small internal package
(e.g., internal/theme/mode.go) that exposes a single NormalizeThemeMode and the
Theme constants, then update internal/tui/styles.go to call that shared
NormalizeThemeMode instead of duplicating the switch logic; ensure you keep the
same returned constants (ThemeNoColor, ThemeNeon, ThemeCustom, ThemeDefault) and
update imports accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31627952-f3ae-45d3-8808-43a09e25b642

📥 Commits

Reviewing files that changed from the base of the PR and between 5eebcda and 7ef5207.

📒 Files selected for processing (7)
  • internal/app/model.go
  • internal/app/update_test.go
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/tui/styles.go
  • internal/tui/styles_test.go
  • tasks/feature_spec.md
✅ Files skipped from review due to trivial changes (1)
  • internal/app/update_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/config/config_test.go
  • internal/tui/styles_test.go
  • internal/config/config.go
  • tasks/feature_spec.md

@harumiWeb harumiWeb merged commit 3966eb2 into main Apr 6, 2026
9 checks passed
@harumiWeb harumiWeb deleted the feat/color-mode branch April 7, 2026 10:33
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.

feat: no-color / 高コントラスト表示オプションを追加する

2 participants