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;