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

davsclaus pushed a commit to branch quick-fix/run-and-quit
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 90107b56793ada8ce3419c47740f5721fcb0d9a7
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 17:11:54 2026 +0200

    chore: camel-jbang - the run form warns about a taken port, and quitting 
says which integrations started here keep running
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-settings.adoc       |  2 +-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  3 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  1 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 20 +++++-
 .../dsl/jbang/core/commands/tui/DialogHelper.java  | 38 ++++++++++
 .../dsl/jbang/core/commands/tui/LaunchManager.java | 18 +++++
 .../dsl/jbang/core/commands/tui/PopupManager.java  | 42 ++++++++++-
 .../dsl/jbang/core/commands/tui/PortCheck.java     | 83 ++++++++++++++++++++++
 .../jbang/core/commands/tui/RunOptionsForm.java    | 41 ++++++++++-
 .../jbang/core/commands/tui/DialogHelperTest.java  | 17 +++++
 .../jbang/core/commands/tui/PopupManagerTest.java  | 23 ++++++
 .../dsl/jbang/core/commands/tui/PortCheckTest.java | 62 ++++++++++++++++
 12 files changed, 344 insertions(+), 6 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
index 96e3ab5bcba1..85e17f2d1bc5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
@@ -35,7 +35,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.
+| 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. Integrations started from the TUI keep 
running after it quits (until the terminal closes); when some do, the 
confirmation says how many, and *s* stops them and quits.
 |===
 
 === Sending Test Messages
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 71a2c91b5205..c124ccdd6e76 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -68,7 +68,8 @@ Spring Boot, Quarkus, or JBang. This makes it easy to try any 
example on all thr
 changing a single line of code. The first three run the example in a separate 
JVM that only
 contains the dependencies of the example (like a production deployment), while 
JBang runs it
 in-process in the Camel CLI JVM, which starts faster but has the CLI on the 
classpath as well. You can also set the integration name, toggle dev mode, and
-add extra dependencies.
+add extra dependencies. When the port the app will listen on (the one you set, 
else 8080) is taken already, the form
+says by whom, so a second app with an HTTP server does not fail to start.
 
 The example starts running in the background. The TUI auto-selects it as soon 
as it appears.
 From there you can explore tabs, watch messages flow, inspect the route 
diagram, and experiment.
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 9b0884f339cb..e648baf0beba 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
@@ -144,6 +144,7 @@ class ActionsPopup {
                  Runnable burstCallback, Set<String> stoppingPids) {
         this.runningNames = runningNames;
         this.integrations = integrations;
+        runOptionsForm.setIntegrations(integrations);
         this.infraServices = infraServices;
         this.captionOverlay = captionOverlay;
         this.screenshotAction = screenshotAction;
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 32fec82f9560..57f648a60b54 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
@@ -533,7 +533,21 @@ public class CamelMonitor extends CamelCommand {
      */
     void quitTui(boolean confirm) {
         if (confirm && ctx.confirmActions) {
-            popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> 
runner.quit());
+            LaunchManager launches = actionsPopup.getLaunchManager();
+            long running = launches.runningLaunchCount();
+            if (running > 0) {
+                // what was started from here keeps running when the TUI 
quits: say so, and offer to stop it
+                String what = running == 1
+                        ? "1 integration started here keeps running"
+                        : running + " integrations started here keep running";
+                popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", 
what, () -> runner.quit(),
+                        's', running == 1 ? "stop it and quit" : "stop them 
and quit", () -> {
+                            launches.stopLaunched();
+                            runner.quit();
+                        });
+            } else {
+                popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () 
-> runner.quit());
+            }
         } else {
             runner.quit();
         }
