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 cb491a3a065c camel-tui: Keystroke overlay, sorting, and UI
improvements (#23440)
cb491a3a065c is described below
commit cb491a3a065c04d746ba8f29147578e15931ccdf
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 20:35:21 2026 +0200
camel-tui: Keystroke overlay, sorting, and UI improvements (#23440)
* camel-tui: Add keystroke overlay for recording mode
Shows recent keystrokes right-aligned in the footer during recording,
with bright-to-dim fade over 2 seconds. Activated automatically with
--record or toggled via F2 menu "Show/Hide Keystrokes".
Co-Authored-By: Claude <[email protected]>
* camel-tui: Add sorting to health/http tabs, fix log page navigation,
default chart to single
- Health tab: add sort by group/name/status (default: name)
- HTTP tab: add TOTAL to sortable columns
- Log tab: wire PgUp/PgDn into handleKeyEvent
- Overview chart defaults to single instead of all
Co-Authored-By: Claude <[email protected]>
* camel-tui: Make F2 global, add adoc fallback for bundled example docs
- F2 actions menu now works on all tabs, not just overview
- F2 hint shown in footer on all tabs after Esc
- Bundled examples fall back to README.adoc if README.md not found
Co-Authored-By: Claude <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 22 +++-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 115 +++++++++++++++++++--
.../dsl/jbang/core/commands/tui/HealthTab.java | 42 ++++++--
.../camel/dsl/jbang/core/commands/tui/HttpTab.java | 5 +-
.../camel/dsl/jbang/core/commands/tui/LogTab.java | 8 ++
5 files changed, 172 insertions(+), 20 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 673ea1ee3e0a..bb12dfcec2b6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -60,11 +60,14 @@ class ActionsPopup {
private static final int ACTION_RUN_EXAMPLE = 0;
private static final int ACTION_SHOW_DOCS = 1;
private static final int ACTION_SCREENSHOT = 2;
- private static final int ACTION_COUNT = 3;
+ private static final int ACTION_SHOW_KEYSTROKES = 3;
+ private static final int ACTION_COUNT = 4;
private final Supplier<Set<String>> runningNames;
private final Supplier<List<IntegrationInfo>> integrations;
private final Runnable screenshotAction;
+ private final Runnable toggleKeystrokes;
+ private final Supplier<Boolean> keystrokesEnabled;
private MonitorContext ctx;
private boolean showActionsMenu;
@@ -93,10 +96,12 @@ class ActionsPopup {
private long launchNotificationExpiry;
ActionsPopup(Supplier<Set<String>> runningNames,
Supplier<List<IntegrationInfo>> integrations,
- Runnable screenshotAction) {
+ Runnable screenshotAction, Runnable toggleKeystrokes,
Supplier<Boolean> keystrokesEnabled) {
this.runningNames = runningNames;
this.integrations = integrations;
this.screenshotAction = screenshotAction;
+ this.toggleKeystrokes = toggleKeystrokes;
+ this.keystrokesEnabled = keystrokesEnabled;
}
void setContext(MonitorContext ctx) {
@@ -221,6 +226,9 @@ class ActionsPopup {
} else if (sel == ACTION_SCREENSHOT) {
showActionsMenu = false;
screenshotAction.run();
+ } else if (sel == ACTION_SHOW_KEYSTROKES) {
+ showActionsMenu = false;
+ toggleKeystrokes.run();
}
}
}
@@ -296,10 +304,14 @@ class ActionsPopup {
Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
frame.renderWidget(Clear.INSTANCE, popup);
+ String keystrokeLabel = keystrokesEnabled.get()
+ ? " Hide Keystrokes"
+ : " Show Keystrokes";
ListWidget list = ListWidget.builder()
.items(ListItem.from(" Run an example..."),
ListItem.from(" Show Documentation"),
- ListItem.from(" Take Screenshot"))
+ ListItem.from(" Take Screenshot"),
+ ListItem.from(keystrokeLabel))
.highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
.highlightSymbol("")
.scrollMode(ScrollMode.NONE)
@@ -541,6 +553,10 @@ class ActionsPopup {
boolean isAdoc = false;
if (bundled) {
content = DocHelper.loadResourceContent("examples/" + name +
"/README.md");
+ if (content == null) {
+ content = DocHelper.loadResourceContent("examples/" + name +
"/README.adoc");
+ isAdoc = content != null;
+ }
} else {
String base =
"https://raw.githubusercontent.com/apache/camel-jbang-examples/main/" + name +
"/";
content = DocHelper.downloadContent(base + "README.md");
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 413230648096..f34daf997371 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
@@ -192,7 +192,7 @@ public class CamelMonitor extends CamelCommand {
private static final int CHART_ALL = 0;
private static final int CHART_SINGLE = 1;
private static final int CHART_OFF = 2;
- private int chartMode = CHART_ALL;
+ private int chartMode = CHART_SINGLE;
private volatile long lastRefresh;
private boolean showKillConfirm;
@@ -200,6 +200,8 @@ public class CamelMonitor extends CamelCommand {
private volatile String screenshotMessage;
private volatile long screenshotMessageTime;
private volatile boolean pendingScreenshot;
+ private boolean recording;
+ private final List<KeyRecord> recentKeys = new ArrayList<>();
private final ActionsPopup actionsPopup = new ActionsPopup(
() -> data.get().stream()
@@ -209,7 +211,9 @@ public class CamelMonitor extends CamelCommand {
() -> data.get().stream()
.filter(i -> !i.vanishing)
.collect(Collectors.toList()),
- () -> pendingScreenshot = true);
+ () -> pendingScreenshot = true,
+ () -> recording = !recording,
+ () -> recording);
private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
private TuiRunner runner;
@@ -247,6 +251,8 @@ public class CamelMonitor extends CamelCommand {
System.setProperty("tamboui.record.fps", "10");
}
+ recording = record != null;
+
// to make ServiceLoader work with tamboui for downloaded JARs
Thread.currentThread().setContextClassLoader(classLoader);
TuiHelper.preloadClasses(classLoader);
@@ -289,6 +295,12 @@ public class CamelMonitor extends CamelCommand {
private boolean handleEvent(Event event, TuiRunner runner) {
if (event instanceof KeyEvent ke) {
+ if (recording) {
+ String label = keyLabel(ke);
+ if (label != null) {
+ recentKeys.add(new KeyRecord(label,
System.currentTimeMillis()));
+ }
+ }
if (actionsPopup.isVisible()) {
return actionsPopup.handleKeyEvent(ke);
}
@@ -396,6 +408,12 @@ public class CamelMonitor extends CamelCommand {
return true;
}
+ // F2 opens actions menu (global)
+ if (ke.isKey(KeyCode.F2)) {
+ actionsPopup.open();
+ return true;
+ }
+
// Tab-specific keys — delegate to active tab first
int tab = tabsState.selected();
MonitorTab activeTab = activeTab();
@@ -509,12 +527,6 @@ public class CamelMonitor extends CamelCommand {
showKillConfirm = true;
return true;
}
- // Overview tab: F2 opens actions menu
- if (tab == TAB_OVERVIEW && ke.isKey(KeyCode.F2)) {
- actionsPopup.open();
- return true;
- }
-
// Delegate remaining keys to active tab
if (activeTab != null && activeTab.handleKeyEvent(ke)) {
return true;
@@ -523,6 +535,10 @@ public class CamelMonitor extends CamelCommand {
if (event instanceof TickEvent) {
long now = System.currentTimeMillis();
actionsPopup.tick(now);
+ if (recording && !recentKeys.isEmpty()) {
+ long cutoff = now - 2000;
+ recentKeys.removeIf(k -> k.timestamp() < cutoff);
+ }
long interval = routesTab.isShowDiagram() ?
Math.max(refreshInterval, 1000) : refreshInterval;
if (now - lastRefresh >= interval) {
refreshData();
@@ -534,6 +550,62 @@ public class CamelMonitor extends CamelCommand {
return false;
}
+ private String keyLabel(KeyEvent ke) {
+ if (ke.isKey(KeyCode.ENTER)) {
+ return "Enter";
+ }
+ if (ke.isKey(KeyCode.ESCAPE)) {
+ return "Esc";
+ }
+ if (ke.isKey(KeyCode.TAB)) {
+ return ke.hasShift() ? "⇧Tab" : "Tab";
+ }
+ if (ke.isKey(KeyCode.UP)) {
+ return "↑";
+ }
+ if (ke.isKey(KeyCode.DOWN)) {
+ return "↓";
+ }
+ if (ke.isKey(KeyCode.LEFT)) {
+ return "←";
+ }
+ if (ke.isKey(KeyCode.RIGHT)) {
+ return "→";
+ }
+ if (ke.isKey(KeyCode.PAGE_UP)) {
+ return "PgUp";
+ }
+ if (ke.isKey(KeyCode.PAGE_DOWN)) {
+ return "PgDn";
+ }
+ if (ke.isKey(KeyCode.HOME)) {
+ return "Home";
+ }
+ if (ke.isKey(KeyCode.END)) {
+ return "End";
+ }
+ if (ke.isKey(KeyCode.BACKSPACE)) {
+ return "⌫";
+ }
+ for (int i = 1; i <= 12; i++) {
+ try {
+ KeyCode fKey = KeyCode.valueOf("F" + i);
+ if (ke.isKey(fKey)) {
+ return "F" + i;
+ }
+ } catch (IllegalArgumentException e) {
+ break;
+ }
+ }
+ if (ke.code() == KeyCode.CHAR) {
+ String s = ke.string();
+ if (!s.isEmpty()) {
+ return s;
+ }
+ }
+ return null;
+ }
+
private boolean handleTabKey(int tab) {
if (tab != TAB_OVERVIEW) {
selectCurrentIntegration();
@@ -1499,10 +1571,34 @@ public class CamelMonitor extends CamelCommand {
if (tab != null) {
tab.renderFooter(spans);
+ // Insert F2 after the first hint (Esc) — each hint is 2 spans
(key + label)
+ int insertPos = Math.min(2, spans.size());
+ List<Span> f2Spans = new ArrayList<>();
+ hint(f2Spans, "F2", "actions");
+ spans.addAll(insertPos, f2Spans);
} else {
renderOverviewFooter(spans);
}
+ if (recording && !recentKeys.isEmpty()) {
+ long now = System.currentTimeMillis();
+ List<Span> keySpans = new ArrayList<>();
+ int maxKeys = Math.min(recentKeys.size(), 8);
+ List<KeyRecord> visible = recentKeys.subList(recentKeys.size() -
maxKeys, recentKeys.size());
+ for (KeyRecord kr : visible) {
+ long age = now - kr.timestamp();
+ Style style = age < 1000
+ ? Style.EMPTY.fg(Color.WHITE).bold().onBlue()
+ : Style.EMPTY.dim();
+ keySpans.add(Span.styled(" " + kr.label() + " ", style));
+ }
+ int hintsWidth = spans.stream().mapToInt(s -> s.width()).sum();
+ int keystrokeWidth = keySpans.stream().mapToInt(s ->
s.width()).sum();
+ int gap = Math.max(1, area.width() - hintsWidth - keystrokeWidth);
+ spans.add(Span.raw(" ".repeat(gap)));
+ spans.addAll(keySpans);
+ }
+
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
}
@@ -2800,6 +2896,9 @@ public class CamelMonitor extends CamelCommand {
return TuiHelper.objToLong(o);
}
+ record KeyRecord(String label, long timestamp) {
+ }
+
record VanishingInfo(IntegrationInfo info, long startTime) {
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
index b95750f04f90..2da1b9fbf79f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
@@ -33,11 +33,18 @@ import dev.tamboui.widgets.table.Row;
import dev.tamboui.widgets.table.Table;
import dev.tamboui.widgets.table.TableState;
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
class HealthTab implements MonitorTab {
+ private static final String[] SORT_COLUMNS = { "group", "name", "status" };
+
private final MonitorContext ctx;
private final TableState tableState = new TableState();
private boolean showOnlyDown;
+ private String sort = "name";
+ private int sortIndex = 1;
+ private boolean sortReversed;
HealthTab(MonitorContext ctx) {
this.ctx = ctx;
@@ -45,6 +52,16 @@ class HealthTab implements MonitorTab {
@Override
public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isChar('s')) {
+ sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+ sort = SORT_COLUMNS[sortIndex];
+ sortReversed = false;
+ return true;
+ }
+ if (ke.isChar('S')) {
+ sortReversed = !sortReversed;
+ return true;
+ }
if (ke.isCharIgnoreCase('d')) {
showOnlyDown = !showOnlyDown;
return true;
@@ -73,7 +90,8 @@ class HealthTab implements MonitorTab {
return;
}
- List<HealthCheckInfo> healthChecks = getFilteredHealthChecks(info);
+ List<HealthCheckInfo> healthChecks = new
ArrayList<>(getFilteredHealthChecks(info));
+ healthChecks.sort(this::sortHealth);
List<Row> rows = new ArrayList<>();
for (HealthCheckInfo hc : healthChecks) {
@@ -123,9 +141,9 @@ class HealthTab implements MonitorTab {
Table table = Table.builder()
.rows(rows)
.header(Row.from(
- Cell.from(Span.styled("GROUP", Style.EMPTY.bold())),
- Cell.from(Span.styled("NAME", Style.EMPTY.bold())),
- Cell.from(Span.styled("STATUS", Style.EMPTY.bold())),
+ Cell.from(Span.styled(sortLabel("GROUP", "group",
sort, sortReversed), sortStyle("group", sort))),
+ Cell.from(Span.styled(sortLabel("NAME", "name", sort,
sortReversed), sortStyle("name", sort))),
+ Cell.from(Span.styled(sortLabel("STATUS", "status",
sort, sortReversed), sortStyle("status", sort))),
Cell.from(Span.styled("KIND", Style.EMPTY.bold())),
Cell.from(Span.styled("MESSAGE", Style.EMPTY.bold()))))
.widths(
@@ -142,15 +160,25 @@ class HealthTab implements MonitorTab {
@Override
public void renderFooter(List<Span> spans) {
- MonitorContext.hint(spans, "Esc", "back");
- MonitorContext.hint(spans, "d", "toggle DOWN");
- MonitorContext.hint(spans, "1-9", "tabs");
+ hint(spans, "Esc", "back");
+ hint(spans, "s", "sort");
+ hint(spans, "d", "toggle DOWN");
+ hint(spans, "1-9", "tabs");
}
boolean isShowOnlyDown() {
return showOnlyDown;
}
+ private int sortHealth(HealthCheckInfo a, HealthCheckInfo b) {
+ int result = switch (sort) {
+ case "name" -> compareStr(a.name, b.name);
+ case "status" -> compareStr(a.state, b.state);
+ default -> compareStr(a.group, b.group);
+ };
+ return sortReversed ? -result : result;
+ }
+
List<HealthCheckInfo> getFilteredHealthChecks(IntegrationInfo info) {
if (showOnlyDown) {
return info.healthChecks.stream().filter(hc ->
"DOWN".equals(hc.state)).toList();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index c9ef20aed030..15c2a0bf74a4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -51,7 +51,7 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
class HttpTab implements MonitorTab {
- private static final String[] SORT_COLUMNS = { "method", "path",
"consumes", "produces", "source" };
+ private static final String[] SORT_COLUMNS = { "method", "path", "total",
"consumes", "produces", "source" };
private static final Set<String> OPENAPI_HTTP_VERBS
= Set.of("get", "post", "put", "delete", "patch", "options",
"head", "trace");
@@ -202,6 +202,7 @@ class HttpTab implements MonitorTab {
visible.sort((a, b) -> {
int result = switch (sort) {
case "path" -> compareStr(a.path, b.path);
+ case "total" -> Long.compare(a.hits, b.hits);
case "source" -> Boolean.compare(b.fromRest, a.fromRest);
case "consumes" -> compareStr(a.consumes, b.consumes);
case "produces" -> compareStr(a.produces, b.produces);
@@ -315,7 +316,7 @@ class HttpTab implements MonitorTab {
Row header = Row.from(
Cell.from(Span.styled(sortLabel("METHOD", "method"),
sortStyle("method"))),
Cell.from(Span.styled(sortLabel("PATH", "path"),
sortStyle("path"))),
- rightCell("TOTAL", 8, Style.EMPTY.bold()),
+ rightCell(sortLabel("TOTAL", "total"), 8, sortStyle("total")),
Cell.from(Span.styled(sortLabel("CONSUMES", "consumes"),
sortStyle("consumes"))),
Cell.from(Span.styled(sortLabel("PRODUCES", "produces"),
sortStyle("produces"))),
Cell.from(Span.styled(sortLabel("SOURCE", "source"),
sortStyle("source"))),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
index 6c1c3164ce19..a66d0661a549 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
@@ -139,6 +139,14 @@ class LogTab implements MonitorTab {
return true;
}
}
+ if (ke.isPageUp()) {
+ pageUp();
+ return true;
+ }
+ if (ke.isPageDown()) {
+ pageDown();
+ return true;
+ }
if (ke.isHome()) {
followMode = false;
scroll = 0;