This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tui-web-headless-defaults in repository https://gitbox.apache.org/repos/asf/camel.git
commit 64b3625d96de87bbd942f56da3a24856764abf13 Author: Claus Ibsen <[email protected]> AuthorDate: Sun Oct 25 20:06:40 2026 +0100 chore: camel-jbang - camel tui --web serves browsers only without a terminal, --defaults, utility routes shown out of the box - camel tui --web without an interactive terminal (a script, a CI job, a background shell) failed with "requires an interactive terminal". Each browser session is a TUI of its own, so it now serves browsers only, prints the address and runs until Ctrl+C. --mcp drives the terminal's screen and is refused in this mode. - --defaults starts the TUI with the default settings, as a new user sees it: the saved camel.tui.* settings are not read, and changes last for the session only. It holds for the browser sessions of --web too. - The utility routes are shown in the architecture and the topology out of the box, so a project shows all its routes; u hides them. The --theme and --panel-theme checks move from CamelMonitor to MonitorStartup to keep CamelMonitor under its size cap. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[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 +- .../jbang/core/commands/tui/ArchitectureView.java | 3 +- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 41 +++---- .../jbang/core/commands/tui/MonitorStartup.java | 67 +++++++++++ .../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/MonitorStartupTest.java | 133 +++++++++++++++++++++ .../jbang/core/commands/tui/SourceSizeTest.java | 2 +- .../jbang/core/commands/tui/TuiUserConfigTest.java | 20 ++++ 17 files changed, 317 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/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/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/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/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());
