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 55aafdf0b0a6 chore: Use non-blinking software block cursor in TUI
55aafdf0b0a6 is described below

commit 55aafdf0b0a68ac7c249134824ff12c694137645
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 8 07:11:09 2026 +0200

    chore: Use non-blinking software block cursor in TUI
    
    Replace the hardware terminal cursor in ShellPanel with a software-rendered
    reversed-video cursor. The hardware cursor's blink timer resets on every
    TamboUI frame redraw, causing it to appear half-shown on each keypress.
    The software cursor renders the character at the cursor position with
    reversed style, matching AiPanel. Also replace the blinking half-block
    cursor in CaptionOverlay with a non-blinking full-block character.
    
    Closes #24449
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../jbang/core/commands/tui/CaptionOverlay.java    |  3 +-
 .../dsl/jbang/core/commands/tui/ShellPanel.java    | 57 ++++++++-----
 .../jbang/core/commands/tui/ShellPanelTest.java    | 93 ++++++++++++++++++++++
 3 files changed, 129 insertions(+), 24 deletions(-)

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 e0db9b680868..c2eda8dfff1f 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
@@ -190,8 +190,7 @@ class CaptionOverlay {
     private void renderInline(Frame frame, Rect area) {
         Style style = Style.EMPTY.fg(Color.WHITE).bold();
         String text = inlineBuffer != null ? inlineBuffer.toString() : "";
-        boolean cursorVisible = (System.currentTimeMillis() / 500) % 2 == 0;
-        String display = text + (cursorVisible ? "▌" : " ");
+        String display = text + "█";
 
         String[] parts = display.split("\\\\n", -1);
         List<Line> lines = new ArrayList<>();
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 2f58b6ff00c0..bfb383f5d5e6 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
@@ -94,8 +94,6 @@ class ShellPanel {
     private int lastHeight;
     private int scrollOffset;
     private int lastHistorySize;
-    private int lastCursorX = -1;
-    private int lastCursorY = -1;
     private Rect lastArea;
     private volatile boolean shellExited;
 
@@ -286,7 +284,7 @@ class ShellPanel {
         if (scrollOffset > 0) {
             lines = renderScrolledView(screen, innerWidth, innerHeight);
         } else {
-            lines = renderLiveView(screen, innerWidth, innerHeight);
+            lines = renderLiveView(screen, innerWidth, innerHeight, cursor[0], 
cursor[1]);
         }
 
         // Split the inner area: content (fill) + scrollbar (1 col) when 
history exists
@@ -316,22 +314,6 @@ class ShellPanel {
                         .overflow(Overflow.CLIP)
                         .build(),
                 contentArea);
-
-        // Position the hardware cursor only when it has moved, so the 
terminal's
-        // blink timer is not reset on every frame.
-        if (scrollOffset == 0 && cursor[1] >= 0 && cursor[1] < innerHeight
-                && cursor[0] >= 0 && cursor[0] < innerWidth) {
-            int cx = contentArea.x() + cursor[0];
-            int cy = contentArea.y() + cursor[1];
-            if (cx != lastCursorX || cy != lastCursorY) {
-                frame.setCursorPosition(cx, cy);
-                lastCursorX = cx;
-                lastCursorY = cy;
-            }
-        } else {
-            lastCursorX = -1;
-            lastCursorY = -1;
-        }
     }
 
     void renderFooter(List<Span> spans) {
@@ -340,10 +322,11 @@ class ShellPanel {
         TuiHelper.hint(spans, "PgUp/Dn", "scroll");
     }
 
-    private List<Line> renderLiveView(long[] screen, int width, int height) {
+    private List<Line> renderLiveView(long[] screen, int width, int height, 
int cursorCol, int cursorRow) {
         List<Line> lines = new ArrayList<>(height);
         for (int row = 0; row < height; row++) {
-            lines.add(convertRow(screen, row * width, width));
+            int cc = (row == cursorRow) ? cursorCol : -1;
+            lines.add(convertRow(screen, row * width, width, cc));
         }
         return lines;
     }
@@ -351,7 +334,7 @@ class ShellPanel {
     private List<Line> renderScrolledView(long[] screen, int width, int 
height) {
         List<long[]> history = screenTerminal.getHistory();
         if (history.isEmpty()) {
-            return renderLiveView(screen, width, height);
+            return renderLiveView(screen, width, height, -1, -1);
         }
 
         int totalLines = history.size() + height;
@@ -376,6 +359,22 @@ class ShellPanel {
     }
 
     static Line convertRow(long[] buffer, int offset, int width) {
+        return convertRow(buffer, offset, width, -1);
+    }
+
+    /**
+     * Converts a row of {@link ScreenTerminal} cells into a TamboUI {@link 
Line}.
+     * <p>
+     * When {@code cursorCol} is non-negative, the character at that column is 
rendered with reversed video to act as a
+     * software block cursor. This avoids the hardware terminal cursor whose 
blink timer resets on every TamboUI frame
+     * redraw, causing the cursor to appear half-shown on each keypress.
+     *
+     * @param buffer    the screen buffer (flat array of 64-bit cells)
+     * @param offset    start offset of this row in the buffer
+     * @param width     number of columns in this row
+     * @param cursorCol column index of the cursor in this row, or -1 for no 
cursor
+     */
+    static Line convertRow(long[] buffer, int offset, int width, int 
cursorCol) {
         List<Span> spans = new ArrayList<>();
         int col = 0;
         while (col < width) {
@@ -384,10 +383,24 @@ class ShellPanel {
             long attr = ScreenTerminal.cellAttr(cell);
             Style style = convertCellToStyle(cell);
 
+            // Cursor column: emit a single reversed-video span so the cursor
+            // is always visible and does not blink (software block cursor).
+            if (col == cursorCol) {
+                spans.add(Span.styled(
+                        String.valueOf(Character.toChars(ch == 0 ? ' ' : ch)),
+                        style.reversed()));
+                col++;
+                continue;
+            }
+
             StringBuilder sb = new StringBuilder();
             sb.appendCodePoint(ch == 0 ? ' ' : ch);
             int nextCol = col + 1;
             while (nextCol < width) {
+                // Stop batching before the cursor column so it gets its own 
span
+                if (nextCol == cursorCol) {
+                    break;
+                }
                 long nextCell = buffer[offset + nextCol];
                 if (ScreenTerminal.cellAttr(nextCell) != attr) {
                     break;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
index 6c1d5d2d58ba..580e6f786680 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
@@ -156,6 +156,99 @@ class ShellPanelTest {
         assertEquals("ABC", rawContent(line));
     }
 
+    // ---- convertRow cursor tests ----
+
+    @Test
+    void convertRowCursorReversesCharacter() {
+        // Cursor at column 1 should reverse that character
+        long[] buffer = new long[] { 'A', 'B', 'C' };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3, 1);
+        List<Span> spans = line.spans();
+        // Should split into: "A", reversed "B", "C"
+        assertEquals(3, spans.size());
+        assertEquals("A", spans.get(0).content());
+        assertEquals("B", spans.get(1).content());
+        
assertTrue(spans.get(1).style().effectiveModifiers().contains(Modifier.REVERSED));
+        assertEquals("C", spans.get(2).content());
+    }
+
+    @Test
+    void convertRowCursorAtStart() {
+        long[] buffer = new long[] { 'X', 'Y', 'Z' };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3, 0);
+        List<Span> spans = line.spans();
+        assertEquals(2, spans.size());
+        assertEquals("X", spans.get(0).content());
+        
assertTrue(spans.get(0).style().effectiveModifiers().contains(Modifier.REVERSED));
+        assertEquals("YZ", spans.get(1).content());
+    }
+
+    @Test
+    void convertRowCursorAtEnd() {
+        long[] buffer = new long[] { 'A', 'B', 'C' };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3, 2);
+        List<Span> spans = line.spans();
+        assertEquals(2, spans.size());
+        assertEquals("AB", spans.get(0).content());
+        assertEquals("C", spans.get(1).content());
+        
assertTrue(spans.get(1).style().effectiveModifiers().contains(Modifier.REVERSED));
+    }
+
+    @Test
+    void convertRowCursorOnNullCodepoint() {
+        // Cursor on a null codepoint (empty cell) should show reversed space
+        long[] buffer = new long[] { 'A', 0, 'C' };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3, 1);
+        List<Span> spans = line.spans();
+        assertEquals(3, spans.size());
+        assertEquals(" ", spans.get(1).content());
+        
assertTrue(spans.get(1).style().effectiveModifiers().contains(Modifier.REVERSED));
+    }
+
+    @Test
+    void convertRowNoCursorWhenNegative() {
+        // cursorCol = -1 means no cursor (same as the no-arg overload)
+        long[] buffer = new long[] { 'A', 'B', 'C' };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3, -1);
+        List<Span> spans = line.spans();
+        // All same attr → single merged span, no reversed
+        assertEquals(1, spans.size());
+        assertEquals("ABC", spans.get(0).content());
+        
assertFalse(spans.get(0).style().effectiveModifiers().contains(Modifier.REVERSED));
+    }
+
+    @Test
+    void convertRowCursorSplitsBoldRun() {
+        // Two bold cells with cursor on the second should split the run
+        long attr = 0x8L << 24; // Bold
+        long[] buffer = new long[] {
+                'A' | (attr << 32),
+                'B' | (attr << 32),
+                'C' | (attr << 32)
+        };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3, 1);
+        List<Span> spans = line.spans();
+        assertEquals(3, spans.size());
+        // First span: bold A
+        assertEquals("A", spans.get(0).content());
+        
assertTrue(spans.get(0).style().effectiveModifiers().contains(Modifier.BOLD));
+        
assertFalse(spans.get(0).style().effectiveModifiers().contains(Modifier.REVERSED));
+        // Second span: bold + reversed B (cursor)
+        assertEquals("B", spans.get(1).content());
+        
assertTrue(spans.get(1).style().effectiveModifiers().contains(Modifier.BOLD));
+        
assertTrue(spans.get(1).style().effectiveModifiers().contains(Modifier.REVERSED));
+        // Third span: bold C
+        assertEquals("C", spans.get(2).content());
+        
assertTrue(spans.get(2).style().effectiveModifiers().contains(Modifier.BOLD));
+        
assertFalse(spans.get(2).style().effectiveModifiers().contains(Modifier.REVERSED));
+    }
+
     // ---- encodeKeyEvent tests ----
 
     @Test

Reply via email to