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