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;

Reply via email to