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 685ac395eb08 CAMEL-23727: camel-jbang - TUI shell panel improvements 
(#23915)
685ac395eb08 is described below

commit 685ac395eb08e572a6437c95e4ff916250b5f52d
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jun 10 16:44:26 2026 +0200

    CAMEL-23727: camel-jbang - TUI shell panel improvements (#23915)
    
    - Use proper Block border with rounded corners matching other TUI tabs
    - Detect embedded shell context to prevent full-screen commands from
      corrupting the TUI (log --follow, top --watch run once with hint)
    - Redirect clearScreen() and AnsiConsole.out() through virtual terminal
      when running inside the shell panel
    - Add keyboard scrollback via Shift+PageUp/Down using ScreenTerminal
      history buffer (uses reflection for JLine compatibility)
    - Auto-close shell panel when user types 'exit', with fresh restart on
      next open
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../core/commands/action/ActionWatchCommand.java   |  15 +-
 .../jbang/core/commands/action/CamelLogAction.java |  19 ++-
 .../core/commands/process/ProcessWatchCommand.java |  15 +-
 .../dsl/jbang/core/common/EnvironmentHelper.java   |   7 +
 .../dsl/jbang/core/commands/tui/ShellPanel.java    | 188 +++++++++++++++------
 5 files changed, 187 insertions(+), 57 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java
index c992157ef981..1ad675063cb2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java
@@ -20,9 +20,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.CommandHelper;
+import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.util.StopWatch;
 import org.jline.jansi.Ansi;
 import org.jline.jansi.AnsiConsole;
+import org.jline.terminal.Terminal;
 import picocli.CommandLine;
 
 abstract class ActionWatchCommand extends ActionBaseCommand {
@@ -41,7 +43,10 @@ abstract class ActionWatchCommand extends ActionBaseCommand {
     @Override
     public Integer doCall() throws Exception {
         int exit;
-        if (watch) {
+        if (watch && EnvironmentHelper.isEmbedded()) {
+            printer().println("Tip: use the TUI tabs for live monitoring");
+            exit = doWatchCall();
+        } else if (watch) {
             Thread t = new Thread(() -> {
                 waitUserTask = waitForUserEnter();
                 waitUserTask.run();
@@ -72,7 +77,13 @@ abstract class ActionWatchCommand extends ActionBaseCommand {
     }
 
     protected void clearScreen() {
-        AnsiConsole.out().print(Ansi.ansi().eraseScreen().cursor(1, 1));
+        Terminal t = EnvironmentHelper.getActiveTerminal();
+        if (t != null) {
+            t.writer().print("\033[2J\033[H");
+            t.writer().flush();
+        } else {
+            AnsiConsole.out().print(Ansi.ansi().eraseScreen().cursor(1, 1));
+        }
     }
 
     protected boolean watchWait(StopWatch watch) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
index 11842af8bf31..e1be84e7f314 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
@@ -41,6 +41,7 @@ import org.apache.camel.catalog.impl.TimePatternConverter;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.CommandHelper;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
@@ -132,6 +133,11 @@ public class CamelLogAction extends ActionBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
+        if (follow && EnvironmentHelper.isEmbedded()) {
+            follow = false;
+            printer().println("Tip: press F3 to switch to the Log tab for live 
log streaming");
+        }
+
         Map<Long, Row> rows = new LinkedHashMap<>();
 
         // find new pids
@@ -391,7 +397,12 @@ public class CamelLogAction extends ActionBaseCommand {
                     colors.put(name, color);
                 }
                 String n = String.format("%-" + nameMaxWidth + "s", name);
-                AnsiConsole.out().print(Ansi.ansi().fg(color).a(n).a("| 
").reset());
+                String prefix = Ansi.ansi().fg(color).a(n).a("| 
").reset().toString();
+                if (EnvironmentHelper.isEmbedded()) {
+                    printer().print(prefix);
+                } else {
+                    AnsiConsole.out().print(prefix);
+                }
             }
         } else {
             line = unescapeAnsi(line);
@@ -422,7 +433,11 @@ public class CamelLogAction extends ActionBaseCommand {
             line = before != null ? before + "---" + after : after;
         }
         if (loggingColor) {
-            AnsiConsole.out().println(line);
+            if (EnvironmentHelper.isEmbedded()) {
+                printer().println(line);
+            } else {
+                AnsiConsole.out().println(line);
+            }
         } else {
             printer().println(line);
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessWatchCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessWatchCommand.java
index ccdf4502a5e9..15b02ee4cd6c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessWatchCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessWatchCommand.java
@@ -20,9 +20,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.CommandHelper;
+import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.util.StopWatch;
 import org.jline.jansi.Ansi;
 import org.jline.jansi.AnsiConsole;
+import org.jline.terminal.Terminal;
 import picocli.CommandLine;
 
 /**
@@ -48,7 +50,10 @@ abstract class ProcessWatchCommand extends 
ProcessBaseCommand {
     public Integer doCall() throws Exception {
         int exit;
         final AtomicBoolean running = new AtomicBoolean(true);
-        if (watch) {
+        if (watch && EnvironmentHelper.isEmbedded()) {
+            printer().println("Tip: use the TUI tabs for live monitoring");
+            exit = doProcessWatchCall();
+        } else if (watch) {
             Thread t = new Thread(() -> {
                 waitUserTask = new CommandHelper.ReadConsoleTask(() -> 
running.set(false));
                 waitUserTask.run();
@@ -80,7 +85,13 @@ abstract class ProcessWatchCommand extends 
ProcessBaseCommand {
     }
 
     protected void clearScreen() {
-        AnsiConsole.out().print(Ansi.ansi().eraseScreen().cursor(1, 1));
+        Terminal t = EnvironmentHelper.getActiveTerminal();
+        if (t != null) {
+            t.writer().print("\033[2J\033[H");
+            t.writer().flush();
+        } else {
+            AnsiConsole.out().print(Ansi.ansi().eraseScreen().cursor(1, 1));
+        }
     }
 
     protected abstract Integer doProcessWatchCall() throws Exception;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java
index 3e402369cddb..184bbb33429f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java
@@ -59,6 +59,13 @@ public final class EnvironmentHelper {
         return activeTerminal;
     }
 
+    /**
+     * Returns true if the current command is running inside the TUI's 
embedded shell panel.
+     */
+    public static boolean isEmbedded() {
+        return activeTerminal != null;
+    }
+
     /**
      * Sets the selected Camel process name/PID. Called by the TUI to make the 
selected integration available to
      * subcommands like ask.
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 7b5943da835a..b3d1f4d0dd37 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,13 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.lang.reflect.Method;
 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.Color;
 import dev.tamboui.style.Overflow;
@@ -35,6 +35,9 @@ import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
@@ -79,6 +82,9 @@ class ShellPanel {
 
     private int lastWidth;
     private int lastHeight;
+    private int scrollOffset;
+    private int lastHistorySize;
+    private volatile boolean shellExited;
 
     void setContext(MonitorContext ctx) {
         this.ctx = ctx;
@@ -98,8 +104,9 @@ class ShellPanel {
 
     void open() {
         visible = true;
-        if (startError != null) {
+        if (startError != null || shellExited) {
             startError = null;
+            shellExited = false;
             screenTerminal = null;
             virtualTerminal = null;
         }
@@ -125,6 +132,20 @@ class ShellPanel {
             return true;
         }
 
+        // Shift+PageUp/Down for scrollback through history
+        if (ke.isKey(KeyCode.PAGE_UP) && ke.hasShift()) {
+            int histSize = screenTerminal != null ? 
getHistorySize(screenTerminal) : 0;
+            scrollOffset = Math.min(scrollOffset + lastHeight, histSize);
+            return true;
+        }
+        if (ke.isKey(KeyCode.PAGE_DOWN) && ke.hasShift()) {
+            scrollOffset = Math.max(0, scrollOffset - lastHeight);
+            return true;
+        }
+
+        // Any regular key input resets scrollback to live view
+        scrollOffset = 0;
+
         // Forward everything else to the virtual terminal
         if (virtualTerminal != null) {
             try {
@@ -144,9 +165,21 @@ class ShellPanel {
             return;
         }
 
-        // Reserve 1 row for separator line at top
-        int innerWidth = area.width();
-        int innerHeight = area.height() - 1;
+        if (shellExited) {
+            close();
+            return;
+        }
+
+        // Render border matching other tabs
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(Title.from(Line.from(Span.styled(" Shell ", 
Style.EMPTY.bold()))))
+                .build();
+        frame.renderWidget(block, area);
+        Rect inner = block.inner(area);
+
+        int innerWidth = inner.width();
+        int innerHeight = inner.height();
 
         // Start shell on first render (we now know the size)
         if (screenTerminal == null && innerWidth > 2 && innerHeight > 2) {
@@ -163,26 +196,12 @@ class ShellPanel {
             lastHeight = innerHeight;
         }
 
-        // Split: separator line + content
-        List<Rect> chunks = Layout.vertical()
-                .constraints(Constraint.length(1), Constraint.fill())
-                .split(area);
-
-        // Render separator line with title
-        String sep = "─".repeat(Math.max(0, innerWidth - 8));
-        frame.renderWidget(
-                Paragraph.from(Line.from(
-                        Span.styled("── ", Style.EMPTY.dim()),
-                        Span.styled("Shell", Style.EMPTY.bold()),
-                        Span.styled(" " + sep, Style.EMPTY.dim()))),
-                chunks.get(0));
-
         // Show error from shell thread crash
         if (startError != null) {
             frame.renderWidget(
                     Paragraph.from(Line.from(
                             Span.styled(startError, 
Style.EMPTY.fg(Color.LIGHT_RED)))),
-                    chunks.get(1));
+                    inner);
             return;
         }
 
@@ -200,35 +219,18 @@ class ShellPanel {
             return;
         }
 
-        // Convert to TamboUI lines
-        List<Line> lines = new ArrayList<>(innerHeight);
-        for (int row = 0; row < innerHeight; row++) {
-            List<Span> spans = new ArrayList<>();
-            int col = 0;
-            while (col < innerWidth) {
-                long cell = screen[row * innerWidth + col];
-                int ch = (int) (cell & 0xffffffffL);
-                long attr = cell >>> 32;
-                Style style = convertAttrToStyle(attr);
-
-                // Merge consecutive cells with same attributes
-                StringBuilder sb = new StringBuilder();
-                sb.appendCodePoint(ch == 0 ? ' ' : ch);
-                int nextCol = col + 1;
-                while (nextCol < innerWidth) {
-                    long nextCell = screen[row * innerWidth + nextCol];
-                    long nextAttr = nextCell >>> 32;
-                    if (nextAttr != attr) {
-                        break;
-                    }
-                    int nextCh = (int) (nextCell & 0xffffffffL);
-                    sb.appendCodePoint(nextCh == 0 ? ' ' : nextCh);
-                    nextCol++;
-                }
-                spans.add(Span.styled(sb.toString(), style));
-                col = nextCol;
-            }
-            lines.add(Line.from(spans));
+        // Auto-follow: reset scroll when new history appears
+        int histSize = getHistorySize(screenTerminal);
+        if (histSize > lastHistorySize && scrollOffset > 0) {
+            scrollOffset = 0;
+        }
+        lastHistorySize = histSize;
+
+        List<Line> lines;
+        if (scrollOffset > 0) {
+            lines = renderScrolledView(screen, innerWidth, innerHeight);
+        } else {
+            lines = renderLiveView(screen, innerWidth, innerHeight);
         }
 
         frame.renderWidget(
@@ -236,13 +238,77 @@ class ShellPanel {
                         .text(Text.from(lines))
                         .overflow(Overflow.CLIP)
                         .build(),
-                chunks.get(1));
+                inner);
     }
 
     void renderFooter(List<Span> spans) {
         MonitorContext.hint(spans, "F6", "close");
         int nextPct = SPLIT_PERCENTS[(splitIndex + 1) % SPLIT_PERCENTS.length];
         MonitorContext.hint(spans, "Shift+F6", nextPct + "%");
+        MonitorContext.hint(spans, "Shift+PgUp/Dn", "scroll");
+    }
+
+    private List<Line> renderLiveView(long[] screen, int width, int height) {
+        List<Line> lines = new ArrayList<>(height);
+        for (int row = 0; row < height; row++) {
+            lines.add(convertRow(screen, row * width, width));
+        }
+        return lines;
+    }
+
+    private List<Line> renderScrolledView(long[] screen, int width, int 
height) {
+        List<long[]> history = getHistory(screenTerminal);
+        if (history.isEmpty()) {
+            return renderLiveView(screen, width, height);
+        }
+
+        int totalLines = history.size() + height;
+        int viewStart = Math.max(0, totalLines - scrollOffset - height);
+
+        List<Line> lines = new ArrayList<>(height);
+        for (int i = 0; i < height; i++) {
+            int lineIdx = viewStart + i;
+            if (lineIdx < history.size()) {
+                long[] histLine = history.get(lineIdx);
+                lines.add(convertRow(histLine, 0, Math.min(histLine.length, 
width)));
+            } else {
+                int screenRow = lineIdx - history.size();
+                if (screenRow >= 0 && screenRow < height) {
+                    lines.add(convertRow(screen, screenRow * width, width));
+                } else {
+                    lines.add(Line.from(Span.raw("")));
+                }
+            }
+        }
+        return lines;
+    }
+
+    private static Line convertRow(long[] buffer, int offset, int width) {
+        List<Span> spans = new ArrayList<>();
+        int col = 0;
+        while (col < width) {
+            long cell = buffer[offset + col];
+            int ch = (int) (cell & 0xffffffffL);
+            long attr = cell >>> 32;
+            Style style = convertAttrToStyle(attr);
+
+            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) {
+                    break;
+                }
+                int nextCh = (int) (nextCell & 0xffffffffL);
+                sb.appendCodePoint(nextCh == 0 ? ' ' : nextCh);
+                nextCol++;
+            }
+            spans.add(Span.styled(sb.toString(), style));
+            col = nextCol;
+        }
+        return Line.from(spans);
     }
 
     private String startError;
@@ -338,6 +404,7 @@ class ShellPanel {
                     .build()) {
                 EnvironmentHelper.setActiveTerminal(terminal);
                 shell.run();
+                shellExited = true;
             } finally {
                 EnvironmentHelper.setActiveTerminal(null);
                 EnvironmentHelper.setSelectedProcess(null);
@@ -468,6 +535,25 @@ class ShellPanel {
         };
     }
 
+    @SuppressWarnings("unchecked")
+    private static List<long[]> getHistory(ScreenTerminal st) {
+        try {
+            Method m = ScreenTerminal.class.getMethod("getHistory");
+            return (List<long[]>) m.invoke(st);
+        } catch (Exception e) {
+            return Collections.emptyList();
+        }
+    }
+
+    private static int getHistorySize(ScreenTerminal st) {
+        try {
+            Method m = ScreenTerminal.class.getMethod("getHistorySize");
+            return (int) m.invoke(st);
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
     private static class DelegateOutputStream extends OutputStream {
         volatile OutputStream delegate;
 

Reply via email to