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 e5064a18c94f CAMEL-23615: camel-jbang - TUI organize F2 actions menu
with dividers (#23599)
e5064a18c94f is described below
commit e5064a18c94fb40b3a387b8245a8e6f2e030552b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 28 15:15:46 2026 +0200
CAMEL-23615: camel-jbang - TUI organize F2 actions menu with dividers
(#23599)
* CAMEL-23634: camel-jbang - TUI Send Message dialog
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: Use standard HINT_KEY_STYLE for Send Message popup
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: camel-jbang - TUI Send Message add headers support
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: camel-jbang - TUI Send Message clipboard paste support
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: camel-jbang - TUI fix F5 refresh on History tab
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: Fix F5 refresh in History tab
F5 now triggers a one-shot refresh of history data instead of being a no-op.
Co-Authored-By: Claude <[email protected]>
* CAMEL-23615: camel-jbang - TUI organize F2 actions menu with dividers
Group actions into four sections separated by dividers:
User Actions, Diagnostics, Recording, and MCP.
Also sync bundled tui-hello-world example.
Co-Authored-By: Claude <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
---
.../examples/camel-jbang-example-catalog.json | 19 +++
.../examples/tui-hello-world/tui-hello-world.yaml | 36 +++++
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 157 +++++++++++++++------
3 files changed, 168 insertions(+), 44 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
index fca58c3f0788..d88e50b1dc0f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
@@ -456,6 +456,25 @@
"timer-log.camel.yaml"
]
},
+ {
+ "name": "tui-hello-world",
+ "title": "TUI Hello World",
+ "description": "Say hello via TUI Send Message (F2) or CLI",
+ "level": "beginner",
+ "tags": [
+ "beginner",
+ "direct",
+ "send",
+ "tui"
+ ],
+ "bundled": true,
+ "requiresDocker": false,
+ "hasCitrusTests": false,
+ "files": [
+ "README.md",
+ "tui-hello-world.yaml"
+ ]
+ },
{
"name": "xslt",
"title": "XSLT Transformation",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/tui-hello-world.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/tui-hello-world.yaml
new file mode 100644
index 000000000000..bc7f913d837d
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/tui-hello-world.yaml
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+- route:
+ id: welcome
+ from:
+ uri: timer:welcome
+ parameters:
+ repeatCount: 1
+ steps:
+ - setBody:
+ constant: "Welcome! Open the TUI (camel tui) and press F2 → Send
Message to say hello."
+ - log: "${body}"
+
+- route:
+ id: greet
+ from:
+ uri: direct:greet
+ steps:
+ - log: "Received: ${body}"
+ - setBody:
+ simple: "Hello ${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 2b65b0441824..e569c49861fc 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
@@ -56,20 +56,27 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLa
class ActionsPopup {
- private static final int ACTION_RUN_EXAMPLE = 0;
- private static final int ACTION_SHOW_DOCS = 1;
- private static final int ACTION_CAPTION = 2;
- private static final int ACTION_SCREENSHOT = 3;
- private static final int ACTION_SHOW_KEYSTROKES = 4;
- private static final int ACTION_TAPE_RECORDING = 5;
- private static final int ACTION_TAPE_INSTRUCTIONS = 6;
- private static final int ACTION_DOCTOR = 7;
- private static final int ACTION_CLASSPATH = 8;
- private static final int ACTION_MCP_INFO = 9;
- private static final int ACTION_MCP_LOG = 10;
- private static final int ACTION_SEND_MESSAGE = 11;
- private static final int ACTION_RESET_STATS = 12;
- private static final int ACTION_STOP_ALL = 13;
+ // Group 1: User Actions
+ private static final int ACTION_SEND_MESSAGE = 0;
+ private static final int ACTION_RUN_EXAMPLE = 1;
+ private static final int ACTION_SHOW_DOCS = 2;
+ // Group 2: Diagnostics
+ private static final int ACTION_DOCTOR = 3;
+ private static final int ACTION_CLASSPATH = 4;
+ private static final int ACTION_RESET_STATS = 5;
+ private static final int ACTION_STOP_ALL = 6;
+ // Group 3: Recording & Presentation
+ private static final int ACTION_SCREENSHOT = 7;
+ private static final int ACTION_TAPE_RECORDING = 8;
+ private static final int ACTION_TAPE_INSTRUCTIONS = 9;
+ private static final int ACTION_CAPTION = 10;
+ private static final int ACTION_SHOW_KEYSTROKES = 11;
+ // Group 4: MCP
+ private static final int ACTION_MCP_INFO = 12;
+ private static final int ACTION_MCP_LOG = 13;
+
+ private static final int[] GROUP_SIZES = { 3, 4, 5 };
+ private static final int MCP_GROUP_SIZE = 2;
private final Supplier<Set<String>> runningNames;
private final Supplier<List<IntegrationInfo>> integrations;
@@ -161,8 +168,61 @@ class ActionsPopup {
mcpLogPopup.setActivityLog(activityLog);
}
- private int actionCount() {
- return mcpEnabled ? 14 : 12;
+ private int visualActionCount() {
+ if (mcpEnabled) {
+ // 3 + 4 + 5 + 2 actions = 14, plus 3 dividers = 17
+ return 14 + 3;
+ } else {
+ // 3 + 4 + 5 actions = 12, plus 2 dividers = 14
+ return 12 + 2;
+ }
+ }
+
+ private boolean isDividerIndex(int visualIndex) {
+ int pos = 0;
+ for (int i = 0; i < GROUP_SIZES.length; i++) {
+ pos += GROUP_SIZES[i];
+ if (visualIndex == pos) {
+ return true;
+ }
+ pos++;
+ }
+ if (mcpEnabled) {
+ pos += MCP_GROUP_SIZE;
+ }
+ return false;
+ }
+
+ private int resolveAction(int visualIndex) {
+ int dividers = 0;
+ int pos = 0;
+ int groupCount = mcpEnabled ? GROUP_SIZES.length + 1 :
GROUP_SIZES.length;
+ for (int i = 0; i < groupCount; i++) {
+ int gs = i < GROUP_SIZES.length ? GROUP_SIZES[i] : MCP_GROUP_SIZE;
+ pos += gs;
+ if (visualIndex < pos + dividers) {
+ break;
+ }
+ if (i < groupCount - 1) {
+ dividers++;
+ pos++;
+ }
+ }
+ return visualIndex - dividers;
+ }
+
+ private void navigateActionsMenu(int direction) {
+ int total = visualActionCount();
+ Integer current = actionsMenuState.selected();
+ int next = (current != null ? current : 0) + direction;
+ next = Math.max(0, Math.min(next, total - 1));
+ while (isDividerIndex(next) && next > 0 && next < total - 1) {
+ next += direction;
+ }
+ next = Math.max(0, Math.min(next, total - 1));
+ if (!isDividerIndex(next)) {
+ actionsMenuState.select(next);
+ }
}
boolean isVisible() {
@@ -190,7 +250,7 @@ class ActionsPopup {
if (showActionsMenu) {
List<String> items = getActionLabels();
Integer sel = actionsMenuState.selected();
- return new SelectionContext("popup", items, sel != null ? sel :
-1, items.size(), "Actions");
+ return new SelectionContext("popup", items, sel != null ? sel :
-1, visualActionCount(), "Actions");
}
return null;
}
@@ -205,22 +265,29 @@ class ActionsPopup {
List<String> getActionLabels() {
List<String> labels = new ArrayList<>();
+ // Group 1: User Actions
+ labels.add("Send Message");
labels.add("Run an example...");
labels.add("Show Documentation");
- labels.add("Caption...");
+ labels.add("───");
+ // Group 2: Diagnostics
+ labels.add("Run Doctor");
+ labels.add("Show Classpath");
+ labels.add("Reset Stats");
+ labels.add("Stop All");
+ labels.add("───");
+ // Group 3: Recording & Presentation
labels.add("Take Screenshot");
- labels.add(keystrokesEnabled.get() ? "Hide Keystrokes" : "Show
Keystrokes");
labels.add(tapeRecordingActive.get() ? "Stop Tape Recording" : "Start
Tape Recording");
labels.add("Tape Recording Guide");
- labels.add("Run Doctor");
- labels.add("Show Classpath");
+ labels.add("Caption...");
+ labels.add(keystrokesEnabled.get() ? "Hide Keystrokes" : "Show
Keystrokes");
+ // Group 4: MCP
if (mcpEnabled) {
+ labels.add("───");
labels.add("MCP Info");
labels.add("MCP Log");
}
- labels.add("Send Message");
- labels.add("Reset Stats");
- labels.add("Stop All");
return labels;
}
@@ -349,9 +416,9 @@ class ActionsPopup {
if (ke.isCancel()) {
showActionsMenu = false;
} else if (ke.isUp()) {
- actionsMenuState.selectPrevious();
+ navigateActionsMenu(-1);
} else if (ke.isDown()) {
- actionsMenuState.selectNext(actionCount());
+ navigateActionsMenu(1);
} else if (ke.isConfirm()) {
Integer sel = actionsMenuState.selected();
if (sel != null) {
@@ -504,7 +571,7 @@ class ActionsPopup {
// ---- Rendering ----
private void renderActionsMenu(Frame frame, Rect area) {
- int count = actionCount();
+ int count = visualActionCount();
int popupW = 40;
int popupH = 2 + count;
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
@@ -512,6 +579,7 @@ class ActionsPopup {
Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
frame.renderWidget(Clear.INSTANCE, popup);
+ String divider = " ─────────────────────────────────";
// extra space after ⌨️ because it renders narrower than other emoji
String keystrokeLabel = keystrokesEnabled.get()
? " ⌨️ Hide Keystrokes"
@@ -519,26 +587,34 @@ class ActionsPopup {
String stopLabel = stopAllPopup.hasBothGroups()
? " 🛑 Stop All..."
: " 🛑 Stop All";
+ String tapeLabel = tapeRecordingActive.get()
+ ? " ⏹️ Stop Tape Recording (Ctrl+R)"
+ : " ⏺️ Start Tape Recording (Ctrl+R)";
+
List<ListItem> items = new ArrayList<>();
+ // Group 1: User Actions
+ items.add(ListItem.from(" 📩 Send Message"));
items.add(ListItem.from(" 🐪 Run an example..."));
items.add(ListItem.from(" 📖 Show Documentation"));
- items.add(ListItem.from(" 💬 Caption..."));
+ items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
+ // Group 2: Diagnostics
+ items.add(ListItem.from(" 🩺 Run Doctor"));
+ items.add(ListItem.from(" 📦 Show Classpath"));
+ items.add(ListItem.from(" 🔄 Reset Stats"));
+ items.add(ListItem.from(stopLabel));
+ items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
+ // Group 3: Recording & Presentation
items.add(ListItem.from(" 📸 Take Screenshot"));
- items.add(ListItem.from(keystrokeLabel));
- String tapeLabel = tapeRecordingActive.get()
- ? " ⏹️ Stop Tape Recording (Ctrl+R)"
- : " ⏺️ Start Tape Recording (Ctrl+R)";
items.add(ListItem.from(tapeLabel));
items.add(ListItem.from(" 📄 Tape Recording Guide"));
- items.add(ListItem.from(" 🩺 Run Doctor"));
- items.add(ListItem.from(" 📦 Show Classpath"));
+ items.add(ListItem.from(" 💬 Caption..."));
+ items.add(ListItem.from(keystrokeLabel));
+ // Group 4: MCP
if (mcpEnabled) {
+ items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
items.add(ListItem.from(" 🤖 MCP Info"));
items.add(ListItem.from(" 📋 MCP Log"));
}
- items.add(ListItem.from(" 📩 Send Message"));
- items.add(ListItem.from(" 🔄 Reset Stats"));
- items.add(ListItem.from(stopLabel));
ListWidget list = ListWidget.builder()
.items(items.toArray(ListItem[]::new))
.highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
@@ -806,13 +882,6 @@ class ActionsPopup {
}
}
- private int resolveAction(int index) {
- if (!mcpEnabled && index >= ACTION_MCP_INFO) {
- return index + 2;
- }
- return index;
- }
-
private void openSendMessage() {
if (ctx == null) {
return;