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 9730682eb7d4 CAMEL-23830: Make TUI shell ANSI colors readable on dark
terminals
9730682eb7d4 is described below
commit 9730682eb7d4f5aefff0a639b26e7d64b95cb5ab
Author: Adriano Machado <[email protected]>
AuthorDate: Thu Jun 25 00:54:28 2026 -0400
CAMEL-23830: Make TUI shell ANSI colors readable on dark terminals
Map JLine's 12-bit encoded ANSI palette colors to terminal-themed
Color.ansi() values instead of literal RGB, so standard colors like
red are rendered using the host terminal's theme rather than a fixed
dim value that is hard to read on dark backgrounds. True-color cells
such as the orange shell prompt keep their literal RGB value.
Closes #24237
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
.../dsl/jbang/core/commands/tui/ShellPanel.java | 61 +++++++++++++++---
.../core/commands/tui/ShellPanelColorTest.java | 75 ++++++++++++++++++++++
2 files changed, 126 insertions(+), 10 deletions(-)
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 21da1a341e54..b4f43e06639e 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
@@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.List;
import dev.tamboui.layout.Rect;
+import dev.tamboui.style.AnsiColor;
import dev.tamboui.style.Color;
import dev.tamboui.style.Overflow;
import dev.tamboui.style.Style;
@@ -506,25 +507,65 @@ class ShellPanel {
// Foreground color (if set)
if ((y & 0x1) != 0) {
- int fg = (int) ((attr >> 12) & 0xFFF);
- int r = ((fg >> 8) & 0xF) * 17;
- int g = ((fg >> 4) & 0xF) * 17;
- int b = (fg & 0xF) * 17;
- style = style.fg(Color.rgb(r, g, b));
+ style = style.fg(resolveColor((int) ((attr >> 12) & 0xFFF)));
}
// Background color (if set)
if ((y & 0x2) != 0) {
- int bg = (int) (attr & 0xFFF);
- int r = ((bg >> 8) & 0xF) * 17;
- int g = ((bg >> 4) & 0xF) * 17;
- int b = (bg & 0xF) * 17;
- style = style.bg(Color.rgb(r, g, b));
+ style = style.bg(resolveColor((int) (attr & 0xFFF)));
}
return style;
}
+ /**
+ * Converts a 12-bit (4-bit-per-channel) color value from the {@link
ScreenTerminal} attribute word into a TamboUI
+ * {@link Color}.
+ * <p>
+ * When the value matches one of the 16 standard ANSI palette colors, a
themed {@link Color#ansi(AnsiColor)} is
+ * returned so the host terminal applies its own scheme (keeping, for
example, red error output and the command
+ * highlighter legible on dark backgrounds). Any other value is a
true-color cell (such as the orange shell prompt)
+ * and is expanded to its literal RGB value.
+ */
+ static Color resolveColor(int rgb12) {
+ AnsiColor ansi = ansiColorFor(rgb12);
+ if (ansi != null) {
+ return Color.ansi(ansi);
+ }
+ // Expand each 4-bit channel back to 8 bits (0xN -> 0xNN, i.e. * 17).
+ int r = ((rgb12 >> 8) & 0xF) * 17;
+ int g = ((rgb12 >> 4) & 0xF) * 17;
+ int b = (rgb12 & 0xF) * 17;
+ return Color.rgb(r, g, b);
+ }
+
+ /**
+ * Maps a 12-bit color value to the standard ANSI palette color it
encodes, or {@code null} if it does not match one
+ * of the 16 ANSI colors. The values are {@link ScreenTerminal}'s palette
(xterm defaults) reduced to the top nibble
+ * of each channel, matching how {@code ScreenTerminal} stores them in the
cell attribute.
+ */
+ static AnsiColor ansiColorFor(int rgb12) {
+ return switch (rgb12) {
+ case 0x000 -> AnsiColor.BLACK;
+ case 0x800 -> AnsiColor.RED;
+ case 0x080 -> AnsiColor.GREEN;
+ case 0x880 -> AnsiColor.YELLOW;
+ case 0x008 -> AnsiColor.BLUE;
+ case 0x808 -> AnsiColor.MAGENTA;
+ case 0x088 -> AnsiColor.CYAN;
+ case 0xccc -> AnsiColor.WHITE;
+ case 0x888 -> AnsiColor.BRIGHT_BLACK;
+ case 0xf00 -> AnsiColor.BRIGHT_RED;
+ case 0x0f0 -> AnsiColor.BRIGHT_GREEN;
+ case 0xff0 -> AnsiColor.BRIGHT_YELLOW;
+ case 0x00f -> AnsiColor.BRIGHT_BLUE;
+ case 0xf0f -> AnsiColor.BRIGHT_MAGENTA;
+ case 0x0ff -> AnsiColor.BRIGHT_CYAN;
+ case 0xfff -> AnsiColor.BRIGHT_WHITE;
+ default -> null;
+ };
+ }
+
private static byte[] encodeKeyEvent(KeyEvent ke) {
if (ke.code() == KeyCode.CHAR) {
char ch = ke.character();
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
new file mode 100644
index 000000000000..586fc5d36351
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelColorTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 dev.tamboui.style.AnsiColor;
+import dev.tamboui.style.Color;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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
+ // is reconstructed as a literal RGB(136,0,0) that is unreadable on dark
backgrounds.
+
+ @Test
+ void detectsStandardAnsiColors() {
+ assertEquals(AnsiColor.BLACK, ShellPanel.ansiColorFor(0x000));
+ assertEquals(AnsiColor.RED, ShellPanel.ansiColorFor(0x800));
+ assertEquals(AnsiColor.GREEN, ShellPanel.ansiColorFor(0x080));
+ assertEquals(AnsiColor.YELLOW, ShellPanel.ansiColorFor(0x880));
+ assertEquals(AnsiColor.BLUE, ShellPanel.ansiColorFor(0x008));
+ assertEquals(AnsiColor.MAGENTA, ShellPanel.ansiColorFor(0x808));
+ assertEquals(AnsiColor.CYAN, ShellPanel.ansiColorFor(0x088));
+ assertEquals(AnsiColor.WHITE, ShellPanel.ansiColorFor(0xccc));
+ }
+
+ @Test
+ void detectsBrightAnsiColors() {
+ assertEquals(AnsiColor.BRIGHT_BLACK, ShellPanel.ansiColorFor(0x888));
+ assertEquals(AnsiColor.BRIGHT_RED, ShellPanel.ansiColorFor(0xf00));
+ assertEquals(AnsiColor.BRIGHT_GREEN, ShellPanel.ansiColorFor(0x0f0));
+ assertEquals(AnsiColor.BRIGHT_YELLOW, ShellPanel.ansiColorFor(0xff0));
+ assertEquals(AnsiColor.BRIGHT_BLUE, ShellPanel.ansiColorFor(0x00f));
+ assertEquals(AnsiColor.BRIGHT_MAGENTA, ShellPanel.ansiColorFor(0xf0f));
+ assertEquals(AnsiColor.BRIGHT_CYAN, ShellPanel.ansiColorFor(0x0ff));
+ assertEquals(AnsiColor.BRIGHT_WHITE, ShellPanel.ansiColorFor(0xfff));
+ }
+
+ @Test
+ void leavesTrueColorValuesUnmapped() {
+ // The shell prompt's orange (0xF69123) reduces to 0xf92, which is not
a palette color and must stay true-color.
+ assertNull(ShellPanel.ansiColorFor(0xf92));
+ assertNull(ShellPanel.ansiColorFor(0x123));
+ }
+
+ @Test
+ void resolvesAnsiRedToThemedColorNotDimRgb() {
+ // The core of the bug fix: ANSI red must become a terminal-themed
color, not the dim RGB(136,0,0)
+ // that the old code produced.
+ assertEquals(Color.ansi(AnsiColor.RED),
ShellPanel.resolveColor(0x800));
+ }
+
+ @Test
+ void resolvesTrueColorToLiteralRgb() {
+ // A non-palette value keeps its literal colour, with each 4-bit
channel expanded back to 8 bits (* 17).
+ assertEquals(Color.rgb(0xf * 17, 0x9 * 17, 0x2 * 17),
ShellPanel.resolveColor(0xf92));
+ }
+}