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 021ca4ea26a2 Camel TUI: Make popups own the footer on every tab and
confirm process kill/stop
021ca4ea26a2 is described below
commit 021ca4ea26a2e3da3e56fa2ab9d5f68cf4d22d2e
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 8 09:20:44 2026 +0200
Camel TUI: Make popups own the footer on every tab and confirm process
kill/stop
- Popups (Actions menu and its sub-dialogs, F10 process control, confirm
dialogs) now replace the bottom hint bar regardless of the active tab.
Previously this only happened on the Overview tab, so on Source and
other tabs the tab's own hints stayed visible underneath the popup.
- Stop, Restart and Kill in the F10 menu now honor the Confirm Actions
setting and ask before terminating a process, like Quit and Stop All.
- Confirm dialogs share one key contract: Enter accepts, Esc cancels and
any other key is swallowed instead of silently dismissing the dialog.
- The confirm dialogs and the editor's discard prompt now contribute
footer hints; the duplicated "F10 run" hint on Overview is removed.
Camel TUI: Add a shared dialog kit so confirm and input dialogs look the
same
Introduce DialogHelper with one confirm-dialog renderer, one single-field
input-dialog renderer and one placement formula (centered, upper third),
plus TuiHelper.hintLine/hintTitle for footer-style key badges inside a
dialog and FormHelper.renderTextField for the caret-painting text field.
Migrated to the kit:
- Quit/Kill/generic confirm (PopupManager), Delete file (FileActionsPopup)
and Discard changes (SourceViewer) now share the same box, accent color
(warning, or error for irreversible actions) and badge-style key hints
instead of hand-built bold text.
- Rename/new file and Refactor input dialogs use the shared input dialog.
- Settings, Run options, Open Project and Infra port dialogs use the shared
text field instead of seven copy-pasted TextInput blocks; the run form's
error line uses the error color.
- Help, Options viewer and Doc viewer build their bottom-border hints from
the same spans as their footer, so the two can no longer disagree.
- Open Project, Run options and AI Provider dialogs use the shared
placement instead of magic offsets.
- AI Provider popup now contributes footer hints; trailing double spaces
in the Files browser footer and a broken key badge in the AI log fixed.
Camel TUI: Settle the key-hint vocabulary across popups
One rule set for the hints a popup shows:
- Lists and menus: "↑↓ navigate" (the shared icon constant, no more
literal arrows or "Up/Down select") then "Enter" with its verb.
- Esc is "cancel" when pending input or a preview would be discarded
(confirm dialogs, text entry, Settings, Theme, Open Project),
"close" for menus, pickers and viewers (Actions, F10 run menu, Doctor,
AI/MCP log, doc picker, Send Message, AI provider, infra browser) and
"back" only when Esc returns to a parent popup (submenus, run form,
example category, rename input).
The SQL row editor's in-popup "F5=Save Esc=Cancel" line now uses the
shared badge-style hint line like every other dialog.
Camel TUI: Show viewer key hints in the footer bar only
The Help, Doc, Options and AI log viewers drew their key hints both in
the popup's bottom border and in the footer bar. Now that both use the
same badge style the repetition is obvious, so the in-border copy is
removed and the footer bar remains the single place for hints.
Camel TUI: Drop the implied "↑↓ navigate" and "↑↓ scroll" footer hints
Arrow keys moving a list or scrolling a viewer is the default expectation
in the TUI, so the badge only cost footer width (which matters because
the footer drops F-key hints when it overflows). Removed from all 57
footers that showed it. The arrow badge stays only where it does
something a user would not guess: "step through path" in the trace views,
"history" in the Open Project and HTTP probe fields, "preview" in the
Theme picker, and "select" next to "PgUp/Dn detail" in the two-pane
AI and MCP log popups.
Camel TUI: Size the Doctor popup to its content so detail rows are not
clipped
The popup had a fixed width of 62 columns, which cut off the second
line of the AI and Ollama checks (e.g. "Set ANTHROPIC_API_KEY,
AZURE_OPENAI_*, G").
It is now as wide as its longest line plus a right margin, clamped to
the screen.
Camel TUI: Word-wrap Doctor detail rows instead of widening the popup
Keep the Doctor popup at a fixed comfortable width and wrap the dimmed
detail rows under a check on word boundaries, so long hints such as the
list of AI API key variables neither get clipped nor stretch the popup
across the screen. Error messages from Maven resolution are wrapped in
full instead of being truncated to 40 characters.
Camel TUI: Make the Setup AI page a real guide to configuring the AI panel
The F2 -> AI & MCP -> Setup AI page only described how to connect an
MCP client, which MCP Info already covers. It now mirrors the AI section
of the TUI user manual: which provider is currently detected, the
environment variables and detection order, pinning a provider in
Settings, running Ollama locally with the recommended tool-calling
models, OpenAI-compatible servers, the AI panel keys and slash commands,
and a short pointer to MCP for driving the TUI from a coding agent.
The user manual now cross-references the in-TUI page and the Settings
keys.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-tui.adoc | 4 +
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 4 +-
.../dsl/jbang/core/commands/tui/ActivityTab.java | 1 -
.../dsl/jbang/core/commands/tui/AiLogPopup.java | 5 +-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 4 +
.../core/commands/tui/AiProviderSwitchPopup.java | 9 +-
.../dsl/jbang/core/commands/tui/BeansTab.java | 1 -
.../dsl/jbang/core/commands/tui/BrowseTab.java | 1 -
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 34 +++--
.../dsl/jbang/core/commands/tui/CatalogTab.java | 1 -
.../jbang/core/commands/tui/CircuitBreakerTab.java | 1 -
.../dsl/jbang/core/commands/tui/ClasspathTab.java | 1 -
.../jbang/core/commands/tui/ConfigurationTab.java | 1 -
.../dsl/jbang/core/commands/tui/CveAuditTab.java | 1 -
.../dsl/jbang/core/commands/tui/DialogHelper.java | 158 +++++++++++++++++++++
.../jbang/core/commands/tui/DocViewerPopup.java | 119 ++++++++++------
.../dsl/jbang/core/commands/tui/DoctorPopup.java | 62 +++++---
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 2 -
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 1 -
.../core/commands/tui/ExampleBrowserPopup.java | 6 +-
.../jbang/core/commands/tui/FileActionsPopup.java | 77 ++--------
.../dsl/jbang/core/commands/tui/FilesBrowser.java | 3 +-
.../dsl/jbang/core/commands/tui/FolderBrowser.java | 1 -
.../jbang/core/commands/tui/FolderInputPopup.java | 34 ++---
.../dsl/jbang/core/commands/tui/FormHelper.java | 29 ++++
.../dsl/jbang/core/commands/tui/HelpOverlay.java | 10 +-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 3 -
.../camel/dsl/jbang/core/commands/tui/HttpTab.java | 1 -
.../jbang/core/commands/tui/InfraBrowserPopup.java | 20 +--
.../camel/dsl/jbang/core/commands/tui/LogTab.java | 2 -
.../core/commands/tui/MavenDependenciesTab.java | 1 -
.../dsl/jbang/core/commands/tui/McpFacade.java | 7 +-
.../dsl/jbang/core/commands/tui/McpLogPopup.java | 2 +-
.../dsl/jbang/core/commands/tui/MetricsTab.java | 1 -
.../dsl/jbang/core/commands/tui/NetworkTab.java | 1 -
.../core/commands/tui/OptionsViewerPopup.java | 17 ---
.../dsl/jbang/core/commands/tui/OverviewTab.java | 5 +-
.../dsl/jbang/core/commands/tui/PopupManager.java | 72 ++--------
.../core/commands/tui/ProcessControlPopup.java | 38 ++++-
.../dsl/jbang/core/commands/tui/ProcessTab.java | 1 -
.../dsl/jbang/core/commands/tui/RefactorPopup.java | 30 +---
.../dsl/jbang/core/commands/tui/RoutesTab.java | 1 -
.../jbang/core/commands/tui/RunOptionsForm.java | 45 ++----
.../jbang/core/commands/tui/SendMessagePopup.java | 2 +-
.../dsl/jbang/core/commands/tui/SettingsPopup.java | 20 +--
.../dsl/jbang/core/commands/tui/SourceTab.java | 1 -
.../dsl/jbang/core/commands/tui/SourceViewer.java | 41 ++----
.../dsl/jbang/core/commands/tui/SpansTab.java | 2 -
.../dsl/jbang/core/commands/tui/SqlQueryTab.java | 10 +-
.../dsl/jbang/core/commands/tui/SqlTraceTab.java | 1 -
.../dsl/jbang/core/commands/tui/StartupTab.java | 1 -
.../dsl/jbang/core/commands/tui/ThemePopup.java | 2 +-
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 1 -
.../jbang/core/commands/tui/TransformersTab.java | 1 -
.../dsl/jbang/core/commands/tui/TuiHelper.java | 21 +++
.../jbang/core/commands/tui/TypeConvertersTab.java | 1 -
.../core/commands/tui/BeansTabRenderTest.java | 3 +-
.../jbang/core/commands/tui/DialogHelperTest.java | 118 +++++++++++++++
.../core/commands/tui/DoctorPopupWrapTest.java | 57 ++++++++
.../core/commands/tui/OverviewTabRenderTest.java | 1 -
.../jbang/core/commands/tui/PopupManagerTest.java | 45 ++++++
.../core/commands/tui/ProcessControlPopupTest.java | 158 +++++++++++++++++++++
.../core/commands/tui/ProcessTabRenderTest.java | 1 -
63 files changed, 850 insertions(+), 454 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 174c298f9810..7acf42b6c811 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -752,6 +752,10 @@ Press *F8* to open the built-in AI prompt panel. The panel
auto-detects a provid
Press *Ctrl+P* inside the AI panel to switch provider or model at any time.
+The same guidance, together with the provider that is currently detected, is
available inside the TUI
+via *F2* -> _AI & MCP_ -> _Setup AI_. Use *F2* -> _Settings_ to pin a
provider, model or base URL
+(`camel.tui.ai.provider`, `camel.tui.ai.model`, `camel.tui.ai.url`) regardless
of the environment.
+
==== Using Ollama (local, no API key)
Install Ollama natively for best performance — the native binary uses GPU
acceleration
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 403865fae1dd..a0b94aa5b9fd 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
@@ -929,15 +929,13 @@ class ActionsPopup {
}
if (gotoTabPopup.isVisible()) {
hint(spans, "type", "filter");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "Enter", "go to");
hintLast(spans, "Esc", "back");
return;
}
if (showActionsMenu) {
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "Enter", "select");
- hintLast(spans, "Esc", currentSubmenu != null ? "back" : "cancel");
+ hintLast(spans, "Esc", currentSubmenu != null ? "back" : "close");
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
index 52bc50b324c8..d691fa9413f3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
@@ -289,7 +289,6 @@ class ActivityTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
TuiHelper.hint(spans, "Esc", "back");
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
TuiHelper.hint(spans, "PgUp/Dn", "detail");
if (!wordWrap) {
TuiHelper.hint(spans, TuiIcons.HINT_H, "h-scroll");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
index 90cb734186d0..575f3280b900 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
@@ -32,7 +32,6 @@ import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.block.Title;
import dev.tamboui.widgets.list.ListItem;
import dev.tamboui.widgets.list.ListState;
import dev.tamboui.widgets.list.ListWidget;
@@ -102,8 +101,6 @@ class AiLogPopup {
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(" AI Log ")
- .titleBottom(Title.from(Line.from(
- Span.styled(" Esc", Theme.hintKey()), Span.raw("
back "))))
.build();
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
@@ -124,7 +121,7 @@ class AiLogPopup {
void renderFooter(List<Span> spans) {
hint(spans, TuiIcons.HINT_SCROLL, "select");
hint(spans, "PgUp/Dn", "detail");
- hintLast(spans, "Esc", "back");
+ hintLast(spans, "Esc", "close");
}
private void renderMaster(Frame frame, Rect area) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 904455a6b085..482663d0a05f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -1252,6 +1252,10 @@ class AiPanel {
}
void renderFooter(List<Span> spans) {
+ if (providerSwitchPopup.isVisible()) {
+ providerSwitchPopup.renderFooter(spans);
+ return;
+ }
TuiHelper.hint(spans, "F8", "close");
if (statsView) {
TuiHelper.hint(spans, "Ctrl+U", "chat");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiProviderSwitchPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiProviderSwitchPopup.java
index 21656cd2d045..ac19042dedcd 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiProviderSwitchPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiProviderSwitchPopup.java
@@ -133,9 +133,7 @@ final class AiProviderSwitchPopup {
void render(Frame frame, Rect area) {
int popupW = Math.max(1, Math.min(72, area.width() - 4));
int popupH = Math.max(1, Math.min(choices.size() + 2, area.height() -
4));
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
- Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+ Rect popup = DialogHelper.centered(area, popupW, popupH);
this.popupRect = popup;
frame.renderWidget(Clear.INSTANCE, popup);
Block block = Block.builder()
@@ -156,4 +154,9 @@ final class AiProviderSwitchPopup {
.build();
frame.renderStatefulWidget(list, block.inner(popup), listState);
}
+
+ void renderFooter(List<Span> spans) {
+ TuiHelper.hint(spans, "Enter", "select");
+ TuiHelper.hintLast(spans, "Esc", "close");
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
index 200eedef6b7e..552f7dba8177 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
@@ -346,7 +346,6 @@ class BeansTab extends AbstractTableTab {
hint(spans, "/", "filter");
}
hint(spans, "Tab", detailFocused ? "table" : "detail");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "scroll");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
index 0e39410bbf67..f1f7fa2a2319 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
@@ -507,7 +507,6 @@ class BrowseTab extends AbstractTab {
hint(spans, "Esc", "back");
if (view == VIEW_DETAIL) {
hint(spans, "Tab", detailFocused ? "messages" : "detail");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "p", "pretty" + (prettyPrint ? " [on]" : ""));
} else if (view == VIEW_MESSAGES) {
hint(spans, "r", "refresh");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 8bbd59f202da..b39116a00c6e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -74,6 +74,7 @@ import sun.misc.Signal;
import static org.apache.camel.dsl.jbang.core.commands.tui.TabRegistry.*;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
@Command(name = "monitor",
description = "Live dashboard for monitoring Camel integrations",
@@ -393,6 +394,16 @@ public class CamelMonitor extends CamelCommand {
CamelMonitor.this.restartSelectedProcess();
}
+ @Override
+ public void showKillConfirm() {
+ popupManager.showKillConfirm();
+ }
+
+ @Override
+ public void showConfirm(String title, String message, Runnable
onConfirm) {
+ popupManager.showConfirm(title, message, onConfirm);
+ }
+
@Override
public void onRunPhantom(IntegrationInfo phantom) {
actionsPopup.openRunOptionsForPhantom(phantom);
@@ -2263,16 +2274,23 @@ public class CamelMonitor extends CamelCommand {
return;
}
+ // Modal popups own the footer no matter which tab is active, so the
hints always describe
+ // the keys that the topmost dialog will actually receive.
if (filesBrowser.isVisible()) {
filesBrowser.renderFooter(spans);
+ } else if (popupManager.isKillConfirmVisible() ||
popupManager.isConfirmVisible()) {
+ hint(spans, "Enter", "confirm");
+ hintLast(spans, "Esc", "cancel");
} else if (popupManager.isSwitchPopupVisible()) {
- hint(spans, "Up/Down", "select");
hint(spans, "Enter", "switch");
- hint(spans, "Esc", "close");
+ hintLast(spans, "Esc", "close");
} else if (popupManager.isMorePopupVisible()) {
- hint(spans, "Up/Down", "select");
hint(spans, "Enter", "open");
- hint(spans, "Esc", "close");
+ hintLast(spans, "Esc", "close");
+ } else if (actionsPopup.isVisible()) {
+ actionsPopup.renderFooter(spans);
+ } else if (processControlPopup.isVisible()) {
+ processControlPopup.renderFooter(spans);
} else if (shellPanel.isOpen()) {
shellPanel.renderFooter(spans);
} else if (aiPanel.isOpen()) {
@@ -2401,14 +2419,6 @@ public class CamelMonitor extends CamelCommand {
}
private int renderOverviewFooter(List<Span> spans) {
- if (actionsPopup.isVisible()) {
- actionsPopup.renderFooter(spans);
- return 0;
- }
- if (processControlPopup.isVisible()) {
- processControlPopup.renderFooter(spans);
- return 0;
- }
tabRegistry.overviewTab().renderFooter(spans);
int fKeyTotal = insertFKeyHints(spans);
return fKeyTotal;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
index 9b412effeec8..e217e2307a8f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -426,7 +426,6 @@ class CatalogTab extends AbstractTableTab {
} else {
hint(spans, "/", "filter");
}
- hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
}
private int sortEntry(CatalogEntry a, CatalogEntry b) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
index 57b95420b547..32bedc3a0a27 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
@@ -164,7 +164,6 @@ class CircuitBreakerTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "s", "sort");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index 43bee09c26db..66635b2c3323 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -288,7 +288,6 @@ class ClasspathTab extends AbstractTab {
} else {
hint(spans, "/", "filter");
}
- hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
}
private void loadClasspath() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index 31494d01f090..03f696a635a5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -453,7 +453,6 @@ class ConfigurationTab extends AbstractTableTab {
public void renderFooter(List<Span> spans) {
super.renderFooter(spans);
hint(spans, "Tab", detailFocused ? "table" : "detail");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "scroll");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
index 8344a48bf128..f8771b1cbcbf 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
@@ -373,7 +373,6 @@ class CveAuditTab extends AbstractTableTab {
hint(spans, "Esc", "back");
hint(spans, "r", "rescan");
super.renderFooter(spans);
- hintLast(spans, "↑↓", "navigate");
}
private void rescan() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
new file mode 100644
index 000000000000..afd8acd1d076
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
@@ -0,0 +1,158 @@
+/*
+ * 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 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.text.Text;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+/**
+ * Shared building blocks for the small modal dialogs of the TUI so they all
look and behave the same.
+ * <p>
+ * Conventions:
+ * <ul>
+ * <li>Small dialogs (confirm, single text input) are centered horizontally
and sit in the upper third of the screen
+ * ({@link #centered(Rect, int, int)}). Tall list browsers and menus stay
anchored just below the tab bar instead.</li>
+ * <li>Confirm dialogs use the warning color, or the error color for
irreversible actions such as kill or delete. The
+ * title, border and message all share that one accent color.</li>
+ * <li>Key hints inside a dialog use the same badge style as the footer bar
({@link TuiHelper#hintLine}).</li>
+ * <li>Every dialog clears the area beneath it before drawing.</li>
+ * </ul>
+ */
+final class DialogHelper {
+
+ /** Height of a confirm dialog: border, blank, message, blank, hints,
border. */
+ static final int CONFIRM_HEIGHT = 6;
+
+ /** Height of a single-field input dialog: border, blank, field, blank,
border. */
+ static final int INPUT_HEIGHT = 5;
+
+ private static final int CONFIRM_MIN_WIDTH = 34;
+ private static final int INPUT_MIN_WIDTH = 50;
+ private static final int INPUT_MAX_WIDTH = 64;
+
+ private DialogHelper() {
+ }
+
+ /**
+ * Computes the rectangle for a dialog of the given preferred size:
centered horizontally and placed in the upper
+ * third of {@code area} vertically, clamped so it always fits inside the
area.
+ */
+ static Rect centered(Rect area, int popupW, int popupH) {
+ int w = Math.max(1, Math.min(popupW, area.width()));
+ int h = Math.max(1, Math.min(popupH, area.height()));
+ int x = area.left() + Math.max(0, (area.width() - w) / 2);
+ int y = area.top() + Math.max(0, (area.height() - h) / 3);
+ return new Rect(x, y, w, h);
+ }
+
+ /**
+ * Clamps a dialog width to {@code [min, max]} while keeping a two-cell
margin to each side of {@code area}. When
+ * the area is narrower than {@code min} the dialog shrinks to fit rather
than overflowing.
+ */
+ static int clampWidth(Rect area, int min, int max) {
+ int w = Math.min(max, area.width() - 4);
+ w = Math.max(min, w);
+ return Math.max(1, Math.min(w, area.width() - 2));
+ }
+
+ /**
+ * Renders a confirm dialog that is accepted with Enter and cancelled with
Esc.
+ *
+ * @param title dialog title without surrounding spaces, e.g. {@code
"Confirm Quit"}
+ * @param message the question, e.g. {@code "Quit the TUI?"}
+ * @param danger {@code true} for irreversible actions (kill, delete) to
use the error color instead of warning
+ * @return the rectangle the dialog was drawn in, for mouse
hit-testing
+ */
+ static Rect renderConfirm(Frame frame, Rect area, String title, String
message, boolean danger) {
+ return renderConfirm(frame, area, title, message, danger, "Enter",
"confirm");
+ }
+
+ /**
+ * Renders a confirm dialog with a custom accept key, for dialogs that
deliberately do not accept Enter.
+ *
+ * @see #renderConfirm(Frame, Rect, String, String, boolean)
+ */
+ static Rect renderConfirm(
+ Frame frame, Rect area, String title, String message, boolean
danger,
+ String acceptKey, String acceptLabel) {
+ Style accent = danger ? Theme.error() : Theme.warning();
+ String titleText = " " + title + " ";
+ String msg = message.trim();
+ int popupW = clampWidth(area, CONFIRM_MIN_WIDTH, Math.max(msg.length()
+ 6, titleText.length() + 4));
+ Rect popup = centered(area, popupW, CONFIRM_HEIGHT);
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(accent)
+ .title(Title.from(Line.from(Span.styled(titleText,
accent.bold()))))
+ .build();
+ frame.renderWidget(block, popup);
+ Rect inner = block.inner(popup);
+ frame.renderWidget(
+ Paragraph.builder()
+ .centered()
+ .text(Text.from(
+ Line.empty(),
+ Line.from(Span.styled(msg, accent.bold())),
+ Line.empty(),
+ TuiHelper.hintLine(acceptKey, acceptLabel,
"Esc", "cancel")))
+ .build(),
+ inner);
+ return popup;
+ }
+
+ /**
+ * Renders a dialog with a single text field, e.g. for entering a new file
name.
+ *
+ * @param title dialog title without surrounding spaces
+ * @param state the text input state; the caret is painted so the
user sees where they type
+ * @param placeholder dimmed text shown while the field is empty, may be
{@code null}
+ * @return the rectangle the dialog was drawn in, for mouse
hit-testing
+ */
+ static Rect renderInputDialog(Frame frame, Rect area, String title,
TextInputState state, String placeholder) {
+ int popupW = clampWidth(area, INPUT_MIN_WIDTH, INPUT_MAX_WIDTH);
+ Rect popup = centered(area, popupW, INPUT_HEIGHT);
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" " + title + " ")
+ .build();
+ frame.renderWidget(block, popup);
+ Rect inner = block.inner(popup);
+
+ // Place the field on the middle row with a small horizontal margin,
leaving a blank line above and below so
+ // the dialog does not feel cramped.
+ int pad = 2;
+ int fieldW = Math.max(1, inner.width() - 2 * pad);
+ int fieldY = inner.top() + Math.max(0, (inner.height() - 1) / 2);
+ FormHelper.renderTextField(frame, new Rect(inner.left() + pad, fieldY,
fieldW, 1), state, true, placeholder);
+ return popup;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
index ee753c3ac59d..ce1f0c11de77 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
@@ -47,6 +47,7 @@ import dev.tamboui.widgets.list.ScrollMode;
import dev.tamboui.widgets.paragraph.Paragraph;
import dev.tamboui.widgets.scrollbar.Scrollbar;
import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.dsl.jbang.core.common.OllamaDoctorSupport;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
@@ -280,17 +281,15 @@ class DocViewerPopup {
void renderFooter(List<Span> spans) {
if (showViewer) {
hint(spans, "Esc", "back");
- hint(spans, "↑↓", "scroll");
if (docContent != null) {
hint(spans, "t", "toc");
}
if (catalogEntryName != null) {
- hintLast(spans, "o", "options");
+ hint(spans, "o", "options");
}
} else if (showPicker) {
- hint(spans, "↑↓", "navigate");
hint(spans, "Enter", "view");
- hintLast(spans, "Esc", "back");
+ hintLast(spans, "Esc", "close");
}
}
@@ -381,31 +380,80 @@ class DocViewerPopup {
}
void openSetupAI() {
+ // current detection, mirroring the Doctor popup so both screens agree
+ String cloud = DoctorPopup.resolveCloudAiProvider();
+ OllamaDoctorSupport.Status ollama = OllamaDoctorSupport.detect();
+ String status;
+ if (cloud != null) {
+ status = "**Detected:** " + cloud + " (from environment)";
+ } else if (ollama.running() && ollama.models() != null &&
!ollama.models().isEmpty()) {
+ status = "**Detected:** Ollama at " +
OllamaDoctorSupport.formatDisplayHost(ollama.baseUrl())
+ + " with " +
OllamaDoctorSupport.modelCountLabel(ollama.models());
+ } else if (ollama.running()) {
+ status = "**Detected:** Ollama is running but has no models. Run
`ollama pull qwen2.5:32b`.";
+ } else {
+ status = "**Status:** No AI provider detected. Set an API key or
start Ollama, then press F8.";
+ }
String url = "http://localhost:" + mcpPort + "/mcp";
- String client = mcpConnectedClient != null ? mcpConnectedClient.get()
: null;
- String status = client != null
- ? "**Connected:** " + client + "\n\nYour AI agent is already
connected and ready to use."
- : "**Status:** Waiting for connection";
- openMarkdown("Setup MCP",
- "# Setup MCP\n\n"
- + status + "\n\n"
- + "## Connect Claude Code\n\n"
- + "Run this command in your terminal:\n\n"
- + " claude mcp add --transport http
camel-tui " + url + "\n\n"
- + "Then start a new Claude Code session. The
TUI footer will turn green\n"
- + "when the AI agent connects.\n\n"
- + "## Alternative: .mcp.json\n\n"
- + "A `.mcp.json` file is auto-generated in
the current directory while the\n"
- + "TUI runs with `--mcp`. AI agents that
scan for `.mcp.json` will discover\n"
- + "the MCP server automatically.\n\n"
- + "## What the AI Can Do\n\n"
- + "Once connected, your AI agent can:\n\n"
- + "- See the TUI screen and follow your key
presses\n"
- + "- Navigate tabs and select integrations\n"
- + "- Read route diagrams and health status\n"
- + "- Send test messages to endpoints\n"
- + "- Record VHS tapes for documentation\n\n"
- + "Try asking: *\"What's on my Camel TUI
screen right now?\"*\n");
+ openMarkdown("Setup AI",
+ "# Setup AI\n\n"
+ + status + "\n\n"
+ + "Press **F8** to open the AI prompt panel.
The panel answers questions about your running\n"
+ + "integrations by calling built-in tools
(routes, logs, errors, traces, health).\n\n"
+ + "## 1. Choose a provider\n\n"
+ + "The panel auto-detects a provider from
environment variables, in this order:\n\n"
+ + "| Environment | Provider |\n"
+ + "|-------------|----------|\n"
+ + "| `ANTHROPIC_API_KEY` | Anthropic Claude
|\n"
+ + "| `CLOUD_ML_REGION` +
`ANTHROPIC_VERTEX_PROJECT_ID` | Vertex AI |\n"
+ + "| `AZURE_OPENAI_API_KEY` +
`AZURE_OPENAI_ENDPOINT` | Azure OpenAI |\n"
+ + "| `GEMINI_API_KEY` | Google Gemini |\n"
+ + "| `OPENAI_API_KEY` | OpenAI |\n"
+ + "| `WATSONX_APIKEY` | IBM watsonx.ai |\n"
+ + "| `LLM_API_KEY` + `LLM_BASE_URL` | Any
OpenAI-compatible server |\n"
+ + "| Ollama at `localhost:11434` | Local
Ollama (no key needed) |\n\n"
+ + "Export the variable before starting the
TUI, for example:\n\n"
+ + " export ANTHROPIC_API_KEY=sk-ant-...\n"
+ + " camel tui\n\n"
+ + "To pin a provider, model or URL regardless
of the environment, open\n"
+ + "**F2 → Settings** and set *AI Provider*,
*AI Model* and *AI Base URL*\n"
+ + "(stored as `camel.tui.ai.provider`,
`camel.tui.ai.model`, `camel.tui.ai.url`).\n\n"
+ + "## 2. Local AI with Ollama (no API
key)\n\n"
+ + "Install Ollama natively for GPU
acceleration, pull a model, then press F8:\n\n"
+ + " brew install ollama
# macOS\n"
+ + " curl -fsSL
https://ollama.com/install.sh | sh # Linux\n"
+ + " ollama pull qwen2.5:32b\n\n"
+ + "The AI panel relies on tool calling.
Models smaller than ~14B do not call tools\n"
+ + "reliably and answer from training data
instead. Use at least 14B; 32B is recommended.\n\n"
+ + "| Model | RAM | Notes |\n"
+ + "|-------|-----|-------|\n"
+ + "| `qwen2.5:14b` | ~9 GB | Minimum
recommended |\n"
+ + "| `qwen2.5:32b` | ~20 GB | Best balance of
speed and quality |\n"
+ + "| `deepseek-r1:32b` | ~20 GB | Strong
reasoning |\n"
+ + "| `hermes3:70b` | ~43 GB | Excellent tool
calling, needs 64 GB+ |\n"
+ + "| `llama3.3:70b` | ~43 GB | Best open
model, needs 64 GB+ |\n\n"
+ + "`camel infra run ollama` runs Ollama in
Docker without GPU acceleration, which is\n"
+ + "much slower. Prefer the native install for
development.\n\n"
+ + "## 3. OpenAI-compatible local servers\n\n"
+ + "LM Studio, vLLM, llama.cpp, GPT4All and
similar servers work through `LLM_BASE_URL`:\n\n"
+ + " export LLM_API_KEY=any-value\n"
+ + " export
LLM_BASE_URL=http://localhost:1234\n"
+ + " camel tui\n\n"
+ + "`OPENAI_BASE_URL` is accepted as an
alternative to `LLM_BASE_URL`.\n\n"
+ + "## 4. Using the AI panel\n\n"
+ + "- **F8** opens and closes the panel;
**Enter** sends the prompt\n"
+ + "- **Ctrl+P** (or `/provider`) switches
provider or model for the session\n"
+ + "- `/model <name>` switches the model,
`/clear` resets the conversation\n"
+ + "- `/run`, `/infra` and `/send` run Camel
CLI commands from the prompt\n"
+ + "- **Ctrl+U** toggles the usage view with
token consumption\n"
+ + "- `/help` lists all slash commands;
**↑/↓** recalls earlier prompts\n\n"
+ + "Check **F2 → Run Doctor** to verify the
detected provider and Ollama models.\n\n"
+ + "## 5. Let an AI coding agent drive the TUI
(optional)\n\n"
+ + "Start the TUI with `--mcp` to expose an
MCP server, then connect an agent such as\n"
+ + "Claude Code:\n\n"
+ + " claude mcp add --transport http
camel-tui " + url + "\n\n"
+ + "The footer turns green when the agent
connects. See **F2 → AI & MCP → MCP Info**\n"
+ + "for the available tools and a `.mcp.json`
example.\n");
}
void openMcpInfo() {
@@ -488,24 +536,9 @@ class DocViewerPopup {
} else {
title = Title.from(" " + docTitle + " ");
}
- List<Span> footerSpans = new ArrayList<>();
- footerSpans.add(Span.styled(" Esc ", Theme.hintKey()));
- footerSpans.add(Span.raw(" back "));
- footerSpans.add(Span.styled(" ↑↓ ", Theme.hintKey()));
- footerSpans.add(Span.raw(" scroll "));
- if (docContent != null) {
- footerSpans.add(Span.styled(" t ", Theme.hintKey()));
- footerSpans.add(Span.raw(" toc "));
- }
- if (catalogEntryName != null) {
- footerSpans.add(Span.styled(" o ", Theme.hintKey()));
- footerSpans.add(Span.raw(" options "));
- }
- Title footer = Title.from(Line.from(footerSpans));
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(title)
- .titleBottom(footer)
.build();
if (docLines != null) {
frame.renderWidget(block, area);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
index f5f00b2da491..048a90839c1a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
@@ -54,6 +54,43 @@ class DoctorPopup {
private int mcpPort;
private Supplier<String> mcpConnectedClient;
+ /** Fixed popup width; detail rows are word-wrapped to fit instead of
widening the popup. */
+ private static final int POPUP_WIDTH = 64;
+ /** Detail rows are indented to line up with the value column. */
+ private static final String DETAIL_INDENT = " ";
+ private static final int DETAIL_WIDTH = POPUP_WIDTH - 2 -
DETAIL_INDENT.length() - 1;
+
+ /**
+ * Adds a dimmed detail row under a check, word-wrapped so it never runs
into the popup border.
+ */
+ private static void addDetail(List<Line> result, String text) {
+ for (String part : wrapWords(text, DETAIL_WIDTH)) {
+ result.add(Line.from(Span.styled(DETAIL_INDENT + part,
Style.EMPTY.dim())));
+ }
+ }
+
+ static List<String> wrapWords(String text, int width) {
+ List<String> out = new ArrayList<>();
+ if (text == null || text.isBlank()) {
+ return out;
+ }
+ StringBuilder line = new StringBuilder();
+ for (String word : text.trim().split("\\s+")) {
+ if (line.length() > 0 && line.length() + 1 + word.length() >
width) {
+ out.add(line.toString());
+ line.setLength(0);
+ }
+ if (line.length() > 0) {
+ line.append(' ');
+ }
+ line.append(word);
+ }
+ if (line.length() > 0) {
+ out.add(line.toString());
+ }
+ return out;
+ }
+
boolean isVisible() {
return visible;
}
@@ -98,7 +135,7 @@ class DoctorPopup {
if (lines == null || lines.isEmpty()) {
return;
}
- int popupW = Math.min(62, area.width() - 4);
+ int popupW = Math.min(POPUP_WIDTH, area.width() - 4);
int popupH = Math.min(lines.size() + 2, area.height() - 4);
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
int y = area.top() + 2;
@@ -116,7 +153,7 @@ class DoctorPopup {
}
void renderFooter(List<Span> spans) {
- hintLast(spans, "Esc", "back");
+ hintLast(spans, "Esc", "close");
}
// ---- Checks ----
@@ -143,7 +180,7 @@ class DoctorPopup {
Span.raw(String.format("%-30s", version + " (" + vendor +
")")),
Span.raw(" " + emoji)));
if (status != null) {
- result.add(Line.from(Span.styled(" " + status,
Style.EMPTY.dim())));
+ addDetail(result, status);
}
}
@@ -201,16 +238,14 @@ class DoctorPopup {
Span.styled(String.format("%-14s", "Maven"),
Theme.muted()),
Span.raw(String.format("%-30s", "Resolution failed")),
Span.raw(" " + TuiIcons.FAIL)));
- result.add(Line.from(Span.styled(" " +
TuiHelper.truncate(e.getMessage(), 40),
- Style.EMPTY.dim())));
+ addDetail(result, e.getMessage());
} catch (Exception e) {
result.add(Line.from(
Span.raw(TuiIcons.indent(TuiIcons.INFRA)),
Span.styled(String.format("%-14s", "Maven"),
Theme.muted()),
Span.raw(String.format("%-30s", "Error")),
Span.raw(" " + TuiIcons.FAIL)));
- result.add(Line.from(Span.styled(" " +
TuiHelper.truncate(e.getMessage(), 40),
- Style.EMPTY.dim())));
+ addDetail(result, e.getMessage());
}
}
@@ -313,10 +348,8 @@ class DoctorPopup {
Span.styled(String.format("%-14s", "AI"), Theme.muted()),
Span.raw(String.format("%-30s", "No API key configured")),
Span.raw(" " + TuiIcons.WARN)));
- result.add(Line.from(Span.styled(
- " Set ANTHROPIC_API_KEY,
AZURE_OPENAI_*, GEMINI_API_KEY, OPENAI_API_KEY,"
- + " WATSONX_APIKEY, or start
Ollama",
- Style.EMPTY.dim())));
+ addDetail(result, "Set ANTHROPIC_API_KEY, AZURE_OPENAI_*,
GEMINI_API_KEY, OPENAI_API_KEY,"
+ + " WATSONX_APIKEY, or start Ollama");
}
}
@@ -373,9 +406,7 @@ class DoctorPopup {
Span.styled(String.format("%-14s", "Ollama"),
Theme.muted()),
Span.raw(String.format("%-30s", "Not detected
(optional)")),
Span.raw(" " + TuiIcons.WARN)));
- result.add(Line.from(Span.styled(
- " " + TuiHelper.truncate("Start Ollama
(ollama serve) for local AI", 40),
- Style.EMPTY.dim())));
+ addDetail(result, "Start Ollama (ollama serve) for local AI");
}
}
@@ -394,8 +425,7 @@ class DoctorPopup {
Span.styled(String.format("%-14s", "MCP"),
Theme.muted()),
Span.raw(String.format("%-30s", "Listening on port " +
mcpPort)),
Span.raw(" " + TuiIcons.WARN)));
- result.add(Line.from(Span.styled(" No AI
client connected",
- Style.EMPTY.dim())));
+ addDetail(result, "No AI client connected");
}
} else {
result.add(Line.from(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index 305cf77220da..5801e48c71ba 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -392,7 +392,6 @@ class EndpointsTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "s", "sort");
String[] filterLabels = { "all", "remote", "remote+stub" };
hint(spans, "f", "filter [" + filterLabels[filter] + "]");
@@ -405,7 +404,6 @@ class EndpointsTab extends AbstractTableTab {
hint(spans, "d", "detail " + (panelMode == PANEL_DETAIL ? "[on]" :
"[off]"));
if (panelMode == PANEL_DETAIL) {
hint(spans, "Tab", detailFocused ? "table" : "detail");
- hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index 32df7513bfd1..d46270942355 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -430,7 +430,6 @@ class ErrorsTab extends AbstractTableTab {
return;
}
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "PgUp/Dn", "detail");
if (!wordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
index 54b8addc6f4d..e0fc29e29368 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
@@ -224,11 +224,11 @@ class ExampleBrowserPopup {
}
void renderFooter(List<Span> spans) {
- TuiHelper.hint(spans, "↑↓", "navigate");
TuiHelper.hint(spans, "r", "run");
- TuiHelper.hint(spans, "Enter", currentFolder != null ? "run..." :
"open/run...");
+ TuiHelper.hint(spans, "Enter", currentFolder != null ? "run..." :
"open");
TuiHelper.hint(spans, "d", "docs");
- TuiHelper.hintLast(spans, "Esc", "back");
+ // inside a category Esc returns to the top level, at the top level it
closes the browser
+ TuiHelper.hintLast(spans, "Esc", currentFolder != null ? "back" :
"close");
}
SelectionContext getSelectionContext() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
index 3a62de0ab9ad..b8c58096c0ee 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
@@ -21,11 +21,9 @@ import java.util.List;
import dev.tamboui.layout.Padding;
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.text.Text;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.widgets.Clear;
@@ -33,13 +31,11 @@ import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
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;
/**
* File-actions menu for the Source tab file list (opened with F12). Presents
basic file management (new file, new
@@ -222,14 +218,16 @@ class FileActionsPopup {
}
private boolean handleConfirmKey(KeyEvent ke) {
- // Delete is a destructive action: only an explicit "y" confirms it.
Enter must NOT delete, so it
- // (and Esc, "n", or any other key) simply returns to the menu.
+ // Delete is a destructive action: only an explicit "y" confirms it.
Enter must NOT delete. Esc (or "n")
+ // returns to the menu; any other key is swallowed so a stray
keystroke neither deletes nor dismisses.
if (ke.code() == KeyCode.CHAR && "y".equalsIgnoreCase(ke.string())) {
result = new Request(Action.DELETE, null);
close();
return true;
}
- phase = Phase.MENU;
+ if (ke.isCancel() || (ke.code() == KeyCode.CHAR &&
"n".equalsIgnoreCase(ke.string()))) {
+ phase = Phase.MENU;
+ }
return true;
}
@@ -285,69 +283,13 @@ class FileActionsPopup {
}
private void renderInput(Frame frame, Rect area) {
- int popupW = Math.max(50, Math.min(64, area.width() - 4));
- popupW = Math.min(popupW, area.width() - 2);
- int popupH = 5;
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
- Rect popup = new Rect(x, y, popupW, Math.min(popupH, area.height()));
- this.popupRect = popup;
-
- frame.renderWidget(Clear.INSTANCE, popup);
- Block block = Block.builder()
- .borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .borderStyle(Theme.borderFocused())
- .title(Title.from(Line.from(Span.styled(" " + inputTitle + "
", Theme.title().bold()))))
- .build();
- frame.renderWidget(block, popup);
- Rect inner = block.inner(popup);
-
- // Place the input on the middle row with a small horizontal margin,
leaving a blank line above and below so
- // the dialog does not feel cramped.
- int pad = 2;
- int fieldW = Math.max(1, inner.width() - 2 * pad);
- int fieldY = inner.top() + Math.max(0, (inner.height() - 1) / 2);
- Rect field = new Rect(inner.left() + pad, fieldY, fieldW, 1);
-
- TextInput textInput = TextInput.builder()
- .cursorStyle(Style.EMPTY.reversed())
- .placeholder("name")
- .build();
- // Use renderWithCursor (not renderStatefulWidget, which calls
render() and paints no cursor cell) so the
- // caret is visible while typing the name.
- textInput.renderWithCursor(field, frame.buffer(), inputState, frame);
+ this.popupRect = DialogHelper.renderInputDialog(frame, area,
inputTitle, inputState, "name");
}
private void renderConfirm(Frame frame, Rect area) {
- String msg = "Delete " + targetName + "?";
- int popupW = Math.max(40, Math.min(60, msg.length() + 6));
- popupW = Math.min(popupW, area.width() - 4);
- int popupH = 6;
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
- Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
- this.popupRect = popup;
-
- frame.renderWidget(Clear.INSTANCE, popup);
- Block block = Block.builder()
- .borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .borderStyle(Theme.warning())
- .title(Title.from(Line.from(Span.styled(" " + TuiIcons.DELETE
+ " Delete file? ", Theme.warning().bold()))))
- .build();
- frame.renderWidget(block, popup);
- Rect inner = block.inner(popup);
- frame.renderWidget(
- Paragraph.builder()
- .centered()
- .text(Text.from(
- Line.empty(),
- Line.from(Span.styled(msg,
Theme.warning().bold())),
- Line.empty(),
- Line.from(
- Span.styled("y", Style.EMPTY.bold()),
Span.raw(" delete "),
- Span.styled("Esc",
Style.EMPTY.bold()), Span.raw(" cancel"))))
- .build(),
- inner);
+ // Deleting a file is irreversible, so this dialog is error-styled and
deliberately accepts "y" only.
+ this.popupRect = DialogHelper.renderConfirm(frame, area,
TuiIcons.DELETE + " Delete file?",
+ "Delete " + targetName + "?", true, "y", "delete");
}
void renderFooter(List<Span> spans) {
@@ -356,7 +298,6 @@ class FileActionsPopup {
}
switch (phase) {
case MENU -> {
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
TuiHelper.hint(spans, "Enter", "select");
TuiHelper.hintLast(spans, "Esc", "close");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
index 3a70c9a7042a..a22ebb8b92c4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
@@ -365,9 +365,8 @@ class FilesBrowser {
if (sourceViewer.isVisible()) {
sourceViewer.renderFooter(spans);
} else {
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
TuiHelper.hint(spans, "Enter", "open");
- TuiHelper.hint(spans, "Esc", "close");
+ TuiHelper.hintLast(spans, "Esc", "close");
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
index afa358b68989..8f6f76e08e6e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
@@ -409,7 +409,6 @@ class FolderBrowser {
sourceViewer.renderFooter(spans);
return;
}
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
TuiHelper.hint(spans, "Enter", fileSelectMode ? "select" : "open");
if (!fileSelectMode) {
TuiHelper.hint(spans, "Tab", "select");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
index a98f315fb090..58fad1cc8979 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
@@ -23,9 +23,7 @@ import java.util.List;
import java.util.function.BiConsumer;
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;
@@ -34,9 +32,7 @@ import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.input.TextInput;
import dev.tamboui.widgets.input.TextInputState;
-import dev.tamboui.widgets.paragraph.Paragraph;
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
class FolderInputPopup {
@@ -201,11 +197,11 @@ class FolderInputPopup {
folderBrowser.renderFooter(spans);
} else if (showInput) {
if (!folderHistory.isEmpty()) {
- TuiHelper.hint(spans, "↑↓", "history");
+ TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "history");
}
TuiHelper.hint(spans, "Tab", "browse");
TuiHelper.hint(spans, "Enter", "open");
- TuiHelper.hintLast(spans, "Esc", "back");
+ TuiHelper.hintLast(spans, "Esc", "cancel");
}
}
@@ -332,10 +328,7 @@ class FolderInputPopup {
private void renderInput(Frame frame, Rect area) {
int popupW = Math.min(70, 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() - 17) / 4);
- Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+ Rect popup = DialogHelper.centered(area, popupW,
DialogHelper.INPUT_HEIGHT);
frame.renderWidget(Clear.INSTANCE, popup);
@@ -346,20 +339,15 @@ class FolderInputPopup {
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
+ int pad = 1;
int labelW = 9;
- int fieldW = inner.width() - labelW;
- int row = inner.top();
- int ix = inner.left();
-
- Rect labelArea = new Rect(ix, row, labelW, 1);
- frame.renderWidget(Paragraph.from(Line.from(Span.styled("Folder:",
Style.EMPTY.bold()))), labelArea);
- Rect inputArea = new Rect(ix + labelW, row, fieldW, 1);
- TextInput textInput = TextInput.builder()
- .cursorStyle(Style.EMPTY.reversed())
- .placeholder("/path/to/folder")
- .build();
- // renderWithCursor (not renderStatefulWidget) so the caret is visible
while typing
- textInput.renderWithCursor(inputArea, frame.buffer(), inputState,
frame);
+ int fieldW = Math.max(1, inner.width() - labelW - 2 * pad);
+ // middle row, with a blank line above and below like the other input
dialogs
+ int row = inner.top() + Math.max(0, (inner.height() - 1) / 2);
+ int ix = inner.left() + pad;
+
+ FormHelper.renderLabel(frame, ix, row, labelW, "Folder:", true);
+ FormHelper.renderTextField(frame, new Rect(ix + labelW, row, fieldW,
1), inputState, true, "/path/to/folder");
}
private void doLaunchFolder(String folder, String pomPath, String
displayName, List<String> extraArgs) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
index 5dcb71b56f3f..5e7579981bf3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FormHelper.java
@@ -23,6 +23,7 @@ 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.input.TextInput;
import dev.tamboui.widgets.input.TextInputState;
import dev.tamboui.widgets.paragraph.Paragraph;
@@ -58,6 +59,34 @@ final class FormHelper {
frame.renderWidget(Paragraph.from(Line.from(Span.styled(label,
style))), labelArea);
}
+ /**
+ * Renders a single-line text field. The active field paints a reversed
caret at the cursor so the user can see
+ * where they type; an inactive field shows its text, or the placeholder
dimmed while it is empty.
+ *
+ * @param state the field state, may be {@code null} for an inactive
field with no value yet
+ * @param active whether the field currently has focus
+ * @param placeholder dimmed text shown while the field is empty, may be
{@code null}
+ */
+ static void renderTextField(Frame frame, Rect area, TextInputState state,
boolean active, String placeholder) {
+ if (active && state != null) {
+ TextInput.Builder builder =
TextInput.builder().cursorStyle(Style.EMPTY.reversed());
+ if (placeholder != null) {
+ builder.placeholder(placeholder);
+ }
+ // renderWithCursor (not renderStatefulWidget, which paints no
cursor cell) so the caret is visible
+ builder.build().renderWithCursor(area, frame.buffer(), state,
frame);
+ return;
+ }
+ String text = state != null ? state.text() : "";
+ if (text.isEmpty()) {
+ if (placeholder != null) {
+
frame.renderWidget(Paragraph.from(Line.from(Span.styled(placeholder,
Style.EMPTY.dim()))), area);
+ }
+ } else {
+ frame.renderWidget(Paragraph.from(Line.from(Span.raw(text))),
area);
+ }
+ }
+
static void handlePaste(String text, TextInputState target) {
if (text == null || text.isEmpty() || target == null) {
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
index 2e1dcafaf377..ca2aa1a6b4c6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java
@@ -21,7 +21,6 @@ import java.util.List;
import dev.tamboui.layout.Rect;
import dev.tamboui.markdown.MarkdownView;
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;
@@ -29,9 +28,7 @@ import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.block.Title;
-import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
class HelpOverlay {
@@ -91,12 +88,10 @@ class HelpOverlay {
frame.renderWidget(Clear.INSTANCE, area);
Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() -
4, area.height() - 2);
+ // key hints are shown in the footer bar only, not repeated in the
bottom border
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(" Help ")
- .titleBottom(Title.from(Line.from(
- Span.styled(" F1/? ", Theme.hintKey()), Span.raw("
close "),
- Span.styled(" " + TuiIcons.HINT_SCROLL + " ",
Theme.hintKey()), Span.raw(" scroll "))))
.build();
MarkdownView view = MarkdownView.builder()
@@ -109,7 +104,6 @@ class HelpOverlay {
}
void renderFooter(List<Span> spans) {
- hint(spans, TuiIcons.HINT_SCROLL, "scroll");
- hintLast(spans, "Esc", "close");
+ hintLast(spans, "Esc/F1", "close");
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 41be33d787b7..4db498ef1f9d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -802,7 +802,6 @@ class HistoryTab extends AbstractTab {
if (!showWaterfall) {
hint(spans, "Tab", detailFocused ? "table" : "detail");
}
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (!showWaterfall && !traceWordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
}
@@ -815,7 +814,6 @@ class HistoryTab extends AbstractTab {
hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : "
[off]"));
} else if (tracerActive) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "s", "sort");
hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
hint(spans, "d", "diagram");
@@ -826,7 +824,6 @@ class HistoryTab extends AbstractTab {
if (!showWaterfall) {
hint(spans, "Tab", detailFocused ? "table" : "detail");
}
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (!showWaterfall && !historyWordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 4b60ca179152..b724005b38ef 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -234,7 +234,6 @@ class HttpTab extends AbstractTableTab {
}
if (showSpec) {
hint(spans, "c/Esc", "close");
- hint(spans, TuiIcons.HINT_SCROLL, "scroll");
hintLast(spans, "PgUp/PgDn", "page");
return;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
index 1c0cbcda56ec..8d5d7b7b2355 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java
@@ -42,7 +42,6 @@ import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.input.TextInput;
import dev.tamboui.widgets.input.TextInputState;
import dev.tamboui.widgets.list.ListItem;
import dev.tamboui.widgets.list.ListState;
@@ -195,7 +194,6 @@ class InfraBrowserPopup {
if (showPortDialog) {
boolean hasMultiImpl = selectedService != null &&
selectedService.implementations().size() > 1;
if (hasMultiImpl) {
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (portDialogRow == 0) {
TuiHelper.hint(spans, "Space", "cycle");
}
@@ -203,9 +201,8 @@ class InfraBrowserPopup {
TuiHelper.hint(spans, "Enter", "run");
TuiHelper.hintLast(spans, "Esc", "back");
} else if (showBrowser) {
- TuiHelper.hint(spans, "↑↓", "navigate");
TuiHelper.hint(spans, "Enter", "select");
- TuiHelper.hintLast(spans, "Esc", "back");
+ TuiHelper.hintLast(spans, "Esc", "close");
}
}
@@ -458,20 +455,7 @@ class InfraBrowserPopup {
Rect portLabelArea = new Rect(ix, row, labelW, 1);
frame.renderWidget(Paragraph.from(Line.from(Span.styled("Port:",
portLabelStyle))), portLabelArea);
Rect portArea = new Rect(ix + labelW, row, fieldW, 1);
- if (portDialogRow == 1) {
- TextInput textInput = TextInput.builder()
- .cursorStyle(Style.EMPTY.reversed())
- .placeholder("default")
- .build();
- // renderWithCursor (not renderStatefulWidget) so the caret is
painted on the active field
- textInput.renderWithCursor(portArea, frame.buffer(), portState,
frame);
- } else {
- String portText = portState != null ? portState.text() : "";
- frame.renderWidget(Paragraph.from(Line.from(
- Span.styled(portText.isEmpty() ? "default" : portText,
- portText.isEmpty() ? Style.EMPTY.dim() :
Style.EMPTY))),
- portArea);
- }
+ FormHelper.renderTextField(frame, portArea, portState, portDialogRow
== 1, "default");
}
private void handlePortInput(KeyEvent ke) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
index c09af0e382c3..98777f9319c8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
@@ -414,7 +414,6 @@ class LogTab extends AbstractTab {
}
if (showLogLevelPopup) {
hint(spans, "Esc", "cancel");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "Enter", "set level");
return;
}
@@ -424,7 +423,6 @@ class LogTab extends AbstractTab {
} else {
hint(spans, "Esc", "back");
}
- hint(spans, TuiIcons.HINT_SCROLL, "scroll");
search.renderSearchHints(spans);
hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
if (!ctx.isInfraSelected()) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
index 27feb4d7e24a..828b12368248 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
@@ -320,7 +320,6 @@ class MavenDependenciesTab extends AbstractTableTab {
} else {
hint(spans, "/", "filter");
}
- hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
}
private int sortDep(DependencyLoader.DepEntry a, DependencyLoader.DepEntry
b) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index fcc6af4bfd90..3fc3a2c2f016 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -40,6 +40,7 @@ import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
/**
* Facade that exposes monitor state and actions to the MCP server.
@@ -612,13 +613,11 @@ class McpFacade {
filesBrowser.renderFooter(spans);
} else if (bridge.isSwitchPopupVisible() ||
bridge.isMorePopupVisible()) {
if (bridge.isSwitchPopupVisible()) {
- hint(spans, "Up/Down", "select");
hint(spans, "Enter", "switch");
- hint(spans, "Esc", "close");
+ hintLast(spans, "Esc", "close");
} else {
- hint(spans, "Up/Down", "select");
hint(spans, "Enter", "open");
- hint(spans, "Esc", "close");
+ hintLast(spans, "Esc", "close");
}
} else {
MonitorTab tab = bridge.activeTab();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
index cbfe67e6208a..79d5b984e4b4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
@@ -128,7 +128,7 @@ class McpLogPopup {
void renderFooter(List<Span> spans) {
hint(spans, TuiIcons.HINT_SCROLL, "select");
hint(spans, "PgUp/Dn", "detail");
- hintLast(spans, "Esc", "back");
+ hintLast(spans, "Esc", "close");
}
private void renderMaster(Frame frame, Rect area) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
index 4ef4cd0da358..0dd26bc5478f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
@@ -616,7 +616,6 @@ class MetricsTab extends AbstractTableTab {
public void renderFooter(List<Span> spans) {
if (showRaw) {
hint(spans, "Esc", "close");
- hint(spans, TuiIcons.HINT_SCROLL, "scroll");
hint(spans, "PgUp/Dn", "page");
hint(spans, "F5", "refresh");
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
index 6b925a9b2e2a..7e23b8cdd4a7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
@@ -404,7 +404,6 @@ class NetworkTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "s", "sort");
String chartLabel = switch (chartMode) {
case CHART_ALL -> "[all]";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
index 779232cf7ec8..578140eaceec 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
@@ -382,7 +382,6 @@ class OptionsViewerPopup {
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(buildTitle())
- .titleBottom(buildFooter())
.build();
frame.renderWidget(block, area);
@@ -436,24 +435,8 @@ class OptionsViewerPopup {
return Title.from(Line.from(spans));
}
- private Title buildFooter() {
- List<Span> spans = new ArrayList<>();
- spans.add(Span.styled(" Esc ", Theme.hintKey()));
- spans.add(Span.raw(" back "));
- spans.add(Span.styled(" ↑↓ ", Theme.hintKey()));
- spans.add(Span.raw(" scroll "));
- if (tabLabels != null && tabLabels.length > 1) {
- spans.add(Span.styled(" ←→ ", Theme.hintKey()));
- spans.add(Span.raw(" tab "));
- }
- spans.add(Span.styled(" d ", Theme.hintKey()));
- spans.add(Span.raw(" doc "));
- return Title.from(Line.from(spans));
- }
-
void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, "↑↓", "scroll");
if (tabLabels != null && tabLabels.length > 1) {
hint(spans, "←→", "tab");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index c659fc645d13..e1c7bda1c0fc 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -1033,7 +1033,6 @@ class OverviewTab extends AbstractTab {
if (ctx.selectedPid != null) {
hint(spans, "Esc", "unselect");
}
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (!ctx.infraData.get().isEmpty()) {
hint(spans, "Tab", infraFocused ? "integrations" : "infra");
}
@@ -1050,9 +1049,7 @@ class OverviewTab extends AbstractTab {
default -> "[off]";
});
}
- if (ctx.selectedPid != null) {
- hint(spans, "F10", "run");
- }
+ // F10 is added by the global F-key hints, so it must not be repeated
here.
}
@Override
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index 2f7250a2c4ae..2184594eed6e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -26,7 +26,6 @@ import dev.tamboui.terminal.Frame;
import dev.tamboui.text.CharWidth;
import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
-import dev.tamboui.text.Text;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.MouseEvent;
@@ -39,7 +38,6 @@ 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 dev.tamboui.widgets.scrollbar.Scrollbar;
import dev.tamboui.widgets.scrollbar.ScrollbarState;
@@ -410,11 +408,14 @@ class PopupManager {
return true;
}
+ // Confirm dialogs share one key contract: Enter accepts, Esc cancels, and
every other key is
+ // swallowed so a stray keystroke can neither dismiss nor trigger a
destructive action.
+
private boolean handleKillConfirmKeys(KeyEvent ke) {
if (ke.isConfirm()) {
showKillConfirm = false;
callbacks.stopSelectedProcess(true);
- } else {
+ } else if (ke.isCancel()) {
showKillConfirm = false;
}
return true;
@@ -428,7 +429,7 @@ class PopupManager {
if (cb != null) {
cb.run();
}
- } else {
+ } else if (ke.isCancel()) {
showConfirm = false;
confirmCallback = null;
}
@@ -695,69 +696,14 @@ class PopupManager {
}
void renderKillConfirm(Frame frame, Rect area) {
- String name = ctx.selectedName();
- String msg = " Kill " + name + " (PID: " + ctx.selectedPid + ")? ";
- int popupW = Math.max(34, msg.length() + 4);
- int popupH = 6;
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
- 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).borders(Borders.ALL)
- .borderStyle(Theme.error())
- .title(" Confirm Kill ")
- .build();
- frame.renderWidget(block, popup);
- Rect inner = block.inner(popup);
- frame.renderWidget(
- Paragraph.builder()
- .centered()
- .text(Text.from(
- Line.empty(),
- Line.from(Span.styled(msg,
Theme.error().bold())),
- Line.empty(),
- Line.from(
- Span.styled("Enter",
Style.EMPTY.bold()),
- Span.raw(" confirm "),
- Span.styled("Esc", Style.EMPTY.bold()),
- Span.raw(" cancel"))))
- .build(),
- inner);
+ String msg = "Kill " + ctx.selectedName() + " (PID: " +
ctx.selectedPid + ")?";
+ DialogHelper.renderConfirm(frame, area, "Confirm Kill", msg, true);
}
void renderConfirm(Frame frame, Rect area) {
String msg = confirmMessage != null ? confirmMessage : "";
- String title = confirmTitle != null ? " " + confirmTitle + " " : "
Confirm ";
- int popupW = Math.max(34, Math.max(msg.length() + 4, title.length() +
4));
- int popupH = 6;
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
- 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).borders(Borders.ALL)
- .borderStyle(Theme.warning())
- .title(title)
- .build();
- frame.renderWidget(block, popup);
- Rect inner = block.inner(popup);
- frame.renderWidget(
- Paragraph.builder()
- .centered()
- .text(Text.from(
- Line.empty(),
- Line.from(Span.styled(msg,
Theme.warning().bold())),
- Line.empty(),
- Line.from(
- Span.styled("Enter",
Style.EMPTY.bold()),
- Span.raw(" confirm "),
- Span.styled("Esc", Style.EMPTY.bold()),
- Span.raw(" cancel"))))
- .build(),
- inner);
+ String title = confirmTitle != null ? confirmTitle : "Confirm";
+ DialogHelper.renderConfirm(frame, area, title, msg, false);
}
int[] morePopupShortcut(KeyEvent ke) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
index 8e8891343908..fd7e156e71df 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
@@ -63,6 +63,16 @@ class ProcessControlPopup {
void restartSelectedProcess();
+ /**
+ * Shows the kill confirm dialog; the dialog itself force-kills the
selected process on Enter.
+ */
+ void showKillConfirm();
+
+ /**
+ * Shows a generic confirm dialog that runs {@code onConfirm} on Enter
and does nothing on Esc.
+ */
+ void showConfirm(String title, String message, Runnable onConfirm);
+
void onRunPhantom(IntegrationInfo phantom);
void onStopAll();
@@ -181,7 +191,7 @@ class ProcessControlPopup {
void renderFooter(List<Span> spans) {
hint(spans, "Enter", "select");
- hintLast(spans, "Esc", "cancel");
+ hintLast(spans, "Esc", "close");
}
private void executeAction(ControlAction action) {
@@ -204,10 +214,30 @@ class ProcessControlPopup {
}
case STOP_ROUTES -> actions.sendRouteCommand(ctx.selectedPid, "*",
"stop");
case START_ROUTES -> actions.sendRouteCommand(ctx.selectedPid,
"*", "start");
- case RESTART -> actions.restartSelectedProcess();
- case STOP -> actions.stopSelectedProcess(false);
- case KILL -> actions.stopSelectedProcess(true);
+ case RESTART -> confirmThen("Confirm Restart", "Restart", () ->
actions.restartSelectedProcess());
+ case STOP -> confirmThen("Confirm Stop", "Stop", () ->
actions.stopSelectedProcess(false));
+ case KILL -> {
+ // Kill has a dedicated (error-styled) confirm dialog that
performs the force-kill itself.
+ if (ctx.confirmActions) {
+ actions.showKillConfirm();
+ } else {
+ actions.stopSelectedProcess(true);
+ }
+ }
case STOP_ALL -> actions.onStopAll();
}
}
+
+ /**
+ * Runs {@code action} directly when confirmations are disabled in
settings, otherwise asks first. Stop, Restart and
+ * Kill all terminate a running process, so they share the same "are you
sure?" gate as Quit and Stop All.
+ */
+ private void confirmThen(String title, String verb, Runnable action) {
+ if (!ctx.confirmActions) {
+ action.run();
+ return;
+ }
+ String msg = " " + verb + " " + ctx.selectedName() + " (PID: " +
ctx.selectedPid + ")? ";
+ actions.showConfirm(title, msg, action);
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
index 5eb1cb033b47..6ca0e4ebb8c1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
@@ -199,7 +199,6 @@ class ProcessTab extends AbstractTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "scroll");
hint(spans, "w", "wrap [" + (wrap ? "on" : "off") + "]");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
index d9355a2d1976..a24dc69cef76 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
@@ -21,7 +21,6 @@ import java.util.List;
import dev.tamboui.layout.Padding;
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;
@@ -32,7 +31,6 @@ import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
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;
@@ -244,32 +242,7 @@ class RefactorPopup {
}
private void renderInput(Frame frame, Rect area) {
- int popupW = Math.max(50, Math.min(64, area.width() - 4));
- popupW = Math.min(popupW, area.width() - 2);
- int popupH = 5;
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
- Rect popup = new Rect(x, y, popupW, Math.min(popupH, area.height()));
-
- frame.renderWidget(Clear.INSTANCE, popup);
- Block block = Block.builder()
- .borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .borderStyle(Theme.borderFocused())
- .title(Title.from(Line.from(Span.styled(" " + inputTitle + "
", Theme.title().bold()))))
- .build();
- frame.renderWidget(block, popup);
- Rect inner = block.inner(popup);
-
- int pad = 2;
- int fieldW = Math.max(1, inner.width() - 2 * pad);
- int fieldY = inner.top() + Math.max(0, (inner.height() - 1) / 2);
- Rect field = new Rect(inner.left() + pad, fieldY, fieldW, 1);
-
- TextInput textInput = TextInput.builder()
- .cursorStyle(Style.EMPTY.reversed())
- .placeholder(inputPlaceholder)
- .build();
- textInput.renderWithCursor(field, frame.buffer(), inputState, frame);
+ DialogHelper.renderInputDialog(frame, area, inputTitle, inputState,
inputPlaceholder);
}
void renderFooter(List<Span> spans) {
@@ -278,7 +251,6 @@ class RefactorPopup {
}
switch (phase) {
case MENU -> {
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
TuiHelper.hint(spans, "Enter", "select");
TuiHelper.hintLast(spans, "Esc", "close");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 8ca40eaf89be..0d25a4927d81 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -890,7 +890,6 @@ class RoutesTab extends AbstractTab {
hint(spans, "n", "description" + (diagram.isShowDescription() ? "
[on]" : " [off]"));
} else {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "Enter", "diagram");
hint(spans, "s", "sort");
hint(spans, "n", "description" + (showDescription ? " [on]" : "
[off]"));
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 89c5797ebf7b..3a3b2e2629f3 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
@@ -31,7 +31,6 @@ import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.input.TextInput;
import dev.tamboui.widgets.input.TextInputState;
import dev.tamboui.widgets.paragraph.Paragraph;
@@ -42,6 +41,8 @@ class RunOptionsForm {
private static final int PAGE_OPTIONS = 0;
private static final int PAGE_PROPERTIES = 1;
+ /** Fixed height of the options page; the properties page is aligned to
it. */
+ private static final int PAGE1_HEIGHT = 18;
// Row indices for page 0
private static final int ROW_NAME = 0;
@@ -216,7 +217,6 @@ class RunOptionsForm {
hintLast(spans, "Esc", "back");
} else {
hint(spans, TuiIcons.KEY_LEFT, "options");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "+", "add");
hint(spans, "Enter", "launch");
hintLast(spans, "Esc", "back");
@@ -504,10 +504,8 @@ class RunOptionsForm {
private void renderOptionsPage(Frame frame, Rect area) {
int popupW = Math.min(68, area.width() - 4);
- int popupH = errorMessage != null ? 19 : 18;
- int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
- Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+ int popupH = errorMessage != null ? PAGE1_HEIGHT + 1 : PAGE1_HEIGHT;
+ Rect popup = DialogHelper.centered(area, popupW, popupH);
frame.renderWidget(Clear.INSTANCE, popup);
@@ -610,7 +608,7 @@ class RunOptionsForm {
rowY++;
Rect errorArea = new Rect(innerX, rowY, innerW, 1);
frame.renderWidget(Paragraph.from(Line.from(
- Span.styled(TuiIcons.HEALTH_WARN + " " + errorMessage,
Style.EMPTY.bold()))), errorArea);
+ Span.styled(TuiIcons.HEALTH_WARN + " " + errorMessage,
Theme.error().bold()))), errorArea);
}
}
@@ -618,11 +616,9 @@ class RunOptionsForm {
int popupW = Math.min(100, 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);
- // use same y-offset as page 1 (based on page 1's fixed height) so
both pages align
- int page1H = 18;
- int y = area.top() + Math.max(0, (area.height() - page1H) / 4);
- Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+ // use the same y-offset as page 1 (based on page 1's fixed height) so
both pages align
+ Rect page1 = DialogHelper.centered(area, popupW, PAGE1_HEIGHT);
+ Rect popup = new Rect(page1.x(), page1.y(), page1.width(),
Math.min(popupH, area.height()));
frame.renderWidget(Clear.INSTANCE, popup);
@@ -841,9 +837,7 @@ class RunOptionsForm {
}
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);
+ FormHelper.renderLabel(frame, x, y, w, label, selected);
}
private void renderTextInput(Frame frame, int x, int y, int w,
TextInputState state, boolean active) {
@@ -852,24 +846,9 @@ class RunOptionsForm {
private void renderTextInputWithHint(
Frame frame, int x, int y, int w, TextInputState state, boolean
active, String hint) {
- Rect inputArea = new Rect(x, y, w, 1);
- if (active) {
- TextInput textInput = TextInput.builder()
- .cursorStyle(Style.EMPTY.reversed())
- .build();
- // renderWithCursor (not renderStatefulWidget) so the caret is
painted on the active field
- textInput.renderWithCursor(inputArea, frame.buffer(), state,
frame);
- } else {
- String text = state.text();
- if (text.isEmpty() && hint != null) {
- frame.renderWidget(Paragraph.from(Line.from(
- Span.styled(hint, Style.EMPTY.dim()))), inputArea);
- } else {
- Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
- frame.renderWidget(Paragraph.from(Line.from(
- Span.styled(text.isEmpty() ? "—" : text, style))),
inputArea);
- }
- }
+ // an inactive empty field without a hint shows a dash so the row does
not look blank
+ String placeholder = hint != null ? hint : (active ? null : "—");
+ FormHelper.renderTextField(frame, new Rect(x, y, w, 1), state, active,
placeholder);
}
private void renderCycler(Frame frame, int x, int y, int w, String[]
labels, int active, boolean selected) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
index 73771658249f..af274382e9c3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
@@ -1125,7 +1125,7 @@ class SendMessagePopup {
fileBrowser.renderFooter(spans);
return;
}
- hint(spans, "Esc", "back");
+ hint(spans, "Esc", "close");
hint(spans, "Tab", "fields");
hint(spans, "F5", "send");
hint(spans, "Ctrl+F", "file");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
index 514dc89bbfdf..a4a403695dd7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -30,7 +30,6 @@ import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.input.TextInput;
import dev.tamboui.widgets.input.TextInputState;
import dev.tamboui.widgets.paragraph.Paragraph;
import org.apache.camel.dsl.jbang.core.commands.LlmClient;
@@ -466,7 +465,6 @@ class SettingsPopup {
}
void renderFooter(List<Span> spans) {
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB ||
selectedRow == ROW_SELECT_TAB
|| selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER
|| selectedRow == ROW_CONFIRM_ACTIONS || selectedRow ==
ROW_VALIDATE_ON_SAVE
@@ -546,8 +544,7 @@ class SettingsPopup {
}
private void renderLabel(Frame frame, int x, int y, int w, String label,
boolean selected) {
- Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim();
- frame.renderWidget(Paragraph.from(Line.from(Span.styled(label,
style))), new Rect(x, y, w, 1));
+ FormHelper.renderLabel(frame, x, y, w, label, selected);
}
private void renderValue(Frame frame, int x, int y, int w, String text,
boolean selected) {
@@ -562,20 +559,7 @@ class SettingsPopup {
private void renderTextInput(
Frame frame, int x, int y, int w, TextInputState input, boolean
active,
String placeholder) {
- Rect area = new Rect(x, y, w, 1);
- if (active) {
- TextInput textInput = TextInput.builder()
- .cursorStyle(Style.EMPTY.reversed())
- .placeholder(placeholder)
- .build();
- // renderWithCursor (not renderStatefulWidget) so the caret is
painted on the active field
- textInput.renderWithCursor(area, frame.buffer(), input, frame);
- } else {
- String text = input.text();
- Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
- frame.renderWidget(Paragraph.from(Line.from(
- Span.styled(text.isEmpty() ? placeholder : text, style))),
area);
- }
+ FormHelper.renderTextField(frame, new Rect(x, y, w, 1), input, active,
placeholder);
}
// ---- Test accessors ----
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index dfecbff2fb6a..696aed167974 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -428,7 +428,6 @@ class SourceTab extends AbstractTab {
TuiHelper.hint(spans, "Tab", "files");
}
} else {
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
TuiHelper.hint(spans, "Enter", "open");
if (currentDir != null && rootDir != null &&
!currentDir.equals(rootDir)) {
TuiHelper.hint(spans, "Bksp", "parent");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 87fdee6d8b01..2aa7c2876f55 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -3002,8 +3002,7 @@ class SourceViewer {
wrapText(msg, innerW, allLines);
}
allLines.add(Line.empty());
- allLines.add(Line.from(Span.raw(" "),
- Span.styled("Esc", Style.EMPTY.bold()), Span.raw(" close")));
+ allLines.add(TuiHelper.hintLine("Esc", "close"));
int contentH = allLines.size();
int popupH = Math.min(contentH + 2, area.height() - 4);
@@ -3017,6 +3016,7 @@ class SourceViewer {
+ (validationErrors.size() > 1 ? "s" : "") + " ";
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(Theme.error())
.title(Title.from(Line.from(Span.styled(titleText,
Theme.error().bold()))))
.build();
frame.renderWidget(block, popup);
@@ -3036,33 +3036,7 @@ class SourceViewer {
}
private void renderDiscardPopup(Frame frame, Rect area) {
- int popupW = Math.max(40, Math.min(44, area.width() - 4));
- popupW = Math.min(popupW, area.width() - 2);
- int popupH = 6;
- 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, popupW, popupH);
-
- frame.renderWidget(Clear.INSTANCE, popup);
-
- Block block = Block.builder()
- .borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .borderStyle(Theme.warning())
- .title(Title.from(Line.from(Span.styled(" Discard Changes? ",
Theme.warning().bold()))))
- .build();
- frame.renderWidget(block, popup);
- Rect inner = block.inner(popup);
-
- frame.renderWidget(
- Paragraph.builder().centered().text(Text.from(
- Line.empty(),
- Line.from(Span.raw("Unsaved changes will be lost.")),
- Line.empty(),
- Line.from(
- Span.styled("Enter", Style.EMPTY.bold()),
Span.raw(" confirm "),
- Span.styled("Esc", Style.EMPTY.bold()),
Span.raw(" cancel"))))
- .build(),
- inner);
+ DialogHelper.renderConfirm(frame, area, "Discard Changes?", "Unsaved
changes will be lost.", false);
}
private static void wrapText(String text, int width, List<Line> out) {
@@ -3081,14 +3055,17 @@ class SourceViewer {
}
void renderFooter(List<Span> spans) {
+ if (pendingDiscard) {
+ TuiHelper.hint(spans, "Enter", "confirm");
+ TuiHelper.hintLast(spans, "Esc", "cancel");
+ return;
+ }
if (editMode && validationErrors != null) {
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "scroll");
TuiHelper.hintLast(spans, "Esc", "close");
return;
}
if (editMode && diffOverlay) {
TuiHelper.hint(spans, "Esc/F7", "close diff");
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "scroll");
return;
}
if (editMode) {
@@ -3121,7 +3098,6 @@ class SourceViewer {
}
if (markdownMode) {
TuiHelper.hint(spans, "Esc/c", "close");
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "scroll");
TuiHelper.hint(spans, "Space", "format");
TuiHelper.hint(spans, "PgUp/PgDn", "page");
if (isEditable()) {
@@ -3141,7 +3117,6 @@ class SourceViewer {
if (isEditable()) {
TuiHelper.hint(spans, "F4", "edit");
}
- TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (isMarkdownFile || currentRouteId != null) {
TuiHelper.hint(spans, "Space", "format");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
index d3c293fa5b47..d1c53ba727ab 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
@@ -646,7 +646,6 @@ class SpansTab extends AbstractTab {
hint(spans, "F5", "refresh");
hint(spans, "c", camelOnly ? "camel-only [on]" : "camel-only
[off]");
hint(spans, "p", showProcessors ? "processors [on]" : "processors
[off]");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "page");
} else if (filterInputActive) {
spans.add(Span.styled(" /", Theme.label().bold()));
@@ -663,7 +662,6 @@ class SpansTab extends AbstractTab {
} else {
hint(spans, "/", "filter");
}
- hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
index 1556e821c752..3405ab1bc418 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
@@ -807,13 +807,8 @@ class SqlQueryTab extends AbstractTab {
int footerY = inner.top() + visibleRows + 1;
if (footerY < popup.bottom() - 1) {
Rect footerArea = new Rect(inner.left(), footerY, inner.width(),
1);
- frame.renderWidget(Paragraph.from(Line.from(
- Span.styled(" F5", Theme.label().bold()),
- Span.styled("=Save ", Theme.muted()),
- Span.styled("Esc", Theme.label().bold()),
- Span.styled("=Cancel ", Theme.muted()),
- Span.styled("*", Theme.muted()),
- Span.styled("=Primary Key", Theme.muted()))), footerArea);
+ frame.renderWidget(Paragraph.from(
+ TuiHelper.hintLine("F5", "save", "Esc", "cancel", "*",
"primary key")), footerArea);
}
}
@@ -837,7 +832,6 @@ class SqlQueryTab extends AbstractTab {
} else {
hint(spans, "Esc", "back");
hint(spans, "Tab", "input");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (isEditable()) {
hint(spans, "F4", "edit");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
index 738a1c501fe5..15fad69d4692 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
@@ -387,7 +387,6 @@ class SqlTraceTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hint(spans, "Home/End", "top/end");
hint(spans, "PgUp/Dn", "detail");
hint(spans, "F4", "edit SQL");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
index 60d04399d968..25a6b2428528 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -255,7 +255,6 @@ class StartupTab extends AbstractTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "scroll");
hintLast(spans, "PgUp/Dn", "page");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemePopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemePopup.java
index 88ac5babaf0e..b6ad559aae01 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemePopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemePopup.java
@@ -180,7 +180,7 @@ class ThemePopup {
void renderFooter(List<Span> spans) {
hint(spans, TuiIcons.HINT_SCROLL, "preview");
hint(spans, "Enter", "apply");
- hintLast(spans, "Esc", "back");
+ hintLast(spans, "Esc", "cancel");
}
List<String> getLabels() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
index ba959cc04d49..ba1e5e66b687 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
@@ -297,7 +297,6 @@ class ThreadsTab extends AbstractTableTab {
hint(spans, "s", "sort");
hint(spans, "f", "filter [" + FILTER_LABELS[filter] + "]");
hint(spans, "Tab", detailFocused ? "table" : "trace");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "scroll");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
index 4ab4369e7e89..c7487bee19b4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
@@ -129,7 +129,6 @@ class TransformersTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "s", "sort");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index fb6a4d1ff941..6f2b6305e353 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -452,6 +452,27 @@ final class TuiHelper {
spans.add(Span.raw(" " + label));
}
+ /**
+ * Builds one line of key hints in the footer style, e.g. {@code
hintLine("Enter", "confirm", "Esc", "cancel")}. Use
+ * this for hints drawn inside a dialog so they match the footer bar.
+ *
+ * @param keyLabelPairs alternating key and label, must have an even length
+ */
+ static Line hintLine(String... keyLabelPairs) {
+ if (keyLabelPairs.length % 2 != 0) {
+ throw new IllegalArgumentException("keyLabelPairs must be
key/label pairs");
+ }
+ List<Span> spans = new ArrayList<>();
+ for (int i = 0; i < keyLabelPairs.length; i += 2) {
+ if (i + 2 < keyLabelPairs.length) {
+ hint(spans, keyLabelPairs[i], keyLabelPairs[i + 1]);
+ } else {
+ hintLast(spans, keyLabelPairs[i], keyLabelPairs[i + 1]);
+ }
+ }
+ return Line.from(spans);
+ }
+
static boolean contains(Rect rect, int x, int y) {
return rect != null
&& x >= rect.x() && x < rect.x() + rect.width()
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
index 2d6045d2329a..0a067fb8d910 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
@@ -207,7 +207,6 @@ class TypeConvertersTab extends AbstractTableTab {
} else {
hint(spans, "/", "filter");
}
- hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "s", "sort");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
index 428a4ca38c5c..422feb74d9d4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
@@ -87,8 +87,7 @@ class BeansTabRenderTest {
assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
assertTrue(footer.contains("scope"), "Footer should contain scope
hint");
assertTrue(footer.contains("filter"), "Footer should contain filter
hint");
- assertTrue(footer.contains("navigate"), "Footer should contain
navigate hint");
- assertTrue(footer.contains("scroll"), "Footer should contain scroll
hint");
+ assertTrue(footer.contains("scroll"), "Footer should contain PgUp/Dn
scroll hint");
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
new file mode 100644
index 000000000000..c97c809c8899
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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 dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.widgets.input.TextInputState;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for the shared dialog building blocks in {@link DialogHelper} and the
hint helpers in {@link TuiHelper}.
+ */
+class DialogHelperTest {
+
+ private static final Rect AREA = new Rect(0, 0, 120, 40);
+
+ @Test
+ void centeredPlacesDialogInUpperThird() {
+ Rect r = DialogHelper.centered(AREA, 40, 10);
+ assertThat(r.x()).isEqualTo(40);
+ assertThat(r.y()).isEqualTo(10);
+ assertThat(r.width()).isEqualTo(40);
+ assertThat(r.height()).isEqualTo(10);
+ }
+
+ @Test
+ void centeredClampsToArea() {
+ Rect r = DialogHelper.centered(new Rect(5, 5, 20, 8), 40, 10);
+ assertThat(r.x()).isEqualTo(5);
+ assertThat(r.y()).isEqualTo(5);
+ assertThat(r.width()).isEqualTo(20);
+ assertThat(r.height()).isEqualTo(8);
+ }
+
+ @Test
+ void clampWidthKeepsMarginAndBounds() {
+ assertThat(DialogHelper.clampWidth(AREA, 34, 20)).isEqualTo(34);
+ assertThat(DialogHelper.clampWidth(AREA, 34, 60)).isEqualTo(60);
+ assertThat(DialogHelper.clampWidth(AREA, 34, 500)).isEqualTo(116);
+ // narrower than the minimum: shrink to fit rather than overflow
+ assertThat(DialogHelper.clampWidth(new Rect(0, 0, 30, 10), 34,
60)).isEqualTo(28);
+ }
+
+ @Test
+ void confirmDialogShowsTitleMessageAndFooterStyleHints() {
+ Buffer buffer = Buffer.empty(AREA);
+ Frame frame = Frame.forTesting(buffer);
+
+ Rect popup = DialogHelper.renderConfirm(frame, AREA, "Confirm Quit",
"Quit the TUI?", false);
+
+ String rendered = HealthTabRenderTest.bufferToString(buffer);
+ assertThat(rendered).contains(" Confirm Quit ");
+ assertThat(rendered).contains("Quit the TUI?");
+ assertThat(rendered).contains(" Enter confirm");
+ assertThat(rendered).contains(" Esc cancel");
+ assertThat(popup.height()).isEqualTo(DialogHelper.CONFIRM_HEIGHT);
+ assertThat(popup.width()).isGreaterThanOrEqualTo(34);
+ }
+
+ @Test
+ void confirmDialogCanUseCustomAcceptKey() {
+ Buffer buffer = Buffer.empty(AREA);
+ Frame frame = Frame.forTesting(buffer);
+
+ DialogHelper.renderConfirm(frame, AREA, "Delete file?", "Delete
foo.yaml?", true, "y", "delete");
+
+ String rendered = HealthTabRenderTest.bufferToString(buffer);
+ assertThat(rendered).contains(" y delete");
+ assertThat(rendered).contains(" Esc cancel");
+ assertThat(rendered).doesNotContain("Enter");
+ }
+
+ @Test
+ void inputDialogShowsTitleAndPlaceholder() {
+ Buffer buffer = Buffer.empty(AREA);
+ Frame frame = Frame.forTesting(buffer);
+
+ Rect popup = DialogHelper.renderInputDialog(frame, AREA, "New File",
new TextInputState(), "name");
+
+ String rendered = HealthTabRenderTest.bufferToString(buffer);
+ assertThat(rendered).contains(" New File ");
+ assertThat(rendered).contains("name");
+ assertThat(popup.height()).isEqualTo(DialogHelper.INPUT_HEIGHT);
+ }
+
+ @Test
+ void hintLineMatchesFooterHints() {
+ Line line = TuiHelper.hintLine("Enter", "confirm", "Esc", "cancel");
+ assertThat(line.spans()).extracting(Span::content)
+ .containsExactly(" Enter ", " confirm ", " Esc ", " cancel");
+ }
+
+ @Test
+ void hintLineRejectsOddArguments() {
+ assertThatThrownBy(() -> TuiHelper.hintLine("Enter"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupWrapTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupWrapTest.java
new file mode 100644
index 000000000000..3477ec42db75
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupWrapTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the word wrapping used for the detail rows of the Doctor popup.
+ */
+class DoctorPopupWrapTest {
+
+ @Test
+ void wrapsOnWordBoundaries() {
+ List<String> lines = DoctorPopup.wrapWords(
+ "Set ANTHROPIC_API_KEY, AZURE_OPENAI_*, GEMINI_API_KEY,
OPENAI_API_KEY, WATSONX_APIKEY, or start Ollama",
+ 41);
+ assertThat(lines).containsExactly(
+ "Set ANTHROPIC_API_KEY, AZURE_OPENAI_*,",
+ "GEMINI_API_KEY, OPENAI_API_KEY,",
+ "WATSONX_APIKEY, or start Ollama");
+ assertThat(lines).allSatisfy(l ->
assertThat(l.length()).isLessThanOrEqualTo(41));
+ }
+
+ @Test
+ void shortTextStaysOnOneLine() {
+ assertThat(DoctorPopup.wrapWords("No AI client connected",
41)).containsExactly("No AI client connected");
+ }
+
+ @Test
+ void longWordIsKeptWhole() {
+ assertThat(DoctorPopup.wrapWords("a bbbbbbbbbbbbbbbbbbbb c",
10)).containsExactly("a", "bbbbbbbbbbbbbbbbbbbb", "c");
+ }
+
+ @Test
+ void blankTextProducesNoRows() {
+ assertThat(DoctorPopup.wrapWords(" ", 41)).isEmpty();
+ assertThat(DoctorPopup.wrapWords(null, 41)).isEmpty();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
index c16b38d1395b..2d83070df68b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
@@ -209,7 +209,6 @@ class OverviewTabRenderTest {
.reduce("", String::concat);
assertTrue(footer.contains("sort"), "Footer should contain sort hint");
- assertTrue(footer.contains("navigate"), "Footer should contain
navigate hint");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
index c95c96e6008f..b648b287ea3a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
+import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.KeyModifiers;
import org.junit.jupiter.api.BeforeEach;
@@ -143,4 +144,48 @@ class PopupManagerTest {
// 'z' matches nothing
assertArrayEquals(new int[] { -1, 0 },
popupManager.morePopupShortcut(KeyEvent.ofChar('z', KeyModifiers.NONE)));
}
+
+ // ---- Confirm dialog key contract: Enter accepts, Esc cancels, anything
else is swallowed ----
+
+ @Test
+ void confirmRunsCallbackOnEnter() {
+ boolean[] ran = { false };
+ popupManager.showConfirm("Confirm Quit", " Quit? ", () -> ran[0] =
true);
+ assertTrue(popupManager.isConfirmVisible());
+
+ popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER,
KeyModifiers.NONE), 0, 2);
+ assertTrue(ran[0], "Enter must run the confirm callback");
+ assertFalse(popupManager.isConfirmVisible(), "Confirm should close
after Enter");
+ }
+
+ @Test
+ void confirmCancelsOnEscapeWithoutRunningCallback() {
+ boolean[] ran = { false };
+ popupManager.showConfirm("Confirm Quit", " Quit? ", () -> ran[0] =
true);
+
+ popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE,
KeyModifiers.NONE), 0, 2);
+ assertFalse(ran[0], "Esc must not run the confirm callback");
+ assertFalse(popupManager.isConfirmVisible(), "Confirm should close
after Esc");
+ }
+
+ @Test
+ void confirmIgnoresOtherKeys() {
+ boolean[] ran = { false };
+ popupManager.showConfirm("Confirm Quit", " Quit? ", () -> ran[0] =
true);
+
+ assertTrue(popupManager.handleKeyEvent(KeyEvent.ofChar('x'), 0, 2),
"key is swallowed by the modal");
+ assertFalse(ran[0], "a stray key must not confirm");
+ assertTrue(popupManager.isConfirmVisible(), "a stray key must not
dismiss the confirm");
+ }
+
+ @Test
+ void killConfirmIgnoresOtherKeysAndCancelsOnEscape() {
+ popupManager.showKillConfirm();
+
+ popupManager.handleKeyEvent(KeyEvent.ofChar('x'), 0, 2);
+ assertTrue(popupManager.isKillConfirmVisible(), "a stray key must not
dismiss the kill confirm");
+
+ popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE,
KeyModifiers.NONE), 0, 2);
+ assertFalse(popupManager.isKillConfirmVisible(), "Esc cancels the kill
confirm");
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopupTest.java
new file mode 100644
index 000000000000..6c75b2dc2517
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopupTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that the F10 process-control popup gates Stop, Restart and Kill
behind a confirm dialog when confirmations are
+ * enabled, and runs them directly when they are disabled.
+ */
+class ProcessControlPopupTest {
+
+ private MonitorContext ctx;
+ private ProcessControlPopup popup;
+ private final List<String> calls = new ArrayList<>();
+ private final List<Runnable> pendingConfirms = new ArrayList<>();
+
+ @BeforeEach
+ void setUp() {
+ IntegrationInfo info = new IntegrationInfo();
+ info.pid = "4242";
+ info.name = "my-app";
+
+ ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new
AtomicReference<>(List.of()));
+ ctx.selectedPid = "4242";
+
+ popup = new ProcessControlPopup(ctx);
+ popup.setActions(new ProcessControlPopup.ControlActions() {
+ @Override
+ public void sendRouteCommand(String pid, String routeId, String
command) {
+ calls.add("route:" + command);
+ }
+
+ @Override
+ public void stopSelectedProcess(boolean forceKill) {
+ calls.add(forceKill ? "kill" : "stop");
+ }
+
+ @Override
+ public void restartSelectedProcess() {
+ calls.add("restart");
+ }
+
+ @Override
+ public void showKillConfirm() {
+ calls.add("confirm:kill");
+ }
+
+ @Override
+ public void showConfirm(String title, String message, Runnable
onConfirm) {
+ calls.add("confirm:" + title + ":" + message.trim());
+ pendingConfirms.add(onConfirm);
+ }
+
+ @Override
+ public void onRunPhantom(IntegrationInfo phantom) {
+ calls.add("run-phantom");
+ }
+
+ @Override
+ public void onStopAll() {
+ calls.add("stop-all");
+ }
+
+ @Override
+ public boolean hasRunningProcesses() {
+ return false;
+ }
+ });
+ }
+
+ // A non-phantom integration without routes lists: Restart (0), Stop (1),
Kill (2)
+
+ private void select(int index) {
+ popup.open();
+ assertThat(popup.isVisible()).isTrue();
+ for (int i = 0; i < index; i++) {
+ popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN,
KeyModifiers.NONE));
+ }
+ popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+ assertThat(popup.isVisible()).isFalse();
+ }
+
+ @Test
+ void killAsksForConfirmationWhenEnabled() {
+ ctx.confirmActions = true;
+ select(2);
+ assertThat(calls).containsExactly("confirm:kill");
+ }
+
+ @Test
+ void killRunsDirectlyWhenConfirmationsDisabled() {
+ ctx.confirmActions = false;
+ select(2);
+ assertThat(calls).containsExactly("kill");
+ }
+
+ @Test
+ void stopAsksForConfirmationAndRunsOnAccept() {
+ ctx.confirmActions = true;
+ select(1);
+ assertThat(calls).containsExactly("confirm:Confirm Stop:Stop my-app
(PID: 4242)?");
+
+ pendingConfirms.get(0).run();
+ assertThat(calls).containsExactly("confirm:Confirm Stop:Stop my-app
(PID: 4242)?", "stop");
+ }
+
+ @Test
+ void restartAsksForConfirmationAndRunsOnAccept() {
+ ctx.confirmActions = true;
+ select(0);
+ assertThat(calls).containsExactly("confirm:Confirm Restart:Restart
my-app (PID: 4242)?");
+
+ pendingConfirms.get(0).run();
+ assertThat(calls).last().isEqualTo("restart");
+ }
+
+ @Test
+ void stopAndRestartRunDirectlyWhenConfirmationsDisabled() {
+ ctx.confirmActions = false;
+ select(1);
+ select(0);
+ assertThat(calls).containsExactly("stop", "restart");
+ }
+
+ @Test
+ void escapeClosesWithoutRunningAnything() {
+ ctx.confirmActions = false;
+ popup.open();
+ popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE,
KeyModifiers.NONE));
+ assertThat(popup.isVisible()).isFalse();
+ assertThat(calls).isEmpty();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java
index c068635f6354..d521d7402208 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java
@@ -98,7 +98,6 @@ class ProcessTabRenderTest {
.map(Span::content)
.reduce("", String::concat);
- assertTrue(footer.contains("scroll"), "Footer should contain scroll
hint");
assertTrue(footer.contains("wrap"), "Footer should contain wrap hint");
assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
}