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

davsclaus pushed a commit to branch tui-run-options-and-features
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 14cfe7a318a076ed010d58e33acf088a11aab434
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 21:49:26 2026 +0200

    camel-tui: Add Stop All to F2 actions menu
    
    When only integrations or only infra services are running, stops them
    immediately. When both are running, shows a checkbox dialog to select
    which groups to stop.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  42 +++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   3 +
 .../dsl/jbang/core/commands/tui/StopAllPopup.java  | 228 +++++++++++++++++++++
 3 files changed, 268 insertions(+), 5 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 0c21c440add7..f5c4826f1b46 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
@@ -62,7 +62,8 @@ class ActionsPopup {
     private static final int ACTION_SCREENSHOT = 2;
     private static final int ACTION_SHOW_KEYSTROKES = 3;
     private static final int ACTION_DOCTOR = 4;
-    private static final int ACTION_COUNT = 5;
+    private static final int ACTION_STOP_ALL = 5;
+    private static final int ACTION_COUNT = 6;
 
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
@@ -92,6 +93,7 @@ class ActionsPopup {
     private int docScroll;
 
     private final DoctorPopup doctorPopup = new DoctorPopup();
+    private final StopAllPopup stopAllPopup;
 
     private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
     private String launchNotification;
@@ -99,12 +101,14 @@ class ActionsPopup {
     private long launchNotificationExpiry;
 
     ActionsPopup(Supplier<Set<String>> runningNames, 
Supplier<List<IntegrationInfo>> integrations,
+                 Supplier<List<InfraInfo>> infraServices,
                  Runnable screenshotAction, Runnable toggleKeystrokes, 
Supplier<Boolean> keystrokesEnabled) {
         this.runningNames = runningNames;
         this.integrations = integrations;
         this.screenshotAction = screenshotAction;
         this.toggleKeystrokes = toggleKeystrokes;
         this.keystrokesEnabled = keystrokesEnabled;
+        this.stopAllPopup = new StopAllPopup(integrations, infraServices);
     }
 
     void setContext(MonitorContext ctx) {
@@ -113,7 +117,7 @@ class ActionsPopup {
 
     boolean isVisible() {
         return showActionsMenu || showExampleBrowser || showNameInput || 
showDocPicker || showDocViewer
-                || doctorPopup.isVisible();
+                || doctorPopup.isVisible() || stopAllPopup.isVisible();
     }
 
     void open() {
@@ -128,6 +132,7 @@ class ActionsPopup {
         showDocPicker = false;
         showDocViewer = false;
         doctorPopup.close();
+        stopAllPopup.close();
     }
 
     String notification() {
@@ -214,6 +219,10 @@ class ActionsPopup {
             }
             return true;
         }
+        if (stopAllPopup.handleKeyEvent(ke)) {
+            checkStopAllNotification();
+            return true;
+        }
         if (doctorPopup.handleKeyEvent(ke)) {
             return true;
         }
@@ -240,6 +249,10 @@ class ActionsPopup {
                     } else if (sel == ACTION_DOCTOR) {
                         showActionsMenu = false;
                         doctorPopup.open();
+                    } else if (sel == ACTION_STOP_ALL) {
+                        showActionsMenu = false;
+                        stopAllPopup.open();
+                        checkStopAllNotification();
                     }
                 }
             }
@@ -267,9 +280,16 @@ class ActionsPopup {
         if (doctorPopup.isVisible()) {
             doctorPopup.render(frame, area);
         }
+        if (stopAllPopup.isVisible()) {
+            stopAllPopup.render(frame, area);
+        }
     }
 
     void renderFooter(List<Span> spans) {
+        if (stopAllPopup.isVisible()) {
+            stopAllPopup.renderFooter(spans);
+            return;
+        }
         if (doctorPopup.isVisible()) {
             doctorPopup.renderFooter(spans);
             return;
@@ -322,15 +342,20 @@ class ActionsPopup {
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
 
         frame.renderWidget(Clear.INSTANCE, popup);
+        // extra space after ⌨️ because it renders narrower than other emoji
         String keystrokeLabel = keystrokesEnabled.get()
-                ? "  ⌨️ Hide Keystrokes"
-                : "  ⌨️ Show Keystrokes";
+                ? "  ⌨️  Hide Keystrokes"
+                : "  ⌨️  Show Keystrokes";
+        String stopLabel = stopAllPopup.hasBothGroups()
+                ? "  🛑 Stop All..."
+                : "  🛑 Stop All";
         ListWidget list = ListWidget.builder()
                 .items(ListItem.from("  🐪 Run an example..."),
                         ListItem.from("  📖 Show Documentation"),
                         ListItem.from("  📸 Take Screenshot"),
                         ListItem.from(keystrokeLabel),
-                        ListItem.from("  🩺 Run Doctor"))
+                        ListItem.from("  🩺 Run Doctor"),
+                        ListItem.from(stopLabel))
                 .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
                 .highlightSymbol("")
                 .scrollMode(ScrollMode.NONE)
@@ -602,6 +627,13 @@ class ActionsPopup {
         launchNotificationExpiry = System.currentTimeMillis() + 10000;
     }
 
+    private void checkStopAllNotification() {
+        String msg = stopAllPopup.consumeNotification();
+        if (msg != null) {
+            setNotification(msg, false);
+        }
+    }
+
     // ---- Name Input ----
 
     private void openNameInput() {
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 f34daf997371..42271e55bb5c 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
@@ -211,6 +211,9 @@ public class CamelMonitor extends CamelCommand {
             () -> data.get().stream()
                     .filter(i -> !i.vanishing)
                     .collect(Collectors.toList()),
+            () -> infraData.get().stream()
+                    .filter(i -> !i.vanishing)
+                    .collect(Collectors.toList()),
             () -> pendingScreenshot = true,
             () -> recording = !recording,
             () -> recording);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java
new file mode 100644
index 000000000000..bbf7d69750de
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.function.Supplier;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class StopAllPopup {
+
+    private final Supplier<List<IntegrationInfo>> integrations;
+    private final Supplier<List<InfraInfo>> infraServices;
+
+    private boolean visible;
+    private boolean checkIntegrations = true;
+    private boolean checkInfra = true;
+    private int selectedRow;
+    private int integrationCount;
+    private int infraCount;
+
+    private String notification;
+
+    StopAllPopup(Supplier<List<IntegrationInfo>> integrations, 
Supplier<List<InfraInfo>> infraServices) {
+        this.integrations = integrations;
+        this.infraServices = infraServices;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    boolean hasBothGroups() {
+        List<IntegrationInfo> ints = integrations.get();
+        List<InfraInfo> infras = infraServices.get();
+        long ic = ints.stream().filter(i -> !i.vanishing).count();
+        long fc = infras.stream().filter(i -> !i.vanishing).count();
+        return ic > 0 && fc > 0;
+    }
+
+    void open() {
+        List<IntegrationInfo> ints = integrations.get();
+        List<InfraInfo> infras = infraServices.get();
+        integrationCount = (int) ints.stream().filter(i -> 
!i.vanishing).count();
+        infraCount = (int) infras.stream().filter(i -> !i.vanishing).count();
+
+        if (integrationCount == 0 && infraCount == 0) {
+            notification = "No running processes to stop";
+            return;
+        }
+
+        if (integrationCount > 0 && infraCount == 0) {
+            stopIntegrations();
+            return;
+        }
+        if (infraCount > 0 && integrationCount == 0) {
+            stopInfraServices();
+            return;
+        }
+
+        checkIntegrations = true;
+        checkInfra = true;
+        selectedRow = 0;
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    String consumeNotification() {
+        String msg = notification;
+        notification = null;
+        return msg;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+        } else if (ke.isUp()) {
+            selectedRow = 0;
+        } else if (ke.isDown()) {
+            selectedRow = 1;
+        } else if (ke.isChar(' ')) {
+            if (selectedRow == 0) {
+                checkIntegrations = !checkIntegrations;
+            } else {
+                checkInfra = !checkInfra;
+            }
+        } else if (ke.isConfirm()) {
+            visible = false;
+            executeStop();
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        int popupW = Math.min(42, area.width() - 4);
+        int popupH = 6;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        String intLabel = (checkIntegrations ? "[x]" : "[ ]") + " All 
integrations (" + integrationCount + ")";
+        String infraLabel = (checkInfra ? "[x]" : "[ ]") + " All infra 
services (" + infraCount + ")";
+
+        Style normalStyle = Style.EMPTY;
+        Style selectedStyle = Style.EMPTY.bold().reversed();
+
+        Line intLine = Line.from(Span.styled("  " + intLabel, selectedRow == 0 
? selectedStyle : normalStyle));
+        Line infraLine = Line.from(Span.styled("  " + infraLabel, selectedRow 
== 1 ? selectedStyle : normalStyle));
+
+        Paragraph para = Paragraph.builder()
+                .text(Text.from(Line.from(""), intLine, infraLine))
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" 🛑 Stop All ")
+                        .titleBottom(Title.from(Line.from(
+                                Span.styled(" Space", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" toggle │"),
+                                Span.styled(" Enter", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" confirm │"),
+                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" cancel "))))
+                        .build())
+                .build();
+        frame.renderWidget(para, popup);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "Space", "toggle");
+        hint(spans, "Enter", "confirm");
+        hintLast(spans, "Esc", "cancel");
+    }
+
+    private void executeStop() {
+        int stoppedInt = 0;
+        int stoppedInfra = 0;
+        if (checkIntegrations) {
+            stoppedInt = stopIntegrations();
+        }
+        if (checkInfra) {
+            stoppedInfra = stopInfraServices();
+        }
+        if (stoppedInt == 0 && stoppedInfra == 0 && notification == null) {
+            notification = "Nothing selected to stop";
+        }
+    }
+
+    private int stopIntegrations() {
+        List<IntegrationInfo> ints = integrations.get();
+        int count = 0;
+        for (IntegrationInfo info : ints) {
+            if (info.vanishing || info.pid == null) {
+                continue;
+            }
+            try {
+                long pid = Long.parseLong(info.pid);
+                ProcessHandle.of(pid).ifPresent(ProcessHandle::destroy);
+                count++;
+            } catch (NumberFormatException e) {
+                // skip
+            }
+        }
+        if (count > 0) {
+            notification = "Stopping " + count + " integration" + (count > 1 ? 
"s" : "");
+        }
+        return count;
+    }
+
+    private int stopInfraServices() {
+        List<InfraInfo> infras = infraServices.get();
+        Path camelDir = CommandLineHelper.getCamelDir();
+        int count = 0;
+        for (InfraInfo info : infras) {
+            if (info.vanishing || info.pid == null) {
+                continue;
+            }
+            PathUtils.deleteFile(camelDir.resolve("infra-" + info.alias + "-" 
+ info.pid + ".json"));
+            try {
+                long pid = Long.parseLong(info.pid);
+                ProcessHandle.of(pid).ifPresent(ProcessHandle::destroy);
+                count++;
+            } catch (NumberFormatException e) {
+                // skip
+            }
+        }
+        if (count > 0) {
+            String prev = notification;
+            String msg = "Stopping " + count + " infra service" + (count > 1 ? 
"s" : "");
+            notification = prev != null ? prev + " and " + msg.substring(0, 
1).toLowerCase() + msg.substring(1) : msg;
+        }
+        return count;
+    }
+}

Reply via email to