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 d13bd70c6b7a CAMEL-24795: camel-jbang - TUI F2 actions menu gets a
Quit entry (#26554)
d13bd70c6b7a is described below
commit d13bd70c6b7a4960366abc4f552881d8f8475c74
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 17 18:44:20 2026 +0200
CAMEL-24795: camel-jbang - TUI F2 actions menu gets a Quit entry (#26554)
* CAMEL-24795: camel-jbang - TUI F2 actions menu ends with a Quit entry
that takes the same confirm and shutdown path as q
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-24795: camel-jbang - test that the F2 Quit entry runs the wired
quit action
Review feedback on #26554.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-tui.adoc | 3 +-
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 25 +++++++++++++++++
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 20 ++++++++++----
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../src/main/resources/tui/help/overview.md | 2 +-
.../jbang/core/commands/tui/ActionsPopupTest.java | 32 ++++++++++++++++++++++
6 files changed, 76 insertions(+), 7 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index eabd716fb5f2..accb6284a792 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -534,6 +534,7 @@ Press *F2* to open the actions menu with quick access to
common operations:
| Settings... | Change the theme, the starting tab, and the default
run-from-folder.
| Take Screenshot | Export the current screen as SVG, text, or ANSI art.
| Start/Stop Tape Recording | Record your session as a `.tape` file for demos.
+| Quit | Quit the TUI, with the same confirmation as pressing *q*. The last
entry of the menu, so quitting is one menu away on tabs and in input fields
where *q* is taken by something else.
|===
=== Sending Test Messages
@@ -667,7 +668,7 @@ line breaks are sent as *Enter*, so a multi-line paste runs
line by line as in a
| *F8* / *Shift+F8* | Toggle the AI prompt panel / cycle its height
| *F10* | Run menu (run, stop, restart, kill)
| *Shift+F5* | Take screenshot
-| *Ctrl+C* / *Q* | Quit
+| *Ctrl+C* / *Q* / *F2* → Quit | Quit
| *Esc* | Close popup / go back / return to Overview
|===
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 b882e4b67a0d..ca2caa18951d 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,6 +60,7 @@ class ActionsPopup {
DOCTOR,
RESET_STATS,
SHELL,
+ QUIT,
SCREEN_SUBMENU,
SETTINGS,
MCP_SUBMENU,
@@ -96,6 +97,7 @@ class ActionsPopup {
private Runnable browseFilesAction;
private Runnable camelAnimationAction;
private Runnable switchIntegrationAction;
+ private Runnable quitAction;
private final Supplier<Boolean> tapeRecordingActive;
private MonitorContext ctx;
private boolean mcpEnabled;
@@ -212,6 +214,14 @@ class ActionsPopup {
this.switchIntegrationAction = switchIntegrationAction;
}
+ /**
+ * What the Quit entry does: the monitor's own quit path, confirmation
included, so quitting is one menu away on
+ * every tab and in every input field where {@code q} is taken.
+ */
+ void setQuitAction(Runnable quitAction) {
+ this.quitAction = quitAction;
+ }
+
void setGotoTabSupport(List<TabRegistry.TabEntry> entries, Runnable
callback) {
gotoTabPopup.setTabEntries(entries, callback);
settingsPopup.setTabEntries(entries);
@@ -288,6 +298,7 @@ class ActionsPopup {
flat.add(Action.MCP_SUBMENU);
flat.add(null);
flat.add(Action.SHELL);
+ flat.add(Action.QUIT);
return flat;
}
@@ -437,6 +448,7 @@ class ActionsPopup {
labels.add(mcpEnabled ? "AI & MCP..." : "AI...");
labels.add("───");
labels.add("Shell");
+ labels.add("Quit");
return labels;
}
@@ -701,6 +713,11 @@ class ActionsPopup {
browseFilesAction.run();
}
}
+ } else if (action == Action.QUIT) {
+ showActionsMenu = false;
+ if (quitAction != null) {
+ quitAction.run();
+ }
} else if (action == Action.DOCTOR) {
showActionsMenu = false;
doctorPopup.open();
@@ -1016,6 +1033,7 @@ class ActionsPopup {
// Group 4: Shell
items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
items.add(ListItem.from(" >_ Shell (F6)"));
+ items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.QUIT, "Quit")));
ListWidget list = ListWidget.builder()
.items(items.toArray(ListItem[]::new))
.highlightStyle(Theme.selectionBg())
@@ -1400,6 +1418,7 @@ class ActionsPopup {
case "reset stats" -> Action.RESET_STATS;
case "settings" -> Action.SETTINGS;
case "shell" -> Action.SHELL;
+ case "quit", "exit" -> Action.QUIT;
case "take screenshot" -> Action.SCREENSHOT;
case "reset screen" -> Action.RESET_SCREEN;
case "start tape recording", "stop tape recording" ->
Action.TAPE_RECORDING;
@@ -1498,6 +1517,12 @@ class ActionsPopup {
}
openShellAction.run();
}
+ case QUIT -> {
+ if (quitAction == null) {
+ return false;
+ }
+ quitAction.run();
+ }
default -> {
return false;
}
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 2cb781fffb7c..c3cab66ae2b2 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
@@ -505,6 +505,20 @@ public class CamelMonitor extends CamelCommand {
actionsPopup.setBrowseFilesAction(this::openFilesPopup);
actionsPopup.setSwitchIntegrationAction(
() -> popupManager.openSwitchPopup(ctx.selectedPid,
getNonVanishingIntegrations()));
+ actionsPopup.setQuitAction(() -> quitTui(true));
+ }
+
+ /**
+ * Quits this session the way {@code q} does. With confirmations on (the
default) a dialog asks first; Ctrl+C and a
+ * session with confirmations off quit at once. The F2 menu's Quit entry
and the {@code tui_action} MCP tool go
+ * through here too, so quitting behaves the same however it is asked for.
+ */
+ void quitTui(boolean confirm) {
+ if (confirm && ctx.confirmActions) {
+ popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () ->
runner.quit());
+ } else {
+ runner.quit();
+ }
}
/**
@@ -1311,11 +1325,7 @@ public class CamelMonitor extends CamelCommand {
// Each session (the local terminal, or a browser tab connected via
--web) owns an
// independent CamelMonitor/TuiRunner, so quitting here only ends this
session.
if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
- if (!ke.isCtrlC() && ctx.confirmActions) {
- popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", ()
-> runner.quit());
- } else {
- runner.quit();
- }
+ quitTui(!ke.isCtrlC());
return true;
}
if (ke.isCtrlC()) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 3dbfd01a570a..554f1e0f6dc2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -70,6 +70,7 @@ final class TuiIcons {
static final String KEYSTROKES = "🔤";
static final String SLEEP = "💤";
static final String STOP = "🛑";
+ static final String QUIT = "🚪";
static final String RECORD = "🔴";
static final String DOCTOR = "🩺";
static final String RESET = "🔄";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index c78e0a8e20f9..04ea7dd0a446 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -152,7 +152,7 @@ are recorded in `F2` → `AI & MCP` → `AI Log`.
## Run
- `F10` — open run popup (run, stop routes, start routes, restart, stop, kill)
-- `q` — quit the TUI
+- `q` — quit the TUI; `F2` → `Quit` does the same from any tab, popup or input
field where `q` is taken
By default, stop/restart actions show a confirmation dialog before executing.
You can turn this off in Settings (`F2` → `Settings...` → `Confirm`).
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
index cccffe7b1c35..8ebda7b7aed2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
@@ -16,11 +16,18 @@
*/
package org.apache.camel.dsl.jbang.core.commands.tui;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import dev.tamboui.layout.Rect;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for {@link ActionsPopup#listItemAt}, which maps a click to an entry
in a single-line, bordered list popup. The
@@ -40,6 +47,31 @@ class ActionsPopupTest {
assertEquals(ActionsPopup.Action.RUN_INFRA,
ActionsPopup.actionForLabel("Run Dev/Infra Service..."));
assertEquals(ActionsPopup.Action.TAPE_RECORDING,
ActionsPopup.actionForLabel("Stop Tape Recording"));
assertEquals(ActionsPopup.Action.SHOW_KEYSTROKES,
ActionsPopup.actionForLabel("Hide Keystrokes"));
+ assertEquals(ActionsPopup.Action.QUIT,
ActionsPopup.actionForLabel("Quit"));
+ assertEquals(ActionsPopup.Action.QUIT,
ActionsPopup.actionForLabel("exit"));
+ }
+
+ @Test
+ void quitIsTheLastMenuEntryAndRunsTheWiredAction() {
+ ActionsPopup popup = new ActionsPopup(
+ Set::of, List::of, List::of, null, () -> {
+ }, () -> {
+ }, () -> false, () -> {
+ }, () -> false, () -> {
+ }, new HashSet<>());
+ List<String> labels = popup.getActionLabels();
+ assertEquals("Quit", labels.get(labels.size() - 1), "the last entry of
the main menu");
+
+ // nothing wired (a test harness, or before the monitor is up): the
action reports it did nothing
+ assertFalse(popup.executeActionByName("quit"));
+
+ AtomicBoolean quit = new AtomicBoolean();
+ popup.setQuitAction(() -> quit.set(true));
+ assertTrue(popup.executeActionByName("quit"), "tui_action Quit runs
the monitor's quit path");
+ assertTrue(quit.get());
+ quit.set(false);
+ assertTrue(popup.executeActionByName("Quit"), "the menu label works as
well as the kebab name");
+ assertTrue(quit.get());
}
@Test