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;
 

Reply via email to