@@ -2772,6 +2786,10 @@ public class CamelMonitor extends CamelCommand {
             filesBrowser.renderFooter(spans);
         } else if (popupManager.isKillConfirmVisible() || 
popupManager.isConfirmVisible()) {
             hint(spans, "Enter", "confirm");
+            String extra = popupManager.isConfirmVisible() ? 
popupManager.confirmExtraHint() : null;
+            if (extra != null) {
+                hint(spans, extra.substring(0, 1), extra.substring(2));
+            }
             hintLast(spans, "Esc", "cancel");
         } else if (popupManager.isSwitchPopupVisible()) {
             hint(spans, "Enter", "switch");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
index afd8acd1d076..d645a2ac92a7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
@@ -127,6 +130,41 @@ final class DialogHelper {
         return popup;
     }
 
+    /**
+     * Renders a confirm dialog with a line that tells more and, when {@code 
extraKey} is given, a second choice on its
+     * own key between Enter and Esc.
+     */
+    static Rect renderConfirm(
+            Frame frame, Rect area, String title, String message, String 
detail, String extraKey, String extraLabel) {
+        Style accent = Theme.warning();
+        String titleText = " " + title + " ";
+        String msg = message.trim();
+        Line hints = extraKey != null
+                ? TuiHelper.hintLine("Enter", "confirm", extraKey, extraLabel, 
"Esc", "cancel")
+                : TuiHelper.hintLine("Enter", "confirm", "Esc", "cancel");
+        int contentW = Math.max(msg.length(), Math.max(detail != null ? 
detail.length() : 0, hints.width()));
+        int popupW = clampWidth(area, CONFIRM_MIN_WIDTH, Math.max(contentW + 
6, titleText.length() + 4));
+        Rect popup = centered(area, popupW, CONFIRM_HEIGHT + (detail != null ? 
1 : 0));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .borderStyle(accent)
+                .title(Title.from(Line.from(Span.styled(titleText, 
accent.bold()))))
+                .build();
+        frame.renderWidget(block, popup);
+        List<Line> lines = new ArrayList<>();
+        lines.add(Line.empty());
+        lines.add(Line.from(Span.styled(msg, accent.bold())));
+        if (detail != null) {
+            lines.add(Line.from(Span.styled(detail, Theme.muted())));
+        }
+        lines.add(Line.empty());
+        lines.add(hints);
+        
frame.renderWidget(Paragraph.builder().centered().text(Text.from(lines)).build(),
 block.inner(popup));
+        return popup;
+    }
+
     /**
      * Renders a dialog with a single text field, e.g. for entering a new file 
name.
      *
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
index c169ad862787..f5502aaf9e51 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
@@ -43,6 +43,8 @@ class LaunchManager {
     private final Supplier<List<InfraInfo>> infraServices;
     // added from the UI thread and from tool threads (MCP, the example 
launcher), read on the UI thread
     private final Queue<PendingLaunch> pendingLaunches = new 
ConcurrentLinkedQueue<>();
+    // integrations started from the TUI (not infra services, which run in the 
background on their own)
+    private final Queue<Process> launched = new ConcurrentLinkedQueue<>();
     private DeferredLaunch deferredLaunch;
     private volatile String pendingAutoSelect;
     private BiConsumer<String, Boolean> notificationCallback;
@@ -161,6 +163,22 @@ class LaunchManager {
     void addPendingLaunch(String name, Process process, Path outputFile) {
         pendingLaunches.add(new PendingLaunch(name, process, outputFile, 
System.currentTimeMillis()));
         pendingAutoSelect = name;
+        launched.add(process);
+    }
+
+    /** The integrations started from this TUI that still run: they keep 
running when the TUI quits. */
+    long runningLaunchCount() {
+        launched.removeIf(p -> !p.isAlive());
+        return launched.size();
+    }
+
+    /** Stops the integrations started from this TUI (the camel launcher and 
the JVM it started). */
+    void stopLaunched() {
+        for (Process p : launched) {
+            p.descendants().forEach(ProcessHandle::destroy);
+            p.destroy();
+        }
+        launched.clear();
     }
 
     void addPendingLaunchNoAutoSelect(String name, Process process, Path 
outputFile) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index 600bb1a35a24..544753f51a92 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -90,6 +90,10 @@ class PopupManager {
     private String confirmMessage;
     private Runnable confirmCallback;
     private Runnable cancelCallback;
+    private String confirmDetail;
+    private char confirmExtraKey;
+    private String confirmExtraLabel;
+    private Runnable confirmExtraCallback;
 
     // Last rendered popup rects for mouse hit-testing
     private Rect lastMorePopupRect;
@@ -176,6 +180,11 @@ class PopupManager {
         return showKillConfirm;
     }
 
+    /** The second choice of the confirm dialog as "key label", or null when 
it has none. */
+    String confirmExtraHint() {
+        return confirmExtraCallback != null ? confirmExtraKey + " " + 
confirmExtraLabel : null;
+    }
+
     boolean isConfirmVisible() {
         return showConfirm;
     }
@@ -245,9 +254,26 @@ class PopupManager {
         this.confirmMessage = message;
         this.confirmCallback = onConfirm;
         this.cancelCallback = onCancel;
+        this.confirmDetail = null;
+        this.confirmExtraLabel = null;
+        this.confirmExtraCallback = null;
         this.showConfirm = true;
     }
 
+    /**
+     * As {@link #showConfirm(String, String, Runnable)}, with a line that 
tells more, and a second choice on its own
+     * key (for example quit, or stop what was started and quit).
+     */
+    void showConfirm(
+            String title, String message, String detail, Runnable onConfirm, 
char extraKey, String extraLabel,
+            Runnable onExtra) {
+        showConfirm(title, message, onConfirm, null);
+        this.confirmDetail = detail;
+        this.confirmExtraKey = extraKey;
+        this.confirmExtraLabel = extraLabel;
+        this.confirmExtraCallback = onExtra;
+    }
+
     /** Closes the generic confirm dialog without running either callback (for 
example on a timeout). */
     void dismissConfirm() {
         showConfirm = false;
@@ -437,7 +463,14 @@ class PopupManager {
     }
 
     private boolean handleConfirmKeys(KeyEvent ke) {
-        if (ke.isConfirm()) {
+        if (confirmExtraCallback != null && ke.isChar(confirmExtraKey)) {
+            showConfirm = false;
+            Runnable cb = confirmExtraCallback;
+            confirmCallback = null;
+            cancelCallback = null;
+            confirmExtraCallback = null;
+            cb.run();
+        } else if (ke.isConfirm()) {
             showConfirm = false;
             Runnable cb = confirmCallback;
             confirmCallback = null;
@@ -724,7 +757,12 @@ class PopupManager {
     void renderConfirm(Frame frame, Rect area) {
         String msg = confirmMessage != null ? confirmMessage : "";
         String title = confirmTitle != null ? confirmTitle : "Confirm";
-        DialogHelper.renderConfirm(frame, area, title, msg, false);
+        if (confirmDetail != null || confirmExtraCallback != null) {
+            DialogHelper.renderConfirm(frame, area, title, msg, confirmDetail,
+                    confirmExtraCallback != null ? 
String.valueOf(confirmExtraKey) : null, confirmExtraLabel);
+        } else {
+            DialogHelper.renderConfirm(frame, area, title, msg, false);
+        }
     }
 
     int[] morePopupShortcut(KeyEvent ke) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java
new file mode 100644
index 000000000000..2cfbf7092757
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java
@@ -0,0 +1,83 @@
+/*
+ * 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.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * Whether the HTTP port of an app about to be run is taken already, and by 
whom: a Spring Boot app then fails with
+ * APPLICATION FAILED TO START, and a Camel app cannot start its HTTP server.
+ */
+final class PortCheck {
+
+    /** The port an app listens on when none is given (Camel, Spring Boot and 
Quarkus alike). */
+    static final int DEFAULT_PORT = 8080;
+
+    private PortCheck() {
+    }
+
+    /** Whether something listens on the port already. */
+    static boolean inUse(int port) {
+        if (port <= 0) {
+            return false;
+        }
+        try (ServerSocket socket = new ServerSocket()) {
+            socket.setReuseAddress(false);
+            socket.bind(new InetSocketAddress(port));
+            return false;
+        } catch (IOException e) {
+            return true;
+        }
+    }
+
+    /** The integration that serves HTTP on the port, from what the TUI knows 
of the running ones; null if none. */
+    static String owner(int port, List<IntegrationInfo> integrations) {
+        for (IntegrationInfo info : integrations) {
+            if (info.vanishing || info.phantom) {
+                continue;
+            }
+            for (HttpEndpointInfo ep : info.httpEndpoints) {
+                if (ep.url != null && portOf(ep.url) == port) {
+                    return info.name != null ? info.name : info.pid;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** The warning for the run form, or null when the port is free. */
+    static String warning(int port, List<IntegrationInfo> integrations) {
+        if (!inUse(port)) {
+            return null;
+        }
+        String owner = owner(port, integrations);
+        return "Port " + port + " is taken by " + (owner != null ? owner : 
"another process")
+               + "; set another if this app serves HTTP";
+    }
+
+    private static int portOf(String url) {
+        try {
+            return URI.create(url).getPort();
+        } catch (IllegalArgumentException e) {
+            return -1;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index b463e69d6f4e..f2d3f3f35c71 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Supplier;
 
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
@@ -65,6 +66,11 @@ class RunOptionsForm {
     private int page;
     private int selectedRow;
     private String errorMessage;
+    // the running integrations, to say who holds the port; the check is 
cached as binding a socket is not free
+    private Supplier<List<IntegrationInfo>> integrations = List::of;
+    private String checkedPort;
+    private long checkedAt;
+    private String portWarning;
 
     private static final String[] MAX_MODES = { "Max seconds:", "Max 
messages:", "Max idle secs:" };
     private static final String[] MAX_FLAGS = { "--max-seconds=", 
"--max-messages=", "--max-idle-seconds=" };
@@ -170,6 +176,32 @@ class RunOptionsForm {
         this.errorMessage = error;
     }
 
+    void setIntegrations(Supplier<List<IntegrationInfo>> integrations) {
+        this.integrations = integrations;
+    }
+
+    /**
+     * A warning when the port the app will listen on (the one given, else 
8080) is taken already; null when it is free.
+     * Checked again every couple of seconds, or when the port changes.
+     */
+    String portWarning(long now) {
+        String text = portInput != null ? portInput.text().trim() : "";
+        if (text.equals(checkedPort) && now - checkedAt < 2000) {
+            return portWarning;
+        }
+        checkedPort = text;
+        checkedAt = now;
+        int port;
+        try {
+            port = text.isEmpty() ? PortCheck.DEFAULT_PORT : 
Integer.parseInt(text);
+        } catch (NumberFormatException e) {
+            portWarning = null;
+            return null;
+        }
+        portWarning = PortCheck.warning(port, integrations.get());
+        return portWarning;
+    }
+
     boolean isJaegerExport() {
         return otelAgent && otelExportTarget == 1;
     }
@@ -506,7 +538,8 @@ class RunOptionsForm {
     private void renderOptionsPage(Frame frame, Rect area) {
         // wide enough for the runtime cycler to show all runtimes (Camel 
Main, Spring Boot, Quarkus, JBang)
         int popupW = Math.min(80, area.width() - 4);
-        int popupH = errorMessage != null ? PAGE1_HEIGHT + 1 : PAGE1_HEIGHT;
+        String warning = portWarning(System.currentTimeMillis());
+        int popupH = PAGE1_HEIGHT + (errorMessage != null ? 1 : 0) + (warning 
!= null ? 1 : 0);
         Rect popup = DialogHelper.centered(area, popupW, popupH);
 
         frame.renderWidget(Clear.INSTANCE, popup);
@@ -606,6 +639,12 @@ class RunOptionsForm {
 
         renderCheckbox(frame, innerX, rowY, innerW, "Java Flight Recorder 
(JFR)", jfrEnabled, selectedRow == ROW_JFR);
 
+        if (warning != null) {
+            rowY++;
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(TuiIcons.HEALTH_WARN + " " + warning, 
Theme.warning()))),
+                    new Rect(innerX, rowY, innerW, 1));
+        }
         if (errorMessage != null) {
             rowY++;
             Rect errorArea = new Rect(innerX, rowY, innerW, 1);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
index c97c809c8899..19c1750ca8e6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
@@ -90,6 +90,23 @@ class DialogHelperTest {
         assertThat(rendered).doesNotContain("Enter");
     }
 
+    @Test
+    void confirmDialogWithADetailAndASecondChoice() {
+        Buffer buffer = Buffer.empty(AREA);
+        Frame frame = Frame.forTesting(buffer);
+
+        Rect popup = DialogHelper.renderConfirm(frame, AREA, "Confirm Quit", 
"Quit the TUI?",
+                "2 integrations started here keep running", "s", "stop them 
and quit");
+
+        String rendered = HealthTabRenderTest.bufferToString(buffer);
+        assertThat(rendered).contains("Quit the TUI?");
+        assertThat(rendered).contains("2 integrations started here keep 
running");
+        assertThat(rendered).contains(" Enter  confirm");
+        assertThat(rendered).contains(" s  stop them and quit");
+        assertThat(rendered).contains(" Esc  cancel");
+        assertThat(popup.height()).isEqualTo(DialogHelper.CONFIRM_HEIGHT + 1);
+    }
+
     @Test
     void inputDialogShowsTitleAndPlaceholder() {
         Buffer buffer = Buffer.empty(AREA);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
index b648b287ea3a..216ce7911d1c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
@@ -178,6 +178,29 @@ class PopupManagerTest {
         assertTrue(popupManager.isConfirmVisible(), "a stray key must not 
dismiss the confirm");
     }
 
+    @Test
+    void confirmWithASecondChoice() {
+        // quitting while integrations started from the TUI run: quit, or stop 
them and quit
+        List<String> ran = new ArrayList<>();
+        popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", "1 
integration started here keeps running",
+                () -> ran.add("quit"), 's', "stop it and quit", () -> 
ran.add("stop"));
+        assertEquals("s stop it and quit", popupManager.confirmExtraHint());
+
+        popupManager.handleKeyEvent(KeyEvent.ofChar('x'), 0, 2);
+        assertTrue(popupManager.isConfirmVisible());
+        popupManager.handleKeyEvent(KeyEvent.ofChar('s'), 0, 2);
+        assertEquals(List.of("stop"), ran);
+        assertFalse(popupManager.isConfirmVisible());
+
+        // a plain confirm after it has no second choice left over
+        popupManager.showConfirm("Confirm Quit", " Quit? ", () -> 
ran.add("plain"));
+        assertEquals(null, popupManager.confirmExtraHint());
+        popupManager.handleKeyEvent(KeyEvent.ofChar('s'), 0, 2);
+        assertEquals(List.of("stop"), ran);
+        popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE), 0, 2);
+        assertEquals(List.of("stop", "plain"), ran);
+    }
+
     @Test
     void killConfirmIgnoresOtherKeysAndCancelsOnEscape() {
         popupManager.showKillConfirm();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java
new file mode 100644
index 000000000000..8964cfd1df5b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The run form warns when the port of the app is taken already, and names the 
integration that holds it.
+ */
+class PortCheckTest {
+
+    @Test
+    void aTakenPortIsFoundWithItsOwner() throws Exception {
+        try (ServerSocket taken = new ServerSocket()) {
+            taken.bind(new InetSocketAddress(0));
+            int port = taken.getLocalPort();
+            assertThat(PortCheck.inUse(port)).isTrue();
+
+            IntegrationInfo holder = new IntegrationInfo();
+            holder.pid = "42";
+            holder.name = "circuit-breaker";
+            HttpEndpointInfo ep = new HttpEndpointInfo();
+            ep.url = "http://0.0.0.0:"; + port + "/hello";
+            holder.httpEndpoints.add(ep);
+
+            assertThat(PortCheck.warning(port, List.of(holder)))
+                    .isEqualTo("Port " + port + " is taken by circuit-breaker; 
set another if this app serves HTTP");
+            assertThat(PortCheck.warning(port, List.of())).contains("taken by 
another process");
+        }
+    }
+
+    @Test
+    void aFreePortHasNoWarning() throws Exception {
+        int port;
+        try (ServerSocket probe = new ServerSocket(0)) {
+            port = probe.getLocalPort();
+        }
+        assertThat(PortCheck.inUse(port)).isFalse();
+        assertThat(PortCheck.warning(port, List.of())).isNull();
+        assertThat(PortCheck.inUse(0)).isFalse();
+    }
+}

Reply via email to