You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/05/12 13:02:36 UTC

(camel) branch camel-23485-ascii-art-renderer created (now 2910bbde9040)

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

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


      at 2910bbde9040 CAMEL-23485: camel-diagram - Support ascii/unicode themes in DiagramDevConsole

This branch includes the following new commits:

     new ae686d9fe996 CAMEL-23485: camel-diagram - Add ASCII art renderer
     new e22f003db138 CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer
     new d1e2aa4f45c8 CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer
     new b4d492f02c80 CAMEL-23485: camel-diagram - Support --theme=ascii in route-diagram command
     new 3363f7c54f91 CAMEL-23485: camel-diagram - Add scope boxes to ASCII art renderer
     new 7e93029a3e5b CAMEL-23485: camel-diagram - Add --theme=unicode with box-drawing characters
     new 2910bbde9040 CAMEL-23485: camel-diagram - Support ascii/unicode themes in DiagramDevConsole

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 06/07: CAMEL-23485: camel-diagram - Add --theme=unicode with box-drawing characters

Posted by da...@apache.org.
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 7e93029a3e5b7834f8ddb705e9f064c421284ca9
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue May 12 14:44:56 2026 +0200

    CAMEL-23485: camel-diagram - Add --theme=unicode with box-drawing characters
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 .../camel-diagram/src/main/docs/diagram.adoc       |  33 +++++++
 .../camel/diagram/RouteDiagramAsciiRenderer.java   | 102 ++++++++++++++++-----
 .../org/apache/camel/diagram/RouteDiagramTest.java |  63 +++++++++++++
 .../camel-jbang-cmd-route-diagram.adoc             |   2 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../commands/action/CamelRouteDiagramAction.java   |  25 +++--
 6 files changed, 191 insertions(+), 36 deletions(-)

diff --git a/components/camel-diagram/src/main/docs/diagram.adoc b/components/camel-diagram/src/main/docs/diagram.adoc
index c9525206ede9..8792f2073f4d 100644
--- a/components/camel-diagram/src/main/docs/diagram.adoc
+++ b/components/camel-diagram/src/main/docs/diagram.adoc
@@ -183,3 +183,36 @@ route1
 
 Scope boxes (for filter, split, doTry, etc.) are rendered with dashed borders using `:` for vertical
 and `- - -` for horizontal lines.
