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 fa87e72e9cd540c7f667983f3283f5420bcb6aac Author: Claus Ibsen <[email protected]> AuthorDate: Thu May 21 22:59:14 2026 +0200 camel-tui: Add properties editor page to run options form The Run dialog now has a second page for editing application.properties from the selected example. Properties are loaded from bundled resources or downloaded from GitHub for online examples. Modified values are passed as --prop=key=value overrides when launching. Co-Authored-By: Claude <[email protected]> --- .../examples/timer-log/timer-log.camel.yaml | 2 +- .../dsl/jbang/core/commands/tui/ActionsPopup.java | 2 +- .../jbang/core/commands/tui/RunOptionsForm.java | 359 +++++++++++++++++---- 3 files changed, 296 insertions(+), 67 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml index 8488774e48d5..6a40ad378d2e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml @@ -23,5 +23,5 @@ period: "{{timer.period}}" steps: - setBody: - simple: "{{greeting.message}} (message #${exchangeProperty.CamelTimerCounter})" + simple: "{{greeting.message}}" - log: "${body}" 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 c50789f79964..2db68d8234be 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 @@ -662,7 +662,7 @@ class ActionsPopup { String baseName = example.getStringOrDefault("name", ""); String autoName = generateUniqueName(baseName); showExampleBrowser = false; - runOptionsForm.open(autoName, baseName); + runOptionsForm.open(autoName, baseName, ExampleHelper.isBundled(example)); } private String generateUniqueName(String baseName) { 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 b51130ceeb49..f7934c4874b9 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 @@ -39,7 +39,10 @@ import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLa class RunOptionsForm { - // Row indices + private static final int PAGE_OPTIONS = 0; + private static final int PAGE_PROPERTIES = 1; + + // Row indices for page 0 private static final int ROW_NAME = 0; private static final int ROW_PORT = 1; private static final int ROW_MAX_SECONDS = 2; @@ -49,6 +52,7 @@ class RunOptionsForm { private static final int ROW_COUNT = 6; private boolean visible; + private int page; private int selectedRow; // Text fields @@ -63,11 +67,15 @@ class RunOptionsForm { private String exampleTitle; + // Properties (page 2) + private List<PropertyEntry> properties; + private int selectedProperty; + boolean isVisible() { return visible; } - void open(String defaultName, String exampleName) { + void open(String defaultName, String exampleName, boolean bundled) { nameInput = new TextInputState(defaultName != null ? defaultName : ""); portInput = new TextInputState(""); maxSecondsInput = new TextInputState(""); @@ -75,7 +83,10 @@ class RunOptionsForm { observe = false; backlogTrace = false; selectedRow = ROW_NAME; + page = PAGE_OPTIONS; + selectedProperty = 0; exampleTitle = exampleName != null ? exampleName : "Run"; + loadProperties(exampleName, bundled); visible = true; } @@ -98,18 +109,113 @@ class RunOptionsForm { if (ke.isConfirm()) { return true; } + + if (page == PAGE_OPTIONS) { + return handleOptionsPage(ke); + } else { + return handlePropertiesPage(ke); + } + } + + void render(Frame frame, Rect area) { + if (page == PAGE_OPTIONS) { + renderOptionsPage(frame, area); + } else { + renderPropertiesPage(frame, area); + } + } + + void renderFooter(List<Span> spans) { + if (page == PAGE_OPTIONS) { + if (hasProperties()) { + hint(spans, "Tab", "next"); + } else { + hint(spans, "Tab", "next"); + } + if (selectedRow >= ROW_DEV) { + hint(spans, "Space", "toggle"); + } + if (hasProperties()) { + hint(spans, "→", "properties"); + } + hint(spans, "Enter", "launch"); + hintLast(spans, "Esc", "back"); + } else { + hint(spans, "←", "options"); + hint(spans, "↑↓", "navigate"); + 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"); + } + if (properties != null) { + for (PropertyEntry pe : properties) { + String current = pe.valueInput().text(); + if (!current.equals(pe.originalValue())) { + args.add("--prop=" + pe.key() + "=" + current); + } + } + } + return args; + } + + // ---- Options page (page 0) ---- + + private boolean handleOptionsPage(KeyEvent ke) { if (ke.isUp()) { selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT; return true; } - if (ke.isDown() || ke.isFocusNext()) { - selectedRow = (selectedRow + 1) % ROW_COUNT; + if (ke.isDown()) { + if (selectedRow == ROW_TRACE && hasProperties()) { + page = PAGE_PROPERTIES; + selectedProperty = 0; + } else { + selectedRow = (selectedRow + 1) % ROW_COUNT; + } + return true; + } + if (ke.isFocusNext()) { + if (selectedRow == ROW_TRACE && hasProperties()) { + page = PAGE_PROPERTIES; + selectedProperty = 0; + } else { + selectedRow = (selectedRow + 1) % ROW_COUNT; + } return true; } if (ke.isFocusPrevious()) { selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT; return true; } + if (ke.isRight() && hasProperties() && selectedRow >= ROW_DEV) { + page = PAGE_PROPERTIES; + selectedProperty = 0; + return true; + } // Checkbox rows: Space toggles if (ke.isChar(' ') && selectedRow >= ROW_DEV) { @@ -125,35 +231,61 @@ class RunOptionsForm { 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()); - } - } + handleTextInput(ke, active, selectedRow == ROW_PORT || selectedRow == ROW_MAX_SECONDS); } return true; } return true; } - void render(Frame frame, Rect area) { + // ---- Properties page (page 1) ---- + + private boolean handlePropertiesPage(KeyEvent ke) { + if (ke.isUp()) { + if (selectedProperty == 0) { + page = PAGE_OPTIONS; + selectedRow = ROW_TRACE; + } else { + selectedProperty--; + } + return true; + } + if (ke.isDown()) { + if (selectedProperty < properties.size() - 1) { + selectedProperty++; + } + return true; + } + if (ke.isFocusPrevious()) { + if (selectedProperty == 0) { + page = PAGE_OPTIONS; + selectedRow = ROW_TRACE; + } else { + selectedProperty--; + } + return true; + } + if (ke.isFocusNext()) { + if (selectedProperty < properties.size() - 1) { + selectedProperty++; + } + return true; + } + if (ke.isLeft() && properties.get(selectedProperty).valueInput().cursorPosition() == 0) { + page = PAGE_OPTIONS; + selectedRow = ROW_TRACE; + return true; + } + + // Text editing for selected property + TextInputState active = properties.get(selectedProperty).valueInput(); + handleTextInput(ke, active, false); + return true; + } + + // ---- Rendering ---- + + private void renderOptionsPage(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); @@ -162,14 +294,33 @@ class RunOptionsForm { frame.renderWidget(Clear.INSTANCE, popup); + String title = " Run: " + exampleTitle; + if (hasProperties()) { + title += " (1/2) "; + } else { + title += " "; + } + + List<Span> bottomSpans = new ArrayList<>(); + bottomSpans.add(Span.styled(" Tab", MonitorContext.HINT_KEY_STYLE)); + bottomSpans.add(Span.raw(" next")); + if (hasProperties()) { + bottomSpans.add(Span.raw(" │")); + bottomSpans.add(Span.styled(" →", MonitorContext.HINT_KEY_STYLE)); + bottomSpans.add(Span.raw(" properties")); + } + bottomSpans.add(Span.raw(" │")); + bottomSpans.add(Span.styled(" Space", MonitorContext.HINT_KEY_STYLE)); + bottomSpans.add(Span.raw(" toggle │")); + bottomSpans.add(Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE)); + bottomSpans.add(Span.raw(" launch │")); + bottomSpans.add(Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE)); + bottomSpans.add(Span.raw(" back ")); + 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 ")))) + .title(title) + .titleBottom(Title.from(Line.from(bottomSpans))) .build(); frame.renderWidget(block, popup); @@ -179,68 +330,119 @@ class RunOptionsForm { 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"); - } + private void renderPropertiesPage(Frame frame, Rect area) { + int popupW = Math.min(70, area.width() - 4); + int propCount = properties != null ? properties.size() : 0; + int popupH = Math.min(propCount + 2, Math.min(20, area.height() - 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())); - List<String> buildArgs() { - List<String> args = new ArrayList<>(); - String name = nameInput.text().trim(); - if (!name.isEmpty()) { - args.add("--name=" + name); + frame.renderWidget(Clear.INSTANCE, popup); + + Block block = Block.builder() + .borderType(BorderType.ROUNDED) + .title(" Run: " + exampleTitle + " — Properties (2/2) ") + .titleBottom(Title.from(Line.from( + Span.styled(" ←", MonitorContext.HINT_KEY_STYLE), Span.raw(" options │"), + Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), Span.raw(" navigate │"), + 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 maxKeyLen = 0; + for (PropertyEntry pe : properties) { + maxKeyLen = Math.max(maxKeyLen, pe.key().length()); } - String port = portInput.text().trim(); - if (!port.isEmpty()) { - args.add("--port=" + port); + int labelW = Math.min(maxKeyLen + 2, innerW / 2); + int fieldW = innerW - labelW; + + int rowY = popup.top() + 1; + int visibleRows = popup.height() - 2; + int scrollOffset = Math.max(0, selectedProperty - visibleRows + 1); + + for (int i = scrollOffset; i < properties.size() && (rowY - popup.top() - 1) < visibleRows; i++) { + PropertyEntry pe = properties.get(i); + boolean selected = (i == selectedProperty); + String keyLabel = pe.key() + ":"; + renderLabel(frame, innerX, rowY, labelW, TuiHelper.truncate(keyLabel, labelW), selected); + + boolean modified = !pe.valueInput().text().equals(pe.originalValue()); + if (selected) { + renderTextInput(frame, innerX + labelW, rowY, fieldW, pe.valueInput(), true); + } else { + String text = pe.valueInput().text(); + Style style = modified ? Style.EMPTY.bold() : Style.EMPTY; + Rect inputArea = new Rect(innerX + labelW, rowY, fieldW, 1); + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(text.isEmpty() ? "—" : text, style))), inputArea); + } + rowY++; } - String maxSec = maxSecondsInput.text().trim(); - if (!maxSec.isEmpty() && !"0".equals(maxSec)) { - args.add("--max-seconds=" + maxSec); + } + + // ---- Properties loading ---- + + private void loadProperties(String exampleName, boolean bundled) { + properties = new ArrayList<>(); + if (exampleName == null || exampleName.isEmpty()) { + return; } - if (devMode) { - args.add("--dev"); + String content; + if (bundled) { + content = DocHelper.loadResourceContent("examples/" + exampleName + "/application.properties"); + } else { + content = DocHelper.downloadContent( + "https://raw.githubusercontent.com/apache/camel-jbang-examples/main/" + + exampleName + "/application.properties"); } - if (observe) { - args.add("--observe"); + if (content == null || content.isBlank()) { + return; } - if (backlogTrace) { - args.add("--backlog-trace"); + for (String line : content.split("\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#")) { + continue; + } + int eq = trimmed.indexOf('='); + if (eq > 0) { + String key = trimmed.substring(0, eq).trim(); + String value = trimmed.substring(eq + 1).trim(); + properties.add(new PropertyEntry(key, value, new TextInputState(value))); + } } - return args; } + private boolean hasProperties() { + return properties != null && !properties.isEmpty(); + } + + // ---- Shared helpers ---- + private TextInputState activeInput() { return switch (selectedRow) { case ROW_NAME -> nameInput; @@ -250,6 +452,30 @@ class RunOptionsForm { }; } + private void handleTextInput(KeyEvent ke, TextInputState active, boolean digitsOnly) { + 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) { + if (digitsOnly) { + if (Character.isDigit(ke.character())) { + active.insert(ke.character()); + } + } else { + active.insert(ke.character()); + } + } + } + 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); @@ -278,4 +504,7 @@ class RunOptionsForm { frame.renderWidget(Paragraph.from(Line.from( Span.styled(" " + box + " " + label, style))), cbArea); } + + record PropertyEntry(String key, String originalValue, TextInputState valueInput) { + } }
