This is an automated email from the ASF dual-hosted git repository.

gnodet 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 ab0bc36add49 chore: shell panel UX improvements and JLine 4.3 API 
migration (#24328)
ab0bc36add49 is described below

commit ab0bc36add4973b3f4d73bf8136f5ff0f8e45a62
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 1 09:54:09 2026 +0200

    chore: shell panel UX improvements and JLine 4.3 API migration (#24328)
    
    Shell panel UX and reliability improvements for the TUI monitor:
    
    - Configurable split height (Shift+F6 cycles 25/50/75/100%)
    - Scrollback history with PageUp/Down, mouse wheel, and scrollbar
    - Auto-follow on new output or key input
    - Simplified prompt (camel> instead of camel 4.21.0-SNAPSHOT>)
    - Hardware cursor with position tracking for blinking
    - Footer hints show action verb + current state: resize (50%)
    - Exception logging via System.Logger in CamelMonitor
    - Fullscreen mode at 100% split
    - Uses JLine 4.3 public API (getHistory, getHistorySize, cell decoding)
      instead of reflection on private fields
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/dsl/jbang/core/commands/Shell.java       |  10 +-
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |   2 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  25 +++-
 .../dsl/jbang/core/commands/tui/ShellPanel.java    | 156 ++++++++++++---------
 .../jbang/core/commands/tui/ShellPanelTest.java    | 117 +++++++++++++---
 5 files changed, 204 insertions(+), 106 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
index 5d8a3e0b7ba7..68e8af4fcdbe 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
@@ -80,7 +80,7 @@ public class Shell extends CamelCommand {
 
         // org.jline.shell.Shell is used via FQCN to avoid clash with this 
class name
         ShellBuilder builder = org.jline.shell.Shell.builder()
-                .prompt(() -> buildPrompt(camelVersion, colorEnabled))
+                .prompt(() -> buildPrompt(colorEnabled))
                 .rightPrompt(() -> buildRightPrompt(colorEnabled))
                 .groups(registry, new PosixCommandGroup(), new 
InteractiveCommandGroup())
                 .historyFile(history)
@@ -114,16 +114,12 @@ public class Shell extends CamelCommand {
         return 0;
     }
 
-    private static String buildPrompt(String camelVersion, boolean 
colorEnabled) {
+    private static String buildPrompt(boolean colorEnabled) {
         if (!colorEnabled) {
-            return camelVersion != null ? "camel " + camelVersion + "> " : 
"camel> ";
+            return "camel> ";
         }
         AttributedStringBuilder sb = new AttributedStringBuilder();
         sb.append("camel", 
AttributedStyle.DEFAULT.bold().foregroundRgb(CAMEL_ORANGE));
-        if (camelVersion != null) {
-            sb.append(" ");
-            sb.append(camelVersion, 
AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
-        }
         sb.append("> ", AttributedStyle.DEFAULT);
         return sb.toAnsi();
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 62cb48158807..f700cddaee5b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -526,7 +526,7 @@ class AiPanel {
 
     void renderFooter(List<Span> spans) {
         MonitorContext.hint(spans, "F8", "close");
-        MonitorContext.hint(spans, "Shift+F8", panelPercent() + "%");
+        MonitorContext.hint(spans, "Shift+F8", "resize (" + 
SPLIT_PERCENTS[splitIndex] + "%)");
         MonitorContext.hint(spans, "PgUp/Dn", "scroll");
         if (!thinking.get()) {
             MonitorContext.hint(spans, "Enter", "send");
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 6550316f5b4d..4a49103dde62 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
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -69,6 +71,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TabRegistry.*;
          sortOptions = false)
 public class CamelMonitor extends CamelCommand {
 
+    private static final Logger LOG = 
System.getLogger(CamelMonitor.class.getName());
     private static final long DEFAULT_REFRESH_MS = 100;
 
     // Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is 
the true minimum
@@ -800,12 +803,17 @@ public class CamelMonitor extends CamelCommand {
         ctx.shellPercent = shellPanel.isOpen() ? shellPanel.panelPercent()
                 : aiPanel.isOpen() ? aiPanel.panelPercent() : 0;
         if (shellPanel.isOpen()) {
-            List<Rect> splitChunks = Layout.vertical()
-                    .constraints(Constraint.percentage(100 - 
shellPanel.panelPercent()),
-                            Constraint.percentage(shellPanel.panelPercent()))
-                    .split(contentArea);
-            renderContent(frame, splitChunks.get(0));
-            shellPanel.render(frame, splitChunks.get(1));
+            if (shellPanel.panelPercent() >= 100) {
+                // At 100% the shell fills the entire content area
+                shellPanel.render(frame, contentArea);
+            } else {
+                List<Rect> splitChunks = Layout.vertical()
+                        .constraints(Constraint.percentage(100 - 
shellPanel.panelPercent()),
+                                
Constraint.percentage(shellPanel.panelPercent()))
+                        .split(contentArea);
+                renderContent(frame, splitChunks.get(0));
+                shellPanel.render(frame, splitChunks.get(1));
+            }
         } else if (aiPanel.isOpen()) {
             List<Rect> splitChunks = Layout.vertical()
                     .constraints(Constraint.percentage(100 - 
aiPanel.panelPercent()),
@@ -1141,6 +1149,7 @@ public class CamelMonitor extends CamelCommand {
         try {
             pid = Long.parseLong(ctx.selectedPid);
         } catch (NumberFormatException e) {
+            LOG.log(Level.DEBUG, "Cannot parse selected PID: {0}", 
ctx.selectedPid);
             return;
         }
         if (isInfraSelected()) {
@@ -1184,6 +1193,7 @@ public class CamelMonitor extends CamelCommand {
         try {
             pid = Long.parseLong(ctx.selectedPid);
         } catch (NumberFormatException e) {
+            LOG.log(Level.DEBUG, "Cannot parse selected PID for restart: {0}", 
ctx.selectedPid);
             return;
         }
         IntegrationInfo info = findSelectedIntegration();
@@ -1601,6 +1611,7 @@ public class CamelMonitor extends CamelCommand {
             Files.writeString(path, json);
             return path;
         } catch (IOException e) {
+            LOG.log(Level.WARNING, "Failed to write .mcp.json: {0}", 
e.getMessage());
             return null;
         }
     }
@@ -1610,7 +1621,7 @@ public class CamelMonitor extends CamelCommand {
             try {
                 Files.deleteIfExists(path);
             } catch (IOException e) {
-                // best effort
+                LOG.log(Level.DEBUG, "Failed to delete .mcp.json: {0}", 
e.getMessage());
             }
         }
     }
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 d8a0f98d47bf..8eea84b52b23 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
@@ -18,13 +18,15 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.lang.reflect.Field;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.AnsiColor;
 import dev.tamboui.style.Color;
@@ -43,10 +45,11 @@ 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.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.Printer;
-import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.jline.builtins.InteractiveCommandGroup;
 import org.jline.builtins.PosixCommandGroup;
 import org.jline.picocli.PicocliCommandRegistry;
@@ -74,6 +77,7 @@ import org.jline.utils.ScreenTerminalOutputStream;
  */
 class ShellPanel {
 
+    private static final Logger LOG = 
System.getLogger(ShellPanel.class.getName());
     private static final int[] SPLIT_PERCENTS = { 25, 50, 75, 100 };
     private static final int MOUSE_SCROLL_LINES = 3;
 
@@ -85,10 +89,14 @@ class ShellPanel {
     private LineDisciplineTerminal virtualTerminal;
     private Thread shellThread;
 
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+
     private int lastWidth;
     private int lastHeight;
     private int scrollOffset;
     private int lastHistorySize;
+    private int lastCursorX = -1;
+    private int lastCursorY = -1;
     private Rect lastArea;
     private volatile boolean shellExited;
 
@@ -140,7 +148,7 @@ class ShellPanel {
 
         // PageUp/Down for scrollback through history
         if (ke.isKey(KeyCode.PAGE_UP)) {
-            int histSize = screenTerminal != null ? 
getHistorySize(screenTerminal) : 0;
+            int histSize = screenTerminal != null ? 
screenTerminal.getHistorySize() : 0;
             scrollOffset = Math.min(scrollOffset + lastHeight, histSize);
             return true;
         }
@@ -159,8 +167,12 @@ class ShellPanel {
                 if (bytes != null && bytes.length > 0) {
                     virtualTerminal.processInputBytes(bytes);
                 }
-            } catch (IOException | ArrayIndexOutOfBoundsException e) {
-                // terminal closed or buffer resized concurrently
+            } catch (IOException e) {
+                // terminal closed — expected during shutdown
+                LOG.log(Level.DEBUG, "Terminal I/O error forwarding key 
event", e);
+            } catch (ArrayIndexOutOfBoundsException e) {
+                // ScreenTerminal buffer resized concurrently
+                LOG.log(Level.DEBUG, "Buffer resize race during key 
forwarding", e);
             }
         }
         return true;
@@ -177,7 +189,7 @@ class ShellPanel {
             return false;
         }
         if (me.kind() == MouseEventKind.SCROLL_UP) {
-            int histSize = screenTerminal != null ? 
getHistorySize(screenTerminal) : 0;
+            int histSize = screenTerminal != null ? 
screenTerminal.getHistorySize() : 0;
             scrollOffset = Math.min(scrollOffset + MOUSE_SCROLL_LINES, 
histSize);
             return true;
         }
@@ -246,22 +258,17 @@ class ShellPanel {
             screenTerminal.dump(screen, cursor);
         } catch (ArrayIndexOutOfBoundsException e) {
             // buffer resized concurrently — skip this frame
+            LOG.log(Level.DEBUG, "Buffer resize race during screen dump — 
skipping frame", e);
             return;
         }
 
         // Auto-follow: reset scroll when new history appears
-        int histSize = getHistorySize(screenTerminal);
+        int histSize = screenTerminal.getHistorySize();
         if (histSize > lastHistorySize && scrollOffset > 0) {
             scrollOffset = 0;
         }
         lastHistorySize = histSize;
 
-        // Show a block cursor by toggling the reversed attribute on the cell 
at the cursor position
-        if (scrollOffset == 0 && cursor[1] >= 0 && cursor[1] < innerHeight
-                && cursor[0] >= 0 && cursor[0] < innerWidth) {
-            screen[cursor[1] * innerWidth + cursor[0]] ^= (1L << 57);
-        }
-
         List<Line> lines;
         if (scrollOffset > 0) {
             lines = renderScrolledView(screen, innerWidth, innerHeight);
@@ -269,17 +276,54 @@ class ShellPanel {
             lines = renderLiveView(screen, innerWidth, innerHeight);
         }
 
+        // Split the inner area: content (fill) + scrollbar (1 col) when 
history exists
+        int totalLines = histSize + innerHeight;
+        boolean showScrollbar = totalLines > innerHeight;
+        Rect contentArea;
+        if (showScrollbar) {
+            List<Rect> hChunks = Layout.horizontal()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(inner);
+            contentArea = hChunks.get(0);
+
+            // Map scrollOffset (lines-from-bottom) to top-down position for 
ScrollbarState
+            int viewStart = Math.max(0, totalLines - scrollOffset - 
innerHeight);
+            scrollbarState
+                    .contentLength(totalLines)
+                    .viewportContentLength(innerHeight)
+                    .position(viewStart);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+        } else {
+            contentArea = inner;
+        }
+
         frame.renderWidget(
                 Paragraph.builder()
                         .text(Text.from(lines))
                         .overflow(Overflow.CLIP)
                         .build(),
-                inner);
+                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) {
         MonitorContext.hint(spans, "F6", "close");
-        MonitorContext.hint(spans, "Shift+F6", SPLIT_PERCENTS[splitIndex] + 
"%");
+        MonitorContext.hint(spans, "Shift+F6", "resize (" + 
SPLIT_PERCENTS[splitIndex] + "%)");
         MonitorContext.hint(spans, "PgUp/Dn", "scroll");
     }
 
@@ -292,7 +336,7 @@ class ShellPanel {
     }
 
     private List<Line> renderScrolledView(long[] screen, int width, int 
height) {
-        List<long[]> history = getHistory(screenTerminal);
+        List<long[]> history = screenTerminal.getHistory();
         if (history.isEmpty()) {
             return renderLiveView(screen, width, height);
         }
@@ -323,20 +367,19 @@ class ShellPanel {
         int col = 0;
         while (col < width) {
             long cell = buffer[offset + col];
-            int ch = (int) (cell & 0xffffffffL);
-            long attr = cell >>> 32;
-            Style style = convertAttrToStyle(attr);
+            int ch = ScreenTerminal.cellCodePoint(cell);
+            long attr = ScreenTerminal.cellAttr(cell);
+            Style style = convertCellToStyle(cell);
 
             StringBuilder sb = new StringBuilder();
             sb.appendCodePoint(ch == 0 ? ' ' : ch);
             int nextCol = col + 1;
             while (nextCol < width) {
                 long nextCell = buffer[offset + nextCol];
-                long nextAttr = nextCell >>> 32;
-                if (nextAttr != attr) {
+                if (ScreenTerminal.cellAttr(nextCell) != attr) {
                     break;
                 }
-                int nextCh = (int) (nextCell & 0xffffffffL);
+                int nextCh = ScreenTerminal.cellCodePoint(nextCell);
                 sb.appendCodePoint(nextCh == 0 ? ' ' : nextCh);
                 nextCol++;
             }
@@ -391,8 +434,6 @@ class ShellPanel {
                     return "Camel";
                 }
             };
-            String camelVersion = VersionHelper.extractCamelVersion();
-
             // Redirect command output (printer()) through the virtual terminal
             // so it renders in the shell panel instead of the TUI's real 
terminal
             CamelJBangMain main = (CamelJBangMain) 
CamelJBangMain.getCommandLine().getCommand();
@@ -431,7 +472,7 @@ class ShellPanel {
 
             try (Shell shell = Shell.builder()
                     .terminal(terminal)
-                    .prompt(() -> buildPrompt(camelVersion))
+                    .prompt(ShellPanel::buildPrompt)
                     .groups(registry, new PosixCommandGroup(), new 
InteractiveCommandGroup())
                     .historyCommands(true)
                     .helpCommands(true)
@@ -456,13 +497,9 @@ class ShellPanel {
         }
     }
 
-    private static String buildPrompt(String camelVersion) {
+    private static String buildPrompt() {
         AttributedStringBuilder sb = new AttributedStringBuilder();
         sb.append("camel", 
AttributedStyle.DEFAULT.bold().foregroundRgb(0xF69123));
-        if (camelVersion != null) {
-            sb.append(" ");
-            sb.append(camelVersion, 
AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
-        }
         sb.append("> ", AttributedStyle.DEFAULT);
         return sb.toAnsi();
     }
@@ -476,49 +513,41 @@ class ShellPanel {
             try {
                 virtualTerminal.close();
             } catch (IOException e) {
-                // ignore
+                LOG.log(Level.DEBUG, "Error closing virtual terminal during 
shutdown", e);
             }
             virtualTerminal = null;
         }
         screenTerminal = null;
     }
 
-    // Attribute mask from ScreenTerminal:
-    //   0xYXFFFBBB00000000L
-    //   X: Bit 0=Underline, Bit 1=Negative, Bit 2=Concealed, Bit 3=Bold
-    //   Y: Bit 0=FG set, Bit 1=BG set, Bit 2=Dim, Bit 3=Italic
-    //   F: Foreground r-g-b (3 hex nibbles)
-    //   B: Background r-g-b (3 hex nibbles)
-    static Style convertAttrToStyle(long attr) {
+    /**
+     * Converts a {@link ScreenTerminal} 64-bit cell value into a TamboUI 
{@link Style}, using JLine's public
+     * cell-decoding helpers.
+     */
+    static Style convertCellToStyle(long cell) {
         Style style = Style.EMPTY;
 
-        int x = (int) ((attr >> 24) & 0xF);
-        int y = (int) ((attr >> 28) & 0xF);
-
-        if ((x & 0x8) != 0) {
+        if (ScreenTerminal.cellBold(cell)) {
             style = style.bold();
         }
-        if ((x & 0x1) != 0) {
+        if (ScreenTerminal.cellUnderline(cell)) {
             style = style.underlined();
         }
-        if ((x & 0x2) != 0) {
+        if (ScreenTerminal.cellInverse(cell)) {
             style = style.reversed();
         }
-        if ((y & 0x4) != 0) {
+        if (ScreenTerminal.cellDim(cell)) {
             style = style.dim();
         }
-        if ((y & 0x8) != 0) {
+        if (ScreenTerminal.cellItalic(cell)) {
             style = style.italic();
         }
 
-        // Foreground color (if set)
-        if ((y & 0x1) != 0) {
-            style = style.fg(resolveColor((int) ((attr >> 12) & 0xFFF)));
+        if (ScreenTerminal.cellHasForeground(cell)) {
+            style = 
style.fg(resolveColor(ScreenTerminal.cellForeground(cell)));
         }
-
-        // Background color (if set)
-        if ((y & 0x2) != 0) {
-            style = style.bg(resolveColor((int) (attr & 0xFFF)));
+        if (ScreenTerminal.cellHasBackground(cell)) {
+            style = 
style.bg(resolveColor(ScreenTerminal.cellBackground(cell)));
         }
 
         return style;
@@ -616,22 +645,6 @@ class ShellPanel {
         };
     }
 
-    @SuppressWarnings("unchecked")
-    private static List<long[]> getHistory(ScreenTerminal st) {
-        try {
-            Field f = ScreenTerminal.class.getDeclaredField("history");
-            f.setAccessible(true);
-            List<long[]> history = (List<long[]>) f.get(st);
-            return history != null ? history : Collections.emptyList();
-        } catch (Exception e) {
-            return Collections.emptyList();
-        }
-    }
-
-    private static int getHistorySize(ScreenTerminal st) {
-        return getHistory(st).size();
-    }
-
     private static class DelegateOutputStream extends OutputStream {
         volatile OutputStream delegate;
 
@@ -642,6 +655,7 @@ class ShellPanel {
                     delegate.write(b);
                 } catch (ArrayIndexOutOfBoundsException e) {
                     // ScreenTerminal buffer resized concurrently — safe to 
ignore
+                    LOG.log(Level.TRACE, "Buffer resize race in write(int)", 
e);
                 }
             }
         }
@@ -653,6 +667,7 @@ class ShellPanel {
                     delegate.write(b, off, len);
                 } catch (ArrayIndexOutOfBoundsException e) {
                     // ScreenTerminal buffer resized concurrently — safe to 
ignore
+                    LOG.log(Level.TRACE, "Buffer resize race in 
write(byte[])", e);
                 }
             }
         }
@@ -664,6 +679,7 @@ class ShellPanel {
                     delegate.flush();
                 } catch (ArrayIndexOutOfBoundsException e) {
                     // ScreenTerminal buffer resized concurrently — safe to 
ignore
+                    LOG.log(Level.TRACE, "Buffer resize race in flush()", e);
                 }
             }
         }
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 18f5c5ab0cc9..a59f197c7fa7 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
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 
 import dev.tamboui.style.Modifier;
@@ -256,64 +257,130 @@ class ShellPanelTest {
         assertNull(result);
     }
 
-    // ---- convertAttrToStyle tests ----
+    // ---- convertCellToStyle tests ----
+    // convertCellToStyle takes a full 64-bit cell (attr in upper 32, 
codepoint in lower 32).
+    // Helper to build a cell from a 32-bit attr value.
+    private static long cellWithAttr(long attr) {
+        return attr << 32;
+    }
 
     @Test
-    void convertAttrToStyleNoFlags() {
-        Style style = ShellPanel.convertAttrToStyle(0);
+    void convertCellToStyleNoFlags() {
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0));
         assertTrue(style.effectiveModifiers().isEmpty());
     }
 
     @Test
-    void convertAttrToStyleBold() {
+    void convertCellToStyleBold() {
         // Bold = bit 3 of X nibble (bits 24-27)
-        long attr = 0x08000000L;
-        Style style = ShellPanel.convertAttrToStyle(attr);
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x08000000L));
         assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
     }
 
     @Test
-    void convertAttrToStyleUnderline() {
-        long attr = 0x01000000L;
-        Style style = ShellPanel.convertAttrToStyle(attr);
+    void convertCellToStyleUnderline() {
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x01000000L));
         assertTrue(style.effectiveModifiers().contains(Modifier.UNDERLINED));
     }
 
     @Test
-    void convertAttrToStyleReversed() {
-        long attr = 0x02000000L;
-        Style style = ShellPanel.convertAttrToStyle(attr);
+    void convertCellToStyleReversed() {
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x02000000L));
         assertTrue(style.effectiveModifiers().contains(Modifier.REVERSED));
     }
 
     @Test
-    void convertAttrToStyleDim() {
+    void convertCellToStyleDim() {
         // Dim = bit 2 of Y nibble (bits 28-31) → 0x4 << 28
-        long attr = 0x40000000L;
-        Style style = ShellPanel.convertAttrToStyle(attr);
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x40000000L));
         assertTrue(style.effectiveModifiers().contains(Modifier.DIM));
     }
 
     @Test
-    void convertAttrToStyleItalic() {
+    void convertCellToStyleItalic() {
         // Italic = bit 3 of Y nibble → 0x8 << 28
-        long attr = 0x80000000L;
-        Style style = ShellPanel.convertAttrToStyle(attr);
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x80000000L));
         assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
     }
 
     @Test
-    void convertAttrToStyleCombinedFgBgBoldItalic() {
+    void convertCellToStyleCombinedFgBgBoldItalic() {
         // Y = 0xB (FG set + BG set + italic: bits 0+1+3), X = 0x8 (bold)
         // FFF = 0xF00 (red FG), BBB = 0x080 (green BG)
-        long attr = 0xB8F00080L;
-        Style style = ShellPanel.convertAttrToStyle(attr);
+        Style style = ShellPanel.convertCellToStyle(cellWithAttr(0xB8F00080L));
         assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
         assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
         assertTrue(style.fg().isPresent());
         assertTrue(style.bg().isPresent());
     }
 
+    // ---- panelPercent / cycleHeight tests ----
+
+    @Test
+    void panelPercentDefaultIs50() {
+        ShellPanel panel = new ShellPanel();
+        assertEquals(50, panel.panelPercent());
+    }
+
+    @Test
+    void cycleHeightCyclesThroughPercents() {
+        ShellPanel panel = new ShellPanel();
+        // Default is 50% (index 1)
+        assertEquals(50, panel.panelPercent());
+
+        panel.cycleHeight();
+        assertEquals(75, panel.panelPercent());
+
+        panel.cycleHeight();
+        assertEquals(100, panel.panelPercent());
+
+        panel.cycleHeight();
+        assertEquals(25, panel.panelPercent());
+
+        panel.cycleHeight();
+        assertEquals(50, panel.panelPercent()); // wraps around
+    }
+
+    // ---- renderFooter tests ----
+
+    @Test
+    void renderFooterShowsCurrentPercentage() {
+        ShellPanel panel = new ShellPanel();
+        // Default split is 50%
+        List<Span> spans = new ArrayList<>();
+        panel.renderFooter(spans);
+
+        String footer = spansToString(spans);
+        assertTrue(footer.contains("resize (50%)"), "Footer should show 
'resize (50%)'");
+    }
+
+    @Test
+    void renderFooterUpdatesAfterCycleHeight() {
+        ShellPanel panel = new ShellPanel();
+        panel.cycleHeight(); // now 75%
+
+        List<Span> spans = new ArrayList<>();
+        panel.renderFooter(spans);
+
+        String footer = spansToString(spans);
+        assertTrue(footer.contains("resize (75%)"), "Footer should show 
'resize (75%)' after cycling once");
+    }
+
+    @Test
+    void renderFooterContainsExpectedHints() {
+        ShellPanel panel = new ShellPanel();
+        List<Span> spans = new ArrayList<>();
+        panel.renderFooter(spans);
+
+        String footer = spansToString(spans);
+        assertTrue(footer.contains("F6"), "Footer should contain F6 hint");
+        assertTrue(footer.contains("close"), "Footer should contain 'close' 
label for F6");
+        assertTrue(footer.contains("Shift+F6"), "Footer should contain 
Shift+F6 hint");
+        assertTrue(footer.contains("resize"), "Footer should contain 'resize' 
action label");
+        assertTrue(footer.contains("PgUp/Dn"), "Footer should contain PgUp/Dn 
hint");
+        assertTrue(footer.contains("scroll"), "Footer should contain 'scroll' 
label");
+    }
+
     private static String rawContent(Line line) {
         StringBuilder sb = new StringBuilder();
         for (Span span : line.spans()) {
@@ -321,4 +388,12 @@ class ShellPanelTest {
         }
         return sb.toString();
     }
+
+    private static String spansToString(List<Span> spans) {
+        StringBuilder sb = new StringBuilder();
+        for (Span span : spans) {
+            sb.append(span.content());
+        }
+        return sb.toString();
+    }
 }

Reply via email to