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

davsclaus pushed a commit to branch tui-keystroke-overlay
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 93cddea00fef3d6e150943bf247d6bdfde4a51f4
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 18:03:41 2026 +0200

    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]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 18 +++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 96 +++++++++++++++++++++-
 2 files changed, 110 insertions(+), 4 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..ce5affa808f0 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)
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..f79c0d2be4ce 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
@@ -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);
             }
@@ -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();
@@ -1503,6 +1575,25 @@ public class CamelMonitor extends CamelCommand {
             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 +2891,9 @@ public class CamelMonitor extends CamelCommand {
         return TuiHelper.objToLong(o);
     }
 
+    record KeyRecord(String label, long timestamp) {
+    }
+
     record VanishingInfo(IntegrationInfo info, long startTime) {
     }
 

Reply via email to