+
+Use `--theme=ascii` for plain ASCII art:
+
+[source,bash]
+----
+camel cmd route-diagram MyRoute.java --theme=ascii
+----
+
+== Unicode Rendering
+
+The `unicode` theme uses Unicode box-drawing characters for a cleaner look.
+Node boxes use `┌──┐ │ └──┘`, arrows use `│` and `▼`, and branch junctions use `┴`.
+Scope boxes use `╌` (dashed horizontal) and `╎` (dashed vertical) with no corners.
+
+[source,bash]
+----
+camel cmd route-diagram MyRoute.java --theme=unicode
+----
+
+Example output:
+
+----
+route1
+┌──────────────────────┐
+│      timer:tick      │
+└──────────────────────┘
+           │
+           │
+           ▼
+┌──────────────────────┐
+│        log:a         │
+└──────────────────────┘
+----
diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
index 86c4c6ba1226..1b9ef494b820 100644
--- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
+++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -29,7 +29,7 @@ import static org.apache.camel.diagram.RouteDiagramLayoutEngine.PADDING;
 import static org.apache.camel.diagram.RouteDiagramLayoutEngine.SCOPE_BOX_PAD;
 
 /**
- * Renders route diagrams as plain ASCII art text.
+ * Renders route diagrams as plain ASCII art or Unicode box-drawing text.
  */
 public class RouteDiagramAsciiRenderer {
 
@@ -38,12 +38,34 @@ public class RouteDiagramAsciiRenderer {
     private static final int MIN_BOX_WIDTH = 16;
     private static final int X_DIVISOR = 15;
 
+    // Unicode box-drawing characters
+    private static final char UNI_H = '─';     // ─
+    private static final char UNI_V = '│';     // │
+    private static final char UNI_TL = '┌';    // ┌
+    private static final char UNI_TR = '┐';    // ┐
+    private static final char UNI_BL = '└';    // └
+    private static final char UNI_BR = '┘';    // ┘
+    private static final char UNI_T_DOWN = '┬'; // ┬
+    private static final char UNI_T_UP = '┴';  // ┴
+    private static final char UNI_T_RIGHT = '├'; // ├
+    private static final char UNI_T_LEFT = '┤'; // ┤
+    private static final char UNI_CROSS = '┼'; // ┼
+    private static final char UNI_ARROW = '▼'; // ▼
+    private static final char UNI_DASH_H = '╌'; // ╌
+    private static final char UNI_DASH_V = '╎'; // ╎
+
     private final int nodeWidth;
     private final int boxWidth;
+    private final boolean unicode;
 
     public RouteDiagramAsciiRenderer(int nodeWidth) {
+        this(nodeWidth, false);
+    }
+
+    public RouteDiagramAsciiRenderer(int nodeWidth, boolean unicode) {
         this.nodeWidth = nodeWidth;
         this.boxWidth = Math.max(MIN_BOX_WIDTH, nodeWidth / X_DIVISOR);
+        this.unicode = unicode;
     }
 
     public int getBoxWidth() {
@@ -108,23 +130,26 @@ public class RouteDiagramAsciiRenderer {
             return;
         }
 
-        setChar(grid, row, col, '+');
+        char h = unicode ? UNI_H : '-';
+        char v = unicode ? UNI_V : '|';
+
+        setChar(grid, row, col, unicode ? UNI_TL : '+');
         for (int c = col + 1; c < col + boxWidth - 1; c++) {
-            setChar(grid, row, c, '-');
+            setChar(grid, row, c, h);
         }
-        setChar(grid, row, col + boxWidth - 1, '+');
+        setChar(grid, row, col + boxWidth - 1, unicode ? UNI_TR : '+');
 
         int bottom = row + height - 1;
-        setChar(grid, bottom, col, '+');
+        setChar(grid, bottom, col, unicode ? UNI_BL : '+');
         for (int c = col + 1; c < col + boxWidth - 1; c++) {
-            setChar(grid, bottom, c, '-');
+            setChar(grid, bottom, c, h);
         }
-        setChar(grid, bottom, col + boxWidth - 1, '+');
+        setChar(grid, bottom, col + boxWidth - 1, unicode ? UNI_BR : '+');
 
         for (int i = 0; i < lines.size(); i++) {
             int r = row + 1 + i;
-            setChar(grid, r, col, '|');
-            setChar(grid, r, col + boxWidth - 1, '|');
+            setChar(grid, r, col, v);
+            setChar(grid, r, col + boxWidth - 1, v);
             for (int c = col + 1; c < col + boxWidth - 1; c++) {
                 setChar(grid, r, c, ' ');
             }
@@ -160,30 +185,40 @@ public class RouteDiagramAsciiRenderer {
             return;
         }
 
+        char v = unicode ? UNI_V : '|';
+        char h = unicode ? UNI_H : '-';
+        char arrow = unicode ? UNI_ARROW : 'v';
+
         if (fromCx == toCx) {
             for (int r = fromRow; r < toRow - 1; r++) {
-                plotLine(grid, r, fromCx, '|');
+                plotLine(grid, r, fromCx, v);
             }
-            setChar(grid, toRow - 1, toCx, 'v');
+            setChar(grid, toRow - 1, toCx, arrow);
         } else {
             int midRow = fromRow + (toRow - fromRow) / 2;
 
             for (int r = fromRow; r < midRow; r++) {
-                plotLine(grid, r, fromCx, '|');
+                plotLine(grid, r, fromCx, v);
             }
 
             int minC = Math.min(fromCx, toCx);
             int maxC = Math.max(fromCx, toCx);
             for (int c = minC; c <= maxC; c++) {
-                plotLine(grid, midRow, c, '-');
+                plotLine(grid, midRow, c, h);
+            }
+
+            if (unicode) {
+                setChar(grid, midRow, fromCx, UNI_T_UP);
+                setChar(grid, midRow, toCx, UNI_T_DOWN);
+            } else {
+                setChar(grid, midRow, fromCx, '+');
+                setChar(grid, midRow, toCx, '+');
             }
-            setChar(grid, midRow, fromCx, '+');
-            setChar(grid, midRow, toCx, '+');
 
             for (int r = midRow + 1; r < toRow - 1; r++) {
-                plotLine(grid, r, toCx, '|');
+                plotLine(grid, r, toCx, v);
             }
-            setChar(grid, toRow - 1, toCx, 'v');
+            setChar(grid, toRow - 1, toCx, arrow);
         }
     }
 
@@ -201,11 +236,22 @@ public class RouteDiagramAsciiRenderer {
         int col2 = toCol(bounds.maxX + SCOPE_BOX_PAD);
         int row2 = toRow(bounds.maxY + SCOPE_BOX_PAD);
 
-        drawDashedHLine(grid, row1, col1, col2);
-        drawDashedHLine(grid, row2, col1, col2);
-        for (int r = row1 + 1; r < row2; r++) {
-            setChar(grid, r, col1, ':');
-            setChar(grid, r, col2, ':');
+        if (unicode) {
+            for (int c = col1; c <= col2; c++) {
+                setChar(grid, row1, c, UNI_DASH_H);
+                setChar(grid, row2, c, UNI_DASH_H);
+            }
+            for (int r = row1 + 1; r < row2; r++) {
+                setChar(grid, r, col1, UNI_DASH_V);
+                setChar(grid, r, col2, UNI_DASH_V);
+            }
+        } else {
+            drawDashedHLine(grid, row1, col1, col2);
+            drawDashedHLine(grid, row2, col1, col2);
+            for (int r = row1 + 1; r < row2; r++) {
+                setChar(grid, r, col1, ':');
+                setChar(grid, r, col2, ':');
+            }
         }
     }
 
@@ -307,10 +353,18 @@ public class RouteDiagramAsciiRenderer {
         return ' ';
     }
 
+    private boolean isVertical(char ch) {
+        return ch == '|' || ch == UNI_V;
+    }
+
+    private boolean isHorizontal(char ch) {
+        return ch == '-' || ch == UNI_H;
+    }
+
     private void plotLine(char[][] grid, int row, int col, char ch) {
         char current = getChar(grid, row, col);
-        if ((current == '|' && ch == '-') || (current == '-' && ch == '|')) {
-            setChar(grid, row, col, '+');
+        if (isVertical(current) && isHorizontal(ch) || isHorizontal(current) && isVertical(ch)) {
+            setChar(grid, row, col, unicode ? UNI_CROSS : '+');
         } else {
             setChar(grid, row, col, ch);
         }
diff --git a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index 7024198eef7d..b6ab0d9fd7d8 100644
--- a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++ b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -971,6 +971,69 @@ class RouteDiagramTest {
         assertTrue(result.contains("+ -"), "Scope box should have dashed horizontal borders");
     }
 
+    @Test
+    void testUnicodeDiagramSequential() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "timer:tick", 0));
+        route.nodes.add(node("to", "log:a", 1));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("timer:tick"));
+        assertTrue(result.contains("log:a"));
+        assertTrue(result.contains("┌"), "Should contain Unicode top-left corner");
+        assertTrue(result.contains("─"), "Should contain Unicode horizontal line");
+        assertTrue(result.contains("│"), "Should contain Unicode vertical line");
+        assertTrue(result.contains("▼"), "Should contain Unicode arrow head");
+    }
+
+    @Test
+    void testUnicodeDiagramBranching() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "timer:tick", 0));
+        route.nodes.add(node("choice", "choice()", 1));
+        route.nodes.add(node("when", "when(...)", 2));
+        route.nodes.add(node("to", "log:a", 3));
+        route.nodes.add(node("otherwise", "otherwise()", 2));
+        route.nodes.add(node("to", "log:b", 3));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("choice()"));
+        assertTrue(result.contains("when(...)"));
+        assertTrue(result.contains("┴"), "Should contain Unicode T-up junction for branch split");
+    }
+
+    @Test
+    void testUnicodeDiagramWithScopeBox() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "direct:start", 0));
+        route.nodes.add(node("filter", "filter[header(x)]", 1));
+        route.nodes.add(node("log", "log[filtered]", 2));
+        route.nodes.add(node("to", "to[mock:end]", 1));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("filter[header(x)]"));
+        assertTrue(result.contains("╌"), "Scope box should have Unicode dashed horizontal");
+        assertTrue(result.contains("╎"), "Scope box should have Unicode dashed vertical");
+    }
+
     @Test
     void testAsciiWrapTextShort() {
         List<String> lines = RouteDiagramAsciiRenderer.wrapText("timer:tick", 20);
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 b5cd5154893d..354db16e98f4 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,7 @@ 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 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
+| `--theme` | Color theme preset (dark, light, transparent, ascii, unicode) 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/unicode 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 bca47153fd15..399406bab646 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 c14733d80f07..cde562c5235a 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
@@ -73,10 +73,10 @@ public class CamelRouteDiagramAction extends ActionWatchCommand {
     String output;
 
     @CommandLine.Option(names = { "--theme" },
-                        description = "Color theme preset (dark, light, transparent, ascii) or custom colors "
+                        description = "Color theme preset (dark, light, transparent, ascii, unicode) 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. "
+                                      + "Use bg= for transparent. Use ascii/unicode for plain text output. "
                                       + "Can also be set via DIAGRAM_COLORS env var.",
                         defaultValue = "transparent")
     String theme;
@@ -117,8 +117,8 @@ public class CamelRouteDiagramAction extends ActionWatchCommand {
     public Integer doCall() throws Exception {
         System.setProperty("java.awt.headless", "true");
 
-        boolean ascii = isAsciiTheme();
-        if (!ascii) {
+        boolean textMode = isTextTheme();
+        if (!textMode) {
             String colorSpec = System.getenv("DIAGRAM_COLORS");
             colors = DiagramColors.parse(colorSpec != null ? colorSpec : theme);
         }
@@ -127,13 +127,13 @@ public class CamelRouteDiagramAction extends ActionWatchCommand {
         if (output == null) {
             terminal = TerminalBuilder.builder().system(true).build();
             lineReader = LineReaderBuilder.builder().terminal(terminal).build();
-            if (!ascii) {
+            if (!textMode) {
                 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.");
+                    printer().println("Or use --theme=ascii or --theme=unicode for plain text output.");
                     return 1;
                 }
             }
@@ -209,8 +209,9 @@ public class CamelRouteDiagramAction extends ActionWatchCommand {
                 currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
             }
 
-            if (isAsciiTheme()) {
-                RouteDiagramAsciiRenderer asciiRenderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+            if (isTextTheme()) {
+                RouteDiagramAsciiRenderer asciiRenderer
+                        = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), isUnicodeTheme());
                 String ascii = asciiRenderer.renderDiagram(layoutRoutes, currentY);
 
                 if (output != null) {
@@ -352,8 +353,12 @@ public class CamelRouteDiagramAction extends ActionWatchCommand {
         return RouteDiagramHelper.parseRoutes(jo);
     }
 
-    private boolean isAsciiTheme() {
-        return "ascii".equalsIgnoreCase(theme);
+    private boolean isTextTheme() {
+        return "ascii".equalsIgnoreCase(theme) || "unicode".equalsIgnoreCase(theme);
+    }
+
+    private boolean isUnicodeTheme() {
+        return "unicode".equalsIgnoreCase(theme);
     }
 
     static NodeLabelMode parseNodeLabelMode(String value) {


(camel) 07/07: CAMEL-23485: camel-diagram - Support ascii/unicode themes in DiagramDevConsole

Posted by da...@apache.org.
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 2910bbde90403363e7d2a246662b4dde59d3246c
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue May 12 14:57:33 2026 +0200

    CAMEL-23485: camel-diagram - Support ascii/unicode themes in DiagramDevConsole
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 .../camel-diagram/src/main/docs/diagram.adoc       |  1 +
 .../camel/diagram/DefaultRouteDiagramDumper.java   |  8 ++--
 .../apache/camel/diagram/DiagramDevConsole.java    | 55 +++++++++++++++-------
 .../org/apache/camel/spi/RouteDiagramDumper.java   | 14 +++++-
 4 files changed, 57 insertions(+), 21 deletions(-)

diff --git a/components/camel-diagram/src/main/docs/diagram.adoc b/components/camel-diagram/src/main/docs/diagram.adoc
index 8792f2073f4d..23e545039c03 100644
--- a/components/camel-diagram/src/main/docs/diagram.adoc
+++ b/components/camel-diagram/src/main/docs/diagram.adoc
@@ -204,6 +204,7 @@ camel cmd route-diagram MyRoute.java --theme=unicode
 
 Example output:
 
+[source,text]
 ----
 route1
 ┌──────────────────────┐
diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
index 81ec387197d2..ba0d83311d38 100644
--- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
+++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
@@ -118,12 +118,12 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo
 
     @Override
     public String dumpRoutesAsAsciiArt(
-            String filter, RouteDiagramDumper.NodeLabelMode nodeLabel, int nodeWidth) {
+            String filter, RouteDiagramDumper.NodeLabelMode nodeLabel, int nodeWidth, boolean unicode) {
         DevConsole dc = getCamelContext().getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("route-structure");
         JsonObject root = (JsonObject) dc.call(DevConsole.MediaType.JSON, Map.of("filter", filter));
         var routes = RouteDiagramHelper.parseRoutes(root);
-        return renderAscii(routes, nodeWidth, nodeLabel.name());
+        return renderAscii(routes, nodeWidth, nodeLabel.name(), unicode);
     }
 
     @Override
@@ -163,7 +163,7 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo
     }
 
     private static String renderAscii(
-            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel) {
+            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel, boolean unicode) {
         RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
                 nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
                 RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase()));
@@ -177,7 +177,7 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo
         }
 
         RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(
-                nodeWidth * RouteDiagramLayoutEngine.SCALE);
+                nodeWidth * RouteDiagramLayoutEngine.SCALE, unicode);
         return renderer.renderDiagram(layoutRoutes, currentY);
     }
 
diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
index 74265961117b..e41c3b8cf15f 100644
--- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
+++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
@@ -35,7 +35,7 @@ public class DiagramDevConsole extends AbstractDevConsole {
     public static final String FILTER = "filter";
 
     /**
-     * Theme to use: dark or light
+     * Theme to use: dark, light, transparent, ascii, or unicode
      */
     public static final String THEME = "theme";
 
@@ -84,18 +84,25 @@ public class DiagramDevConsole extends AbstractDevConsole {
 
         try {
             RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(getCamelContext());
-            BufferedImage image = dumper.dumpRoutesAsImage(filter, RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
-                    metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
-            String base64 = dumper.imageToBase64(image);
-            // For HTML embedding:
-            String html = String.format(
-                    "  <body>\n    <img src=\"data:image/png;base64,%s\" alt=\"Route Diagram\">\n  </body>\n",
-                    base64);
-            if (refresh) {
-                html = "<head><meta http-equiv=\"refresh\" content=\"5\"></head>\n" + html;
+            if (isTextTheme(theme)) {
+                String text = dumper.dumpRoutesAsAsciiArt(filter,
+                        RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()),
+                        nodeWidth, isUnicodeTheme(theme));
+                sj.add(text);
+            } else {
+                BufferedImage image = dumper.dumpRoutesAsImage(filter,
+                        RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
+                        metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
+                String base64 = dumper.imageToBase64(image);
+                String html = String.format(
+                        "  <body>\n    <img src=\"data:image/png;base64,%s\" alt=\"Route Diagram\">\n  </body>\n",
+                        base64);
+                if (refresh) {
+                    html = "<head><meta http-equiv=\"refresh\" content=\"5\"></head>\n" + html;
+                }
+                html = "<html>\n" + html + "</html>\n";
+                sj.add(html);
             }
-            html = "<html>\n" + html + "</html>\n";
-            sj.add(html);
         } catch (Exception e) {
             // ignore
         }
@@ -117,10 +124,18 @@ public class DiagramDevConsole extends AbstractDevConsole {
         JsonObject root = new JsonObject();
         try {
             RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(getCamelContext());
-            BufferedImage image = dumper.dumpRoutesAsImage(filter, RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
-                    metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
-            String base64 = dumper.imageToBase64(image);
-            root.put("image", base64);
+            if (isTextTheme(theme)) {
+                String text = dumper.dumpRoutesAsAsciiArt(filter,
+                        RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()),
+                        nodeWidth, isUnicodeTheme(theme));
+                root.put("text", text);
+            } else {
+                BufferedImage image = dumper.dumpRoutesAsImage(filter,
+                        RouteDiagramDumper.Theme.valueOf(theme.toUpperCase()),
+                        metric, RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()), nodeWidth, fontSize);
+                String base64 = dumper.imageToBase64(image);
+                root.put("image", base64);
+            }
         } catch (Exception e) {
             // ignore
         }
@@ -128,4 +143,12 @@ public class DiagramDevConsole extends AbstractDevConsole {
         return root;
     }
 
+    private static boolean isTextTheme(String theme) {
+        return "ascii".equalsIgnoreCase(theme) || "unicode".equalsIgnoreCase(theme);
+    }
+
+    private static boolean isUnicodeTheme(String theme) {
+        return "unicode".equalsIgnoreCase(theme);
+    }
+
 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java b/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java
index 79a6f842229f..1194e2e73f7d 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java
@@ -95,7 +95,7 @@ public interface RouteDiagramDumper {
      * @param filter to filter routes
      */
     default String dumpRoutesAsAsciiArt(String filter) {
-        return dumpRoutesAsAsciiArt(filter, NodeLabelMode.CODE, 180);
+        return dumpRoutesAsAsciiArt(filter, NodeLabelMode.CODE, 180, false);
     }
 
     /**
@@ -106,6 +106,18 @@ public interface RouteDiagramDumper {
      * @param nodeWidth the width in pixels of the node boxes
      */
     default String dumpRoutesAsAsciiArt(String filter, NodeLabelMode nodeLabel, int nodeWidth) {
+        return dumpRoutesAsAsciiArt(filter, nodeLabel, nodeWidth, false);
+    }
+
+    /**
+     * Dumps the routes as ASCII art or Unicode box-drawing text
+     *
+     * @param filter    to filter routes
+     * @param nodeLabel what information to display in the nodes
+     * @param nodeWidth the width in pixels of the node boxes
+     * @param unicode   whether to use Unicode box-drawing characters
+     */
+    default String dumpRoutesAsAsciiArt(String filter, NodeLabelMode nodeLabel, int nodeWidth, boolean unicode) {
         throw new UnsupportedOperationException();
     }
 


(camel) 02/07: CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer

Posted by da...@apache.org.
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 e22f003db138f3ad45afd22c71b55646ae744baa
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue May 12 13:48:21 2026 +0200

    CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 .../camel-diagram/src/main/docs/diagram.adoc       | 88 +++++++++++++++++++++-
 1 file changed, 84 insertions(+), 4 deletions(-)

diff --git a/components/camel-diagram/src/main/docs/diagram.adoc b/components/camel-diagram/src/main/docs/diagram.adoc
index d4517d27a0c8..49b08cdc5d87 100644
--- a/components/camel-diagram/src/main/docs/diagram.adoc
+++ b/components/camel-diagram/src/main/docs/diagram.adoc
@@ -10,14 +10,15 @@
 *Since Camel {since}*
 
 The Diagram module provides route diagram rendering capabilities for Apache Camel routes.
-It can generate visual route diagrams as PNG images representations from route structure data.
+It can generate visual route diagrams as PNG images or plain ASCII art text representations from route structure data.
 
 == Features
 
 * Render route diagrams as PNG images with colored nodes and scope boxes
+* Render route diagrams as plain ASCII art text for terminal output
 * Support for all Camel EIPs: choice, doTry/doCatch, filter, split, loop, multicast, and more
-* Scope boxes visually group branching and scoping EIPs
-* Multiple color themes: dark, light, transparent, or custom
+* Scope boxes visually group branching and scoping EIPs (PNG only)
+* Multiple color themes: dark, light, transparent, or custom (PNG only)
 
 == Usage
 
@@ -35,7 +36,7 @@ Add the `camel-diagram` dependency to your project:
 
 ==== Using Camel Java API
 
-You can use the diagram render with Camel based APIs such as:
+You can use the diagram renderer with the Camel API to render as PNG images:
 
 [source,java]
 ----
@@ -43,6 +44,14 @@ RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(context);
 BufferedImage image = dumper.dumpRoutesAsImage("*", RouteDiagramDumper.Theme.DARK);
 ----
 
+Or render as ASCII art text:
+
+[source,java]
+----
+RouteDiagramDumper dumper = PluginHelper.getRouteDiagramDumper(context);
+String ascii = dumper.dumpRoutesAsAsciiArt("*");
+----
+
 ==== Using standalone Java API
 
 Then use the API to render diagrams:
@@ -68,6 +77,25 @@ BufferedImage image = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagram
 ImageIO.write(image, "PNG", new File("diagram.png"));
 ----
 
+To render as ASCII art instead:
+
+[source,java]
+----
+import org.apache.camel.diagram.*;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.*;
+
+// Parse route structure from JSON
+List<RouteInfo> routes = RouteDiagramHelper.parseRoutes(jsonObject);
+
+// Layout and render as ASCII
+RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+LayoutRoute lr = engine.layoutRoute(routes.get(0), RouteDiagramLayoutEngine.PADDING);
+
+RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+String ascii = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+System.out.println(ascii);
+----
+
 === With Camel JBang
 
 The diagram rendering is used by the `camel cmd route-diagram` command in Camel JBang:
@@ -100,3 +128,55 @@ To use dark theme
 ----
 camel cmd route-diagram MyRoute.java --theme=dark
 ----
+
+== ASCII Art Rendering
+
+The ASCII art renderer produces plain text diagrams using box-drawing characters.
+This is useful for terminal output where images cannot be displayed.
+
+Nodes are drawn as boxes using `+`, `-`, and `|` characters, with arrows using `|` and `v`.
+Branching EIPs (choice, multicast, etc.) produce L-shaped arrows with horizontal connector lines.
+Long labels are automatically wrapped to fit within the box width.
+
+Example output for a simple route:
+
+----
+route1
+    +----------------------+
+    |      timer:tick      |
+    +----------------------+
+                |
+                |
+                |
+                v
+    +----------------------+
+    |        log:a         |
+    +----------------------+
+----
+
+Example output for a branching route with choice:
+
+----
+route1
+                  +----------------------+
+                  |      timer:tick      |
+                  +----------------------+
+                              |
+                              v
+                  +----------------------+
+                  |       choice()       |
+                  +----------------------+
+                              |
+              +---------------+---------------+
+              v                               v
+  +----------------------+        +----------------------+
+  |      when(...)       |        |     otherwise()      |
+  +----------------------+        +----------------------+
+              |                               |
+              v                               v
+  +----------------------+        +----------------------+
+  |        log:a         |        |        log:b         |
+  +----------------------+        +----------------------+
+----
+
+NOTE: Scope boxes (dotted boundaries for filter, split, etc.) are not included in ASCII art rendering.


(camel) 03/07: CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer

Posted by da...@apache.org.
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 d1e2aa4f45c866db8e7ffe0074c907e2fddb8d13
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue May 12 13:49:18 2026 +0200

    CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer
---
 components/camel-diagram/src/main/docs/diagram.adoc | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/components/camel-diagram/src/main/docs/diagram.adoc b/components/camel-diagram/src/main/docs/diagram.adoc
index 49b08cdc5d87..e8e6ca9e417a 100644
--- a/components/camel-diagram/src/main/docs/diagram.adoc
+++ b/components/camel-diagram/src/main/docs/diagram.adoc
@@ -140,6 +140,7 @@ Long labels are automatically wrapped to fit within the box width.
 
 Example output for a simple route:
 
+[source,text]
 ----
 route1
     +----------------------+
@@ -156,6 +157,7 @@ route1
 
 Example output for a branching route with choice:
 
+[source,text]
 ----
 route1
                   +----------------------+


(camel) 05/07: CAMEL-23485: camel-diagram - Add scope boxes to ASCII art renderer

Posted by da...@apache.org.
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 3363f7c54f91154a7d7584aade64dc24f0d87cba
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue May 12 14:29:20 2026 +0200

    CAMEL-23485: camel-diagram - Add scope boxes to ASCII art renderer
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 .../camel-diagram/src/main/docs/diagram.adoc       |  3 +-
 .../camel/diagram/RouteDiagramAsciiRenderer.java   | 52 +++++++++++++++++++++-
 .../org/apache/camel/diagram/RouteDiagramTest.java | 22 +++++++++
 3 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/components/camel-diagram/src/main/docs/diagram.adoc b/components/camel-diagram/src/main/docs/diagram.adoc
index e8e6ca9e417a..c9525206ede9 100644
--- a/components/camel-diagram/src/main/docs/diagram.adoc
+++ b/components/camel-diagram/src/main/docs/diagram.adoc
@@ -181,4 +181,5 @@ route1
   +----------------------+        +----------------------+
 ----
 
-NOTE: Scope boxes (dotted boundaries for filter, split, etc.) are not included in ASCII art rendering.
+Scope boxes (for filter, split, doTry, etc.) are rendered with dashed borders using `:` for vertical
+and `- - -` for horizontal lines.
diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
index d79cf001bf54..86c4c6ba1226 100644
--- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
+++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -20,10 +20,13 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
 
 import static org.apache.camel.diagram.RouteDiagramLayoutEngine.PADDING;
+import static org.apache.camel.diagram.RouteDiagramLayoutEngine.SCOPE_BOX_PAD;
 
 /**
  * Renders route diagrams as plain ASCII art text.
@@ -73,6 +76,12 @@ public class RouteDiagramAsciiRenderer {
         }
         drawText(grid, labelRow, toCol(PADDING), label);
 
+        for (LayoutNode ln : lr.nodes) {
+            if (ln.treeNode != null && RouteDiagramLayoutEngine.hasScope(ln.treeNode)) {
+                drawScopeBox(grid, ln);
+            }
+        }
+
         for (LayoutNode ln : lr.nodes) {
             if (ln.parentNode != null) {
                 if (ln.connectFromMerge) {
@@ -132,7 +141,7 @@ public class RouteDiagramAsciiRenderer {
         int fromCx = centerCol(from);
         int fromBottom = toRow(from.y) + boxHeight(from);
         int toCx = centerCol(to);
-        int toTop = toRow(to.y);
+        int toTop = getTopRow(to);
 
         drawArrowPath(grid, fromCx, fromBottom, toCx, toTop);
     }
@@ -141,7 +150,7 @@ public class RouteDiagramAsciiRenderer {
         int fromCx = toCol(to.mergeCx);
         int fromRow = toRow(to.mergeY);
         int toCx = centerCol(to);
-        int toTop = toRow(to.y);
+        int toTop = getTopRow(to);
 
         drawArrowPath(grid, fromCx, fromRow, toCx, toTop);
     }
@@ -178,6 +187,45 @@ public class RouteDiagramAsciiRenderer {
         }
     }
 
+    private void drawScopeBox(char[][] grid, LayoutNode scopeNode) {
+        TreeNode tn = scopeNode.treeNode;
+        Bounds bounds = new Bounds(
+                scopeNode.x, scopeNode.y,
+                scopeNode.x + nodeWidth, scopeNode.y + scopeNode.height);
+        for (TreeNode child : tn.children) {
+            RouteDiagramLayoutEngine.expandBoundsForBox(child, bounds, nodeWidth);
+        }
+
+        int col1 = toCol(bounds.minX - SCOPE_BOX_PAD);
+        int row1 = toRow(bounds.minY - SCOPE_BOX_PAD);
+        int col2 = toCol(bounds.maxX + SCOPE_BOX_PAD);
+        int row2 = toRow(bounds.maxY + SCOPE_BOX_PAD);
+
+        drawDashedHLine(grid, row1, col1, col2);
+        drawDashedHLine(grid, row2, col1, col2);
+        for (int r = row1 + 1; r < row2; r++) {
+            setChar(grid, r, col1, ':');
+            setChar(grid, r, col2, ':');
+        }
+    }
+
+    private void drawDashedHLine(char[][] grid, int row, int col1, int col2) {
+        for (int c = col1; c <= col2; c++) {
+            if (c == col1 || c == col2) {
+                setChar(grid, row, c, '+');
+            } else if ((c - col1) % 2 == 0) {
+                setChar(grid, row, c, '-');
+            }
+        }
+    }
+
+    private int getTopRow(LayoutNode node) {
+        if (node.treeNode != null && RouteDiagramLayoutEngine.hasScope(node.treeNode)) {
+            return toRow(node.y - SCOPE_BOX_PAD);
+        }
+        return toRow(node.y);
+    }
+
     private int centerCol(LayoutNode node) {
         return toCol(node.x + nodeWidth / 2);
     }
diff --git a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index 40d662a0b330..7024198eef7d 100644
--- a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++ b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -949,6 +949,28 @@ class RouteDiagramTest {
         assertTrue(result.contains("log:a"));
     }
 
+    @Test
+    void testAsciiDiagramWithScopeBox() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "direct:start", 0));
+        route.nodes.add(node("filter", "filter[header(x)]", 1));
+        route.nodes.add(node("log", "log[filtered]", 2));
+        route.nodes.add(node("to", "to[mock:end]", 1));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("filter[header(x)]"));
+        assertTrue(result.contains("log[filtered]"));
+        assertTrue(result.contains("to[mock:end]"));
+        assertTrue(result.contains(":"), "Scope box should have dashed vertical borders");
+        assertTrue(result.contains("+ -"), "Scope box should have dashed horizontal borders");
+    }
+
     @Test
     void testAsciiWrapTextShort() {
         List<String> lines = RouteDiagramAsciiRenderer.wrapText("timer:tick", 20);


(camel) 01/07: CAMEL-23485: camel-diagram - Add ASCII art renderer

Posted by da...@apache.org.
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 ae686d9fe99645a9871f9b22cf919c0381f47a8c
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue May 12 13:43:47 2026 +0200

    CAMEL-23485: camel-diagram - Add ASCII art renderer
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 .../camel/diagram/DefaultRouteDiagramDumper.java   |  29 ++
 .../camel/diagram/RouteDiagramAsciiRenderer.java   | 291 +++++++++++++++++++++
 .../org/apache/camel/diagram/RouteDiagramTest.java | 121 +++++++++
 .../org/apache/camel/spi/RouteDiagramDumper.java   |  20 ++
 4 files changed, 461 insertions(+)

diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
index 364d579cf7dd..81ec387197d2 100644
--- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
+++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
@@ -116,6 +116,16 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo
         return renderImage(routes, theme.name(), fontSize, nodeWidth, nodeLabel.name(), metrics);
     }
 
+    @Override
+    public String dumpRoutesAsAsciiArt(
+            String filter, RouteDiagramDumper.NodeLabelMode nodeLabel, int nodeWidth) {
+        DevConsole dc = getCamelContext().getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("route-structure");
+        JsonObject root = (JsonObject) dc.call(DevConsole.MediaType.JSON, Map.of("filter", filter));
+        var routes = RouteDiagramHelper.parseRoutes(root);
+        return renderAscii(routes, nodeWidth, nodeLabel.name());
+    }
+
     @Override
     public String imageToBase64(BufferedImage image) throws IOException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -152,4 +162,23 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo
         return renderer.renderDiagram(layoutRoutes, currentY, colors);
     }
 
+    private static String renderAscii(
+            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel) {
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
+                nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
+                RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase()));
+
+        List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new ArrayList<>();
+        int currentY = RouteDiagramLayoutEngine.PADDING;
+        for (RouteDiagramLayoutEngine.RouteInfo route : routes) {
+            RouteDiagramLayoutEngine.LayoutRoute lr = engine.layoutRoute(route, currentY);
+            layoutRoutes.add(lr);
+            currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
+        }
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(
+                nodeWidth * RouteDiagramLayoutEngine.SCALE);
+        return renderer.renderDiagram(layoutRoutes, currentY);
+    }
+
 }
diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
new file mode 100644
index 000000000000..d79cf001bf54
--- /dev/null
+++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -0,0 +1,291 @@
+/*
+ * 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.diagram;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+
+import static org.apache.camel.diagram.RouteDiagramLayoutEngine.PADDING;
+
+/**
+ * Renders route diagrams as plain ASCII art text.
+ */
+public class RouteDiagramAsciiRenderer {
+
+    private static final int MAX_WRAP_LINES = 3;
+    private static final int Y_SCALE = 20;
+    private static final int MIN_BOX_WIDTH = 16;
+    private static final int X_DIVISOR = 15;
+
+    private final int nodeWidth;
+    private final int boxWidth;
+
+    public RouteDiagramAsciiRenderer(int nodeWidth) {
+        this.nodeWidth = nodeWidth;
+        this.boxWidth = Math.max(MIN_BOX_WIDTH, nodeWidth / X_DIVISOR);
+    }
+
+    public int getBoxWidth() {
+        return boxWidth;
+    }
+
+    public String renderDiagram(List<LayoutRoute> layoutRoutes, int totalHeight) {
+        int maxPixelX = layoutRoutes.stream()
+                .mapToInt(lr -> lr.maxX).max().orElse(nodeWidth) + PADDING;
+        int gridWidth = toCol(maxPixelX) + boxWidth + 4;
+        int gridHeight = totalHeight / Y_SCALE + 20;
+
+        char[][] grid = new char[gridHeight][gridWidth];
+        for (char[] row : grid) {
+            Arrays.fill(row, ' ');
+        }
+
+        for (LayoutRoute lr : layoutRoutes) {
+            drawRoute(grid, lr);
+        }
+
+        return gridToString(grid);
+    }
+
+    private void drawRoute(char[][] grid, LayoutRoute lr) {
+        int labelRow = toRow(lr.labelY);
+        String label = lr.routeId;
+        if (lr.source != null && !lr.source.isEmpty()) {
+            label += " (" + lr.source + ")";
+        }
+        drawText(grid, labelRow, toCol(PADDING), label);
+
+        for (LayoutNode ln : lr.nodes) {
+            if (ln.parentNode != null) {
+                if (ln.connectFromMerge) {
+                    drawMergeArrow(grid, ln);
+                } else {
+                    drawArrow(grid, ln.parentNode, ln);
+                }
+            }
+        }
+
+        for (LayoutNode ln : lr.nodes) {
+            drawNode(grid, ln);
+        }
+    }
+
+    private void drawNode(char[][] grid, LayoutNode node) {
+        int col = toCol(node.x);
+        int row = toRow(node.y);
+        int innerWidth = boxWidth - 4;
+        List<String> lines = rewrapText(node, innerWidth);
+        int height = 2 + lines.size();
+
+        if (row + height >= grid.length) {
+            return;
+        }
+
+        setChar(grid, row, col, '+');
+        for (int c = col + 1; c < col + boxWidth - 1; c++) {
+            setChar(grid, row, c, '-');
+        }
+        setChar(grid, row, col + boxWidth - 1, '+');
+
+        int bottom = row + height - 1;
+        setChar(grid, bottom, col, '+');
+        for (int c = col + 1; c < col + boxWidth - 1; c++) {
+            setChar(grid, bottom, c, '-');
+        }
+        setChar(grid, bottom, col + boxWidth - 1, '+');
+
+        for (int i = 0; i < lines.size(); i++) {
+            int r = row + 1 + i;
+            setChar(grid, r, col, '|');
+            setChar(grid, r, col + boxWidth - 1, '|');
+            for (int c = col + 1; c < col + boxWidth - 1; c++) {
+                setChar(grid, r, c, ' ');
+            }
+            String text = lines.get(i);
+            if (text.length() > innerWidth) {
+                text = text.substring(0, Math.max(1, innerWidth - 3)) + "...";
+            }
+            int textCol = col + 2 + Math.max(0, (innerWidth - text.length()) / 2);
+            drawText(grid, r, textCol, text);
+        }
+    }
+
+    private void drawArrow(char[][] grid, LayoutNode from, LayoutNode to) {
+        int fromCx = centerCol(from);
+        int fromBottom = toRow(from.y) + boxHeight(from);
+        int toCx = centerCol(to);
+        int toTop = toRow(to.y);
+
+        drawArrowPath(grid, fromCx, fromBottom, toCx, toTop);
+    }
+
+    private void drawMergeArrow(char[][] grid, LayoutNode to) {
+        int fromCx = toCol(to.mergeCx);
+        int fromRow = toRow(to.mergeY);
+        int toCx = centerCol(to);
+        int toTop = toRow(to.y);
+
+        drawArrowPath(grid, fromCx, fromRow, toCx, toTop);
+    }
+
+    private void drawArrowPath(char[][] grid, int fromCx, int fromRow, int toCx, int toRow) {
+        if (fromRow >= toRow) {
+            return;
+        }
+
+        if (fromCx == toCx) {
+            for (int r = fromRow; r < toRow - 1; r++) {
+                plotLine(grid, r, fromCx, '|');
+            }
+            setChar(grid, toRow - 1, toCx, 'v');
+        } else {
+            int midRow = fromRow + (toRow - fromRow) / 2;
+
+            for (int r = fromRow; r < midRow; r++) {
+                plotLine(grid, r, fromCx, '|');
+            }
+
+            int minC = Math.min(fromCx, toCx);
+            int maxC = Math.max(fromCx, toCx);
+            for (int c = minC; c <= maxC; c++) {
+                plotLine(grid, midRow, c, '-');
+            }
+            setChar(grid, midRow, fromCx, '+');
+            setChar(grid, midRow, toCx, '+');
+
+            for (int r = midRow + 1; r < toRow - 1; r++) {
+                plotLine(grid, r, toCx, '|');
+            }
+            setChar(grid, toRow - 1, toCx, 'v');
+        }
+    }
+
+    private int centerCol(LayoutNode node) {
+        return toCol(node.x + nodeWidth / 2);
+    }
+
+    private int boxHeight(LayoutNode node) {
+        return 2 + rewrapText(node, boxWidth - 4).size();
+    }
+
+    private List<String> rewrapText(LayoutNode node, int maxWidth) {
+        String label = String.join("", node.wrappedLines);
+        return wrapText(label, maxWidth);
+    }
+
+    static List<String> wrapText(String text, int maxWidth) {
+        if (maxWidth <= 0 || text.length() <= maxWidth) {
+            return List.of(text);
+        }
+
+        List<String> lines = new ArrayList<>();
+        String remaining = text;
+
+        while (!remaining.isEmpty() && lines.size() < MAX_WRAP_LINES) {
+            if (remaining.length() <= maxWidth) {
+                lines.add(remaining);
+                remaining = "";
+                break;
+            }
+
+            int breakAt = -1;
+            for (int i = 0; i < maxWidth && i < remaining.length(); i++) {
+                char c = remaining.charAt(i);
+                if (c == ' ' || c == ':' || c == '/' || c == '.' || c == ',' || c == '&' || c == '?') {
+                    breakAt = i + 1;
+                }
+            }
+            if (breakAt <= 0) {
+                breakAt = maxWidth;
+            }
+
+            lines.add(remaining.substring(0, breakAt).stripTrailing());
+            remaining = remaining.substring(breakAt).stripLeading();
+        }
+
+        if (!remaining.isEmpty()) {
+            int lastIdx = lines.size() - 1;
+            String lastLine = lines.get(lastIdx);
+            if (lastLine.length() + remaining.length() <= maxWidth) {
+                lines.set(lastIdx, lastLine + remaining);
+            } else {
+                String combined = lastLine + remaining;
+                lines.set(lastIdx, combined.substring(0, Math.max(1, maxWidth - 3)) + "...");
+            }
+        }
+
+        return lines;
+    }
+
+    private int toCol(int pixelX) {
+        if (nodeWidth == 0) {
+            return 0;
+        }
+        return pixelX * boxWidth / nodeWidth;
+    }
+
+    private int toRow(int pixelY) {
+        return pixelY / Y_SCALE;
+    }
+
+    private void setChar(char[][] grid, int row, int col, char ch) {
+        if (row >= 0 && row < grid.length && col >= 0 && col < grid[0].length) {
+            grid[row][col] = ch;
+        }
+    }
+
+    private char getChar(char[][] grid, int row, int col) {
+        if (row >= 0 && row < grid.length && col >= 0 && col < grid[0].length) {
+            return grid[row][col];
+        }
+        return ' ';
+    }
+
+    private void plotLine(char[][] grid, int row, int col, char ch) {
+        char current = getChar(grid, row, col);
+        if ((current == '|' && ch == '-') || (current == '-' && ch == '|')) {
+            setChar(grid, row, col, '+');
+        } else {
+            setChar(grid, row, col, ch);
+        }
+    }
+
+    private void drawText(char[][] grid, int row, int col, String text) {
+        for (int i = 0; i < text.length(); i++) {
+            setChar(grid, row, col + i, text.charAt(i));
+        }
+    }
+
+    private String gridToString(char[][] grid) {
+        int lastRow = 0;
+        for (int r = 0; r < grid.length; r++) {
+            if (!new String(grid[r]).isBlank()) {
+                lastRow = r;
+            }
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int r = 0; r <= lastRow; r++) {
+            sb.append(new String(grid[r]).stripTrailing());
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+}
diff --git a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index 80e8f2359541..40d662a0b330 100644
--- a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++ b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -852,6 +852,127 @@ class RouteDiagramTest {
         assertNull(RouteDiagramHelper.extractSourceName(""));
     }
 
+    @Test
+    void testAsciiDiagramSequential() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "timer:tick", 0));
+        route.nodes.add(node("to", "log:a", 1));
+        route.nodes.add(node("to", "log:b", 1));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("route1"));
+        assertTrue(result.contains("timer:tick"));
+        assertTrue(result.contains("log:a"));
+        assertTrue(result.contains("log:b"));
+        assertTrue(result.contains("+"));
+        assertTrue(result.contains("v"));
+    }
+
+    @Test
+    void testAsciiDiagramBranching() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "timer:tick", 0));
+        route.nodes.add(node("choice", "choice()", 1));
+        route.nodes.add(node("when", "when(...)", 2));
+        route.nodes.add(node("to", "log:a", 3));
+        route.nodes.add(node("otherwise", "otherwise()", 2));
+        route.nodes.add(node("to", "log:b", 3));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("route1"));
+        assertTrue(result.contains("timer:tick"));
+        assertTrue(result.contains("choice()"));
+        assertTrue(result.contains("when(...)"));
+        assertTrue(result.contains("otherwise()"));
+        assertTrue(result.contains("log:a"));
+        assertTrue(result.contains("log:b"));
+        // branching arrows use horizontal lines
+        assertTrue(result.contains("-"), "Branching should contain horizontal arrow lines");
+    }
+
+    @Test
+    void testAsciiDiagramEmptyRoute() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "empty";
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("empty"));
+    }
+
+    @Test
+    void testAsciiDiagramWithSource() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.source = "test.yaml";
+        route.nodes.add(node("from", "timer:tick", 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("route1 (test.yaml)"));
+    }
+
+    @Test
+    void testAsciiDiagramLongLabel() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("from", "kafka:my-topic?brokers=localhost:9092&groupId=myGroup", 0));
+        route.nodes.add(node("to", "log:a", 1));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth());
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("kafka:"));
+        assertTrue(result.contains("log:a"));
+    }
+
+    @Test
+    void testAsciiWrapTextShort() {
+        List<String> lines = RouteDiagramAsciiRenderer.wrapText("timer:tick", 20);
+        assertEquals(1, lines.size());
+        assertEquals("timer:tick", lines.get(0));
+    }
+
+    @Test
+    void testAsciiWrapTextWrap() {
+        List<String> lines = RouteDiagramAsciiRenderer.wrapText("kafka:my-topic?brokers=localhost:9092", 20);
+        assertTrue(lines.size() > 1, "Long text should wrap");
+        String rejoined = String.join("", lines);
+        assertTrue(rejoined.contains("kafka:"));
+        assertTrue(rejoined.contains("9092"));
+    }
+
+    @Test
+    void testAsciiWrapTextTruncate() {
+        String veryLong = "a]".repeat(60);
+        List<String> lines = RouteDiagramAsciiRenderer.wrapText(veryLong, 20);
+        assertTrue(lines.size() <= 3, "Should not exceed 3 lines");
+        assertTrue(lines.get(lines.size() - 1).endsWith("..."), "Truncated text should end with ...");
+    }
+
     private static NodeInfo node(String type, String code, int level) {
         return node(type, code, level, null);
     }
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java b/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java
index 7060cc5d0da2..79a6f842229f 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RouteDiagramDumper.java
@@ -89,4 +89,24 @@ public interface RouteDiagramDumper {
      */
     String imageToBase64(BufferedImage image) throws IOException;
 
+    /**
+     * Dumps the routes as ASCII art text
+     *
+     * @param filter to filter routes
+     */
+    default String dumpRoutesAsAsciiArt(String filter) {
+        return dumpRoutesAsAsciiArt(filter, NodeLabelMode.CODE, 180);
+    }
+
+    /**
+     * Dumps the routes as ASCII art text
+     *
+     * @param filter    to filter routes
+     * @param nodeLabel what information to display in the nodes
+     * @param nodeWidth the width in pixels of the node boxes
+     */
+    default String dumpRoutesAsAsciiArt(String filter, NodeLabelMode nodeLabel, int nodeWidth) {
+        throw new UnsupportedOperationException();
+    }
+
 }


(camel) 04/07: CAMEL-23485: camel-diagram - Support --theme=ascii in route-diagram command

Posted by da...@apache.org.
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 <cl...@gmail.com>
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) <no...@anthropic.com>
---
 .../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;