This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 633c1c9d8859 CAMEL-23839: Fix TUI theme background and add --theme CLI 
option
633c1c9d8859 is described below

commit 633c1c9d88592ef028e464f2c3113afcc5ff6751
Author: Adriano Machado <[email protected]>
AuthorDate: Tue Jul 7 12:58:17 2026 -0400

    CAMEL-23839: Fix TUI theme background and add --theme CLI option
    
    Fix background color rendering by patching unset fg/bg cells after each
    frame. Add --theme=dark|light CLI option to override persisted theme for
    the current session. Introduce ThemeMode enum to centralize dark/light
    parsing, persistence, and activation. Use Theme.info() for integration
    names instead of hardcoded Color.CYAN. Harden persistence logging with
    separate read/write failure flags. Includes tests for base-color
    patching, ThemeMode, and persisted-mode fallback paths.
    
    Co-authored-by: Cursor <[email protected]>
    Co-authored-by: Cursor <[email protected]>
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |   3 +
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  13 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  21 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  55 +++++
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |   2 +-
 .../camel/dsl/jbang/core/commands/tui/Theme.java   | 246 +++++++++++++++------
 .../dsl/jbang/core/commands/tui/ThemeMode.java     |  71 ++++++
 .../tui/ThemeModeCompletionCandidates.java         |  28 +++
 .../dsl/jbang/core/commands/tui/TuiCommand.java    |   9 +
 .../src/main/resources/tui/themes/dark.tcss        |  28 ++-
 .../src/main/resources/tui/themes/light.tcss       |  28 ++-
 .../jbang/core/commands/tui/CamelMonitorTest.java  | 103 +++++++++
 .../tui/ThemeModeCompletionCandidatesTest.java     |  35 +++
 .../dsl/jbang/core/commands/tui/ThemeModeTest.java |  84 +++++++
 .../dsl/jbang/core/commands/tui/ThemeTest.java     | 156 +++++++++++--
 15 files changed, 768 insertions(+), 114 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index e22ff8fd638d..13cd6ce04a09 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -18,6 +18,9 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 The Camel JBang CLI now automatic resolves quarkus version to use,
 instead of hardcoded `3.33.1.1` 
