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 5b925959db57 CAMEL-23572: camel-tui: Add F2 actions menu with example 
browser (#23403)
5b925959db57 is described below

commit 5b925959db57a372fb00cfdaa62a308faad1dde3
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 11:24:22 2026 +0200

    CAMEL-23572: camel-tui: Add F2 actions menu with example browser (#23403)
    
    * CAMEL-23572: camel-tui: Add F2 actions menu with example browser
    
    Adds an F2 actions menu to the TUI overview tab that lets users browse
    and run examples directly. The example browser shows all 21 curated
    examples grouped by level (beginner/intermediate/advanced) with async
    process launch and status notifications.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23572: camel-tui: Polish F2 example browser dialog
    
    - Move F2 hint after q in footer
    - Add emojis (bundled/online/docker) and legend to example browser
    - Widen popup to 100 chars and word-wrap long descriptions
    - Add Page Up/Down support for faster navigation
    - Fix separator skipping for all level headers
    - Use AUTO_SCROLL so list follows selection
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23572: camel-tui: Extract F2 actions menu into ActionsPopup class
    
    Move all F2 actions menu and example browser logic out of CamelMonitor
    into a dedicated ActionsPopup class (~320 lines). CamelMonitor now
    delegates via a simple API (open/close/handleKeyEvent/render/tick).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23572: camel-tui: Add name input dialog and styled dialog footers
    
    Add 'r' key in example browser to launch with custom name, with auto
    clash detection. Style dialog titleBottom footers with yellow+bold keys
    matching the regular footer bar.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 624 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  27 +
 2 files changed, 651 insertions(+)

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
new file mode 100644
index 000000000000..4b1aea0c0805
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -0,0 +1,624 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+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.KeyCode;
+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.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
+import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
+import org.apache.camel.util.json.JsonObject;
+
+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 ActionsPopup {
+
+    private final Supplier<Set<String>> runningNames;
+
+    private boolean showActionsMenu;
+    private final ListState actionsMenuState = new ListState();
+
+    private boolean showExampleBrowser;
+    private final ListState exampleBrowserState = new ListState();
+    private List<JsonObject> exampleCatalog;
+
+    private boolean showNameInput;
+    private TextInputState nameInputState;
+    private JsonObject selectedExample;
+
+    private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
+    private String launchNotification;
+    private boolean launchNotificationError;
+    private long launchNotificationExpiry;
+
+    ActionsPopup(Supplier<Set<String>> runningNames) {
+        this.runningNames = runningNames;
+    }
+
+    boolean isVisible() {
+        return showActionsMenu || showExampleBrowser || showNameInput;
+    }
+
+    void open() {
+        showActionsMenu = true;
+        actionsMenuState.select(0);
+    }
+
+    void close() {
+        showActionsMenu = false;
+        showExampleBrowser = false;
+        showNameInput = false;
+    }
+
+    String notification() {
+        return launchNotification;
+    }
+
+    boolean notificationError() {
+        return launchNotificationError;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (showNameInput) {
+            if (ke.isCancel()) {
+                showNameInput = false;
+                showExampleBrowser = true;
+            } else if (ke.isConfirm()) {
+                launchWithName();
+            } else if (ke.isDeleteBackward()) {
+                nameInputState.deleteBackward();
+            } else if (ke.isDeleteForward()) {
+                nameInputState.deleteForward();
+            } else if (ke.isLeft()) {
+                nameInputState.moveCursorLeft();
+            } else if (ke.isRight()) {
+                nameInputState.moveCursorRight();
+            } else if (ke.isHome()) {
+                nameInputState.moveCursorToStart();
+            } else if (ke.isEnd()) {
+                nameInputState.moveCursorToEnd();
+            } else if (ke.code() == KeyCode.CHAR) {
+                nameInputState.insert(ke.character());
+            }
+            return true;
+        }
+        if (showExampleBrowser) {
+            if (ke.isCancel()) {
+                showExampleBrowser = false;
+                showActionsMenu = true;
+            } else if (ke.isUp()) {
+                navigateExampleBrowser(-1);
+            } else if (ke.isDown()) {
+                navigateExampleBrowser(1);
+            } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+                navigateExampleBrowser(-10);
+            } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+                navigateExampleBrowser(10);
+            } else if (ke.isChar('r')) {
+                openNameInput();
+            } else if (ke.isConfirm()) {
+                launchSelectedExample();
+            }
+            return true;
+        }
+        if (showActionsMenu) {
+            if (ke.isCancel()) {
+                showActionsMenu = false;
+            } else if (ke.isUp()) {
+                actionsMenuState.selectPrevious();
+            } else if (ke.isDown()) {
+                actionsMenuState.selectNext(1);
+            } else if (ke.isConfirm()) {
+                openExampleBrowser();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (showActionsMenu) {
+            renderActionsMenu(frame, area);
+        }
+        if (showExampleBrowser) {
+            renderExampleBrowser(frame, area);
+        }
+        if (showNameInput) {
+            renderNameInput(frame, area);
+        }
+    }
+
+    void renderFooter(List<Span> spans) {
+        if (showNameInput) {
+            hint(spans, "Enter", "launch");
+            hintLast(spans, "Esc", "back");
+            return;
+        }
+        if (showExampleBrowser) {
+            hint(spans, "↑↓", "navigate");
+            hint(spans, "Enter", "run");
+            hint(spans, "n", "name");
+            hintLast(spans, "Esc", "back");
+            return;
+        }
+        if (showActionsMenu) {
+            hint(spans, "↑↓", "navigate");
+            hint(spans, "Enter", "select");
+            hintLast(spans, "Esc", "cancel");
+        }
+    }
+
+    void tick(long now) {
+        monitorPendingLaunches(now);
+        if (launchNotification != null && now > launchNotificationExpiry) {
+            launchNotification = null;
+        }
+    }
+
+    // ---- Rendering ----
+
+    private void renderActionsMenu(Frame frame, Rect area) {
+        int popupW = 32;
+        int popupH = 3;
+        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);
+        ListWidget list = ListWidget.builder()
+                .items(ListItem.from("  Run an example..."))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.NONE)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" Actions ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, actionsMenuState);
+    }
+
+    private void renderExampleBrowser(Frame frame, Rect area) {
+        if (exampleCatalog == null || exampleCatalog.isEmpty()) {
+            return;
+        }
+        int popupW = Math.min(100, area.width() - 4);
+        int popupH = Math.min(exampleCatalog.size() + 10, Math.min(22, 
area.height() - 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);
+
+        List<ListItem> items = buildExampleListItems(popupW - 4);
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" Run an Example (" + exampleCatalog.size() + 
") ")
+                        .titleBottom(Title.from(Line.from(
+                                Span.styled(" Enter", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" run │"),
+                                Span.styled(" r", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" run... │"),
+                                Span.styled(" ↑↓", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" navigate │"),
+                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, exampleBrowserState);
+    }
+
+    private List<ListItem> buildExampleListItems(int width) {
+        List<ListItem> items = new ArrayList<>();
+        String currentLevel = null;
+        for (JsonObject ex : exampleCatalog) {
+            String level = ex.getStringOrDefault("level", "beginner");
+            if (!level.equals(currentLevel)) {
+                currentLevel = level;
+                String header = "── " + capitalize(level) + " ──";
+                items.add(ListItem.from(header).style(Style.EMPTY.dim()));
+            }
+            String name = ex.getStringOrDefault("name", "");
+            String desc = ex.getStringOrDefault("description", "");
+            boolean docker = ExampleHelper.requiresDocker(ex);
+            boolean bundled = ExampleHelper.isBundled(ex);
+
+            String icons = (bundled ? "📦" : "🌐") + (docker ? "🐳" : "  ");
+            int nameCol = Math.min(30, width / 3);
+            String padded = String.format("%-" + nameCol + "s", 
TuiHelper.truncate(name, nameCol));
+            String prefix = " " + icons + " " + padded + " ";
+            int descCol = Math.max(10, width - prefix.length());
+
+            Style style = bundled ? Style.EMPTY : Style.EMPTY.dim();
+            if (desc.length() <= descCol) {
+                items.add(ListItem.from(prefix + desc).style(style));
+            } else {
+                String indent = " ".repeat(prefix.length());
+                List<Line> lines = new ArrayList<>();
+                List<String> wrapped = wrapWords(desc, descCol);
+                lines.add(Line.from(prefix + wrapped.get(0)));
+                for (int w = 1; w < wrapped.size(); w++) {
+                    lines.add(Line.from(indent + wrapped.get(w)));
+                }
+                
items.add(ListItem.from(Text.from(lines.toArray(Line[]::new))).style(style));
+            }
+        }
+        items.add(ListItem.from(""));
+        items.add(ListItem.from(" 📦 = bundled (offline)  🌐 = online (GitHub)  
🐳 = Docker")
+                .style(Style.EMPTY.dim()));
+        return items;
+    }
+
+    private void renderNameInput(Frame frame, Rect area) {
+        int popupW = Math.min(50, area.width() - 4);
+        int popupH = 4;
+        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);
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Name ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" launch │"),
+                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
+                .build();
+        frame.renderWidget(block, popup);
+
+        Rect inner = new Rect(popup.left() + 2, popup.top() + 1, popup.width() 
- 4, 1);
+        frame.renderWidget(Paragraph.from(Line.from(
+                Span.styled("Name for the integration:", Style.EMPTY.dim()))), 
inner);
+
+        Rect inputArea = new Rect(popup.left() + 2, popup.top() + 2, 
popup.width() - 4, 1);
+        TextInput textInput = TextInput.builder()
+                .cursorStyle(Style.EMPTY.reversed())
+                .build();
+        frame.renderStatefulWidget(textInput, inputArea, nameInputState);
+    }
+
+    // ---- Name Input ----
+
+    private void openNameInput() {
+        Integer sel = exampleBrowserState.selected();
+        if (sel == null || isSeparatorIndex(sel)) {
+            return;
+        }
+        JsonObject example = getExampleAtListIndex(sel);
+        if (example == null) {
+            return;
+        }
+        selectedExample = example;
+        String baseName = example.getStringOrDefault("name", "");
+        String autoName = generateUniqueName(baseName);
+        showExampleBrowser = false;
+        showNameInput = true;
+        nameInputState = new TextInputState(autoName);
+    }
+
+    private String generateUniqueName(String baseName) {
+        Set<String> names = runningNames.get();
+        if (!names.contains(baseName)) {
+            return baseName;
+        }
+        for (int i = 2;; i++) {
+            String candidate = baseName + i;
+            if (!names.contains(candidate)) {
+                return candidate;
+            }
+        }
+    }
+
+    private void launchWithName() {
+        if (selectedExample == null || nameInputState == null) {
+            return;
+        }
+        String customName = nameInputState.text().trim();
+        String exampleName = selectedExample.getStringOrDefault("name", "");
+        showNameInput = false;
+        try {
+            List<String> cmd = new 
ArrayList<>(LauncherHelper.getCamelCommand());
+            cmd.add("run");
+            cmd.add("--example=" + exampleName);
+            if (!customName.isEmpty() && !customName.equals(exampleName)) {
+                cmd.add("--name=" + customName);
+            }
+            Path outputFile = Files.createTempFile("camel-example-", ".log");
+            outputFile.toFile().deleteOnExit();
+            ProcessBuilder pb = new ProcessBuilder(cmd);
+            pb.redirectErrorStream(true);
+            pb.redirectOutput(outputFile.toFile());
+            Process process = pb.start();
+            String displayName = customName.isEmpty() ? exampleName : 
customName;
+            pendingLaunches.add(new PendingLaunch(displayName, process, 
outputFile, System.currentTimeMillis()));
+            launchNotification = "Starting: " + displayName;
+            launchNotificationError = false;
+            launchNotificationExpiry = System.currentTimeMillis() + 5000;
+        } catch (Exception e) {
+            launchNotification = "Failed to start: " + exampleName + " - " + 
e.getMessage();
+            launchNotificationError = true;
+            launchNotificationExpiry = System.currentTimeMillis() + 10000;
+        }
+    }
+
+    // ---- Example Browser Navigation ----
+
+    private void openExampleBrowser() {
+        showActionsMenu = false;
+        if (exampleCatalog == null) {
+            exampleCatalog = loadAndSortExamples();
+        }
+        if (exampleCatalog.isEmpty()) {
+            launchNotification = "No examples found";
+            launchNotificationError = true;
+            launchNotificationExpiry = System.currentTimeMillis() + 5000;
+            return;
+        }
+        showExampleBrowser = true;
+        exampleBrowserState.select(1);
+    }
+
+    private List<JsonObject> loadAndSortExamples() {
+        List<JsonObject> catalog = ExampleHelper.loadCatalog();
+        catalog.sort((a, b) -> {
+            int la = levelOrder(a.getStringOrDefault("level", "beginner"));
+            int lb = levelOrder(b.getStringOrDefault("level", "beginner"));
+            if (la != lb) {
+                return Integer.compare(la, lb);
+            }
+            return a.getStringOrDefault("name", 
"").compareTo(b.getStringOrDefault("name", ""));
+        });
+        return catalog;
+    }
+
+    private static int levelOrder(String level) {
+        return switch (level) {
+            case "beginner" -> 0;
+            case "intermediate" -> 1;
+            case "advanced" -> 2;
+            default -> 3;
+        };
+    }
+
+    private void navigateExampleBrowser(int direction) {
+        if (exampleCatalog == null || exampleCatalog.isEmpty()) {
+            return;
+        }
+        int totalItems = countExampleListItems();
+        Integer current = exampleBrowserState.selected();
+        if (current == null) {
+            current = 0;
+        }
+        int next = current + direction;
+        if (next < 0) {
+            next = 0;
+        }
+        if (next >= totalItems) {
+            next = totalItems - 1;
+        }
+        while (isSeparatorIndex(next) && next > 0 && next < totalItems - 1) {
+            next += direction;
+        }
+        if (next < 0) {
+            next = 0;
+        }
+        if (next >= totalItems) {
+            next = totalItems - 1;
+        }
+        if (isSeparatorIndex(next)) {
+            return;
+        }
+        exampleBrowserState.select(next);
+    }
+
+    private int countExampleListItems() {
+        if (exampleCatalog == null) {
+            return 0;
+        }
+        int count = 0;
+        String currentLevel = null;
+        for (JsonObject ex : exampleCatalog) {
+            String level = ex.getStringOrDefault("level", "beginner");
+            if (!level.equals(currentLevel)) {
+                currentLevel = level;
+                count++;
+            }
+            count++;
+        }
+        return count + 2;
+    }
+
+    private boolean isSeparatorIndex(int index) {
+        if (exampleCatalog == null) {
+            return false;
+        }
+        int pos = 0;
+        String currentLevel = null;
+        for (JsonObject ex : exampleCatalog) {
+            String level = ex.getStringOrDefault("level", "beginner");
+            if (!level.equals(currentLevel)) {
+                currentLevel = level;
+                if (pos == index) {
+                    return true;
+                }
+                pos++;
+            }
+            if (pos == index) {
+                return false;
+            }
+            pos++;
+        }
+        return true;
+    }
+
+    private JsonObject getExampleAtListIndex(int index) {
+        if (exampleCatalog == null) {
+            return null;
+        }
+        int pos = 0;
+        String currentLevel = null;
+        for (JsonObject ex : exampleCatalog) {
+            String level = ex.getStringOrDefault("level", "beginner");
+            if (!level.equals(currentLevel)) {
+                currentLevel = level;
+                pos++;
+            }
+            if (pos == index) {
+                return ex;
+            }
+            pos++;
+        }
+        return null;
+    }
+
+    // ---- Process Launch & Monitoring ----
+
+    private void launchSelectedExample() {
+        Integer sel = exampleBrowserState.selected();
+        if (sel == null || isSeparatorIndex(sel)) {
+            return;
+        }
+        JsonObject example = getExampleAtListIndex(sel);
+        if (example == null) {
+            return;
+        }
+        String exampleName = example.getStringOrDefault("name", "");
+        showExampleBrowser = false;
+        try {
+            List<String> cmd = new 
ArrayList<>(LauncherHelper.getCamelCommand());
+            cmd.add("run");
+            cmd.add("--example=" + exampleName);
+            Path outputFile = Files.createTempFile("camel-example-", ".log");
+            outputFile.toFile().deleteOnExit();
+            ProcessBuilder pb = new ProcessBuilder(cmd);
+            pb.redirectErrorStream(true);
+            pb.redirectOutput(outputFile.toFile());
+            Process process = pb.start();
+            pendingLaunches.add(new PendingLaunch(exampleName, process, 
outputFile, System.currentTimeMillis()));
+            launchNotification = "Starting: " + exampleName;
+            launchNotificationError = false;
+            launchNotificationExpiry = System.currentTimeMillis() + 5000;
+        } catch (Exception e) {
+            launchNotification = "Failed to start: " + exampleName + " - " + 
e.getMessage();
+            launchNotificationError = true;
+            launchNotificationExpiry = System.currentTimeMillis() + 10000;
+        }
+    }
+
+    private void monitorPendingLaunches(long now) {
+        Iterator<PendingLaunch> it = pendingLaunches.iterator();
+        while (it.hasNext()) {
+            PendingLaunch pl = it.next();
+            if (!pl.process().isAlive()) {
+                int exitCode = pl.process().exitValue();
+                if (exitCode == 0) {
+                    launchNotification = "Started: " + pl.name();
+                    launchNotificationError = false;
+                    launchNotificationExpiry = now + 5000;
+                } else {
+                    String detail = readFirstLine(pl.outputFile());
+                    launchNotification = "Failed: " + pl.name()
+                                         + (detail != null ? " - " + detail : 
"");
+                    launchNotificationError = true;
+                    launchNotificationExpiry = now + 10000;
+                }
+                it.remove();
+            } else if (now - pl.startTime() > 8000) {
+                launchNotification = "Started: " + pl.name();
+                launchNotificationError = false;
+                launchNotificationExpiry = now + 5000;
+                it.remove();
+            }
+        }
+    }
+
+    // ---- Utilities ----
+
+    private static String readFirstLine(Path file) {
+        try {
+            List<String> lines = Files.readAllLines(file);
+            for (String line : lines) {
+                String trimmed = line.trim();
+                if (!trimmed.isEmpty()) {
+                    return TuiHelper.truncate(trimmed, 60);
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+        return null;
+    }
+
+    private static String capitalize(String s) {
+        if (s == null || s.isEmpty()) {
+            return s;
+        }
+        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
+    }
+
+    private static List<String> wrapWords(String text, int maxWidth) {
+        List<String> lines = new ArrayList<>();
+        StringBuilder line = new StringBuilder();
+        for (String word : text.split(" ")) {
+            if (line.isEmpty()) {
+                line.append(word);
+            } else if (line.length() + 1 + word.length() <= maxWidth) {
+                line.append(' ').append(word);
+            } else {
+                lines.add(line.toString());
+                line.setLength(0);
+                line.append(word);
+            }
+        }
+        if (!line.isEmpty()) {
+            lines.add(line.toString());
+        }
+        return lines;
+    }
+
+    private record PendingLaunch(String name, Process process, Path 
outputFile, long startTime) {
+    }
+}
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 a6197c03a367..85abf5961ae1 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
@@ -193,6 +193,12 @@ public class CamelMonitor extends CamelCommand {
     private volatile long lastRefresh;
     private boolean showKillConfirm;
 
+    private final ActionsPopup actionsPopup = new ActionsPopup(
+            () -> data.get().stream()
+                    .filter(i -> !i.vanishing && i.name != null)
+                    .map(i -> i.name)
+                    .collect(Collectors.toSet()));
+
     private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
     private TuiRunner runner;
 
@@ -270,6 +276,9 @@ public class CamelMonitor extends CamelCommand {
 
     private boolean handleEvent(Event event, TuiRunner runner) {
         if (event instanceof KeyEvent ke) {
+            if (actionsPopup.isVisible()) {
+                return actionsPopup.handleKeyEvent(ke);
+            }
             // Kill confirm dialog: Enter to confirm, Esc/any other key to 
cancel
             if (showKillConfirm) {
                 if (ke.isConfirm()) {
@@ -481,6 +490,11 @@ public class CamelMonitor extends CamelCommand {
                 showKillConfirm = true;
                 return true;
             }
+            // Overview tab: F2 opens actions menu
+            if (tab == TAB_OVERVIEW && ke.isKey(KeyCode.F2)) {
+                actionsPopup.open();
+                return true;
+            }
 
             // Delegate remaining keys to active tab
             if (activeTab != null && activeTab.handleKeyEvent(ke)) {
@@ -489,6 +503,7 @@ public class CamelMonitor extends CamelCommand {
         }
         if (event instanceof TickEvent) {
             long now = System.currentTimeMillis();
+            actionsPopup.tick(now);
             long interval = routesTab.isShowDiagram() ? 
Math.max(refreshInterval, 1000) : refreshInterval;
             if (now - lastRefresh >= interval) {
                 refreshData();
@@ -634,6 +649,7 @@ public class CamelMonitor extends CamelCommand {
         if (showKillConfirm) {
             renderKillConfirm(frame, mainChunks.get(4));
         }
+        actionsPopup.render(frame, mainChunks.get(4));
         renderFooter(frame, mainChunks.get(5));
     }
 
@@ -662,6 +678,11 @@ public class CamelMonitor extends CamelCommand {
                 titleSpans.add(Span.styled("selected: " + selectedName(), 
Style.EMPTY.fg(Color.YELLOW)));
             }
         }
+        if (actionsPopup.notification() != null) {
+            titleSpans.add(Span.raw("  "));
+            Style style = actionsPopup.notificationError() ? 
Style.EMPTY.fg(Color.RED) : Style.EMPTY.fg(Color.GREEN);
+            titleSpans.add(Span.styled(actionsPopup.notification(), style));
+        }
         Line titleLine = Line.from(titleSpans);
 
         frame.renderWidget(
@@ -1447,7 +1468,12 @@ public class CamelMonitor extends CamelCommand {
     }
 
     private void renderOverviewFooter(List<Span> spans) {
+        if (actionsPopup.isVisible()) {
+            actionsPopup.renderFooter(spans);
+            return;
+        }
         hint(spans, "q", "quit");
+        hint(spans, "F2", "actions");
         if (ctx.selectedPid != null) {
             hint(spans, "Esc", ctx.infraTableFocused ? "integrations" : 
"unselect");
         }
@@ -2719,4 +2745,5 @@ public class CamelMonitor extends CamelCommand {
 
     record VanishingInfraInfo(InfraInfo info, long startTime) {
     }
+
 }

Reply via email to