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; + } +}
