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 54307da230f9 CAMEL-23839: camel-jbang TUI - switchable light/dark CSS
theme
54307da230f9 is described below
commit 54307da230f9a31e13ce158b1d686ac580119718
Author: Adriano Machado <[email protected]>
AuthorDate: Wed Jul 1 08:41:21 2026 -0400
CAMEL-23839: camel-jbang TUI - switchable light/dark CSS theme
Introduce CSS-backed light/dark theme for the TUI, replacing hard-coded
inline colors with a central semantic Theme. Two stylesheets ship
(dark.tcss / light.tcss) toggled at runtime with F4, persisted in user
config. Includes visual polish: active-tab highlight, zebra rows,
focus-aware borders, shared empty-state, and MCP indicator theming.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 3 +
.../modules/ROOT/pages/camel-jbang-tui.adoc | 12 +
dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml | 5 +
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 12 +-
.../dsl/jbang/core/commands/tui/AiLogPopup.java | 2 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 67 +++--
.../jbang/core/commands/tui/CaptionOverlay.java | 2 +-
.../dsl/jbang/core/commands/tui/ClasspathTab.java | 2 +-
.../core/commands/tui/DataRefreshService.java | 22 +-
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 2 +-
.../dsl/jbang/core/commands/tui/FormHelper.java | 2 +-
.../dsl/jbang/core/commands/tui/HelpOverlay.java | 4 +-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 2 +-
.../camel/dsl/jbang/core/commands/tui/HttpTab.java | 2 +-
.../dsl/jbang/core/commands/tui/McpFacade.java | 5 +-
.../dsl/jbang/core/commands/tui/McpLogPopup.java | 2 +-
.../jbang/core/commands/tui/MonitorContext.java | 36 ++-
.../dsl/jbang/core/commands/tui/OverviewTab.java | 73 ++++-
.../jbang/core/commands/tui/RunOptionsForm.java | 6 +-
.../jbang/core/commands/tui/SearchHighlighter.java | 6 +-
.../jbang/core/commands/tui/SendMessagePopup.java | 2 +-
.../dsl/jbang/core/commands/tui/ShellPanel.java | 18 +-
.../camel/dsl/jbang/core/commands/tui/Theme.java | 309 +++++++++++++++++++++
.../src/main/resources/tui/themes/dark.tcss | 37 +++
.../src/main/resources/tui/themes/light.tcss | 37 +++
.../core/commands/tui/ShellPanelColorTest.java | 2 +-
.../core/commands/tui/SyntaxHighlighterTest.java | 3 +-
.../dsl/jbang/core/commands/tui/ThemeTest.java | 145 ++++++++++
pom.xml | 1 +
29 files changed, 724 insertions(+), 97 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index 4e8e7d3121d3..2b7ffa53e1b4 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -32,6 +32,9 @@ The project exported by `camel export` now includes
additional guidance for AI c
`readme.md` gained a _For AI coding assistants_ section linking to the Apache
Camel website LLM index,
and a new `AGENTS.md` file is generated at the project root. This applies to
all runtimes (Camel Main, Spring Boot and Quarkus).
+The Camel TUI (`camel tui`) theme is now backed by CSS stylesheets and ships a
switchable light/dark palette.
+Press *F4* to toggle at runtime; the selection is persisted as
`camel.tui.theme` in `.camel-jbang-user.properties`.
+
=== camel-micrometer
The `MicrometerExchangeEventNotifier` now always includes the `routeId` tag on
exchange event metrics.
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 bfe2225b3b38..f2dc24b20ab6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -303,6 +303,17 @@ The Doctor checks your development environment and reports
issues:
* Common port conflicts (8080, 8443, 9090)
* Disk space in temp directory
+== 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.
+
+The brand orange accent is identical in both themes; status colors (success,
+warning, error) and borders adapt for readability on dark and light terminals.
+
+Your choice is remembered: it is saved as `camel.tui.theme` (`dark` or `light`)
+in `.camel-jbang-user.properties` and restored the next time you open the TUI.
+
== Keyboard Shortcuts
=== Global (All Tabs)
@@ -316,6 +327,7 @@ The Doctor checks your development environment and reports
issues:
| *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
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
index 62014014c056..fc0f12a27cc5 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
@@ -58,6 +58,11 @@
<artifactId>tamboui-tui</artifactId>
<version>${tamboui-version}</version>
</dependency>
+ <dependency>
+ <groupId>dev.tamboui</groupId>
+ <artifactId>tamboui-css</artifactId>
+ <version>${tamboui-version}</version>
+ </dependency>
<dependency>
<groupId>dev.tamboui</groupId>
<artifactId>tamboui-widgets</artifactId>
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 cce76250427d..38823a0bca25 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
@@ -497,7 +497,7 @@ class ActionsPopup {
} else if (ke.isConfirm()) {
selectInfraService();
} else if (ke.code() == KeyCode.CHAR) {
- jumpToInfraService(ke.character());
+ jumpToInfraService(ke.string().charAt(0));
}
return true;
}
@@ -1346,7 +1346,7 @@ class ActionsPopup {
} else if (ke.isEnd()) {
folderInputState.moveCursorToEnd();
} else if (ke.code() == KeyCode.CHAR) {
- folderInputState.insert(ke.character());
+ folderInputState.insert(ke.string().charAt(0));
}
}
@@ -1853,9 +1853,9 @@ class ActionsPopup {
String impl =
selectedInfraService.implementations.get(infraImplIndex);
Rect implArea = new Rect(ix + labelW, row, fieldW, 1);
frame.renderWidget(Paragraph.from(Line.from(
- Span.styled("◀ ", MonitorContext.HINT_KEY_STYLE),
+ Span.styled("◀ ", Theme.hintKey()),
Span.raw(impl),
- Span.styled(" ▶", MonitorContext.HINT_KEY_STYLE))),
implArea);
+ Span.styled(" ▶", Theme.hintKey()))), implArea);
row++;
}
@@ -1887,8 +1887,8 @@ class ActionsPopup {
infraPortState.moveCursorToStart();
} else if (ke.isEnd()) {
infraPortState.moveCursorToEnd();
- } else if (ke.code() == KeyCode.CHAR &&
Character.isDigit(ke.character())) {
- infraPortState.insert(ke.character());
+ } else if (ke.code() == KeyCode.CHAR &&
Character.isDigit(ke.string().charAt(0))) {
+ infraPortState.insert(ke.string().charAt(0));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
index 147c75a51a69..d482f8247e29 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
@@ -104,7 +104,7 @@ class AiLogPopup {
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(" AI Log ")
.titleBottom(Title.from(Line.from(
- Span.styled(" Esc",
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+ Span.styled(" Esc", Theme.hintKey()), Span.raw("
back "))))
.build();
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
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 37d15040f9c9..fd2b9b9c0339 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
@@ -131,7 +131,7 @@ public class CamelMonitor extends CamelCommand {
private final FilesBrowser filesBrowser = new FilesBrowser();
private PopupManager popupManager;
- private ClassLoader classLoader;
+ private final ClassLoader classLoader;
public CamelMonitor(CamelJBangMain main, ClassLoader classLoader) {
super(main);
@@ -595,6 +595,10 @@ public class CamelMonitor extends CamelCommand {
}
return true;
}
+ if (!textEditing && ke.isKey(KeyCode.F4)) {
+ Theme.toggle();
+ return true;
+ }
if (ke.isKey(KeyCode.F5) && ke.hasShift()) {
recordingManager.takeScreenshot();
return true;
@@ -851,28 +855,28 @@ public class CamelMonitor extends CamelCommand {
long activeCount = infos.stream().filter(i -> !i.vanishing).count();
List<Span> titleSpans = new ArrayList<>();
- titleSpans.add(Span.styled(" Camel TUI",
Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold()));
+ titleSpans.add(Span.styled(" Camel TUI", Theme.title()));
titleSpans.add(Span.raw(" "));
- titleSpans.add(Span.styled(camelVersion != null ? "v" + camelVersion :
"", Style.EMPTY.fg(Color.GREEN)));
+ titleSpans.add(Span.styled(camelVersion != null ? "v" + camelVersion :
"", Theme.success()));
titleSpans.add(Span.raw(" "));
- titleSpans.add(Span.styled(activeCount + " integration(s)",
Style.EMPTY.fg(Color.CYAN)));
+ titleSpans.add(Span.styled(activeCount + " integration(s)",
Theme.info()));
long activeInfra = dataService.infraData().get().stream().filter(i ->
!i.vanishing).count();
if (activeInfra > 0) {
titleSpans.add(Span.raw(" "));
- titleSpans.add(Span.styled(activeInfra + " infra(s)",
Style.EMPTY.fg(Color.MAGENTA)));
+ titleSpans.add(Span.styled(activeInfra + " infra(s)",
Theme.notice()));
}
if (ctx.selectedPid != null) {
titleSpans.add(Span.raw(" "));
InfraInfo selInfra = findSelectedInfra();
if (selInfra != null) {
- titleSpans.add(Span.styled("selected: " + selectedName(),
Style.EMPTY.fg(Color.MAGENTA)));
+ titleSpans.add(Span.styled("selected: " + selectedName(),
Theme.notice()));
} else {
- titleSpans.add(Span.styled("selected: " + selectedName(),
Style.EMPTY.fg(Color.YELLOW)));
+ titleSpans.add(Span.styled("selected: " + selectedName(),
Theme.warning()));
}
}
if (actionsPopup.notification() != null) {
titleSpans.add(Span.raw(" "));
- Style style = actionsPopup.notificationError() ?
Style.EMPTY.fg(Color.RED) : Style.EMPTY.fg(Color.GREEN);
+ Style style = actionsPopup.notificationError() ? Theme.error() :
Theme.success();
titleSpans.add(Span.styled(actionsPopup.notification(), style));
}
if (monitorNotification != null) {
@@ -880,7 +884,7 @@ public class CamelMonitor extends CamelCommand {
monitorNotification = null;
} else {
titleSpans.add(Span.raw(" "));
- Style style = monitorNotificationError ?
Style.EMPTY.fg(Color.RED) : Style.EMPTY.fg(Color.GREEN);
+ Style style = monitorNotificationError ? Theme.error() :
Theme.success();
titleSpans.add(Span.styled(monitorNotification, style));
}
}
@@ -933,7 +937,7 @@ public class CamelMonitor extends CamelCommand {
private void renderTabs(Frame frame, Rect area) {
boolean compact = area.width() < TABS_FULL_MIN_WIDTH;
String dividerStr = compact ? "|" : " | ";
- Span divider = Span.styled(dividerStr, Style.EMPTY.dim());
+ Span divider = Span.styled(dividerStr, Theme.muted());
boolean infraSelected = isInfraSelected();
if (infraSelected) {
@@ -954,7 +958,7 @@ public class CamelMonitor extends CamelCommand {
Tabs tabs = Tabs.builder()
.titles(labels)
- .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91,
0x23)).bold())
+ .highlightStyle(Theme.accentBg())
.divider(divider)
.build();
@@ -994,7 +998,7 @@ public class CamelMonitor extends CamelCommand {
Tabs tabs = Tabs.builder()
.titles(labels)
- .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91,
0x23)).bold())
+ .highlightStyle(Theme.accentBg())
.divider(divider)
.build();
@@ -1253,8 +1257,8 @@ public class CamelMonitor extends CamelCommand {
if (cmdOpt.isPresent() && argsOpt.isPresent() &&
argsOpt.get().length > 0) {
cmd.add(cmdOpt.get());
Collections.addAll(cmd, argsOpt.get());
- } else if (cmdLineOpt.isPresent()) {
- cmd.addAll(parseCommandLine(cmdLineOpt.get()));
+ } else {
+ cmdLineOpt.ifPresent(s ->
cmd.addAll(parseCommandLine(s)));
}
if (cmd.isEmpty()) {
@@ -1346,7 +1350,7 @@ public class CamelMonitor extends CamelCommand {
String msg = recordingManager.screenshotFlashMessage();
if (msg != null) {
frame.renderWidget(
- Paragraph.from(Line.from(Span.styled(" " + msg,
Style.EMPTY.fg(Color.GREEN)))),
+ Paragraph.from(Line.from(Span.styled(" " + msg,
Theme.success()))),
area);
return;
}
@@ -1401,8 +1405,8 @@ public class CamelMonitor extends CamelCommand {
for (RecordingManager.KeyRecord kr : visible) {
long age = now - kr.timestamp();
Style style = age < 1000
- ? Style.EMPTY.fg(Color.WHITE).bold().onBlue()
- : Style.EMPTY.dim();
+ ? Theme.selectionBg()
+ : Theme.muted();
rightSpans.add(Span.styled(" " + kr.label() + " ", style));
}
}
@@ -1420,17 +1424,17 @@ public class CamelMonitor extends CamelCommand {
if (client != null) {
suffix = active ? " ●" : " ○";
mcpLabel += " (" + client + ")";
- labelStyle = Style.EMPTY.fg(Color.GREEN);
- suffixStyle = Style.EMPTY.fg(active ? Color.GREEN :
Color.DARK_GRAY);
+ labelStyle = Theme.success();
+ suffixStyle = active ? Theme.mcpActive() : Theme.mcpIdle();
} else {
suffix = " ✗";
- labelStyle = Style.EMPTY.dim();
- suffixStyle = Style.EMPTY.fg(Color.RED);
+ labelStyle = Theme.muted();
+ suffixStyle = Theme.mcpDown();
}
rightSpans.add(Span.styled(mcpLabel, labelStyle));
rightSpans.add(Span.styled(suffix, suffixStyle));
if (client == null) {
- rightSpans.add(Span.styled(" F2 → Setup AI",
Style.EMPTY.dim()));
+ rightSpans.add(Span.styled(" F2 → Setup AI", Theme.muted()));
}
}
@@ -1476,6 +1480,7 @@ public class CamelMonitor extends CamelCommand {
}
hint(fKeySpans, "F6", "shell");
hint(fKeySpans, "F8", "AI");
+ hint(fKeySpans, "F4", "theme");
spans.addAll(insertPos, fKeySpans);
// Return total F-key span count. The footer drop loop uses this to
remove pairs from
// the tail (F6, then F3, F2), stopping before the first pair (F1 help
when present).
@@ -1601,14 +1606,16 @@ public class CamelMonitor extends CamelCommand {
private static Path writeMcpJson(int port) {
Path path = Path.of(".mcp.json");
try {
- String json = "{\n"
- + " \"mcpServers\": {\n"
- + " \"camel-tui\": {\n"
- + " \"type\": \"http\",\n"
- + " \"url\": \"http://localhost:" + port +
"/mcp\"\n"
- + " }\n"
- + " }\n"
- + "}\n";
+ String json = """
+ {
+ "mcpServers": {
+ "camel-tui": {
+ "type": "http",
+ "url": "http://localhost:%d/mcp"
+ }
+ }
+ }
+ """.formatted(port);
Files.writeString(path, json);
return path;
} catch (IOException e) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
index c728c4b1ecda..585658274a3a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
@@ -107,7 +107,7 @@ class CaptionOverlay {
inlineLastKeystroke = System.currentTimeMillis();
}
} else if (ke.code() == KeyCode.CHAR) {
- inlineBuffer.append(ke.character());
+ inlineBuffer.append(ke.string());
captionText = inlineBuffer.toString();
inlineLastKeystroke = System.currentTimeMillis();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index ebb2c2deec63..538b2def9ff9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -110,7 +110,7 @@ class ClasspathTab implements MonitorTab {
return true;
}
if (ke.code() == KeyCode.CHAR) {
- fuzzyFilter.appendChar(ke.character());
+ fuzzyFilter.appendChar(ke.string().charAt(0));
refilter();
return true;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index 1120c041aa92..6005b5aee136 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -331,12 +331,9 @@ class DataRefreshService {
boolean stillAlive = infos.stream()
.anyMatch(i -> ctx.selectedPid.equals(i.pid) &&
!i.vanishing);
if (!stillAlive) {
- IntegrationInfo gone = infos.stream()
+ infos.stream()
.filter(i -> ctx.selectedPid.equals(i.pid))
- .findFirst().orElse(null);
- if (gone != null) {
- ctx.lastSelectedName = gone.name;
- }
+ .findFirst().ifPresent(gone -> ctx.lastSelectedName =
gone.name);
ctx.selectedPid = null;
}
}
@@ -368,10 +365,10 @@ class DataRefreshService {
}
if (ctx.selectedPid == null && !infraData.get().isEmpty()
- && infos.stream().noneMatch(i -> !i.vanishing)) {
+ && infos.stream().allMatch(i -> i.vanishing)) {
List<InfraInfo> infras = infraData.get();
if (!infras.isEmpty()) {
- int firstInfraIndex = infos.size() + (infras.size() > 0 ? 1 :
0);
+ int firstInfraIndex = infos.size() + 1;
refreshCtx.onInfraAutoSelected(firstInfraIndex,
infras.get(0).pid);
}
}
@@ -379,7 +376,6 @@ class DataRefreshService {
// ---- Infra data ----
- @SuppressWarnings("unchecked")
private void refreshInfraData() {
List<InfraInfo> infraInfos = new ArrayList<>();
try {
@@ -493,7 +489,6 @@ class DataRefreshService {
traces.set(allTraces);
}
- @SuppressWarnings("unchecked")
private void readTraceFile(String pid, List<TraceEntry> allTraces) {
Path traceFile = CommandLineHelper.getCamelDir().resolve(pid +
"-trace.json");
if (!Files.exists(traceFile)) {
@@ -531,6 +526,9 @@ class DataRefreshService {
}
try {
JsonObject json = (JsonObject) Jsoner.deserialize(line);
+ if (json == null) {
+ continue;
+ }
Object tracesArray = json.get("traces");
if (tracesArray instanceof List<?> traceList) {
for (Object traceObj : traceList) {
@@ -559,7 +557,6 @@ class DataRefreshService {
// ---- Span data ----
- @SuppressWarnings("unchecked")
void refreshSpanData() {
String pid = ctx.selectedPid;
if (pid == null) {
@@ -585,8 +582,8 @@ class DataRefreshService {
JsonArray arr = response.getCollection("spans");
if (arr != null) {
List<SpanEntry> entries = new ArrayList<>();
- for (int i = 0; i < arr.size(); i++) {
- JsonObject spanObj = (JsonObject) arr.get(i);
+ for (Object o : arr) {
+ JsonObject spanObj = (JsonObject) o;
entries.add(SpanEntry.fromJson(spanObj));
}
otelSpans.set(entries);
@@ -625,7 +622,6 @@ class DataRefreshService {
* Load history data for the given PIDs and return the parsed entries. The
caller is responsible for storing the
* entries (e.g. on HistoryTab).
*/
- @SuppressWarnings("unchecked")
List<HistoryEntry> loadHistoryData(List<Long> pids) {
List<HistoryEntry> allEntries = new ArrayList<>();
for (Long pid : pids) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index c8d142196c91..1aa94bbe4439 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -671,7 +671,7 @@ class ErrorsTab implements MonitorTab {
}
private static void hintShowBhpv(List<Span> spans, boolean body, boolean
headers, boolean props, boolean vars) {
- spans.add(Span.styled(" show", HINT_KEY_STYLE));
+ spans.add(Span.styled(" show", Theme.hintKey()));
spans.add(Span.raw(" "));
spans.add(Span.styled(body ? "B" : "b", body ?
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
spans.add(Span.styled(headers ? "H" : "h", headers ?
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
index 98db646f408f..5dcb71b56f3f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
@@ -48,7 +48,7 @@ final class FormHelper {
} else if (ke.isEnd()) {
state.moveCursorToEnd();
} else if (ke.code() == KeyCode.CHAR) {
- state.insert(ke.character());
+ state.insert(ke.string().charAt(0));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
index 6b616ea7e7ee..c375384e20a7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
@@ -95,8 +95,8 @@ class HelpOverlay {
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(" Help ")
.titleBottom(Title.from(Line.from(
- Span.styled(" F1/?", MonitorContext.HINT_KEY_STYLE),
Span.raw(" close "),
- Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE),
Span.raw(" scroll "))))
+ Span.styled(" F1/? ", Theme.hintKey()), Span.raw("
close "),
+ Span.styled(" ↑↓ ", Theme.hintKey()), Span.raw("
scroll "))))
.build();
MarkdownView view = MarkdownView.builder()
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 1b4abc00bf92..80ed21e47f1c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -1696,7 +1696,7 @@ class HistoryTab implements MonitorTab {
}
private static void hintShowBhpv(List<Span> spans, boolean body, boolean
headers, boolean props, boolean vars) {
- spans.add(Span.styled(" show", HINT_KEY_STYLE));
+ spans.add(Span.styled(" show", Theme.hintKey()));
spans.add(Span.raw(" "));
spans.add(Span.styled(body ? "B" : "b", body ?
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
spans.add(Span.styled(headers ? "H" : "h", headers ?
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 01f17ef5afce..b8f8ac56556a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -574,7 +574,7 @@ class HttpTab implements MonitorTab {
return true;
}
if (ke.code() == KeyCode.CHAR) {
- activeInput.insert(ke.character());
+ activeInput.insert(ke.string().charAt(0));
return true;
}
return true;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 15a84d382290..674c4a3f0f3e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -433,7 +433,6 @@ class McpFacade {
return tabRegistry.diagramTab().getTopologyDataAsJson();
}
- @SuppressWarnings("unchecked")
JsonObject getSpanData(String traceId, int limit) {
String pid = ctx.selectedPid;
if (pid == null) {
@@ -465,8 +464,8 @@ class McpFacade {
JsonArray all = response.getCollection("spans");
if (all != null) {
JsonArray filtered = new JsonArray();
- for (int i = 0; i < all.size(); i++) {
- JsonObject span = (JsonObject) all.get(i);
+ for (Object o : all) {
+ JsonObject span = (JsonObject) o;
String tid = span.getString("traceId");
if (tid != null && tid.contains(traceId)) {
filtered.add(span);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
index dea6a75d2be9..6f0f6a469644 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
@@ -109,7 +109,7 @@ class McpLogPopup {
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(" MCP Log ")
.titleBottom(Title.from(Line.from(
- Span.styled(" Esc",
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+ Span.styled(" Esc", Theme.hintKey()), Span.raw("
back "))))
.build();
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 18b2ae68d9c9..63a04388bab2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@@ -32,6 +33,7 @@ import dev.tamboui.tui.TuiRunner;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
import dev.tamboui.widgets.paragraph.Paragraph;
import dev.tamboui.widgets.table.Cell;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -43,7 +45,15 @@ import org.apache.camel.util.json.Jsoner;
*/
class MonitorContext {
- static final Style HINT_KEY_STYLE = Style.EMPTY.fg(Color.YELLOW).bold();
+ /** Small flat-orange camel for empty / no-selection states. */
+ static final String[] SMALL_CAMEL = {
+ " ,,__",
+ "/o. \\___/\\",
+ "\\__/ \\",
+ " | | |",
+ " | | |~",
+ " (_) (_) (_)",
+ };
final AtomicReference<List<IntegrationInfo>> data;
final AtomicReference<List<InfraInfo>> infraData;
@@ -128,23 +138,35 @@ class MonitorContext {
}
static void hint(List<Span> spans, String key, String label) {
- spans.add(Span.styled(" " + key, HINT_KEY_STYLE));
+ spans.add(Span.styled(" " + key + " ", Theme.hintKey()));
spans.add(Span.raw(" " + label + " "));
}
static void hintLast(List<Span> spans, String key, String label) {
- spans.add(Span.styled(" " + key, HINT_KEY_STYLE));
+ spans.add(Span.styled(" " + key + " ", Theme.hintKey()));
spans.add(Span.raw(" " + label));
}
static void renderNoSelection(Frame frame, Rect area) {
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.raw("")));
+ for (String row : SMALL_CAMEL) {
+ lines.add(Line.from(Span.styled(" " + row,
Style.EMPTY.fg(Theme.accent()))));
+ }
+ lines.add(Line.from(Span.raw("")));
+ List<Span> hintSpans = new ArrayList<>();
+ hintSpans.add(Span.raw(" No integration selected. "));
+ hint(hintSpans, "1", "Overview");
+ hint(hintSpans, "?", "Help");
+ lines.add(Line.from(hintSpans));
+
frame.renderWidget(
Paragraph.builder()
- .text(Text.from(Line.from(
- Span.styled(" Select an integration from the
Overview tab (press 1)",
- Style.EMPTY.dim()))))
+ .text(Text.from(lines))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" No integration selected ").build())
+ .title(Title.from(Line.from(
+ Span.styled(" No integration selected
", Theme.title()))))
+ .build())
.build(),
area);
}
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 09b95d7995cf..ebd16308d1d6 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
@@ -199,6 +199,11 @@ class OverviewTab implements MonitorTab {
int infraCount = infraInfos.size();
dividerIndex = infraCount > 0 ? integrationCount : -1;
+ if (integrationCount == 0 && infraCount == 0) {
+ renderEmptyState(frame, area);
+ return;
+ }
+
if (ctx.selectedPid != null) {
for (int i = 0; i < infos.size(); i++) {
if (ctx.selectedPid.equals(infos.get(i).pid)) {
@@ -208,7 +213,7 @@ class OverviewTab implements MonitorTab {
}
for (int i = 0; i < infraInfos.size(); i++) {
if (ctx.selectedPid.equals(infraInfos.get(i).pid)) {
- int tableIndex = integrationCount + (dividerIndex >= 0 ? 1
: 0) + i;
+ int tableIndex = integrationCount + 1 + i;
tableState.select(tableIndex);
break;
}
@@ -232,7 +237,12 @@ class OverviewTab implements MonitorTab {
.split(area);
List<Row> rows = new ArrayList<>();
+ int rowIndex = 0;
for (IntegrationInfo info : infos) {
+ boolean isEven = (rowIndex++ % 2 == 0);
+ // Zebra striping at the row level so the selection highlight
(patched on top) always wins.
+ Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) : Style.EMPTY;
+
if (info.vanishing) {
long elapsed = System.currentTimeMillis() - info.vanishStart;
float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
@@ -252,7 +262,7 @@ class OverviewTab implements MonitorTab {
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle))));
+ Cell.from(Span.styled("", dimStyle))).style(rowBg));
} else {
String stateText = extractState(info.state);
if (stoppingPids.contains(info.pid) ||
"Terminating".equals(stateText)) {
@@ -263,13 +273,12 @@ class OverviewTab implements MonitorTab {
stateText = "Stopped";
}
Style statusStyle = switch (stateText) {
- case "Started", "Running" -> Style.EMPTY.fg(Color.GREEN);
- case "Stopping" -> Style.EMPTY.fg(Color.YELLOW);
- case "Stopped" -> Style.EMPTY.fg(Color.LIGHT_RED);
- default -> Style.EMPTY.fg(Color.YELLOW);
+ case "Started", "Running" -> Theme.success();
+ case "Stopped" -> Theme.error();
+ default -> Theme.warning();
};
- Style failStyle = info.failed > 0 ?
Style.EMPTY.fg(Color.LIGHT_RED).bold() : Style.EMPTY;
+ Style failStyle = info.failed > 0 ? Theme.error().bold() :
Style.EMPTY;
String sinceLastDisplay = formatSinceLast(info);
@@ -296,7 +305,7 @@ class OverviewTab implements MonitorTab {
rightCell(String.valueOf(info.exchangesTotal), 8),
rightCell(String.valueOf(info.failed), 6, failStyle),
rightCell(String.valueOf(info.inflight), 8),
- Cell.from(sinceLastDisplay)));
+ Cell.from(sinceLastDisplay)).style(rowBg));
}
}
@@ -324,6 +333,10 @@ class OverviewTab implements MonitorTab {
}
for (InfraInfo info : infraInfos) {
+ boolean isEven = (rowIndex++ % 2 == 0);
+ Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) : Style.EMPTY;
+ Style statusStyle = info.alive ? Theme.success() : Theme.error();
+
if (info.vanishing) {
long elapsed = System.currentTimeMillis() - info.vanishStart;
float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
@@ -342,9 +355,8 @@ class OverviewTab implements MonitorTab {
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle))));
+ Cell.from(Span.styled("", dimStyle))).style(rowBg));
} else {
- Style statusStyle = info.alive ? Style.EMPTY.fg(Color.GREEN) :
Style.EMPTY.fg(Color.LIGHT_RED);
String statusText = info.alive ? "Running" : "Stopped";
String infraAlias = "🔧 " + info.alias;
String version = info.serviceVersion != null ?
info.serviceVersion : "";
@@ -360,7 +372,7 @@ class OverviewTab implements MonitorTab {
Cell.from(""),
Cell.from(""),
Cell.from(""),
- Cell.from("")));
+ Cell.from("")).style(rowBg));
}
}
@@ -891,6 +903,7 @@ class OverviewTab implements MonitorTab {
- `S` — reverse sort order
- `F2` — actions menu
- `F3` — switch integration
+ - `F4` — toggle light/dark theme
""";
}
@@ -926,4 +939,42 @@ class OverviewTab implements MonitorTab {
result.put("selectedIndex", sel != null ? sel : -1);
return result;
}
+
+ private void renderEmptyState(Frame frame, Rect area) {
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.raw("")));
+ for (String row : MonitorContext.SMALL_CAMEL) {
+ lines.add(Line.from(Span.styled(" " + row,
Style.EMPTY.fg(Theme.accent()).bold())));
+ }
+ lines.add(Line.from(Span.styled(" No Active Camel Integrations
Found", Theme.title())));
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(Span.styled(" 💡 How to monitor integrations:",
Style.EMPTY.bold())));
+ lines.add(Line.from(Span.raw(" Run a route or integration in
another terminal window:")));
+ lines.add(Line.from(Span.styled(" > camel run my-route.yaml",
Theme.success())));
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(Span.styled(" 💻 Or use the embedded JLine shell
panel:", Style.EMPTY.bold())));
+ lines.add(Line.from(List.of(
+ Span.raw(" Press "),
+ Span.styled(" F6 ", Theme.hintKey()),
+ Span.raw(" to open the shell and run commands directly,
e.g.:"))));
+ lines.add(Line.from(Span.styled(" camel> run examples/demo.java",
Theme.success())));
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(List.of(
+ Span.styled(" ❔ For shortcut keys and documentation, press ",
Theme.muted()),
+ Span.styled(" ? ", Theme.hintKey()),
+ Span.styled(" or ", Theme.muted()),
+ Span.styled(" F1 ", Theme.hintKey()),
+ Span.styled(".", Theme.muted()))));
+
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(lines))
+ .block(Block.builder()
+
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line.from(
+ Span.styled(" Camel JBang TUI ",
Theme.title()))))
+ .build())
+ .build(),
+ area);
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index b9cfdbb1aac2..ca29642fff48 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -657,11 +657,11 @@ class RunOptionsForm {
active.moveCursorToEnd();
} else if (ke.code() == KeyCode.CHAR) {
if (digitsOnly) {
- if (Character.isDigit(ke.character())) {
- active.insert(ke.character());
+ if (Character.isDigit(ke.string().charAt(0))) {
+ active.insert(ke.string().charAt(0));
}
} else {
- active.insert(ke.character());
+ active.insert(ke.string().charAt(0));
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
index 878d81b5f249..49a90dadfc2e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
@@ -248,14 +248,14 @@ class SearchHighlighter {
void renderFooterHints(List<Span> spans) {
if (findInputActive) {
- spans.add(Span.styled(" /", HINT_KEY_STYLE));
+ spans.add(Span.styled(" /", Theme.hintKey()));
spans.add(Span.raw(searchInputState.text() + "█ "));
hint(spans, "Enter", "search");
hintLast(spans, "Esc", "cancel");
return;
}
if (highlightInputActive) {
- spans.add(Span.styled(" h:", HINT_KEY_STYLE));
+ spans.add(Span.styled(" h:", Theme.hintKey()));
spans.add(Span.raw(searchInputState.text() + "█ "));
hint(spans, "Enter", "set");
hintLast(spans, "Esc", "cancel");
@@ -271,7 +271,7 @@ class SearchHighlighter {
String pos = findMatches.isEmpty()
? "0/0"
: (findMatchIndex + 1) + "/" + findMatches.size();
- spans.add(Span.styled(" /", HINT_KEY_STYLE));
+ spans.add(Span.styled(" /", Theme.hintKey()));
spans.add(Span.raw("\"" + findTerm + "\" [" + pos + "] "));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
index 82a3fba80f48..04661c6cfa39 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
@@ -346,7 +346,7 @@ class SendMessagePopup {
return true;
}
if (ke.code() == KeyCode.CHAR) {
- activeInput.insert(ke.character());
+ activeInput.insert(ke.string().charAt(0));
return true;
}
return true;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index 8eea84b52b23..be40816284da 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -212,10 +212,11 @@ class ShellPanel {
lastArea = area;
- // Render border matching other tabs
+ // Focused pane: orange border + themed title (an open shell holds
input focus)
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(Title.from(Line.from(Span.styled(" Shell ",
Style.EMPTY.bold()))))
+ .borderStyle(Theme.borderFocused())
+ .title(Title.from(Line.from(Span.styled(" Shell ",
Theme.title()))))
.build();
frame.renderWidget(block, area);
Rect inner = block.inner(area);
@@ -230,9 +231,9 @@ class ShellPanel {
// Handle resize
if (screenTerminal != null && (innerWidth != lastWidth || innerHeight
!= lastHeight)) {
- screenTerminal.setSize(innerWidth, innerHeight);
+ screenTerminal.setSize(Size.of(innerWidth, innerHeight));
if (virtualTerminal != null) {
- virtualTerminal.setSize(new Size(innerWidth, innerHeight));
+ virtualTerminal.setSize(Size.of(innerWidth, innerHeight));
}
lastWidth = innerWidth;
lastHeight = innerHeight;
@@ -403,7 +404,7 @@ class ShellPanel {
DelegateOutputStream delegateOut = new DelegateOutputStream();
virtualTerminal = new LineDisciplineTerminal(
"tui-shell", "screen-256color", delegateOut,
StandardCharsets.UTF_8);
- virtualTerminal.setSize(new Size(width, height));
+ virtualTerminal.setSize(Size.of(width, height));
// Feedback loop: VT100 responses go back as terminal input
OutputStream feedbackOutput = new OutputStream() {
@@ -603,9 +604,10 @@ class ShellPanel {
static byte[] encodeKeyEvent(KeyEvent ke) {
if (ke.code() == KeyCode.CHAR) {
- char ch = ke.character();
- if (ke.hasCtrl()) {
+ String s = ke.string();
+ if (ke.hasCtrl() && s.length() == 1) {
// Ctrl+letter → control character
+ char ch = s.charAt(0);
if (ch >= 'a' && ch <= 'z') {
return new byte[] { (byte) (ch - 'a' + 1) };
}
@@ -613,7 +615,7 @@ class ShellPanel {
return new byte[] { (byte) (ch - 'A' + 1) };
}
}
- return Character.toString(ch).getBytes(StandardCharsets.UTF_8);
+ return s.getBytes(StandardCharsets.UTF_8);
}
return switch (ke.code()) {
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
new file mode 100644
index 000000000000..4391433df555
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
@@ -0,0 +1,309 @@
+/*
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import dev.tamboui.css.Styleable;
+import dev.tamboui.css.engine.StyleEngine;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Central semantic theme for the Camel TUI. Call sites reference intent
(accent, border, success, ...) so the concrete
+ * 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.
+ */
+final class Theme {
+
+ /** Camel brand orange. */
+ static final Color ACCENT = Color.rgb(0xF6, 0x91, 0x23);
+
+ 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_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 Map<String, Style> CACHE = new HashMap<>();
+
+ private static boolean initialized;
+ private static boolean fallbackLogged;
+ private static StyleEngine engine;
+ private static String mode = DARK;
+
+ private Theme() {
+ }
+
+ static Color accent() {
+ StyleEngine e = engine();
+ if (e == null) {
+ return ACCENT;
+ }
+ try {
+ return e.resolve(new Token("accent")).foreground().orElse(ACCENT);
+ } catch (RuntimeException ex) {
+ logFallbackOnce(ex);
+ return ACCENT;
+ }
+ }
+
+ /**
+ * Subtle alternating-row background for zebra striping. Theme-aware (dark
gray on dark, light gray on light) so
+ * stripes stay readable on both terminals. Applied at the row level so it
never overrides the selection highlight.
+ */
+ static Color zebra() {
+ StyleEngine e = engine();
+ if (e == null) {
+ return FALLBACK_ZEBRA;
+ }
+ try {
+ return e.resolve(new
Token("row-alt")).background().orElse(FALLBACK_ZEBRA);
+ } catch (RuntimeException ex) {
+ logFallbackOnce(ex);
+ return FALLBACK_ZEBRA;
+ }
+ }
+
+ /** White-on-orange: active tab highlight. */
+ static Style accentBg() {
+ return style("accent-bg", FALLBACK_ACCENT_BG);
+ }
+
+ /** Black-on-orange chip: key hints in footers and prompts. */
+ static Style hintKey() {
+ return style("hint-key", FALLBACK_HINT_KEY);
+ }
+
+ /** Dim border for unfocused panels. */
+ static Style border() {
+ return style("border", FALLBACK_BORDER);
+ }
+
+ /** Orange border for the focused panel. */
+ static Style borderFocused() {
+ return style("border-focused", FALLBACK_BORDER_FOCUSED);
+ }
+
+ /** Panel and border titles. */
+ static Style title() {
+ return style("title", FALLBACK_TITLE);
+ }
+
+ static Style success() {
+ return style("success", FALLBACK_SUCCESS);
+ }
+
+ static Style warning() {
+ return style("warning", FALLBACK_WARNING);
+ }
+
+ static Style error() {
+ return style("error", FALLBACK_ERROR);
+ }
+
+ static Style muted() {
+ return style("muted", FALLBACK_MUTED);
+ }
+
+ /** Row/selection highlight (matches the existing list highlight). */
+ static Style selectionBg() {
+ return style("selection", FALLBACK_SELECTION);
+ }
+
+ /** Informational accent (header integration count). */
+ static Style info() {
+ return style("info", FALLBACK_INFO);
+ }
+
+ /** Secondary accent (header infra / selected). */
+ static Style notice() {
+ return style("notice", FALLBACK_NOTICE);
+ }
+
+ /** MCP indicator: connected with recent activity. */
+ static Style mcpActive() {
+ return style("mcp-active", FALLBACK_MCP_ACTIVE);
+ }
+
+ /** MCP indicator: connected but idle. */
+ static Style mcpIdle() {
+ return style("mcp-idle", FALLBACK_MCP_IDLE);
+ }
+
+ /** MCP indicator: not connected. */
+ static Style mcpDown() {
+ return style("mcp-down", FALLBACK_MCP_DOWN);
+ }
+
+ /** The active theme mode: {@code "dark"} or {@code "light"}. */
+ static String mode() {
+ engine();
+ return mode;
+ }
+
+ /** Flip the active theme, clear the cache, persist the new value, and
return the new mode. */
+ static synchronized String toggle() {
+ String next = DARK.equals(mode) ? LIGHT : DARK;
+ setMode(next);
+ persist(next);
+ return next;
+ }
+
+ /** Activate a specific mode without persisting. Unknown values fall back
to dark. */
+ static synchronized void setMode(String newMode) {
+ engine();
+ String resolved = DARK.equals(newMode) || LIGHT.equals(newMode) ?
newMode : DARK;
+ if (engine != null) {
+ try {
+ engine.setActiveStylesheet(resolved);
+ } catch (RuntimeException ex) {
+ logFallbackOnce(ex);
+ }
+ }
+ mode = resolved;
+ CACHE.clear();
+ }
+
+ /** Test hook: drop all process-wide state so the next access
reinitializes from config. */
+ static synchronized void resetForTesting() {
+ initialized = false;
+ fallbackLogged = false;
+ engine = null;
+ mode = DARK;
+ CACHE.clear();
+ }
+
+ private static synchronized Style style(String id, Style fallback) {
+ StyleEngine e = engine();
+ 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;
+ }
+ });
+ }
+
+ private static synchronized StyleEngine engine() {
+ if (initialized) {
+ return engine;
+ }
+ initialized = true;
+ mode = loadPersistedMode();
+ try {
+ StyleEngine e = StyleEngine.create();
+ e.loadStylesheet(DARK, "tui/themes/dark.tcss");
+ e.loadStylesheet(LIGHT, "tui/themes/light.tcss");
+ e.setActiveStylesheet(mode);
+ engine = e;
+ } catch (Exception ex) {
+ engine = null;
+ logFallbackOnce(ex);
+ }
+ return engine;
+ }
+
+ private static String loadPersistedMode() {
+ String[] holder = { DARK };
+ try {
+ CommandLineHelper.loadProperties(props -> {
+ String v = props.getProperty(PROP_KEY);
+ if (DARK.equals(v) || LIGHT.equals(v)) {
+ holder[0] = v;
+ }
+ });
+ } catch (RuntimeException ex) {
+ // Config unreadable; keep the default mode.
+ }
+ return holder[0];
+ }
+
+ private static void persist(String newMode) {
+ try {
+ CommandLineHelper.createPropertyFile(false);
+ CommandLineHelper.loadProperties(props -> {
+ props.setProperty(PROP_KEY, newMode);
+ CommandLineHelper.storeProperties(props,
+ new Printer.QuietPrinter(new
Printer.SystemOutPrinter()), false);
+ });
+ } catch (Exception ex) {
+ logFallbackOnce(ex);
+ }
+ }
+
+ private static void logFallbackOnce(Throwable t) {
+ if (!fallbackLogged) {
+ fallbackLogged = true;
+ LOG.warn("Camel TUI theme stylesheet unavailable; using built-in
palette", t);
+ }
+ }
+
+ /** Minimal synthetic {@link Styleable}: an id-only token used for engine
resolution. */
+ private record Token(String id) implements Styleable {
+
+ @Override
+ public Optional<String> cssId() {
+ return Optional.of(id);
+ }
+
+ @Override
+ public Set<String> cssClasses() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Optional<Styleable> cssParent() {
+ return Optional.empty();
+ }
+ }
+}
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
new file mode 100644
index 000000000000..41c141bc8752
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/dark.tcss
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+/* Camel TUI dark theme (default). Single source of color truth for Theme
tokens. */
+
+$brand: #F69123;
+
+#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-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; }
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
new file mode 100644
index 000000000000..de7dee5b7a93
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/light.tcss
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+/* Camel TUI light theme. Brand orange stays truecolor; status hues darkened
for light terminals. */
+
+$brand: #F69123;
+
+#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-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; }
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelColorTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelColorTest.java
index 586fc5d36351..f522ba30300d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelColorTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelColorTest.java
@@ -26,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
class ShellPanelColorTest {
// ScreenTerminal stores colors as the top nibble of each channel of its
xterm palette. The 16 standard
- // ANSI colors must be recognised and mapped to terminal-themed colors,
otherwise (for example) ANSI red
+ // ANSI colors must be recognized and mapped to terminal-themed colors,
otherwise (for example) ANSI red
// is reconstructed as a literal RGB(136,0,0) that is unreadable on dark
backgrounds.
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
index d060b58fdf27..871fffd6b7db 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
@@ -22,6 +22,7 @@ import dev.tamboui.text.Span;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
class SyntaxHighlighterTest {
@@ -77,7 +78,7 @@ class SyntaxHighlighterTest {
void preservesLeadingIndentationUnstyled() {
Line line = SyntaxHighlighter.highlightLine(" camel.x=1",
SyntaxHighlighter.Language.PROPERTIES);
// the indentation is emitted as a raw (unstyled) span
- assertEquals(null, fg(line, " "));
+ assertNull(fg(line, " "));
assertEquals(SyntaxHighlighter.MONOKAI_KEYWORD, fg(line, "camel.x"));
assertEquals(SyntaxHighlighter.MONOKAI_STRING, fg(line, "1"));
assertRoundTrip(line, " camel.x=1");
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
new file mode 100644
index 000000000000..b9be5c9240b9
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class ThemeTest {
+
+ @TempDir
+ Path home;
+
+ @BeforeEach
+ void setUp() {
+ // Isolate user config per test so persistence never touches the real
home dir.
+ CommandLineHelper.useHomeDir(home.toString());
+ Theme.resetForTesting();
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Reset the process-wide engine so toggles do not leak across tests.
+ Theme.resetForTesting();
+ }
+
+ @Test
+ void accentIsBrandOrangeInDefaultTheme() {
+ // Guards palette drift: the brand accent must stay Camel orange
#F69123 and load from CSS.
+ Theme.setMode("dark");
+ assertEquals(Color.rgb(0xF6, 0x91, 0x23), Theme.accent());
+ assertEquals(Color.rgb(0xF6, 0x91, 0x23), Theme.ACCENT);
+ }
+
+ @Test
+ void darkPaletteResolvesExpectedTokens() {
+ // Asserts the dark stylesheet actually loads and each token resolves
to the intended style,
+ // not merely that the accessor exists.
+ 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(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());
+ }
+
+ @Test
+ void lightPaletteDiffersForStatusTokensButKeepsBrand() {
+ // 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());
+ // 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());
+ // Zebra background is theme-aware: light gray on light, unlike the
dark gray used on dark.
+ assertEquals(Color.rgb(0xEB, 0xEB, 0xEB), Theme.zebra());
+ }
+
+ @Test
+ void toggleFlipsModeAndChangesThemeDependentToken() {
+ Theme.setMode("dark");
+ assertEquals("dark", Theme.mode());
+ Style darkBorder = Theme.border();
+
+ String newMode = Theme.toggle();
+
+ assertEquals("light", newMode);
+ assertEquals("light", Theme.mode());
+ assertNotEquals(darkBorder, Theme.border());
+ }
+
+ @Test
+ void togglePersistsAndAFreshLoadActivatesIt() {
+ Theme.setMode("dark");
+ Theme.toggle(); // -> light, persisted to camel.tui.theme
+
+ String[] stored = { null };
+ CommandLineHelper.loadProperties(p -> stored[0] =
p.getProperty("camel.tui.theme"));
+ assertEquals("light", stored[0]);
+
+ // A fresh Theme load (statics reset) must read back the persisted
mode.
+ Theme.resetForTesting();
+ assertEquals("light", Theme.mode());
+ }
+
+ @Test
+ void accessorsNeverReturnNull() {
+ // Resilience: even when resolution is exercised the palette is always
usable.
+ Theme.setMode("dark");
+ assertNotNull(Theme.accentBg());
+ assertNotNull(Theme.hintKey());
+ assertNotNull(Theme.border());
+ assertNotNull(Theme.borderFocused());
+ assertNotNull(Theme.title());
+ assertNotNull(Theme.success());
+ assertNotNull(Theme.warning());
+ assertNotNull(Theme.error());
+ assertNotNull(Theme.muted());
+ assertNotNull(Theme.selectionBg());
+ assertNotNull(Theme.info());
+ assertNotNull(Theme.notice());
+ assertNotNull(Theme.mcpActive());
+ assertNotNull(Theme.mcpIdle());
+ assertNotNull(Theme.mcpDown());
+ assertNotNull(Theme.zebra());
+ }
+}
diff --git a/pom.xml b/pom.xml
index ceaac32c4578..3c13582af509 100644
--- a/pom.xml
+++ b/pom.xml
@@ -441,6 +441,7 @@
<spring.schemas>CAMEL_PROPERTIES_STYLE</spring.schemas>
<sql>DOUBLEDASHES_STYLE</sql>
<tape>SCRIPT_STYLE</tape>
+ <tcss>SLASHSTAR_STYLE</tcss>
<thrift>JAVADOC_STYLE</thrift>
<toml>SCRIPT_STYLE</toml>
<unrealircd.conf>SLASHSTAR_STYLE</unrealircd.conf>