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

Reply via email to