This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 96618afdd800 CAMEL-25445: camel-jbang - text from the integrations no
longer acts on the terminal
96618afdd800 is described below
commit 96618afdd8000915433bdd64aa2ba3b3a9a27447
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 14 12:30:53 2026 +0200
CAMEL-25445: camel-jbang - text from the integrations no longer acts on the
terminal
The TUI shows what the integrations send it: log lines, message bodies and
headers, exceptions, span attributes. TamboUI turns a C0 control character
into a space, but passed a C1 control character (U+0080 to U+009F) on to
the terminal, where the browser terminal of --web acts on it, and a
terminal that lays out bidirectional text acted on the BiDi controls.
After each frame is drawn, ScreenSanitizer now cleans every cell, whichever
view drew it: a control character in a cell of its own becomes a space, and
a control, BiDi or invisible format character joined to a cell is dropped.
ZWJ, ZWNJ and the variation selectors stay, as emoji and some scripts need
them.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
(cherry picked from commit ff0bdf925cc6c0d0f51a07d81a739759cc4daa30)
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 2 +-
.../jbang/core/commands/tui/ScreenSanitizer.java | 90 +++++++++++++++++
.../core/commands/tui/ScreenSanitizerTest.java | 112 +++++++++++++++++++++
3 files changed, 203 insertions(+), 1 deletion(-)
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 2e32d7699dfb..396edad2e3c4 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
@@ -635,7 +635,7 @@ public class CamelMonitor extends CamelCommand {
}
tui.run(
this::handleEvent,
- this::render);
+ frame -> ScreenSanitizer.render(frame, this::render));
} finally {
shellPanel.destroy();
aiPanel.destroy();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizer.java
new file mode 100644
index 000000000000..ce2c28a5b5e8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizer.java
@@ -0,0 +1,90 @@
+/*
+ * 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.function.Consumer;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+
+/**
+ * Keeps the text the integrations send from acting on the terminal
(CAMEL-25445). The TUI shows log lines, message
+ * bodies and headers, exceptions and span attributes as they come. TamboUI
turns a C0 control into a space, but hands a
+ * C1 control (U+0080 to U+009F) to the terminal, and the browser terminal of
{@code --web} acts on it, U+009B as CSI; a
+ * terminal that lays out bidirectional text reorders what follows a BiDi
control. So after a frame is drawn every cell
+ * is cleaned, whichever tab or popup drew it: a control character in a cell
of its own becomes a space, as TamboUI does
+ * for C0, and a control, a BiDi control or an invisible format character
joined to a cell is dropped. The joiners and
+ * selectors that emoji and some scripts need (ZWJ, ZWNJ, the variation
selectors) stay.
+ */
+final class ScreenSanitizer {
+
+ private ScreenSanitizer() {
+ }
+
+ /** Draws a frame, then cleans it. */
+ static void render(Frame frame, Consumer<Frame> draw) {
+ draw.accept(frame);
+ sanitize(frame.buffer());
+ }
+
+ static void sanitize(Buffer buffer) {
+ Rect area = buffer.area();
+ for (int y = area.top(); y < area.bottom(); y++) {
+ for (int x = area.left(); x < area.right(); x++) {
+ Cell cell = buffer.get(x, y);
+ String symbol = cell.symbol();
+ String clean = clean(symbol);
+ if (!clean.equals(symbol)) {
+ buffer.set(x, y, cell.symbol(clean));
+ }
+ }
+ }
+ }
+
+ /** The symbol of a cell without what a terminal would act on; the symbol
itself when there is nothing. */
+ static String clean(String symbol) {
+ if (symbol.isEmpty() || symbol.length() == 1 && symbol.charAt(0) >=
0x20 && symbol.charAt(0) < 0x7F) {
+ return symbol;
+ }
+ if (symbol.codePoints().noneMatch(ScreenSanitizer::unsafe)) {
+ return symbol;
+ }
+ StringBuilder clean = new StringBuilder(symbol.length());
+ int[] codePoints = symbol.codePoints().toArray();
+ for (int i = 0; i < codePoints.length; i++) {
+ if (!unsafe(codePoints[i])) {
+ clean.appendCodePoint(codePoints[i]);
+ } else if (i == 0) {
+ // the cell keeps its width
+ clean.append(' ');
+ }
+ }
+ return clean.toString();
+ }
+
+ static boolean unsafe(int cp) {
+ boolean control = cp < 0x20 || cp >= 0x7F && cp <= 0x9F;
+ // marks, embeddings, overrides and isolates
+ boolean bidi = cp == 0x061C || cp == 0x200E || cp == 0x200F || cp >=
0x202A && cp <= 0x202E
+ || cp >= 0x2066 && cp <= 0x2069;
+ // zero-width space, word joiner, byte order mark, soft hyphen,
Mongolian vowel separator
+ boolean invisible = cp == 0x200B || cp == 0x2060 || cp == 0xFEFF || cp
== 0x00AD || cp == 0x180E;
+ return control || bidi || invisible;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizerTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizerTest.java
new file mode 100644
index 000000000000..b9a04ae5d0c5
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizerTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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 dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Text from the integrations does not act on the terminal (CAMEL-25445): what
a terminal would take as a control is
+ * cleaned out of every cell after a frame is drawn.
+ */
+class ScreenSanitizerTest {
+
+ private static Buffer line(String text) {
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 60, 1));
+ buffer.setString(0, 0, text, Style.EMPTY);
+ return buffer;
+ }
+
+ private static List<String> symbols(Buffer buffer) {
+ List<String> symbols = new ArrayList<>();
+ Rect area = buffer.area();
+ for (int x = area.left(); x < area.right(); x++) {
+ symbols.add(buffer.get(x, 0).symbol());
+ }
+ return symbols;
+ }
+
+ private static String text(Buffer buffer) {
+ return TuiTestHelper.bufferToString(buffer).stripTrailing();
+ }
+
+ @Test
+ void aC1ControlBecomesASpaceInItsCell() {
+ // U+009B is CSI in one character: the browser terminal of --web would
colour what follows red
+ Buffer buffer = line("plain \u009b31mRED\u009b0m after");
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).noneMatch(s ->
s.codePoints().anyMatch(ScreenSanitizer::unsafe));
+ // the parameters show as text, in the cells they had
+ assertThat(text(buffer)).isEqualTo("plain 31mRED 0m after");
+ }
+
+ @Test
+ void aBiDiControlOrAnInvisibleCharacterJoinedToACellIsDropped() {
+ Buffer buffer = line("bidi \u202eDESREVER\u202c end,
ad\u200bmin\ufeff");
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).noneMatch(s ->
s.codePoints().anyMatch(ScreenSanitizer::unsafe));
+ assertThat(text(buffer)).isEqualTo("bidi DESREVER end, admin");
+ }
+
+ @Test
+ void aCellSetWithAControlCharacterBecomesASpace() {
+ // a widget can set a cell directly, which TamboUI does not clean, not
even C0
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 3, 1));
+ buffer.set(0, 0, new Cell("\u001b", Style.EMPTY));
+ buffer.set(1, 0, new Cell("\u009d", Style.EMPTY));
+ assertThat(symbols(buffer)).startsWith("\u001b", "\u009d");
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).startsWith(" ", " ");
+ }
+
+ @Test
+ void emojiAndScriptsStayWhole() {
+ // the joiners and selectors emoji and some scripts need: ZWJ,
variation selector 16, ZWNJ
+ Buffer buffer = line("👩\u200D💻 🇫🇷 ❤\uFE0F می\u200Cخواهم");
+ List<String> before = symbols(buffer);
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).isEqualTo(before);
+ assertThat(String.join("", symbols(buffer))).contains("\u200D",
"\uFE0F", "\u200C");
+ }
+
+ @Test
+ void theFrameIsCleanedAfterItIsDrawn() {
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 10, 1));
+
+ ScreenSanitizer.render(Frame.forTesting(buffer), f ->
f.buffer().setString(0, 0, "\u009b2J ok", Style.EMPTY));
+
+ assertThat(text(buffer)).isEqualTo(" 2J ok");
+ assertThat(ScreenSanitizer.clean("a")).isSameAs("a");
+ }
+}