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 8f0bb8e1df7e chore: camel-jbang - the run form warns about a taken
port, and quitting says which integrations started here keep running (#27222)
8f0bb8e1df7e is described below
commit 8f0bb8e1df7e4bb789bbcc1d5fd452668c948724
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 19:47:45 2026 +0200
chore: camel-jbang - the run form warns about a taken port, and quitting
says which integrations started here keep running (#27222)
Co-Authored-By: Claude Opus 5.5 (1M context) <[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 5eca46356082..ddb670e7b515 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
@@ -534,7 +534,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();
}
@@ -2784,6 +2798,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 8e1fd7a253f9..482b294ec597 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
@@ -46,6 +46,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;
@@ -164,6 +166,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();
+ }
+}