This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch camel-23485-ascii-art-renderer
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b4d492f02c8036d15ebfde7c9f07a6728a49cfa8
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue May 12 14:12:05 2026 +0200

    CAMEL-23485: camel-diagram - Support --theme=ascii in route-diagram command
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../camel-jbang-cmd-route-diagram.adoc             |   4 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../commands/action/CamelRouteDiagramAction.java   | 101 ++++++++++++++-------
 3 files changed, 72 insertions(+), 35 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 d118b519b5c9..b5cd5154893d 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
@@ -25,8 +25,8 @@ camel cmd route-diagram [options]
 | `--ignore-loading-error` | Whether to ignore route loading and compilation 
errors (use this with care!) | false | boolean
 | `--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` | 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. | transparent | String
+| `--output` | Save diagram to a file (PNG for image themes, text for ascii 
theme) |  | String
+| `--theme` | Color theme preset (dark, light, transparent, ascii) 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. Use 
ascii for plain text output. Can also be set via DIAGRAM_COLORS env var. | 
transparent | 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 9e7a209d62c0..bca47153fd15 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/CamelRouteDiagramAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index 165b53b1cd7e..c14733d80f07 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
@@ -26,6 +26,7 @@ import java.util.List;
 
 import javax.imageio.ImageIO;
 
+import org.apache.camel.diagram.RouteDiagramAsciiRenderer;
 import org.apache.camel.diagram.RouteDiagramHelper;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
@@ -68,14 +69,15 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
     int width;
 
     @CommandLine.Option(names = { "--output" },
-                        description = "Save diagram to a PNG file instead of 
displaying in terminal")
+                        description = "Save diagram to a file (PNG for image 
themes, text for ascii theme)")
     String output;
 
     @CommandLine.Option(names = { "--theme" },
-                        description = "Color theme preset (dark, light, 
transparent) or custom colors "
+                        description = "Color theme preset (dark, light, 
transparent, ascii) 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.",
+                                      + "Use bg= for transparent. Use ascii 
for plain text output. "
+                                      + "Can also be set via DIAGRAM_COLORS 
env var.",
                         defaultValue = "transparent")
     String theme;
 
@@ -115,18 +117,26 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
     public Integer doCall() throws Exception {
         System.setProperty("java.awt.headless", "true");
 
-        // if output in terminal then ensure terminal supports this
-        if (output == null) {
+        boolean ascii = isAsciiTheme();
+        if (!ascii) {
             String colorSpec = System.getenv("DIAGRAM_COLORS");
             colors = DiagramColors.parse(colorSpec != null ? colorSpec : 
theme);
+        }
+
+        // if output in terminal then set up terminal
+        if (output == null) {
             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();
+            if (!ascii) {
+                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.");
+                    printer().println("Or use --theme=ascii for plain text 
output.");
+                    return 1;
+                }
+            }
         }
 
         try {
@@ -190,9 +200,6 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
 
             NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel);
             RouteDiagramLayoutEngine engine = new 
RouteDiagramLayoutEngine(boxWidth, fontSize, labelMode);
-            RouteDiagramRenderer renderer = new RouteDiagramRenderer(
-                    engine.getNodeWidth(), fontSize * 
RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(),
-                    pid > 0 && metric);
 
             List<LayoutRoute> layoutRoutes = new ArrayList<>();
             int currentY = RouteDiagramLayoutEngine.PADDING;
@@ -202,27 +209,53 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
                 currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
             }
 
-            BufferedImage image;
-            try {
-                image = renderer.renderDiagram(layoutRoutes, currentY, colors);
-            } catch (IllegalStateException e) {
-                printer().println(e.getMessage());
-                return 1;
-            }
-
-            if (output != null) {
-                File file = new File(output);
-                File parentDir = file.getParentFile();
-                if (parentDir != null) {
-                    parentDir.mkdirs();
+            if (isAsciiTheme()) {
+                RouteDiagramAsciiRenderer asciiRenderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth());
+                String ascii = asciiRenderer.renderDiagram(layoutRoutes, 
currentY);
+
+                if (output != null) {
+                    String fileName = output.endsWith(".png")
+                            ? output.substring(0, output.length() - 4) + 
".txt" : output;
+                    File file = new File(fileName);
+                    File parentDir = file.getParentFile();
+                    if (parentDir != null) {
+                        parentDir.mkdirs();
+                    }
+                    Files.writeString(file.toPath(), ascii);
+                    printer().println("Diagram saved to: " + 
file.getAbsolutePath());
+                } else {
+                    if (watch) {
+                        clearScreen();
+                    }
+                    printer().println(ascii);
                 }
-                ImageIO.write(image, "PNG", file);
-                printer().println("Diagram saved to: " + 
file.getAbsolutePath());
             } else {
-                if (watch) {
-                    clearScreen();
+                RouteDiagramRenderer renderer = new RouteDiagramRenderer(
+                        engine.getNodeWidth(), fontSize * 
RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(),
+                        pid > 0 && metric);
+
+                BufferedImage image;
+                try {
+                    image = renderer.renderDiagram(layoutRoutes, currentY, 
colors);
+                } catch (IllegalStateException e) {
+                    printer().println(e.getMessage());
+                    return 1;
+                }
+
+                if (output != null) {
+                    File file = new File(output);
+                    File parentDir = file.getParentFile();
+                    if (parentDir != null) {
+                        parentDir.mkdirs();
+                    }
+                    ImageIO.write(image, "PNG", file);
+                    printer().println("Diagram saved to: " + 
file.getAbsolutePath());
+                } else {
+                    if (watch) {
+                        clearScreen();
+                    }
+                    doDisplayDiagram(image);
                 }
-                doDisplayDiagram(image);
             }
 
             return 0;
@@ -319,6 +352,10 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
         return RouteDiagramHelper.parseRoutes(jo);
     }
 
+    private boolean isAsciiTheme() {
+        return "ascii".equalsIgnoreCase(theme);
+    }
+
     static NodeLabelMode parseNodeLabelMode(String value) {
         if (value == null || value.isBlank() || 
"code".equalsIgnoreCase(value)) {
             return NodeLabelMode.CODE;

Reply via email to