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 17d91d9159ca8f6a67d7e74d47a8b3278b3095d7
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 22:44:56 2026 +0200

    camel-tui: Replace name input with expanded run options form
    
    The Run dialog now provides Name, Port, Max seconds text fields
    and Dev mode, Observe, Backlog trace checkboxes instead of just
    a name input. Tab navigates between fields, Space toggles checkboxes.
    
    Co-Authored-By: Claude <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  86 ++-----
 .../jbang/core/commands/tui/RunOptionsForm.java    | 281 +++++++++++++++++++++
 2 files changed, 301 insertions(+), 66 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 d6b2217b877b..c50789f79964 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
@@ -40,13 +40,10 @@ 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.dsl.jbang.core.common.PathUtils;
@@ -81,8 +78,7 @@ class ActionsPopup {
     private final ListState exampleBrowserState = new ListState();
     private List<JsonObject> exampleCatalog;
 
-    private boolean showNameInput;
-    private TextInputState nameInputState;
+    private final RunOptionsForm runOptionsForm = new RunOptionsForm();
     private JsonObject selectedExample;
 
     private boolean showDocPicker;
@@ -121,7 +117,7 @@ class ActionsPopup {
     }
 
     boolean isVisible() {
-        return showActionsMenu || showExampleBrowser || showNameInput || 
showDocPicker || showDocViewer
+        return showActionsMenu || showExampleBrowser || 
runOptionsForm.isVisible() || showDocPicker || showDocViewer
                 || doctorPopup.isVisible() || classpathPopup.isVisible()
                 || stopAllPopup.isVisible() || captionOverlay.isInputVisible();
     }
@@ -134,7 +130,7 @@ class ActionsPopup {
     void close() {
         showActionsMenu = false;
         showExampleBrowser = false;
-        showNameInput = false;
+        runOptionsForm.close();
         showDocPicker = false;
         showDocViewer = false;
         doctorPopup.close();
@@ -183,26 +179,14 @@ class ActionsPopup {
             }
             return true;
         }
-        if (showNameInput) {
+        if (runOptionsForm.isVisible()) {
             if (ke.isCancel()) {
-                showNameInput = false;
+                runOptionsForm.close();
                 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());
+            } else {
+                runOptionsForm.handleKeyEvent(ke);
             }
             return true;
         }
@@ -288,8 +272,8 @@ class ActionsPopup {
         if (showExampleBrowser) {
             renderExampleBrowser(frame, area);
         }
-        if (showNameInput) {
-            renderNameInput(frame, area);
+        if (runOptionsForm.isVisible()) {
+            runOptionsForm.render(frame, area);
         }
         if (showDocPicker) {
             renderDocPicker(frame, area);
@@ -339,9 +323,8 @@ class ActionsPopup {
             hintLast(spans, "Esc", "back");
             return;
         }
-        if (showNameInput) {
-            hint(spans, "Enter", "launch");
-            hintLast(spans, "Esc", "back");
+        if (runOptionsForm.isVisible()) {
+            runOptionsForm.renderFooter(spans);
             return;
         }
         if (showExampleBrowser) {
@@ -477,35 +460,6 @@ class ActionsPopup {
         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);
-    }
-
     // ---- Doc Viewer & Picker ----
 
     private void renderDocViewer(Frame frame, Rect area) {
@@ -708,8 +662,7 @@ class ActionsPopup {
         String baseName = example.getStringOrDefault("name", "");
         String autoName = generateUniqueName(baseName);
         showExampleBrowser = false;
-        showNameInput = true;
-        nameInputState = new TextInputState(autoName);
+        runOptionsForm.open(autoName, baseName);
     }
 
     private String generateUniqueName(String baseName) {
@@ -726,26 +679,27 @@ class ActionsPopup {
     }
 
     private void launchWithName() {
-        if (selectedExample == null || nameInputState == null) {
+        if (selectedExample == null) {
             return;
         }
-        String customName = nameInputState.text().trim();
         String exampleName = selectedExample.getStringOrDefault("name", "");
-        showNameInput = false;
+        String displayName = runOptionsForm.name();
+        if (displayName.isEmpty()) {
+            displayName = exampleName;
+        }
+        List<String> extraArgs = runOptionsForm.buildArgs();
+        runOptionsForm.close();
         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);
-            }
+            cmd.addAll(extraArgs);
             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;
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
new file mode 100644
index 000000000000..b51130ceeb49
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -0,0 +1,281 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+
+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.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.paragraph.Paragraph;
+
+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 RunOptionsForm {
+
+    // Row indices
+    private static final int ROW_NAME = 0;
+    private static final int ROW_PORT = 1;
+    private static final int ROW_MAX_SECONDS = 2;
+    private static final int ROW_DEV = 3;
+    private static final int ROW_OBSERVE = 4;
+    private static final int ROW_TRACE = 5;
+    private static final int ROW_COUNT = 6;
+
+    private boolean visible;
+    private int selectedRow;
+
+    // Text fields
+    private TextInputState nameInput;
+    private TextInputState portInput;
+    private TextInputState maxSecondsInput;
+
+    // Checkboxes
+    private boolean devMode;
+    private boolean observe;
+    private boolean backlogTrace;
+
+    private String exampleTitle;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open(String defaultName, String exampleName) {
+        nameInput = new TextInputState(defaultName != null ? defaultName : "");
+        portInput = new TextInputState("");
+        maxSecondsInput = new TextInputState("");
+        devMode = false;
+        observe = false;
+        backlogTrace = false;
+        selectedRow = ROW_NAME;
+        exampleTitle = exampleName != null ? exampleName : "Run";
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    String name() {
+        return nameInput != null ? nameInput.text().trim() : "";
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            return true;
+        }
+        if (ke.isUp()) {
+            selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
+            return true;
+        }
+        if (ke.isDown() || ke.isFocusNext()) {
+            selectedRow = (selectedRow + 1) % ROW_COUNT;
+            return true;
+        }
+        if (ke.isFocusPrevious()) {
+            selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
+            return true;
+        }
+
+        // Checkbox rows: Space toggles
+        if (ke.isChar(' ') && selectedRow >= ROW_DEV) {
+            switch (selectedRow) {
+                case ROW_DEV -> devMode = !devMode;
+                case ROW_OBSERVE -> observe = !observe;
+                case ROW_TRACE -> backlogTrace = !backlogTrace;
+            }
+            return true;
+        }
+
+        // Text field rows: delegate to active input
+        if (selectedRow <= ROW_MAX_SECONDS) {
+            TextInputState active = activeInput();
+            if (active != null) {
+                if (ke.isDeleteBackward()) {
+                    active.deleteBackward();
+                } else if (ke.isDeleteForward()) {
+                    active.deleteForward();
+                } else if (ke.isLeft()) {
+                    active.moveCursorLeft();
+                } else if (ke.isRight()) {
+                    active.moveCursorRight();
+                } else if (ke.isHome()) {
+                    active.moveCursorToStart();
+                } else if (ke.isEnd()) {
+                    active.moveCursorToEnd();
+                } else if (ke.code() == KeyCode.CHAR) {
+                    // port and max-seconds only accept digits
+                    if (selectedRow == ROW_PORT || selectedRow == 
ROW_MAX_SECONDS) {
+                        if (Character.isDigit(ke.character())) {
+                            active.insert(ke.character());
+                        }
+                    } else {
+                        active.insert(ke.character());
+                    }
+                }
+            }
+            return true;
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        int popupW = Math.min(56, area.width() - 4);
+        int popupH = 10;
+        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(" Run: " + exampleTitle + " ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" Tab", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" next │"),
+                        Span.styled(" Space", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" toggle │"),
+                        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);
+
+        int innerX = popup.left() + 2;
+        int innerW = popup.width() - 4;
+        int labelW = 16;
+        int fieldW = innerW - labelW;
+        int rowY = popup.top() + 1;
+
+        // Name
+        renderLabel(frame, innerX, rowY, labelW, "Name:", selectedRow == 
ROW_NAME);
+        renderTextInput(frame, innerX + labelW, rowY, fieldW, nameInput, 
selectedRow == ROW_NAME);
+        rowY++;
+
+        // Port
+        renderLabel(frame, innerX, rowY, labelW, "Port:", selectedRow == 
ROW_PORT);
+        renderTextInput(frame, innerX + labelW, rowY, fieldW, portInput, 
selectedRow == ROW_PORT);
+        rowY++;
+
+        // Max duration
+        renderLabel(frame, innerX, rowY, labelW, "Max seconds:", selectedRow 
== ROW_MAX_SECONDS);
+        renderTextInput(frame, innerX + labelW, rowY, fieldW, maxSecondsInput, 
selectedRow == ROW_MAX_SECONDS);
+        rowY++;
+
+        // Dev mode checkbox
+        renderCheckbox(frame, innerX, rowY, innerW, "Dev mode (live reload)", 
devMode, selectedRow == ROW_DEV);
+        rowY++;
+
+        // Observe checkbox
+        renderCheckbox(frame, innerX, rowY, innerW, "Observe (health + 
metrics)", observe, selectedRow == ROW_OBSERVE);
+        rowY++;
+
+        // Backlog trace checkbox
+        renderCheckbox(frame, innerX, rowY, innerW, "Backlog trace", 
backlogTrace, selectedRow == ROW_TRACE);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "Tab", "next");
+        if (selectedRow >= ROW_DEV) {
+            hint(spans, "Space", "toggle");
+        }
+        hint(spans, "Enter", "launch");
+        hintLast(spans, "Esc", "back");
+    }
+
+    List<String> buildArgs() {
+        List<String> args = new ArrayList<>();
+        String name = nameInput.text().trim();
+        if (!name.isEmpty()) {
+            args.add("--name=" + name);
+        }
+        String port = portInput.text().trim();
+        if (!port.isEmpty()) {
+            args.add("--port=" + port);
+        }
+        String maxSec = maxSecondsInput.text().trim();
+        if (!maxSec.isEmpty() && !"0".equals(maxSec)) {
+            args.add("--max-seconds=" + maxSec);
+        }
+        if (devMode) {
+            args.add("--dev");
+        }
+        if (observe) {
+            args.add("--observe");
+        }
+        if (backlogTrace) {
+            args.add("--backlog-trace");
+        }
+        return args;
+    }
+
+    private TextInputState activeInput() {
+        return switch (selectedRow) {
+            case ROW_NAME -> nameInput;
+            case ROW_PORT -> portInput;
+            case ROW_MAX_SECONDS -> maxSecondsInput;
+            default -> null;
+        };
+    }
+
+    private void renderLabel(Frame frame, int x, int y, int w, String label, 
boolean selected) {
+        Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim();
+        Rect labelArea = new Rect(x, y, w, 1);
+        frame.renderWidget(Paragraph.from(Line.from(Span.styled(label, 
style))), labelArea);
+    }
+
+    private void renderTextInput(Frame frame, int x, int y, int w, 
TextInputState state, boolean active) {
+        Rect inputArea = new Rect(x, y, w, 1);
+        if (active) {
+            TextInput textInput = TextInput.builder()
+                    .cursorStyle(Style.EMPTY.reversed())
+                    .build();
+            frame.renderStatefulWidget(textInput, inputArea, state);
+        } else {
+            String text = state.text();
+            Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(text.isEmpty() ? "—" : text, style))), 
inputArea);
+        }
+    }
+
+    private void renderCheckbox(Frame frame, int x, int y, int w, String 
label, boolean checked, boolean selected) {
+        String box = checked ? "[x]" : "[ ]";
+        Style style = selected ? Style.EMPTY.bold().reversed() : Style.EMPTY;
+        Rect cbArea = new Rect(x, y, w, 1);
+        frame.renderWidget(Paragraph.from(Line.from(
+                Span.styled(" " + box + " " + label, style))), cbArea);
+    }
+}

Reply via email to