(https://github.com/apache/camel/commit/d1f4713ebdf787ab04a31c50a6f07e3ca66f0c2b).
 
+The Camel TUI (`camel tui`) accepts `--theme=dark` or `--theme=light` to 
choose the color palette at startup.
+When omitted, the persisted `camel.tui.theme` value from 
`.camel-jbang-user.properties` is used.
+
 === camel-langchain4j-agent
 
 The `Agent.chat()` method return type has changed from `String` to 
`Result<String>` (from `dev.langchain4j.service.Result`).
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index f2dc24b20ab6..38e0253bc3cb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -306,7 +306,13 @@ The Doctor checks your development environment and reports 
issues:
 == Theme
 
 The TUI ships with two color themes, *dark* (the default) and *light*, defined 
as
-CSS stylesheets. Press *F4* on any screen to toggle between them at runtime.
+CSS stylesheets. Open the *F2* actions menu and choose *Light Theme* or *Dark 
Theme*
+to switch at runtime.
+
+Pass `--theme=dark` or `--theme=light` on the command line to pick the palette 
for
+the current session. The CLI value overrides the persisted preference from
+`.camel-jbang-user.properties`; runtime toggles and the config file still 
apply on
+later launches when `--theme` is omitted.
 
 The brand orange accent is identical in both themes; status colors (success,
 warning, error) and borders adapt for readability on dark and light terminals.
@@ -327,7 +333,6 @@ in `.camel-jbang-user.properties` and restored the next 
time you open the TUI.
 | *F1* / *?* | Context-sensitive help (toggle)
 | *F2* | Actions menu
 | *F3* | Switch between integrations (when multiple running)
-| *F4* | Toggle light / dark theme
 | *Shift+F5* | Take screenshot
 | *Ctrl+R* | Start/stop tape recording
 | *Ctrl+C* / *Q* | Quit
@@ -497,6 +502,10 @@ vhs demo.tape               # .tape -> .gif
 | Screen refresh interval in milliseconds.
 | `100`
 
+| `--theme`
+| Color theme for this session: `dark` or `light`. Overrides the persisted 
`camel.tui.theme` preference when set.
+|
+
 | `--record`
 | Replay a `.tape` file and record the session to an Asciinema `.cast` file.
 |
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 7682441e0c14..9449dc1c06ea 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -88,6 +88,7 @@ class ActionsPopup {
     private final Runnable burstCallback;
     private Runnable resetStatsAction;
     private Runnable resetScreenAction;
+    private Runnable themeToggleAction;
     private Runnable openShellAction;
     private Runnable browseFilesAction;
     private Runnable switchIntegrationAction;
@@ -175,6 +176,10 @@ class ActionsPopup {
         this.resetScreenAction = resetScreenAction;
     }
 
+    void setThemeToggleAction(Runnable themeToggleAction) {
+        this.themeToggleAction = themeToggleAction;
+    }
+
     void setOpenShellAction(Runnable openShellAction) {
         this.openShellAction = openShellAction;
     }
@@ -361,7 +366,7 @@ class ActionsPopup {
         labels.add("Run Doctor");
         labels.add("Reset Stats");
         labels.add("Reset Screen");
-        labels.add("dark".equals(Theme.mode()) ? "Light Theme" : "Dark Theme");
+        labels.add(Theme.isDark() ? "Light Theme" : "Dark Theme");
         labels.add("───");
         // Group 3: Recording & Presentation
         labels.add("Take Screenshot");
@@ -604,7 +609,7 @@ class ActionsPopup {
                         }
                     } else if (action == Action.TOGGLE_THEME) {
                         showActionsMenu = false;
-                        Theme.toggle();
+                        toggleTheme();
                     } else if (action == Action.STOP_ALL) {
                         showActionsMenu = false;
                         stopAllPopup.open();
@@ -850,7 +855,7 @@ class ActionsPopup {
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCTOR, "Run 
Doctor")));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.RESET, "Reset 
Stats")));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CLEAN, "Reset 
Screen")));
-        String themeLabel = "dark".equals(Theme.mode())
+        String themeLabel = Theme.isDark()
                 ? TuiIcons.menuItem(TuiIcons.LIGHT_THEME, "Light Theme")
                 : TuiIcons.menuItem(TuiIcons.DARK_THEME, "Dark Theme");
         items.add(ListItem.from(themeLabel));
@@ -1090,7 +1095,7 @@ class ActionsPopup {
                     resetScreenAction.run();
                 }
             }
-            case TOGGLE_THEME -> Theme.toggle();
+            case TOGGLE_THEME -> toggleTheme();
             case SCREENSHOT -> screenshotAction.run();
             case SHOW_KEYSTROKES -> toggleKeystrokes.run();
             case TAPE_RECORDING -> toggleTapeRecording.run();
@@ -1111,4 +1116,12 @@ class ActionsPopup {
         return gotoTabPopup.consumePendingEntry();
     }
 
+    private void toggleTheme() {
+        if (themeToggleAction != null) {
+            themeToggleAction.run();
+        } else {
+            Theme.toggle();
+        }
+    }
+
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index b57e497f0a77..de71e4d94c70 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -33,6 +33,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
@@ -107,6 +109,11 @@ public class CamelMonitor extends CamelCommand {
                         defaultValue = "8123")
     int mcpPort = 8123;
 
+    @CommandLine.Option(names = { "--theme" },
+                        description = "Color theme: dark or light (overrides 
persisted preference for this session)",
+                        completionCandidates = 
ThemeModeCompletionCandidates.class)
+    String theme;
+
     // State
     private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
     private TabRegistry tabRegistry;
@@ -163,6 +170,16 @@ public class CamelMonitor extends CamelCommand {
     public Integer doCall() throws Exception {
         System.setProperty("java.awt.headless", "true");
 
+        if (theme != null) {
+            if (!Theme.isValidMode(theme)) {
+                String expected = String.join("' or '", ThemeMode.ids());
+                throw new CommandLine.ParameterException(
+                        new CommandLine(this),
+                        "Invalid value for option '--theme': expected '" + 
expected + "', was '" + theme + "'");
+            }
+            Theme.applyStartupMode(theme);
+        }
+
         // Configure TamboUI recording if --record is specified
         if (record != null) {
             Path tapeFile = Path.of(record);
@@ -446,6 +463,10 @@ public class CamelMonitor extends CamelCommand {
             ctx.runner = tui;
             actionsPopup.setScheduler(tui.scheduler());
             actionsPopup.setResetScreenAction(() -> tui.terminal().clear());
+            actionsPopup.setThemeToggleAction(() -> {
+                Theme.toggle();
+                tui.terminal().clear();
+            });
             // Preload diagram data if an integration was auto-selected
             tabRegistry.routesTab().preloadDiagram();
             tabRegistry.diagramTab().preloadDiagram();
@@ -1114,6 +1135,7 @@ public class CamelMonitor extends CamelCommand {
             helpOverlay.render(frame, contentArea);
         }
         renderFooter(frame, mainChunks.get(3));
+        applyThemeBaseColors(frame.buffer(), area);
 
         recordingManager.updateBuffer(frame.buffer());
         recordingManager.processPendingScreenshot();
@@ -1228,6 +1250,7 @@ public class CamelMonitor extends CamelCommand {
 
         int x5 = area.x() + Math.max(0, (area.width() - CharWidth.of(line5)) / 
2);
         frame.buffer().setString(x5, startY + 4, line5, normal);
+        applyThemeBaseColors(frame.buffer(), area);
     }
 
     private void renderTabs(Frame frame, Rect area) {
@@ -1337,6 +1360,38 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
+    /**
+     * Fills cells that still have no explicit fg/bg after widget rendering. 
TamboUI clears the buffer to
+     * {@link Cell#EMPTY} each frame; when bg is unset the terminal falls back 
to its own default (usually black). Only
+     * patches missing colors so selection highlights, zebra stripes, and 
other widget backgrounds are preserved.
+     */
+    static void applyThemeBaseColors(Buffer buffer, Rect area) {
+        Color baseBg = Theme.baseBg();
+        Color baseFg = Theme.baseFg();
+        Rect intersection = buffer.area().intersection(area);
+        for (int y = intersection.top(); y < intersection.bottom(); y++) {
+            for (int x = intersection.left(); x < intersection.right(); x++) {
+                Cell cell = buffer.get(x, y);
+                if (cell.isContinuation()) {
+                    continue;
+                }
+                Style style = cell.style();
+                Color cellBg = style.bg().orElse(null);
+                Color cellFg = style.fg().orElse(null);
+                if (cellBg == null || cellFg == null) {
+                    Style patch = Style.EMPTY;
+                    if (cellBg == null) {
+                        patch = patch.bg(baseBg);
+                    }
+                    if (cellFg == null) {
+                        patch = patch.fg(baseFg);
+                    }
+                    buffer.set(x, y, cell.patchStyle(patch));
+                }
+            }
+        }
+    }
+
     private void computeTabBadges(String[] badgeTexts, Style[] badgeStyles) {
         Style yellow = Style.EMPTY.fg(Color.YELLOW).bold();
         Style cyan = Style.EMPTY.fg(Color.CYAN).bold();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 4af1263a93b8..55914b8cd547 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -328,7 +328,7 @@ class OverviewTab extends AbstractTab {
                 String platformIcon = TuiIcons.runtimeIcon(info.platform != 
null ? info.platform : "");
                 String nameText = platformIcon + " " + (info.name != null ? 
info.name : "");
                 List<Span> nameSpans = new ArrayList<>();
-                nameSpans.add(Span.styled(nameText, 
Style.EMPTY.fg(Color.CYAN)));
+                nameSpans.add(Span.styled(nameText, Theme.info()));
                 if (info.devMode) {
                     nameSpans.add(Span.styled(" [dev]", 
Style.EMPTY.fg(Color.YELLOW).dim()));
                 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
index e3c666174523..748a4ebb7707 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
@@ -39,10 +39,10 @@ import org.slf4j.LoggerFactory;
  * palette lives in one place. Values are resolved from CSS stylesheets 
({@code dark.tcss} / {@code light.tcss}) through
  * a shared {@link StyleEngine}; the active stylesheet can be switched at 
runtime and is persisted to user config.
  * <p/>
- * Palette policy: the brand accent is Camel orange (truecolor), reserved for 
accent and borders. Status colors use ANSI
- * names in dark mode (so they respect the terminal) and explicit darker hex 
in light mode. If a stylesheet is missing
- * or malformed, the facade falls back to the built-in palette below and logs 
once, so a cosmetic failure never crashes
- * the TUI.
+ * Palette policy: the brand accent is Camel orange (truecolor), used for 
accent tokens, focused borders, and titles.
+ * Status colors use explicit truecolor hex values in both themes (no ANSI 
names); accent-bg/hint-key/selection keep
+ * plain white/black foregrounds. Fallbacks mirror {@code dark.tcss}. If a 
stylesheet is missing or malformed, the
+ * facade falls back to the built-in palette below and logs once, so a 
cosmetic failure never crashes the TUI.
  */
 final class Theme {
 
@@ -52,34 +52,38 @@ final class Theme {
     private static final Logger LOG = LoggerFactory.getLogger(Theme.class);
 
     private static final String PROP_KEY = "camel.tui.theme";
-    private static final String DARK = "dark";
-    private static final String LIGHT = "light";
 
     // Fallback palette mirrors the dark stylesheet, used when CSS is 
unavailable.
     private static final Style FALLBACK_ACCENT_BG = 
Style.EMPTY.fg(Color.WHITE).bg(ACCENT).bold();
     private static final Style FALLBACK_HINT_KEY = 
Style.EMPTY.fg(Color.BLACK).bg(ACCENT).bold();
-    private static final Style FALLBACK_BORDER = 
Style.EMPTY.fg(Color.DARK_GRAY);
+    private static final Style FALLBACK_BORDER = 
Style.EMPTY.fg(Color.rgb(0x50, 0x50, 0x50));
     private static final Style FALLBACK_BORDER_FOCUSED = 
Style.EMPTY.fg(ACCENT);
     private static final Style FALLBACK_TITLE = Style.EMPTY.fg(ACCENT).bold();
-    private static final Style FALLBACK_SUCCESS = 
Style.EMPTY.fg(Color.LIGHT_GREEN);
-    private static final Style FALLBACK_WARNING = 
Style.EMPTY.fg(Color.LIGHT_YELLOW);
-    private static final Style FALLBACK_ERROR = 
Style.EMPTY.fg(Color.LIGHT_RED);
-    private static final Style FALLBACK_MUTED = Style.EMPTY.dim();
-    private static final Style FALLBACK_SELECTION = 
Style.EMPTY.fg(Color.WHITE).bold().onBlue();
-    private static final Style FALLBACK_INFO = Style.EMPTY.fg(Color.CYAN);
-    private static final Style FALLBACK_NOTICE = Style.EMPTY.fg(Color.MAGENTA);
-    private static final Style FALLBACK_MCP_ACTIVE = 
Style.EMPTY.fg(Color.LIGHT_GREEN);
-    private static final Style FALLBACK_MCP_IDLE = 
Style.EMPTY.fg(Color.DARK_GRAY);
-    private static final Style FALLBACK_MCP_DOWN = 
Style.EMPTY.fg(Color.LIGHT_RED);
-    private static final Color FALLBACK_ZEBRA = Color.rgb(0x1C, 0x1C, 0x1C);
+    private static final Style FALLBACK_SUCCESS = 
Style.EMPTY.fg(Color.rgb(0x4E, 0xC9, 0xB0));
+    private static final Style FALLBACK_WARNING = 
Style.EMPTY.fg(Color.rgb(0xDC, 0xDC, 0xAA));
+    private static final Style FALLBACK_ERROR = Style.EMPTY.fg(Color.rgb(0xF4, 
0x87, 0x71));
+    private static final Style FALLBACK_MUTED = Style.EMPTY.fg(Color.rgb(0x80, 
0x80, 0x80));
+    private static final Style FALLBACK_SELECTION = 
Style.EMPTY.fg(Color.WHITE).bg(Color.rgb(0x26, 0x4F, 0x78)).bold();
+    private static final Style FALLBACK_INFO = Style.EMPTY.fg(Color.rgb(0x9C, 
0xDC, 0xFE));
+    private static final Style FALLBACK_NOTICE = 
Style.EMPTY.fg(Color.rgb(0xC5, 0x86, 0xC0));
+    private static final Style FALLBACK_MCP_ACTIVE = 
Style.EMPTY.fg(Color.rgb(0x4E, 0xC9, 0xB0));
+    private static final Style FALLBACK_MCP_IDLE = 
Style.EMPTY.fg(Color.rgb(0x60, 0x60, 0x60));
+    private static final Style FALLBACK_MCP_DOWN = 
Style.EMPTY.fg(Color.rgb(0xF4, 0x87, 0x71));
+    private static final Color FALLBACK_ZEBRA = Color.rgb(0x25, 0x25, 0x25);
+    private static final Color FALLBACK_BASE_BG = Color.rgb(0x1E, 0x1E, 0x1E);
+    private static final Color FALLBACK_BASE_FG = Color.rgb(0xD4, 0xD4, 0xD4);
 
     private static final Map<String, Style> CACHE = new HashMap<>();
 
     private static boolean initialized;
-    private static boolean fallbackLogged;
+    private static boolean persistedModeLoaded;
+    private static boolean stylesheetFallbackLogged;
+    private static boolean tokenFallbackLogged;
+    private static boolean persistedReadFallbackLogged;
+    private static boolean persistedWriteFallbackLogged;
     private static boolean testMode;
     private static StyleEngine engine;
-    private static String mode = DARK;
+    private static ThemeMode mode = ThemeMode.DARK;
 
     private Theme() {
     }
@@ -92,7 +96,7 @@ final class Theme {
         try {
             return e.resolve(new Token("accent")).foreground().orElse(ACCENT);
         } catch (RuntimeException ex) {
-            logFallbackOnce(ex);
+            logTokenFallbackOnce(ex);
             return ACCENT;
         }
     }
@@ -109,11 +113,43 @@ final class Theme {
         try {
             return e.resolve(new 
Token("row-alt")).background().orElse(FALLBACK_ZEBRA);
         } catch (RuntimeException ex) {
-            logFallbackOnce(ex);
+            logTokenFallbackOnce(ex);
             return FALLBACK_ZEBRA;
         }
     }
 
+    /**
+     * Base background color for the main content area. Theme-aware (dark on 
dark mode, white on light mode).
+     */
+    static Color baseBg() {
+        StyleEngine e = engine();
+        if (e == null) {
+            return FALLBACK_BASE_BG;
+        }
+        try {
+            return e.resolve(new 
Token("base-bg")).background().orElse(FALLBACK_BASE_BG);
+        } catch (RuntimeException ex) {
+            logTokenFallbackOnce(ex);
+            return FALLBACK_BASE_BG;
+        }
+    }
+
+    /**
+     * Base foreground color for normal text. Theme-aware (light gray on dark 
mode, dark gray on light mode).
+     */
+    static Color baseFg() {
+        StyleEngine e = engine();
+        if (e == null) {
+            return FALLBACK_BASE_FG;
+        }
+        try {
+            return e.resolve(new 
Token("base-fg")).foreground().orElse(FALLBACK_BASE_FG);
+        } catch (RuntimeException ex) {
+            logTokenFallbackOnce(ex);
+            return FALLBACK_BASE_FG;
+        }
+    }
+
     /** White-on-orange: active tab highlight. */
     static Style accentBg() {
         return style("accent-bg", FALLBACK_ACCENT_BG);
@@ -160,7 +196,7 @@ final class Theme {
         return style("selection", FALLBACK_SELECTION);
     }
 
-    /** Informational accent (header integration count). */
+    /** Informational accent (header integration count, integration name text, 
popup prompts). */
     static Style info() {
         return style("info", FALLBACK_INFO);
     }
@@ -188,37 +224,85 @@ final class Theme {
     /** The active theme mode: {@code "dark"} or {@code "light"}. */
     static String mode() {
         engine();
-        return mode;
+        return mode.id();
     }
 
-    /** Flip the active theme, clear the cache, persist the new value, and 
return the new mode. */
+    /** True if the active mode is dark. */
+    static boolean isDark() {
+        engine();
+        return mode == ThemeMode.DARK;
+    }
+
+    /** Flip the active theme mode, persist it (outside test mode), and 
activate it, returning the new mode. */
     static synchronized String toggle() {
-        String next = DARK.equals(mode) ? LIGHT : DARK;
-        setMode(next);
+        ThemeMode next = mode.toggle();
         if (!testMode) {
             persist(next);
         }
-        return next;
+        activate(next, false);
+        return next.id();
     }
 
-    /** Activate a specific mode without persisting. Unknown values fall back 
to dark. */
+    /**
+     * Activate a specific mode without persisting. Unknown values fall back 
to dark.
+     * <p/>
+     * Test-only entry point: unlike {@link #applyStartupMode(String)}, this 
does not mark the persisted preference as
+     * loaded, so outside test mode a later {@link #engine()} 
re-initialization can still overwrite the mode with
+     * whatever is on disk.
+     */
     static synchronized void setMode(String newMode) {
-        String resolved = DARK.equals(newMode) || LIGHT.equals(newMode) ? 
newMode : DARK;
-        mode = resolved;
+        activate(ThemeMode.parseOrDefault(newMode), false);
+    }
+
+    /**
+     * Applies a CLI-selected theme for this process only. Marks the persisted 
preference as already loaded so the value
+     * from {@code --theme} wins over {@code camel.tui.theme} in user config.
+     *
+     * @throws IllegalArgumentException if {@code newMode} is not a known 
theme mode; callers should normally validate
+     *                                  with {@link #isValidMode(String)} 
first to report a friendlier CLI error.
+     */
+    static synchronized void applyStartupMode(String newMode) {
+        ThemeMode parsed = ThemeMode.parse(newMode)
+                .orElseThrow(() -> new IllegalArgumentException("Unknown Camel 
TUI theme mode: " + newMode));
+        activate(parsed, true);
+    }
+
+    /** Whether {@code value} parses to a known theme mode. */
+    static boolean isValidMode(String value) {
+        return ThemeMode.parse(value).isPresent();
+    }
+
+    /** Test hook: enable in-memory-only mode so tests never touch user config 
files. */
+    static synchronized void resetForTesting() {
+        resetForTesting(true);
+    }
+
+    /**
+     * Test hook like {@link #resetForTesting()}, but lets the caller keep 
{@code testMode} disabled so a test can
+     * exercise the real persistence path (disk read/write) against an 
isolated home directory.
+     */
+    static synchronized void resetForTesting(boolean testModeValue) {
+        testMode = testModeValue;
+        stylesheetFallbackLogged = false;
+        tokenFallbackLogged = false;
+        persistedReadFallbackLogged = false;
+        persistedWriteFallbackLogged = false;
+        persistedModeLoaded = false;
         CACHE.clear();
         engine = null;
         initialized = false;
-        engine();
+        mode = ThemeMode.DARK;
     }
 
-    /** Test hook: enable in-memory-only mode so tests never touch user config 
files. */
-    static synchronized void resetForTesting() {
-        testMode = true;
-        fallbackLogged = false;
+    private static synchronized void activate(ThemeMode newMode, boolean 
cliOverride) {
+        mode = newMode;
+        if (cliOverride) {
+            persistedModeLoaded = true;
+        }
         CACHE.clear();
         engine = null;
         initialized = false;
-        mode = DARK;
+        engine();
     }
 
     private static synchronized Style style(String id, Style fallback) {
@@ -226,14 +310,19 @@ final class Theme {
         if (e == null) {
             return fallback;
         }
-        return CACHE.computeIfAbsent(id, key -> {
-            try {
-                return e.resolve(new Token(key)).toStyle();
-            } catch (RuntimeException ex) {
-                logFallbackOnce(ex);
-                return fallback;
-            }
-        });
+        Style cached = CACHE.get(id);
+        if (cached != null) {
+            return cached;
+        }
+        try {
+            Style resolved = e.resolve(new Token(id)).toStyle();
+            CACHE.put(id, resolved);
+            return resolved;
+        } catch (RuntimeException ex) {
+            // Not cached: a transient resolution failure must not permanently 
lock this token to the fallback.
+            logTokenFallbackOnce(ex);
+            return fallback;
+        }
     }
 
     private static synchronized StyleEngine engine() {
@@ -241,22 +330,29 @@ final class Theme {
             return engine;
         }
         initialized = true;
-        if (!testMode) {
-            mode = loadPersistedMode();
+        if (!testMode && !persistedModeLoaded) {
+            ThemeMode[] loaded = { ThemeMode.DARK };
+            if (tryLoadPersistedMode(loaded)) {
+                // Only mark as loaded on success (including "nothing 
persisted yet"). A read failure leaves this
+                // false so the next reinitialization gets another chance 
instead of pinning the session forever.
+                mode = loaded[0];
+                persistedModeLoaded = true;
+            }
         }
         try {
             StyleEngine e = StyleEngine.create();
+            String stylesheet = mode.id();
             // Read CSS content via Theme's own classloader instead of 
delegating
             // to StyleEngine.loadStylesheet() which uses 
StyleEngine.class.getClassLoader().
             // Under parallel test execution the two classloaders can diverge, 
causing
             // intermittent "resource not found" failures in CI.
-            String cssContent = loadCssResource("tui/themes/" + mode + 
".tcss");
-            e.addStylesheet(mode, cssContent);
-            e.setActiveStylesheet(mode);
+            String cssContent = loadCssResource(mode.stylesheetResource());
+            e.addStylesheet(stylesheet, cssContent);
+            e.setActiveStylesheet(stylesheet);
             engine = e;
         } catch (Exception ex) {
             engine = null;
-            logFallbackOnce(ex);
+            logStylesheetFallbackOnce(ex);
         }
         return engine;
     }
@@ -288,41 +384,61 @@ final class Theme {
         }
     }
 
-    private static String loadPersistedMode() {
-        String[] holder = { DARK };
+    private static boolean tryLoadPersistedMode(ThemeMode[] result) {
         try {
             CommandLineHelper.loadProperties(props -> {
-                String v = props.getProperty(PROP_KEY);
-                if (DARK.equals(v) || LIGHT.equals(v)) {
-                    holder[0] = v;
-                }
+                ThemeMode.parse(props.getProperty(PROP_KEY)).ifPresent(parsed 
-> result[0] = parsed);
             });
+            return true;
         } catch (RuntimeException ex) {
-            // Config unreadable; keep the default mode.
+            logPersistedReadFallbackOnce(ex);
+            return false;
         }
-        return holder[0];
     }
 
-    private static void persist(String newMode) {
+    private static void persist(ThemeMode newMode) {
         try {
             CommandLineHelper.createPropertyFile(false);
             CommandLineHelper.loadProperties(props -> {
-                props.setProperty(PROP_KEY, newMode);
+                props.setProperty(PROP_KEY, newMode.id());
                 CommandLineHelper.storeProperties(props,
                         new Printer.QuietPrinter(new 
Printer.SystemOutPrinter()), false);
             });
         } catch (Exception ex) {
-            logFallbackOnce(ex);
+            logPersistedWriteFallbackOnce(ex);
         }
     }
 
-    private static void logFallbackOnce(Throwable t) {
-        if (!fallbackLogged) {
-            fallbackLogged = true;
+    private static void logStylesheetFallbackOnce(Throwable t) {
+        if (!stylesheetFallbackLogged) {
+            stylesheetFallbackLogged = true;
             LOG.warn("Camel TUI theme stylesheet unavailable; using built-in 
palette", t);
         }
     }
 
+    private static void logTokenFallbackOnce(Throwable t) {
+        if (!tokenFallbackLogged) {
+            tokenFallbackLogged = true;
+            LOG.warn("Camel TUI theme token resolution failed; using built-in 
fallback color", t);
+        }
+    }
+
+    private static void logPersistedReadFallbackOnce(Throwable t) {
+        if (!persistedReadFallbackLogged) {
+            persistedReadFallbackLogged = true;
+            LOG.warn("Camel TUI theme preference could not be read; falling 
back to the default theme for this session",
+                    t);
+        }
+    }
+
+    private static void logPersistedWriteFallbackOnce(Throwable t) {
+        if (!persistedWriteFallbackLogged) {
+            persistedWriteFallbackLogged = true;
+            LOG.warn("Camel TUI theme preference could not be saved; the 
selected theme may not persist across restarts",
+                    t);
+        }
+    }
+
     /** Minimal synthetic {@link Styleable}: an id-only token used for engine 
resolution. */
     private record Token(String id) implements Styleable {
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeMode.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeMode.java
new file mode 100644
index 000000000000..812505ae8233
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeMode.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+/** Canonical dark/light palette selector for the Camel TUI. */
+enum ThemeMode {
+
+    DARK("dark"),
+    LIGHT("light");
+
+    private final String id;
+
+    ThemeMode(String id) {
+        this.id = id;
+    }
+
+    /** Lowercase config / CLI value ({@code dark} or {@code light}). */
+    String id() {
+        return id;
+    }
+
+    /** Classpath resource path of this mode's stylesheet, e.g. {@code 
tui/themes/dark.tcss}. */
+    String stylesheetResource() {
+        return "tui/themes/" + id + ".tcss";
+    }
+
+    // Only two modes exist today, so toggling is a simple flip; revisit this 
if a third mode is ever added.
+    ThemeMode toggle() {
+        return this == DARK ? LIGHT : DARK;
+    }
+
+    static Optional<ThemeMode> parse(String value) {
+        if (value == null) {
+            return Optional.empty();
+        }
+        String normalized = value.strip().toLowerCase(Locale.ROOT);
+        for (ThemeMode mode : values()) {
+            if (mode.id.equals(normalized)) {
+                return Optional.of(mode);
+            }
+        }
+        return Optional.empty();
+    }
+
+    static ThemeMode parseOrDefault(String value) {
+        return parse(value).orElse(DARK);
+    }
+
+    static List<String> ids() {
+        return Arrays.stream(values()).map(m -> m.id).toList();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeCompletionCandidates.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeCompletionCandidates.java
new file mode 100644
index 000000000000..fffab74a9619
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeCompletionCandidates.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.Iterator;
+
+/** Picocli tab-completion candidates for {@code --theme}. */
+public class ThemeModeCompletionCandidates implements Iterable<String> {
+
+    @Override
+    public Iterator<String> iterator() {
+        return ThemeMode.ids().iterator();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
index 623720abfcbd..4b3081693e6d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
@@ -50,6 +50,11 @@ public class TuiCommand extends CamelCommand {
                         arity = "0..1")
     String record;
 
+    @CommandLine.Option(names = { "--theme" },
+                        description = "Color theme: dark or light (overrides 
persisted preference for this session)",
+                        completionCandidates = 
ThemeModeCompletionCandidates.class)
+    String theme;
+
     public TuiCommand(CamelJBangMain main, ClassLoader classLoader) {
         super(main);
         this.classLoader = classLoader;
@@ -76,6 +81,10 @@ public class TuiCommand extends CamelCommand {
             args.add("--record");
             args.add(record);
         }
+        if (theme != null) {
+            args.add("--theme");
+            args.add(theme);
+        }
         CamelMonitor cmd = new CamelMonitor(getMain(), classLoader);
         return new CommandLine(cmd).execute(args.toArray(String[]::new));
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/dark.tcss
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/dark.tcss
index 41c141bc8752..123cf359f33b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/dark.tcss
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/dark.tcss
@@ -17,21 +17,25 @@
 /* Camel TUI dark theme (default). Single source of color truth for Theme 
tokens. */
 
 $brand: #F69123;
+$dark-bg: #1E1E1E;
+$dark-fg: #D4D4D4;
 
 #accent         { color: $brand; }
 #accent-bg      { color: white; background: $brand; text-style: bold; }
 #hint-key       { color: black; background: $brand; text-style: bold; }
-#border         { color: dark-gray; }
+#border         { color: #505050; }
 #border-focused { color: $brand; }
 #title          { color: $brand; text-style: bold; }
-#success        { color: light-green; }
-#warning        { color: light-yellow; }
-#error          { color: light-red; }
-#muted          { text-style: dim; }
-#selection      { color: white; background: blue; text-style: bold; }
-#info           { color: cyan; }
-#notice         { color: magenta; }
-#row-alt        { background: #1C1C1C; }
-#mcp-active     { color: light-green; }
-#mcp-idle       { color: dark-gray; }
-#mcp-down       { color: light-red; }
+#success        { color: #4EC9B0; }
+#warning        { color: #DCDCAA; }
+#error          { color: #F48771; }
+#muted          { color: #808080; }
+#selection      { color: white; background: #264F78; text-style: bold; }
+#info           { color: #9CDCFE; }
+#notice         { color: #C586C0; }
+#row-alt        { background: #252525; }
+#mcp-active     { color: #4EC9B0; }
+#mcp-idle       { color: #606060; }
+#mcp-down       { color: #F48771; }
+#base-bg        { background: $dark-bg; }
+#base-fg        { color: $dark-fg; }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/light.tcss
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/light.tcss
index de7dee5b7a93..1cc88e46e3a4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/light.tcss
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/light.tcss
@@ -17,21 +17,25 @@
 /* Camel TUI light theme. Brand orange stays truecolor; status hues darkened 
for light terminals. */
 
 $brand: #F69123;
+$light-bg: #FFFFFF;
+$light-fg: #24292E;
 
 #accent         { color: $brand; }
 #accent-bg      { color: white; background: $brand; text-style: bold; }
 #hint-key       { color: black; background: $brand; text-style: bold; }
-#border         { color: #888888; }
+#border         { color: #C0C0C0; }
 #border-focused { color: $brand; }
 #title          { color: $brand; text-style: bold; }
-#success        { color: #007700; }
-#warning        { color: #996600; }
-#error          { color: #cc0000; }
-#muted          { color: #666666; }
-#selection      { color: white; background: blue; text-style: bold; }
-#info           { color: #006688; }
-#notice         { color: #884488; }
-#row-alt        { background: #EBEBEB; }
-#mcp-active     { color: #007700; }
-#mcp-idle       { color: #888888; }
-#mcp-down       { color: #cc0000; }
+#success        { color: #22863A; }
+#warning        { color: #B08800; }
+#error          { color: #D73A49; }
+#muted          { color: #6A737D; }
+#selection      { color: white; background: #0366D6; text-style: bold; }
+#info           { color: #0366D6; }
+#notice         { color: #6F42C1; }
+#row-alt        { background: #F6F8FA; }
+#mcp-active     { color: #22863A; }
+#mcp-idle       { color: #959DA5; }
+#mcp-down       { color: #D73A49; }
+#base-bg        { background: $light-bg; }
+#base-fg        { color: $light-fg; }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
index 6573b8d00bd8..0f2d3b99ec57 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
@@ -19,19 +19,49 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayList;
 import java.util.List;
 
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
 import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+import picocli.CommandLine;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+@Isolated
 class CamelMonitorTest {
 
+    @BeforeEach
+    void resetTheme() {
+        Theme.resetForTesting();
+        Theme.setMode("dark");
+    }
+
+    // --theme is validated at the very top of doCall(), before any 
TUI/terminal setup, so an invalid value
+    // must be rejected immediately without needing a real terminal.
+
+    @Test
+    void doCallRejectsInvalidThemeBeforeAnyTuiSetup() {
+        CamelMonitor monitor = new CamelMonitor(new CamelJBangMain(), 
Thread.currentThread().getContextClassLoader());
+        monitor.theme = "sepia";
+
+        CommandLine.ParameterException ex = 
assertThrows(CommandLine.ParameterException.class, monitor::doCall,
+                "an invalid --theme value must be rejected before any 
TUI/terminal setup runs");
+        assertTrue(ex.getMessage().contains("sepia"), "the rejected value 
should be echoed back in the error message");
+    }
+
     // '?' is an alternative to F1 for opening the help overlay. Unlike F1, it 
must be suppressed
     // while a text input is focused, otherwise typing '?' in a search or 
probe field would
     // pop up help instead of inserting the character.
@@ -199,4 +229,77 @@ class CamelMonitorTest {
         assertEquals(-1, CamelMonitor.footerRegionAt(startX, endX, 18), 
"column 18 is past the second region");
         assertEquals(-1, CamelMonitor.footerRegionAt(null, endX, 3), "no 
captured geometry yet");
     }
+
+    // applyThemeBaseColors patches unset terminal cells with the active theme 
canvas colors while
+    // preserving widget-specific foreground and background styles.
+
+    @Test
+    void applyThemeBaseColorsPatchesUnsetCells() {
+        Rect area = new Rect(0, 0, 4, 2);
+        Buffer buffer = Buffer.empty(area);
+
+        CamelMonitor.applyThemeBaseColors(buffer, area);
+
+        Cell cell = buffer.get(0, 0);
+        assertEquals(Theme.baseBg(), cell.style().bg().orElse(null), "unset 
cells receive the theme background");
+        assertEquals(Theme.baseFg(), cell.style().fg().orElse(null), "unset 
cells receive the theme foreground");
+    }
+
+    @Test
+    void applyThemeBaseColorsPreservesExplicitSelectionStyle() {
+        Rect area = new Rect(0, 0, 4, 2);
+        Buffer buffer = Buffer.empty(area);
+        Style selection = Theme.selectionBg();
+        buffer.setString(1, 0, "X", selection);
+
+        CamelMonitor.applyThemeBaseColors(buffer, area);
+
+        Cell cell = buffer.get(1, 0);
+        assertEquals(selection.bg().orElse(null), 
cell.style().bg().orElse(null), "selection background is preserved");
+        assertEquals(selection.fg().orElse(null), 
cell.style().fg().orElse(null), "selection foreground is preserved");
+    }
+
+    @Test
+    void applyThemeBaseColorsPatchesOnlyMissingBackground() {
+        Rect area = new Rect(0, 0, 4, 2);
+        Buffer buffer = Buffer.empty(area);
+        Color explicitFg = Color.rgb(0xFF, 0x00, 0x00);
+        buffer.setString(2, 0, "Y", Style.EMPTY.fg(explicitFg));
+
+        CamelMonitor.applyThemeBaseColors(buffer, area);
+
+        Cell cell = buffer.get(2, 0);
+        assertEquals(explicitFg, cell.style().fg().orElse(null), "explicit 
foreground is preserved");
+        assertEquals(Theme.baseBg(), cell.style().bg().orElse(null), "missing 
background is patched");
+    }
+
+    @Test
+    void applyThemeBaseColorsPatchesOnlyMissingForeground() {
+        Rect area = new Rect(0, 0, 4, 2);
+        Buffer buffer = Buffer.empty(area);
+        Color explicitBg = Color.rgb(0x00, 0x00, 0xFF);
+        buffer.setString(3, 0, "Z", Style.EMPTY.bg(explicitBg));
+
+        CamelMonitor.applyThemeBaseColors(buffer, area);
+
+        Cell cell = buffer.get(3, 0);
+        assertEquals(explicitBg, cell.style().bg().orElse(null), "explicit 
background is preserved");
+        assertEquals(Theme.baseFg(), cell.style().fg().orElse(null), "missing 
foreground is patched");
+    }
+
+    @Test
+    void applyThemeBaseColorsUsesActiveThemePalette() {
+        Rect area = new Rect(0, 0, 2, 1);
+        Buffer buffer = Buffer.empty(area);
+        Theme.setMode("dark");
+        Color darkBg = Theme.baseBg();
+
+        Theme.setMode("light");
+        CamelMonitor.applyThemeBaseColors(buffer, area);
+
+        Cell cell = buffer.get(0, 0);
+        assertEquals(Theme.baseBg(), cell.style().bg().orElse(null), "light 
palette background is applied");
+        assertEquals(Theme.baseFg(), cell.style().fg().orElse(null), "light 
palette foreground is applied");
+        assertFalse(darkBg.equals(cell.style().bg().orElse(null)), "patched 
background must follow the active theme");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeCompletionCandidatesTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeCompletionCandidatesTest.java
new file mode 100644
index 000000000000..266a0d083901
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeCompletionCandidatesTest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ThemeModeCompletionCandidatesTest {
+
+    @Test
+    void offersDarkAndLightForShellCompletion() {
+        List<String> candidates = new ArrayList<>();
+        new ThemeModeCompletionCandidates().forEach(candidates::add);
+
+        assertEquals(List.of("dark", "light"), candidates);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeTest.java
new file mode 100644
index 000000000000..e3a104eece5e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeModeTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+class ThemeModeTest {
+
+    @Test
+    void parseAcceptsCanonicalIds() {
+        assertEquals(Optional.of(ThemeMode.DARK), ThemeMode.parse("dark"));
+        assertEquals(Optional.of(ThemeMode.LIGHT), ThemeMode.parse("light"));
+    }
+
+    @Test
+    void parseIsCaseInsensitive() {
+        assertEquals(Optional.of(ThemeMode.LIGHT), ThemeMode.parse("LIGHT"));
+        assertEquals(Optional.of(ThemeMode.LIGHT), ThemeMode.parse("LiGhT"));
+    }
+
+    @Test
+    void parseStripsSurroundingWhitespace() {
+        assertEquals(Optional.of(ThemeMode.DARK), ThemeMode.parse("  dark  "));
+        assertEquals(Optional.of(ThemeMode.DARK), ThemeMode.parse("\tdark\n"));
+    }
+
+    @Test
+    void parseRejectsUnknownOrNullValues() {
+        assertEquals(Optional.empty(), ThemeMode.parse("sepia"));
+        assertEquals(Optional.empty(), ThemeMode.parse(""));
+        assertEquals(Optional.empty(), ThemeMode.parse(null));
+    }
+
+    @Test
+    void parseOrDefaultFallsBackToDarkForUnknownValues() {
+        assertEquals(ThemeMode.DARK, ThemeMode.parseOrDefault("sepia"));
+        assertEquals(ThemeMode.DARK, ThemeMode.parseOrDefault(null));
+        assertEquals(ThemeMode.LIGHT, ThemeMode.parseOrDefault("light"));
+    }
+
+    @Test
+    void toggleFlipsBetweenTheTwoModes() {
+        assertEquals(ThemeMode.LIGHT, ThemeMode.DARK.toggle());
+        assertEquals(ThemeMode.DARK, ThemeMode.LIGHT.toggle());
+    }
+
+    @Test
+    void idsListsBothCanonicalValuesInDeclarationOrder() {
+        assertEquals(List.of("dark", "light"), ThemeMode.ids());
+    }
+
+    @Test
+    void stylesheetResourcePointsAtTheModeSpecificTcssFile() {
+        assertEquals("tui/themes/dark.tcss", 
ThemeMode.DARK.stylesheetResource());
+        assertEquals("tui/themes/light.tcss", 
ThemeMode.LIGHT.stylesheetResource());
+    }
+
+    @Test
+    void idIsDistinctLowercaseCanonicalFormPerMode() {
+        assertEquals("dark", ThemeMode.DARK.id());
+        assertEquals("light", ThemeMode.LIGHT.id());
+        assertNotEquals(ThemeMode.DARK.id(), ThemeMode.LIGHT.id());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
index 2416608df439..91c020f5371e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
@@ -16,16 +16,24 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.api.parallel.Isolated;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @Isolated
 class ThemeTest {
@@ -55,20 +63,22 @@ class ThemeTest {
         Theme.setMode("dark");
         assertEquals(Style.EMPTY.fg(Color.WHITE).bg(Theme.ACCENT).bold(), 
Theme.accentBg());
         assertEquals(Style.EMPTY.fg(Color.BLACK).bg(Theme.ACCENT).bold(), 
Theme.hintKey());
-        assertEquals(Style.EMPTY.fg(Color.DARK_GRAY), Theme.border());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x50, 0x50, 0x50)), 
Theme.border());
         assertEquals(Style.EMPTY.fg(Theme.ACCENT), Theme.borderFocused());
         assertEquals(Style.EMPTY.fg(Theme.ACCENT).bold(), Theme.title());
-        assertEquals(Style.EMPTY.fg(Color.LIGHT_GREEN), Theme.success());
-        assertEquals(Style.EMPTY.fg(Color.LIGHT_YELLOW), Theme.warning());
-        assertEquals(Style.EMPTY.fg(Color.LIGHT_RED), Theme.error());
-        assertEquals(Style.EMPTY.dim(), Theme.muted());
-        assertEquals(Style.EMPTY.fg(Color.WHITE).bold().onBlue(), 
Theme.selectionBg());
-        assertEquals(Style.EMPTY.fg(Color.CYAN), Theme.info());
-        assertEquals(Style.EMPTY.fg(Color.MAGENTA), Theme.notice());
-        assertEquals(Style.EMPTY.fg(Color.LIGHT_GREEN), Theme.mcpActive());
-        assertEquals(Style.EMPTY.fg(Color.DARK_GRAY), Theme.mcpIdle());
-        assertEquals(Style.EMPTY.fg(Color.LIGHT_RED), Theme.mcpDown());
-        assertEquals(Color.rgb(0x1C, 0x1C, 0x1C), Theme.zebra());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x4E, 0xC9, 0xB0)), 
Theme.success());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0xDC, 0xDC, 0xAA)), 
Theme.warning());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0xF4, 0x87, 0x71)), 
Theme.error());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x80, 0x80, 0x80)), 
Theme.muted());
+        assertEquals(Style.EMPTY.fg(Color.WHITE).bg(Color.rgb(0x26, 0x4F, 
0x78)).bold(), Theme.selectionBg());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x9C, 0xDC, 0xFE)), 
Theme.info());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0xC5, 0x86, 0xC0)), 
Theme.notice());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x4E, 0xC9, 0xB0)), 
Theme.mcpActive());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x60, 0x60, 0x60)), 
Theme.mcpIdle());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0xF4, 0x87, 0x71)), 
Theme.mcpDown());
+        assertEquals(Color.rgb(0x25, 0x25, 0x25), Theme.zebra());
+        assertEquals(Color.rgb(0x1E, 0x1E, 0x1E), Theme.baseBg());
+        assertEquals(Color.rgb(0xD4, 0xD4, 0xD4), Theme.baseFg());
     }
 
     @Test
@@ -76,14 +86,17 @@ class ThemeTest {
         // Light theme: status hues are explicit dark hex; brand orange is 
unchanged.
         Theme.setMode("light");
         assertEquals(Color.rgb(0xF6, 0x91, 0x23), Theme.accent());
-        assertEquals(Style.EMPTY.fg(Color.rgb(0x00, 0x77, 0x00)), 
Theme.success());
-        assertEquals(Style.EMPTY.fg(Color.rgb(0x88, 0x88, 0x88)), 
Theme.border());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x22, 0x86, 0x3A)), 
Theme.success());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0xC0, 0xC0, 0xC0)), 
Theme.border());
         // MCP indicator hues track the light palette: idle gray and down red 
differ from the dark ANSI variants.
-        assertEquals(Style.EMPTY.fg(Color.rgb(0x00, 0x77, 0x00)), 
Theme.mcpActive());
-        assertEquals(Style.EMPTY.fg(Color.rgb(0x88, 0x88, 0x88)), 
Theme.mcpIdle());
-        assertEquals(Style.EMPTY.fg(Color.rgb(0xcc, 0x00, 0x00)), 
Theme.mcpDown());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x22, 0x86, 0x3A)), 
Theme.mcpActive());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0x95, 0x9D, 0xA5)), 
Theme.mcpIdle());
+        assertEquals(Style.EMPTY.fg(Color.rgb(0xD7, 0x3A, 0x49)), 
Theme.mcpDown());
         // Zebra background is theme-aware: light gray on light, unlike the 
dark gray used on dark.
-        assertEquals(Color.rgb(0xEB, 0xEB, 0xEB), Theme.zebra());
+        assertEquals(Color.rgb(0xF6, 0xF8, 0xFA), Theme.zebra());
+        // Base colors differ significantly between themes: white background 
and dark text on light mode.
+        assertEquals(Color.rgb(0xFF, 0xFF, 0xFF), Theme.baseBg());
+        assertEquals(Color.rgb(0x24, 0x29, 0x2E), Theme.baseFg());
     }
 
     @Test
@@ -108,6 +121,111 @@ class ThemeTest {
         assertEquals("dark", Theme.mode());
     }
 
+    @Test
+    void applyStartupModeSelectsLightPalette() {
+        Theme.setMode("dark");
+        Theme.applyStartupMode("light");
+        assertEquals("light", Theme.mode());
+        assertEquals(Color.rgb(0xFF, 0xFF, 0xFF), Theme.baseBg());
+    }
+
+    @Test
+    void applyStartupModeAcceptsMixedCase() {
+        Theme.applyStartupMode("LIGHT");
+        assertEquals("light", Theme.mode());
+    }
+
+    @Test
+    void applyStartupModeRejectsUnknownValue() {
+        // Unlike setMode(), an invalid --theme value must never be silently 
coerced to a default: CamelMonitor relies
+        // on isValidMode() for a friendly CLI error, but the type itself must 
also refuse to guess.
+        assertThrows(IllegalArgumentException.class, () -> 
Theme.applyStartupMode("sepia"));
+    }
+
+    @Test
+    void setModeAcceptsMixedCase() {
+        Theme.setMode("LIGHT");
+        assertEquals("light", Theme.mode());
+    }
+
+    @Test
+    void isValidModeAcceptsDarkAndLightOnly() {
+        assertTrue(Theme.isValidMode("dark"));
+        assertTrue(Theme.isValidMode("LIGHT"));
+        assertFalse(Theme.isValidMode("sepia"));
+        assertFalse(Theme.isValidMode(null));
+    }
+
+    @Test
+    void toggleTakesEffectOnFirstActivationOutsideTestMode(@TempDir Path 
tempDir) {
+        // Regression test for the "toggle needs two activations" bug: every 
other test here runs through
+        // Theme.resetForTesting(), which forces testMode=true and 
short-circuits both persist() and the
+        // persisted-mode disk-reload guard in engine() - exactly the two 
paths responsible for the original bug.
+        // This test leaves testMode disabled, against an isolated home 
directory, to actually exercise them.
+        String originalHomeDir = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(tempDir.toString());
+        try {
+            Theme.resetForTesting(false);
+            Theme.setMode("dark");
+            assertEquals("dark", Theme.mode());
+
+            String newMode = Theme.toggle();
+
+            assertEquals("light", newMode, "a single toggle() call must flip 
the mode immediately");
+            assertEquals("light", Theme.mode(), "the effective mode must 
reflect the toggle without a second activation");
+        } finally {
+            CommandLineHelper.useHomeDir(originalHomeDir);
+            Theme.resetForTesting();
+        }
+    }
+
+    @Test
+    void persistedModeReadFailureFallsBackAndRetriesOnNextInit(@TempDir Path 
tempDir) throws Exception {
+        // Regression test for the read-failure branch of 
tryLoadPersistedMode(): a corrupt config file must not crash
+        // the TUI, and must not permanently pin the session to the default 
theme - the very next reinitialization
+        // (triggered here by setMode()) should retry the disk read and pick 
up a since-fixed value.
+        String originalHomeDir = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(tempDir.toString());
+        try {
+            Path configFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+            Files.writeString(configFile, "camel.tui.theme=\\uZZZZ\n");
+
+            Theme.resetForTesting(false);
+            assertEquals("dark", Theme.mode(), "a corrupt config file must 
fall back to the default theme, not crash");
+
+            Files.writeString(configFile, "camel.tui.theme=light\n");
+            Theme.setMode("dark");
+
+            assertEquals("light", Theme.mode(),
+                    "a later reinitialization must retry the persisted-mode 
read instead of being pinned by the earlier failure");
+        } finally {
+            CommandLineHelper.useHomeDir(originalHomeDir);
+            Theme.resetForTesting();
+        }
+    }
+
+    @Test
+    void applyStartupModeOverridesPersistedConfigOutsideTestMode(@TempDir Path 
tempDir) throws Exception {
+        // Regression test for the "--theme wins over camel.tui.theme" 
contract: exercised against real disk I/O
+        // (outside test mode), unlike applyStartupModeSelectsLightPalette 
which runs under testMode=true, where
+        // persistence loading is unconditionally skipped and so can't prove 
an override actually happened.
+        String originalHomeDir = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(tempDir.toString());
+        try {
+            Path configFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+            Files.writeString(configFile, "camel.tui.theme=light\n");
+
+            Theme.resetForTesting(false);
+            Theme.applyStartupMode("dark");
+
+            assertEquals("dark", Theme.mode(), "--theme must win over a 
persisted camel.tui.theme value");
+            assertEquals(Color.rgb(0x1E, 0x1E, 0x1E), Theme.baseBg(), "the 
dark palette must actually be active");
+        } finally {
+            CommandLineHelper.useHomeDir(originalHomeDir);
+            Theme.resetForTesting();
+        }
+    }
+
     @Test
     void accessorsNeverReturnNull() {
         // Resilience: even when resolution is exercised the palette is always 
usable.
@@ -128,5 +246,7 @@ class ThemeTest {
         assertNotNull(Theme.mcpIdle());
         assertNotNull(Theme.mcpDown());
         assertNotNull(Theme.zebra());
+        assertNotNull(Theme.baseBg());
+        assertNotNull(Theme.baseFg());
     }
 }

Reply via email to