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) { }
