This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch wa in repository https://gitbox.apache.org/repos/asf/camel.git
commit 57603b3196b8c63e2d7bfc1bc0570d1c19124f5c Author: Claus Ibsen <[email protected]> AuthorDate: Mon May 11 14:17:16 2026 +0200 CAMEL-23385: camel-diagram - Add watch option --- .../camel-jbang-cmd-route-diagram.adoc | 3 +- .../META-INF/camel-jbang-commands-metadata.json | 2 +- .../core/commands/action/ActionWatchCommand.java | 15 ++- .../commands/action/CamelRouteDiagramAction.java | 113 +++++++++++++-------- 4 files changed, 86 insertions(+), 47 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc index be9889a6c1cd..7a9f17367bb5 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc @@ -26,7 +26,8 @@ camel cmd route-diagram [options] | `--metric` | Whether to include live metrics (only possible for running Camel application) | true | boolean | `--node-label` | What text to display in diagram nodes: code, description, or both (default) | both | String | `--output` | Save diagram to a PNG file instead of displaying in terminal | | String -| `--theme,--colors` | Color theme preset (dark, light, transparent, text) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var. | dark | String +| `--theme,--colors` | Color theme preset (dark, light, transparent) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var. | dark | String +| `--watch` | Execute periodically and showing output fullscreen | | boolean | `--width` | Image width in pixels (0 = auto) | 0 | int | `-h,--help` | Display the help and sub-commands | | boolean |=== diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index 9e3f577faf42..153bae345663 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -2,7 +2,7 @@ "commands": [ { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To [...] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java index 976a356c2337..5bc634eed4dd 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionWatchCommand.java @@ -31,7 +31,8 @@ abstract class ActionWatchCommand extends ActionBaseCommand { description = "Execute periodically and showing output fullscreen") boolean watch; - private CommandHelper.ReadConsoleTask waitUserTask; + private Runnable waitUserTask; + final AtomicBoolean running = new AtomicBoolean(true); protected ActionWatchCommand(CamelJBangMain main) { super(main); @@ -43,7 +44,7 @@ abstract class ActionWatchCommand extends ActionBaseCommand { final AtomicBoolean running = new AtomicBoolean(true); if (watch) { Thread t = new Thread(() -> { - waitUserTask = new CommandHelper.ReadConsoleTask(() -> running.set(false)); + waitUserTask = waitForUserEnter(); waitUserTask.run(); }, "WaitForUser"); t.start(); @@ -53,7 +54,7 @@ abstract class ActionWatchCommand extends ActionBaseCommand { // use 2-sec delay in watch mode try { StopWatch watch = new StopWatch(); - while (running.get() && watch.taken() < 2000) { + while (running.get() && watchWait(watch)) { Thread.sleep(100); } } catch (Exception e) { @@ -67,10 +68,18 @@ abstract class ActionWatchCommand extends ActionBaseCommand { return exit; } + protected Runnable waitForUserEnter() { + return new CommandHelper.ReadConsoleTask(() -> running.set(false)); + } + protected void clearScreen() { AnsiConsole.out().print(Ansi.ansi().eraseScreen().cursor(1, 1)); } + protected boolean watchWait(StopWatch watch) { + return watch.taken() < 2000; + } + protected abstract Integer doWatchCall() throws Exception; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java index c4989bfc8485..40861ac616f5 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java @@ -23,7 +23,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import javax.imageio.ImageIO; @@ -41,17 +40,21 @@ import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.main.KameletMain; import org.apache.camel.support.PatternHelper; +import org.apache.camel.util.StopWatch; import org.apache.camel.util.json.JsonObject; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import org.jline.terminal.impl.TerminalGraphics; import org.jline.terminal.impl.TerminalGraphicsManager; +import org.jline.utils.InfoCmp; import picocli.CommandLine; import picocli.CommandLine.Command; @Command(name = "route-diagram", description = "Display Camel route diagram in the terminal", sortOptions = false, showDefaultValues = true) -public class CamelRouteDiagramAction extends ActionBaseCommand { +public class CamelRouteDiagramAction extends ActionWatchCommand { @CommandLine.Parameters(description = "Source file name, or name/pid of a running Camel integration", arity = "0..1") String name = "*"; @@ -69,7 +72,7 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { String output; @CommandLine.Option(names = { "--theme", "--colors" }, - description = "Color theme preset (dark, light, transparent, text) or custom colors " + description = "Color theme preset (dark, light, transparent) or custom colors " + "(e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or " + "ANSI color names (e.g. from=seagreen:to=steelblue). " + "Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var.", @@ -99,6 +102,11 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { private volatile long pid; + private DiagramColors colors; + private Terminal terminal; + private TerminalGraphics terminalGraphics; + private LineReader lineReader; + public CamelRouteDiagramAction(CamelJBangMain main) { super(main); } @@ -107,9 +115,25 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { public Integer doCall() throws Exception { System.setProperty("java.awt.headless", "true"); - String colorSpec = System.getenv("DIAGRAM_COLORS"); - DiagramColors colors = !"text".equals(theme) ? DiagramColors.parse(colorSpec != null ? colorSpec : theme) : null; + // if output in terminal then ensure terminal supports this + if (output == null) { + String colorSpec = System.getenv("DIAGRAM_COLORS"); + colors = DiagramColors.parse(colorSpec != null ? colorSpec : theme); + terminal = TerminalBuilder.builder().system(true).build(); + terminalGraphics = TerminalGraphicsManager.getBestProtocol(terminal).orElse(null); + if (terminalGraphics == null) { + printer().println("Terminal does not support graphics protocols (Kitty, iTerm2, or Sixel)."); + printer().println("Try running in a supported terminal: Kitty, iTerm2, WezTerm, Ghostty, or VS Code."); + return 1; + } + lineReader = LineReaderBuilder.builder().terminal(terminal).build(); + } + return super.doCall(); + } + + @Override + protected Integer doWatchCall() throws Exception { Path outputFile; int exit = 0; List<Long> pids = findPids(name); @@ -143,7 +167,7 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { List<RouteInfo> routes = parseRoutes(jo); if (routes.isEmpty()) { printer().println("No routes found"); - return 0; + return 1; } if (filter != null) { @@ -155,7 +179,7 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { if (routes.isEmpty()) { printer().println("No routes match filter: " + filter); - return 0; + return 1; } NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel); @@ -164,13 +188,6 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { engine.getNodeWidth(), fontSize * RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(), pid > 0 && metric); - if ("text".equals(theme)) { - for (String line : renderer.printTextDiagram(routes, labelMode)) { - printer().println(line); - } - return 0; - } - List<LayoutRoute> layoutRoutes = new ArrayList<>(); int currentY = RouteDiagramLayoutEngine.PADDING; for (RouteInfo route : routes) { @@ -196,35 +213,10 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { ImageIO.write(image, "PNG", file); printer().println("Diagram saved to: " + file.getAbsolutePath()); } else { - try (Terminal terminal = TerminalBuilder.builder().system(true).build()) { - try { - Optional<TerminalGraphics> protocol = TerminalGraphicsManager.getBestProtocol(terminal); - if (protocol.isPresent()) { - TerminalGraphics.ImageOptions opts = new TerminalGraphics.ImageOptions() - .preserveAspectRatio(true); - if (width > 0) { - opts.width(width); - } - protocol.get().displayImage(terminal, image, opts); - terminal.writer().println(); - terminal.flush(); - } else { - printer().println( - "Terminal does not support graphics protocols (Kitty, iTerm2, or Sixel)."); - printer().println( - "Try running in a supported terminal: Kitty, iTerm2, WezTerm, Ghostty, or VS Code."); - for (String line : renderer.printTextDiagram(routes, labelMode)) { - printer().println(line); - } - } - } catch (IOException | UnsupportedOperationException e) { - printer().println("Failed to display diagram in terminal: " + e.getMessage()); - printer().println("Falling back to text diagram."); - for (String line : renderer.printTextDiagram(routes, labelMode)) { - printer().println(line); - } - } + if (watch) { + clearScreen(); } + doDisplayDiagram(image); } return 0; @@ -233,6 +225,43 @@ public class CamelRouteDiagramAction extends ActionBaseCommand { } } + @Override + protected boolean watchWait(StopWatch watch) { + return watch.taken() < 5000; + } + + @Override + protected void clearScreen() { + if (terminal != null) { + terminal.puts(InfoCmp.Capability.clear_screen); + terminal.flush(); + } + } + + @Override + protected Runnable waitForUserEnter() { + return () -> { + if (lineReader != null) { + try { + lineReader.readLine(); + } catch (Exception e) { + // ignore + } + } + }; + } + + private void doDisplayDiagram(BufferedImage image) throws IOException { + TerminalGraphics.ImageOptions opts = new TerminalGraphics.ImageOptions() + .preserveAspectRatio(true); + if (width > 0) { + opts.width(width); + } + terminalGraphics.displayImage(terminal, image, opts); + terminal.writer().println(); + terminal.flush(); + } + private void doCallPid(Long pid) { this.pid = pid;
