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 0659ea200596 chore: camel-jbang - camel tui --web serves browsers only
without a terminal, --defaults, utility routes shown out of the box (#28459)
0659ea200596 is described below
commit 0659ea20059653f19c34998bb08a4ab3a22ff062
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 20:23:51 2026 +0100
chore: camel-jbang - camel tui --web serves browsers only without a
terminal, --defaults, utility routes shown out of the box (#28459)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-diagram.adoc | 2 +-
.../ROOT/pages/camel-jbang-tui-settings.adoc | 8 +-
.../modules/ROOT/pages/camel-jbang-tui.adoc | 6 +-
.../jbang-commands/camel-jbang-tui-monitor.adoc | 3 +-
.../ROOT/pages/jbang-commands/camel-jbang-tui.adoc | 3 +-
.../camel-jbang-plugin-commands-metadata.json | 2 +-
.../core/commands/tui/AbstractDiagramTab.java | 16 +++
.../jbang/core/commands/tui/ArchitectureView.java | 3 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 41 +++----
.../dsl/jbang/core/commands/tui/DiagramTab.java | 3 +
.../jbang/core/commands/tui/MonitorStartup.java | 67 +++++++++++
.../dsl/jbang/core/commands/tui/RoutesTab.java | 3 +
.../dsl/jbang/core/commands/tui/TuiCommand.java | 15 ++-
.../dsl/jbang/core/commands/tui/TuiUserConfig.java | 34 ++++++
.../dsl/jbang/core/commands/tui/TuiWebServer.java | 7 ++
.../src/main/resources/tui/help/diagram.md | 2 +-
.../core/commands/tui/ArchitectureViewTest.java | 8 +-
.../core/commands/tui/DiagramTabRenderTest.java | 13 ++
.../core/commands/tui/MonitorStartupTest.java | 133 +++++++++++++++++++++
.../jbang/core/commands/tui/SourceSizeTest.java | 2 +-
.../jbang/core/commands/tui/TuiUserConfigTest.java | 20 ++++
21 files changed, 352 insertions(+), 39 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index 4391e0c2a9f8..0a08066a53bb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -184,7 +184,7 @@ Each route has one role:
every group that uses them.
* *Utility* -- plumbing with little business meaning: a route reached only
while handling a failure
(from a `doCatch`, an `onException` or a dead letter channel), a route that
only logs, or one the
- AI marked as utility. Hidden until you press *u*; with *x* on, the routes
reached only on error are shown
+ AI marked as utility. Shown out of the box, so a project shows all its
routes; *u* hides them; with *x* on, the routes reached only on error are shown
in the error handling frame of the topology instead, and in the *Error
handling* box of the architecture.
* *Error handling* -- with *x* on, the routes reached only on error, out of
Utility into a box of their
own, drawn dashed, with a dashed arrow from each group whose failures go
there: a route of the group
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
index 372a7431c1be..835f43a16fd4 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
@@ -155,7 +155,9 @@ Use *↑*/*↓* to move between rows, *Space* (or *←*/*→*) to
cycle a choice
field, *Enter* to save, and *Esc* to cancel.
Each row is stored under a `camel.tui.*` key in the Camel CLI configuration
file, so the same
-setting can also be written there by hand:
+setting can also be written there by hand. To see the TUI as a new user does,
for a demo or to
+rule out a setting, start it with `camel tui --defaults`: the saved settings
are not read, and
+what you change lasts for that session only, so your own settings are left as
they are.
[cols="2,3,6,2",options="header"]
|===
@@ -300,6 +302,10 @@ This starts a web terminal server on `localhost:8090`
(configurable with `--web-
Open `http://localhost:8090` in a browser to get a full xterm.js terminal
driving the same
TUI dashboard, with the same tabs, keyboard shortcuts, and F2 actions menu as
a local session.
+Without an interactive terminal -- started from a script, a CI job or a
background shell -- `camel tui --web`
+serves browsers only: it prints the address and keeps serving until you press
*Ctrl+C* or stop the
+process. The MCP server drives the screen of the terminal, so `--mcp` needs
one and is refused in this mode.
+
Like the MCP server, the web server is bound to `127.0.0.1` only -- it never
listens on
external interfaces -- and there is no authentication beyond that. Each
browser connection
gets its own independent TUI session (its own process discovery and navigation
state), the
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 75e0a8f269d5..e3fb93986b63 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -169,7 +169,7 @@ See the
xref:camel-jbang-tui-source-editor.adoc#_keyboard_shortcuts[keyboard sho
| `8123`
| `--web`
-| Enable the browser-accessible terminal (WebSocket) server.
+| Enable the browser-accessible terminal (WebSocket) server. Without an
interactive terminal (a script, a CI job, a background shell) it serves
browsers only. See xref:camel-jbang-tui-settings.adoc#_web_browser_access[Web
Browser Access].
| `false`
| `--web-port`
@@ -188,6 +188,10 @@ See the
xref:camel-jbang-tui-source-editor.adoc#_keyboard_shortcuts[keyboard sho
| Color theme of the shell (*F6*) and AI (*F8*) panels for this session, or
`same` for the main theme. See
xref:camel-jbang-tui-settings.adoc#_panel_theme[Panel Theme]. Overrides the
persisted `camel.tui.panelTheme` preference when set.
|
+| `--defaults`
+| Start with the default settings, as a new user sees the TUI: the saved
`camel.tui.*` settings are not read, and what you change lasts for the session
only. See xref:camel-jbang-tui-settings.adoc#_settings[Settings].
+| `false`
+
| `--record`
| Play a `.tape` file in the TUI and record it to an Asciinema `.cast` file.
See xref:camel-jbang-tui-recording.adoc#_scripted_recording[Scripted Recording]
for the tape commands.
|
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
index 23d4719d6a96..a2e44edb510c 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
@@ -21,6 +21,7 @@ camel tui monitor [options]
[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
+| `--defaults` | Start with the default settings, as a new user sees the TUI;
changes last for the session and are not saved | | boolean
| `--mcp` | Enable embedded MCP server for AI agent access to the TUI | |
boolean
| `--mcp-port` | MCP server port | 8123 | int
| `--panel-theme` | Color theme of the shell (F6) and AI (F8) panels, or
'same' for the main theme (overrides the Panel Theme setting for this session)
| | String
@@ -30,7 +31,7 @@ camel tui monitor [options]
| `--record-size` | Size of the recorded terminal for --record, as
<cols>x<rows> | 200x50 | String
| `--refresh` | Refresh interval in milliseconds | 100 | long
| `--theme` | Color theme (overrides persisted preference for this session) |
| String
-| `--web` | Enable browser-accessible terminal (WebSocket) server | | boolean
+| `--web` | Enable browser-accessible terminal (WebSocket) server; without an
interactive terminal it serves browsers only | | boolean
| `--web-port` | Web terminal server port | 8090 | int
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
index 436fde02a480..3d074bb98469 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
@@ -30,6 +30,7 @@ camel tui [options]
[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
+| `--defaults` | Start with the default settings, as a new user sees the TUI;
changes last for the session and are not saved | | boolean
| `--mcp` | Enable embedded MCP server for AI agent access to the TUI | |
boolean
| `--mcp-port` | MCP server port | 8123 | int
| `--panel-theme` | Color theme of the shell (F6) and AI (F8) panels, or
'same' for the main theme (overrides the Panel Theme setting for this session)
| | String
@@ -39,7 +40,7 @@ camel tui [options]
| `--record-size` | Size of the recorded terminal for --record, as
<cols>x<rows> | 200x50 | String
| `--refresh` | Refresh interval in milliseconds | 100 | long
| `--theme` | Color theme: dark or light (overrides persisted preference for
this session) | | String
-| `--web` | Enable browser-accessible terminal (WebSocket) server | | boolean
+| `--web` | Enable browser-accessible terminal (WebSocket) server; without an
interactive terminal it serves browsers only | | boolean
| `--web-port` | Web terminal server port | 8090 | int
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
index ccaaca3c9c10..ba0c61b7b15a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
@@ -1,5 +1,5 @@
{
"commands": [
- { "name": "tui", "fullName": "tui", "description": "Camel TUI",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.tui.TuiCommand",
"plugin": "tui", "options": [ { "names": "--mcp", "description": "Enable
embedded MCP server for AI agent access to the TUI", "javaType": "boolean",
"type": "boolean" }, { "names": "--mcp-port", "description": "MCP server port",
"defaultValue": "8123", "javaType": "int", "type": "integer" }, { "names":
"--panel-theme", "description": "Color theme o [...]
+ { "name": "tui", "fullName": "tui", "description": "Camel TUI",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.tui.TuiCommand",
"plugin": "tui", "options": [ { "names": "--defaults", "description": "Start
with the default settings, as a new user sees the TUI; changes last for the
session and are not saved", "javaType": "boolean", "type": "boolean" }, {
"names": "--mcp", "description": "Enable embedded MCP server for AI agent
access to the TUI", "javaType": "boolean", "type" [...]
]
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
index 26dad2c67be3..668d0eb88c54 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
@@ -19,8 +19,10 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import dev.tamboui.text.Span;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.MouseEvent;
@@ -114,6 +116,20 @@ abstract class AbstractDiagramTab extends AbstractTab {
return true;
}
+ /**
+ * The footer of the go to node popup while it is open, in place of the
diagram's: what is typed goes into the
+ * popup, so the keys of the diagram do not apply.
+ */
+ protected final boolean renderGotoNodeFooter(List<Span> spans) {
+ if (!gotoNodePopup.isVisible()) {
+ return false;
+ }
+ TuiHelper.hint(spans, "type", "filter");
+ TuiHelper.hint(spans, "Enter", "go to");
+ TuiHelper.hintLast(spans, "Esc", "back");
+ return true;
+ }
+
/** c: the source of the selected node of a route, or of the route
selected in the topology. */
protected final boolean handleSourceKey(KeyEvent ke) {
if (!diagram.isShowDiagram() || !ke.isChar('c')) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index d29e209ea8e3..1cddd092eec4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -102,7 +102,8 @@ final class ArchitectureView {
private Capabilities picked;
private Capabilities aiCapabilities;
private Capabilities factCapabilities;
- private boolean showUtility;
+ /** Whether the utility routes are shown: on out of the box, so a new
project shows all its routes (u). */
+ private boolean showUtility = true;
/** Whether the external systems are drawn as boxes linked to the groups
that use them (e). */
private boolean showExternal;
/** The kinds of an external system box when it is selected: messages come
in from it, or go out to it. */
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 f498f5d935fb..d81249b92131 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
@@ -66,6 +66,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.ai.ToolConfirms;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.util.json.JsonObject;
import picocli.CommandLine;
@@ -141,7 +142,8 @@ public class CamelMonitor extends CamelCommand {
volatile int mcpPort = 8123;
@CommandLine.Option(names = { "--web" },
- description = "Enable browser-accessible terminal
(WebSocket) server")
+ description = "Enable browser-accessible terminal
(WebSocket) server; without an interactive"
+ + " terminal it serves browsers only")
boolean web;
@CommandLine.Option(names = { "--web-port" },
@@ -160,6 +162,11 @@ public class CamelMonitor extends CamelCommand {
completionCandidates =
ThemeModeCompletionCandidates.class)
String panelTheme;
+ @CommandLine.Option(names = { "--defaults" },
+ description = "Start with the default settings, as a
new user sees the TUI; changes last for the"
+ + " session and are not saved")
+ boolean defaults;
+
// State
private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
private TabRegistry tabRegistry;
@@ -390,8 +397,16 @@ public class CamelMonitor extends CamelCommand {
// the directory is a project to open, not a name filter
name = "*";
}
+ if (defaults) {
+ TuiUserConfig.useDefaults();
+ }
applyThemeOption();
configureRecording();
+ if (MonitorStartup.browserOnly(web, webBackend != null, record !=
null, EnvironmentHelper.isInteractiveTerminal())) {
+ MonitorStartup.serveBrowserOnly(
+ new TuiWebServer(webPort, getMain(), classLoader, name,
refreshInterval, theme), webPort, mcp, printer());
+ return 0;
+ }
MonitorStartup.requireTerminal(webBackend != null, record != null);
recordingManager.init(record != null);
@@ -426,29 +441,9 @@ public class CamelMonitor extends CamelCommand {
return 0;
}
- /**
- * Validates and applies the --theme and --panel-theme options.
- */
+ /** Validates and applies the --theme and --panel-theme options. */
void applyThemeOption() {
- if (theme != null) {
- if (!Theme.isValidMode(theme)) {
- String expected = String.join("' or '", ThemeMode.ids());
- throw new CommandLine.ParameterException(
- new CommandLine(this),
- "Invalid value for option '--theme': expected '" +
expected + "', was '" + theme + "'");
- }
- Theme.applyStartupMode(theme);
- }
- if (panelTheme != null) {
- if (!Theme.isValidMode(panelTheme) &&
!Theme.PANEL_THEME_SAME.equalsIgnoreCase(panelTheme.strip())) {
- String expected = String.join("' or '", ThemeMode.ids());
- throw new CommandLine.ParameterException(
- new CommandLine(this),
- "Invalid value for option '--panel-theme': expected '"
+ Theme.PANEL_THEME_SAME + "' or '" + expected
- + "', was '" + panelTheme +
"'");
- }
- Theme.setPanelTheme(panelTheme);
- }
+ MonitorStartup.applyThemeOptions(new CommandLine(this), theme,
panelTheme);
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index d4caf3728253..b643834187ca 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -683,6 +683,9 @@ class DiagramTab extends AbstractDiagramTab {
@Override
public void renderFooter(List<Span> spans) {
+ if (renderGotoNodeFooter(spans)) {
+ return;
+ }
if (sourceViewer.isVisible()) {
sourceViewer.renderFooter(spans);
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
index 965b236f302b..89da80131784 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
@@ -16,11 +16,14 @@
*/
package org.apache.camel.dsl.jbang.core.commands.tui;
+import java.net.BindException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import picocli.CommandLine;
/**
* What the monitor works out from its options before it starts.
@@ -45,6 +48,31 @@ final class MonitorStartup {
return null;
}
+ /**
+ * Validates and applies the --theme and --panel-theme options.
+ *
+ * @throws CommandLine.ParameterException for a theme that does not exist
+ */
+ static void applyThemeOptions(CommandLine command, String theme, String
panelTheme) {
+ if (theme != null) {
+ if (!Theme.isValidMode(theme)) {
+ String expected = String.join("' or '", ThemeMode.ids());
+ throw new CommandLine.ParameterException(
+ command, "Invalid value for option '--theme': expected
'" + expected + "', was '" + theme + "'");
+ }
+ Theme.applyStartupMode(theme);
+ }
+ if (panelTheme != null) {
+ if (!Theme.isValidMode(panelTheme) &&
!Theme.PANEL_THEME_SAME.equalsIgnoreCase(panelTheme.strip())) {
+ String expected = String.join("' or '", ThemeMode.ids());
+ throw new CommandLine.ParameterException(
+ command, "Invalid value for option '--panel-theme':
expected '" + Theme.PANEL_THEME_SAME + "' or '"
+ + expected + "', was '" + panelTheme + "'");
+ }
+ Theme.setPanelTheme(panelTheme);
+ }
+ }
+
/**
* Fails the monitor without an interactive terminal (a script, a CI job,
an AI agent), instead of drawing in none:
* a browser session (--web) has a terminal of its own, and a recording
(--record) plays a tape headless.
@@ -56,4 +84,43 @@ final class MonitorStartup {
throw CliException.usage("camel tui requires an interactive
terminal");
}
}
+
+ /**
+ * Whether the monitor serves browsers only: --web without a terminal to
draw in (a script, a CI job, a background
+ * shell). Each browser session is a monitor of its own, so the one
started here needs none.
+ */
+ static boolean browserOnly(boolean web, boolean browserSession, boolean
recording, boolean interactiveTerminal) {
+ return web && !browserSession && !recording && !interactiveTerminal;
+ }
+
+ /**
+ * Serves the TUI to browsers until the JVM stops (Ctrl+C) or the server
is stopped. The MCP server drives the
+ * screen of the terminal, which a browser-only monitor does not have, so
--mcp fails here.
+ *
+ * @throws CliException with exit code 2 for --mcp, or when the port is
taken
+ */
+ static void serveBrowserOnly(TuiWebServer server, int port, boolean mcp,
Printer printer) throws Exception {
+ if (mcp) {
+ throw CliException.usage("camel tui --mcp needs an interactive
terminal: without one, --web serves browsers only");
+ }
+ try {
+ server.start();
+ } catch (BindException e) {
+ throw CliException.usage("Web server failed to start: port " + port
+ + " is already in use. Use --web-port to
choose another one.");
+ }
+ Thread stopper = new Thread(server::stop, "camel-tui-web-stop");
+ Runtime.getRuntime().addShutdownHook(stopper);
+ printer.println("Camel TUI in the browser at http://127.0.0.1:" + port
+ "/ (Ctrl+C to stop)");
+ try {
+ server.awaitStop();
+ } finally {
+ server.stop();
+ try {
+ Runtime.getRuntime().removeShutdownHook(stopper);
+ } catch (IllegalStateException e) {
+ // the JVM is already stopping
+ }
+ }
+ }
}
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 b3bafca9928d..9cfcc7dd5a90 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
@@ -757,6 +757,9 @@ class RoutesTab extends AbstractDiagramTab {
@Override
public void renderFooter(List<Span> spans) {
+ if (renderGotoNodeFooter(spans)) {
+ return;
+ }
if (sourceViewer.isVisible()) {
sourceViewer.renderFooter(spans);
} else if (diagram.isShowDiagram()) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
index 4b856cebee69..474fb0871950 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
@@ -41,7 +41,8 @@ public class TuiCommand extends CamelCommand {
int mcpPort = 8123;
@CommandLine.Option(names = { "--web" },
- description = "Enable browser-accessible terminal
(WebSocket) server")
+ description = "Enable browser-accessible terminal
(WebSocket) server; without an interactive"
+ + " terminal it serves browsers only")
boolean web;
@CommandLine.Option(names = { "--web-port" },
@@ -85,6 +86,11 @@ public class TuiCommand extends CamelCommand {
completionCandidates =
ThemeModeCompletionCandidates.class)
String panelTheme;
+ @CommandLine.Option(names = { "--defaults" },
+ description = "Start with the default settings, as a
new user sees the TUI; changes last for the"
+ + " session and are not saved")
+ boolean defaults;
+
public TuiCommand(CamelJBangMain main, ClassLoader classLoader) {
super(main);
this.classLoader = classLoader;
@@ -92,8 +98,8 @@ public class TuiCommand extends CamelCommand {
@Override
public Integer doCall() throws Exception {
- if (record == null) {
- // a recording plays a tape headless
+ if (record == null && !web) {
+ // a recording plays a tape headless, and --web without a terminal
serves browsers only
requireInteractiveTerminal("camel tui");
}
CamelMonitor cmd = new CamelMonitor(getMain(), classLoader);
@@ -153,6 +159,9 @@ public class TuiCommand extends CamelCommand {
args.add("--panel-theme");
args.add(panelTheme);
}
+ if (defaults) {
+ args.add("--defaults");
+ }
return args;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
index 5017040e0079..cc0efea2efc7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.Printer;
@@ -38,15 +40,37 @@ import org.apache.camel.dsl.jbang.core.common.Printer;
*/
final class TuiUserConfig {
+ /** The settings of a session started with --defaults, kept in memory
only; null for the files. */
+ private static volatile Map<String, String> session;
+
private TuiUserConfig() {
}
+ /**
+ * Starts from the default settings and saves nothing (--defaults): a key
reads as unset until the session sets it,
+ * and stays set for the session only. It holds for the whole JVM, so the
browser sessions of --web follow it too.
+ */
+ static void useDefaults() {
+ if (session == null) {
+ session = new ConcurrentHashMap<>();
+ }
+ }
+
+ /** Back to the files, after a test of {@link #useDefaults()}. */
+ static void resetForTesting() {
+ session = null;
+ }
+
/**
* Reads a single {@code camel.tui.*} key. Returns the local value when
the key is present in the local file,
* otherwise the global value, otherwise {@code null}. Propagates a {@link
RuntimeException} on an I/O failure so
* the caller can decide whether to fall back or log.
*/
static String read(String key) {
+ Map<String, String> inMemory = session;
+ if (inMemory != null) {
+ return inMemory.get(key);
+ }
boolean local = livesInLocal(key);
String[] holder = { null };
CommandLineHelper.loadProperties(props -> holder[0] =
props.getProperty(key), local);
@@ -59,6 +83,16 @@ final class TuiUserConfig {
* file are preserved. Propagates a {@link RuntimeException} on failure so
the caller can log.
*/
static void write(String key, String value) {
+ Map<String, String> inMemory = session;
+ if (inMemory != null) {
+ String trimmed = trimToNull(value);
+ if (trimmed == null) {
+ inMemory.remove(key);
+ } else {
+ inMemory.put(key, trimmed);
+ }
+ return;
+ }
boolean local = livesInLocal(key);
try {
CommandLineHelper.createPropertyFile(local);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
index 531d5fda8244..4510ce49aa98 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
@@ -161,6 +161,13 @@ class TuiWebServer {
}
}
+ /** Waits until the server is stopped: a browser-only monitor has nothing
else to do. */
+ void awaitStop() throws InterruptedException {
+ if (serverChannel != null) {
+ serverChannel.closeFuture().sync();
+ }
+ }
+
boolean awaitTermination(long timeout, TimeUnit unit) throws
InterruptedException {
long deadline = System.nanoTime() + unit.toNanos(timeout);
boolean bossDone = bossGroup.terminationFuture().await(Math.max(0,
deadline - System.nanoTime()), TimeUnit.NANOSECONDS);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index 1514eff91eca..6e2c4afb5c6d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -106,7 +106,7 @@ The Architecture level shows the routes grouped by what they
do for the business, one box per group: route groups from the
source, the capabilities the AI project overview proposed (`✦`,
italic), shared services (routes several groups use), Other, and
-Utility (plumbing: error handling, logging; `u` shows it). With
+Utility (plumbing: error handling, logging; `u` hides it). With
`x` on, the routes reached only on error leave Utility for a dashed
Error handling box, with dashed arrows from the groups whose
failures go there. A box lists its route count, entry points,
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
index 867f5a006b06..eba0d126af16 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
@@ -51,7 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* The capability view of the Diagram tab (CAMEL-25147): groups as boxes with
their facts, AI-assisted groups marked,
- * Enter opens a group's routes and a route's diagram, Esc goes back up,
utility hidden until u.
+ * Enter opens a group's routes and a route's diagram, Esc goes back up,
utility shown until u.
*/
@Isolated
@Timeout(60)
@@ -151,14 +151,14 @@ class ArchitectureViewTest {
assertTrue(screen.contains("Shared services"), screen);
// the business view: what the capability achieves (the technical view
shows entry points and systems)
assertTrue(screen.contains(IntegrationSummaryHints.MARK + "Takes
orders."), "what it achieves: " + screen);
- assertFalse(screen.contains("Utility"), "utility hidden by default: "
+ screen);
+ assertTrue(screen.contains("Utility"), "utility shown by default: " +
screen);
// boxes are as wide as their longest line, not wider
String boxTop = screen.lines().filter(l ->
l.contains("\u250c")).findFirst().orElseThrow();
int width = boxTop.indexOf('\u2510', boxTop.indexOf('\u250c')) -
boxTop.indexOf('\u250c') + 1;
assertTrue(width <= "platform-http:/orders".length() + 8, "box width "
+ width + ": " + screen);
view.handleKeyEvent(KeyEvent.ofChar('u'));
- assertTrue(render(view).contains("Utility"));
+ assertFalse(render(view).contains("Utility"), "u hides it");
}
@Test
@@ -232,7 +232,7 @@ class ArchitectureViewTest {
await().atMost(10, TimeUnit.SECONDS).until(() ->
!view.isLoadingForTesting());
assertFalse(render(view, 60).contains("Error handling"), "off by
default");
- // x: dlq, reached only from the doCatch of order, leaves Utility
(hidden) for a box of its own (CAMEL-25466)
+ // x: dlq, reached only from the doCatch of order, leaves Utility for
a box of its own (CAMEL-25466)
errors[0] = true;
view.refresh();
String screen = render(view, 60);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
index 66bc9a1a0131..6feca237f415 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
@@ -24,6 +24,7 @@ import dev.tamboui.text.Span;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -94,6 +95,18 @@ class DiagramTabRenderTest {
"Footer should be empty (no diagram loaded) or contain
diagram-related hints");
}
+ @Test
+ void theGoToNodePopupShowsItsOwnKeysInTheFooter() {
+ DiagramTab tab = new DiagramTab(ctx);
+ tab.gotoNodePopup.open(List.of(), -1);
+ List<Span> footerSpans = new ArrayList<>();
+ tab.renderFooter(footerSpans);
+
+ String footer = footerSpans.stream().map(Span::content).reduce("",
String::concat);
+ assertEquals(" type filter Enter go to Esc back", footer,
+ "what is typed goes into the popup, so the keys of the diagram
are not listed");
+ }
+
@Test
void renderShowsLoadingWhenNoRoutes() {
DiagramTab tab = new DiagramTab(ctx);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartupTest.java
new file mode 100644
index 000000000000..00aa4f3afb06
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartupTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+class MonitorStartupTest {
+
+ @Test
+ void webWithoutATerminalServesBrowsersOnly() {
+ assertThat(MonitorStartup.browserOnly(true, false, false,
false)).isTrue();
+ assertThat(MonitorStartup.browserOnly(true, false, false, true)).as("a
terminal to draw in").isFalse();
+ assertThat(MonitorStartup.browserOnly(false, false, false,
false)).as("no --web").isFalse();
+ assertThat(MonitorStartup.browserOnly(true, true, false, false)).as("a
browser session itself").isFalse();
+ assertThat(MonitorStartup.browserOnly(true, false, true, false)).as("a
recording").isFalse();
+ }
+
+ @Test
+ void camelTuiForwardsDefaults() {
+ TuiCommand command = new TuiCommand(new CamelJBangMain(),
getClass().getClassLoader());
+ new CommandLine(command).parseArgs("--web", "--defaults");
+ assertThat(command.buildArgs()).containsExactly("--web", "--defaults");
+
+ CamelMonitor monitor = new CamelMonitor(new CamelJBangMain(),
getClass().getClassLoader());
+ new
CommandLine(monitor).parseArgs(command.buildArgs().toArray(String[]::new));
+ assertThat(monitor.web).isTrue();
+ assertThat(monitor.defaults).isTrue();
+ }
+
+ @Test
+ void browserOnlyRefusesMcp() {
+ TuiWebServer server = newServer(0);
+
+ assertThatThrownBy(() -> MonitorStartup.serveBrowserOnly(server, 0,
true, new ListPrinter()))
+ .isInstanceOf(CliException.class)
+ .hasMessageContaining("--mcp needs an interactive terminal");
+ }
+
+ @Test
+ void browserOnlyServesUntilTheServerStops() throws Exception {
+ try (Port reserved = AvailablePortFinder.find()) {
+ int port = reserved.getPort();
+ TuiWebServer server = newServer(port);
+ ListPrinter printer = new ListPrinter();
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+ Thread serving = new Thread(() -> {
+ try {
+ MonitorStartup.serveBrowserOnly(server, port, false,
printer);
+ } catch (Throwable e) {
+ failure.set(e);
+ }
+ });
+ serving.start();
+
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress("127.0.0.1", port),
500);
+ }
+ });
+ assertThat(printer.lines).containsExactly(
+ "Camel TUI in the browser at http://127.0.0.1:" + port +
"/ (Ctrl+C to stop)");
+ assertThat(serving.isAlive()).as("waits while the server
runs").isTrue();
+
+ server.stop();
+ serving.join(TimeUnit.SECONDS.toMillis(10));
+ assertThat(serving.isAlive()).isFalse();
+ assertThat(failure.get()).isNull();
+ }
+ }
+
+ private static TuiWebServer newServer(int port) {
+ return new TuiWebServer(
+ port, new CamelJBangMain(),
Thread.currentThread().getContextClassLoader(), null, 200, "dark",
+ terminal -> {
+ });
+ }
+
+ private static final class ListPrinter implements Printer {
+
+ private final List<String> lines = new CopyOnWriteArrayList<>();
+
+ @Override
+ public void println() {
+ lines.add("");
+ }
+
+ @Override
+ public void println(String line) {
+ lines.add(line);
+ }
+
+ @Override
+ public void print(String output) {
+ lines.add(output);
+ }
+
+ @Override
+ public void printf(String format, Object... args) {
+ lines.add(String.format(format, args));
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index 7fe4ceb9278b..cd486eb9b083 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -59,7 +59,7 @@ class SourceSizeTest {
AiPanel.java 3180
AiPanel.runAgentLoop 159
AiUsageStats.render 166
- CamelMonitor.java 2761
+ CamelMonitor.java 2756
DiagramSupport.java 2360
DiagramSupport.loadHighlightedNativeDiagramInBackground 200
EndpointsTab.renderContent 170
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
index ab88f752bd6c..bc136d567b3e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
@@ -45,11 +45,31 @@ class TuiUserConfigTest {
@AfterEach
void tearDown() {
+ TuiUserConfig.resetForTesting();
if (originalHome != null) {
CommandLineHelper.useHomeDir(originalHome);
}
}
+ @Test
+ void defaultsReadNothingAndSaveNothing(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(globalFile, "camel.tui.theme=light\n");
+
+ TuiUserConfig.useDefaults();
+ assertNull(TuiUserConfig.read("camel.tui.theme"), "the saved theme is
not read");
+
+ TuiUserConfig.write("camel.tui.theme", "dracula");
+ assertEquals("dracula", TuiUserConfig.read("camel.tui.theme"), "a
change lasts for the session");
+ TuiUserConfig.write("camel.tui.theme", "");
+ assertNull(TuiUserConfig.read("camel.tui.theme"));
+ assertEquals("camel.tui.theme=light\n", Files.readString(globalFile),
"the file is left as it was");
+
+ TuiUserConfig.resetForTesting();
+ assertEquals("light", TuiUserConfig.read("camel.tui.theme"));
+ }
+
private void useHome(Path dir) {
originalHome = CommandLineHelper.getHomeDir().toString();
CommandLineHelper.useHomeDir(dir.toString());