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");
+    }
+}

Reply via email to