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 27bc1e417858bf4fce613136e1c9668371d9393d
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 23:10:12 2026 +0200

    camel-tui: Add custom property support with + key in run form
    
    Press + on the properties page to add a new key:value property.
    Tab switches between key and value fields. Backspace on an empty
    key removes the entry. Custom properties are passed as --prop=key=value.
    
    Co-Authored-By: Claude <[email protected]>
---
 .../jbang/core/commands/tui/RunOptionsForm.java    | 145 +++++++++++++++++----
 1 file changed, 121 insertions(+), 24 deletions(-)

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 f7934c4874b9..edb640549152 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
@@ -70,6 +70,7 @@ class RunOptionsForm {
     // Properties (page 2)
     private List<PropertyEntry> properties;
     private int selectedProperty;
+    private boolean editingKey;
 
     boolean isVisible() {
         return visible;
@@ -143,6 +144,7 @@ class RunOptionsForm {
         } else {
             hint(spans, "←", "options");
             hint(spans, "↑↓", "navigate");
+            hint(spans, "+", "add");
             hint(spans, "Enter", "launch");
             hintLast(spans, "Esc", "back");
         }
@@ -174,8 +176,12 @@ class RunOptionsForm {
         if (properties != null) {
             for (PropertyEntry pe : properties) {
                 String current = pe.valueInput().text();
-                if (!current.equals(pe.originalValue())) {
-                    args.add("--prop=" + pe.key() + "=" + current);
+                String key = pe.effectiveKey();
+                if (key.isEmpty()) {
+                    continue;
+                }
+                if (pe.isCustom() || !current.equals(pe.originalValue())) {
+                    args.add("--prop=" + key + "=" + current);
                 }
             }
         }
@@ -217,6 +223,12 @@ class RunOptionsForm {
             return true;
         }
 
+        if (ke.isChar('+') && selectedRow >= ROW_DEV) {
+            page = PAGE_PROPERTIES;
+            addCustomProperty();
+            return true;
+        }
+
         // Checkbox rows: Space toggles
         if (ke.isChar(' ') && selectedRow >= ROW_DEV) {
             switch (selectedRow) {
@@ -241,7 +253,12 @@ class RunOptionsForm {
     // ---- Properties page (page 1) ----
 
     private boolean handlePropertiesPage(KeyEvent ke) {
+        if (ke.isChar('+')) {
+            addCustomProperty();
+            return true;
+        }
         if (ke.isUp()) {
+            editingKey = false;
             if (selectedProperty == 0) {
                 page = PAGE_OPTIONS;
                 selectedRow = ROW_TRACE;
@@ -251,38 +268,84 @@ class RunOptionsForm {
             return true;
         }
         if (ke.isDown()) {
+            editingKey = false;
             if (selectedProperty < properties.size() - 1) {
                 selectedProperty++;
             }
             return true;
         }
+        if (ke.isFocusNext()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            if (current.isCustom() && editingKey) {
+                editingKey = false;
+            } else if (selectedProperty < properties.size() - 1) {
+                editingKey = false;
+                selectedProperty++;
+            }
+            return true;
+        }
         if (ke.isFocusPrevious()) {
-            if (selectedProperty == 0) {
+            PropertyEntry current = properties.get(selectedProperty);
+            if (current.isCustom() && !editingKey) {
+                editingKey = true;
+            } else if (selectedProperty == 0) {
+                editingKey = false;
                 page = PAGE_OPTIONS;
                 selectedRow = ROW_TRACE;
             } else {
+                editingKey = false;
                 selectedProperty--;
             }
             return true;
         }
-        if (ke.isFocusNext()) {
-            if (selectedProperty < properties.size() - 1) {
-                selectedProperty++;
+        if (ke.isLeft()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            TextInputState active = editingKey ? current.keyInput() : 
current.valueInput();
+            if (active.cursorPosition() == 0) {
+                if (current.isCustom() && !editingKey) {
+                    editingKey = true;
+                    return true;
+                }
+                page = PAGE_OPTIONS;
+                selectedRow = ROW_TRACE;
+                editingKey = false;
+                return true;
             }
-            return true;
         }
-        if (ke.isLeft() && 
properties.get(selectedProperty).valueInput().cursorPosition() == 0) {
-            page = PAGE_OPTIONS;
-            selectedRow = ROW_TRACE;
-            return true;
+        if (ke.isDeleteBackward()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            if (current.isCustom() && editingKey) {
+                if (current.keyInput().text().isEmpty()) {
+                    properties.remove(selectedProperty);
+                    if (selectedProperty >= properties.size() && 
selectedProperty > 0) {
+                        selectedProperty--;
+                    }
+                    if (properties.isEmpty()) {
+                        page = PAGE_OPTIONS;
+                        selectedRow = ROW_TRACE;
+                    }
+                    return true;
+                }
+            }
         }
 
         // Text editing for selected property
-        TextInputState active = properties.get(selectedProperty).valueInput();
+        PropertyEntry current = properties.get(selectedProperty);
+        TextInputState active = (current.isCustom() && editingKey) ? 
current.keyInput() : current.valueInput();
         handleTextInput(ke, active, false);
         return true;
     }
 
+    private void addCustomProperty() {
+        if (properties == null) {
+            properties = new ArrayList<>();
+        }
+        PropertyEntry entry = new PropertyEntry("", "", new 
TextInputState(""), new TextInputState(""));
+        properties.add(entry);
+        selectedProperty = properties.size() - 1;
+        editingKey = true;
+    }
+
     // ---- Rendering ----
 
     private void renderOptionsPage(Frame frame, Rect area) {
@@ -367,6 +430,7 @@ class RunOptionsForm {
                 .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(" +", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" add │"),
                         Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" launch │"),
                         Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
                 .build();
@@ -388,18 +452,44 @@ class RunOptionsForm {
         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);
+            if (pe.isCustom()) {
+                // Custom entry: editable key and value
+                if (selected && editingKey) {
+                    renderTextInput(frame, innerX, rowY, labelW - 1, 
pe.keyInput(), true);
+                } else {
+                    String keyText = pe.keyInput().text();
+                    Style keyStyle = selected ? Style.EMPTY.bold() : 
(keyText.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY);
+                    Rect keyArea = new Rect(innerX, rowY, labelW - 1, 1);
+                    frame.renderWidget(Paragraph.from(Line.from(
+                            Span.styled(keyText.isEmpty() ? "<key>" : keyText, 
keyStyle))), keyArea);
+                }
+                Rect colonArea = new Rect(innerX + labelW - 1, rowY, 1, 1);
+                frame.renderWidget(Paragraph.from(Line.from(Span.styled(":", 
Style.EMPTY.dim()))), colonArea);
+                if (selected && !editingKey) {
+                    renderTextInput(frame, innerX + labelW, rowY, fieldW, 
pe.valueInput(), true);
+                } else {
+                    String text = pe.valueInput().text();
+                    Style style = text.isEmpty() ? Style.EMPTY.dim() : 
Style.EMPTY;
+                    Rect valArea = new Rect(innerX + labelW, rowY, fieldW, 1);
+                    frame.renderWidget(Paragraph.from(Line.from(
+                            Span.styled(text.isEmpty() ? "<value>" : text, 
style))), valArea);
+                }
             } 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);
+                // Loaded entry: fixed key, editable value
+                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++;
         }
@@ -432,7 +522,7 @@ class RunOptionsForm {
             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)));
+                properties.add(new PropertyEntry(key, value, null, new 
TextInputState(value)));
             }
         }
     }
@@ -505,6 +595,13 @@ class RunOptionsForm {
                 Span.styled(" " + box + " " + label, style))), cbArea);
     }
 
-    record PropertyEntry(String key, String originalValue, TextInputState 
valueInput) {
+    record PropertyEntry(String key, String originalValue, TextInputState 
keyInput, TextInputState valueInput) {
+        boolean isCustom() {
+            return keyInput != null;
+        }
+
+        String effectiveKey() {
+            return keyInput != null ? keyInput.text().trim() : key;
+        }
     }
 }

Reply via email to