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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new b25da666b6f0 CAMEL-23485: camel-diagram - Add ASCII art and Unicode 
text renderers (#23152)
b25da666b6f0 is described below

commit b25da666b6f0c9fa142c378ef4baf53425522d81
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 13 07:22:54 2026 +0200

    CAMEL-23485: camel-diagram - Add ASCII art and Unicode text renderers 
(#23152)
    
    * CAMEL-23485: camel-diagram - Add ASCII art renderer
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * CAMEL-23485: camel-diagram - Update documentation for ASCII art renderer
    
    * CAMEL-23485: camel-diagram - Support --theme=ascii in route-diagram 
command
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * CAMEL-23485: camel-diagram - Add scope boxes to ASCII art renderer
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * CAMEL-23485: camel-diagram - Add --theme=unicode with box-drawing 
characters
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * CAMEL-23485: camel-diagram - Support ascii/unicode themes in 
DiagramDevConsole
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * CAMEL-23485: Address review feedback
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../camel-diagram/src/main/docs/diagram.adoc       | 123 ++++++-
 .../camel/diagram/DefaultRouteDiagramDumper.java   |  29 ++
 .../apache/camel/diagram/DiagramDevConsole.java    |  55 ++-
 .../camel/diagram/RouteDiagramAsciiRenderer.java   | 391 +++++++++++++++++++++
 .../org/apache/camel/diagram/RouteDiagramTest.java | 206 +++++++++++
 .../org/apache/camel/spi/RouteDiagramDumper.java   |  32 ++
 .../camel-jbang-cmd-route-diagram.adoc             |   4 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../commands/action/CamelRouteDiagramAction.java   | 106 ++++--
 9 files changed, 894 insertions(+), 54 deletions(-)

diff --git a/components/camel-diagram/src/main/docs/diagram.adoc 
b/components/camel-diagram/src/main/docs/diagram.adoc
index d4517d27a0c8..87b765dedc59 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
+* 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,92 @@ 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:
+
+[source,text]
+----
+route1
+    +----------------------+
+    |      timer:tick      |
+    +----------------------+
+                |
+                |
+                |
+                v
+    +----------------------+
+    |        log:a         |
+    +----------------------+
+----
+
+Example output for a branching route with choice:
+
+[source,text]
+----
+route1
+                  +----------------------+
+                  |      timer:tick      |
+                  +----------------------+
+                              |
+                              v
+                  +----------------------+
+                  |       choice()       |
+                  +----------------------+
+                              |
+              +---------------+---------------+
+              v                               v
+  +----------------------+        +----------------------+
+  |      when(...)       |        |     otherwise()      |
+  +----------------------+        +----------------------+
+              |                               |
+              v                               v
+  +----------------------+        +----------------------+
+  |        log:a         |        |        log:b         |
+  +----------------------+        +----------------------+
+----
+
+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:
+
+[source,text]
+----
+route1
+┌──────────────────────┐
+│      timer:tick      │
+└──────────────────────┘
+           │
+           │
+           ▼
+┌──────────────────────┐
+│        log:a         │
+└──────────────────────┘
+----
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..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
@@ -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, 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(), unicode);
+    }
+
     @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, boolean unicode) {
+        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, 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/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..853c08ec7cd4
--- /dev/null
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -0,0 +1,391 @@
+/*
+ * 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.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 or Unicode box-drawing 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;
+
+    // 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_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() {
+        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.treeNode != null && 
RouteDiagramLayoutEngine.hasScope(ln.treeNode)) {
+                drawScopeBox(grid, ln);
+            }
+        }
+
+        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;
+        }
+
+        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, h);
+        }
+        setChar(grid, row, col + boxWidth - 1, unicode ? UNI_TR : '+');
+
+        int bottom = row + height - 1;
+        setChar(grid, bottom, col, unicode ? UNI_BL : '+');
+        for (int c = col + 1; c < col + boxWidth - 1; c++) {
+            setChar(grid, bottom, c, h);
+        }
+        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, v);
+            setChar(grid, r, col + boxWidth - 1, v);
+            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 = getTopRow(to);
+
+        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 = getTopRow(to);
+
+        drawArrowPath(grid, fromCx, fromRow, toCx, toTop);
+    }
+
+    private void drawArrowPath(char[][] grid, int fromCx, int fromRow, int 
toCx, int toRow) {
+        if (fromRow >= toRow) {
+            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, 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, v);
+            }
+
+            int minC = Math.min(fromCx, toCx);
+            int maxC = Math.max(fromCx, toCx);
+            for (int c = minC; c <= maxC; 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, '+');
+            }
+
+            for (int r = midRow + 1; r < toRow - 1; r++) {
+                plotLine(grid, r, toCx, v);
+            }
+            setChar(grid, toRow - 1, toCx, arrow);
+        }
+    }
+
+    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);
+
+        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, ':');
+            }
+        }
+    }
+
+    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);
+    }
+
+    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 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 ((isVertical(current) && isHorizontal(ch)) || 
(isHorizontal(current) && isVertical(ch))) {
+            setChar(grid, row, col, unicode ? UNI_CROSS : '+');
+        } 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..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
@@ -852,6 +852,212 @@ 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 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 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);
+        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..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
@@ -89,4 +89,36 @@ 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, false);
+    }
+
+    /**
+     * 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) {
+        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();
+    }
+
 }
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..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
@@ -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, 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 9e7a209d62c0..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 165b53b1cd7e..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
@@ -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, 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. Can also be 
set via DIAGRAM_COLORS env var.",
+                                      + "Use bg= for transparent. Use 
ascii/unicode 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 textMode = isTextTheme();
+        if (!textMode) {
             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 (!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 or --theme=unicode 
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,54 @@ 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 (isTextTheme()) {
+                RouteDiagramAsciiRenderer asciiRenderer
+                        = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme());
+                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 +353,14 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
         return RouteDiagramHelper.parseRoutes(jo);
     }
 
+    private boolean isTextTheme() {
+        return "ascii".equalsIgnoreCase(theme) || 
"unicode".equalsIgnoreCase(theme);
+    }
+
+    private boolean isUnicodeTheme() {
+        return "unicode".equalsIgnoreCase(theme);
+    }
+
     static NodeLabelMode parseNodeLabelMode(String value) {
         if (value == null || value.isBlank() || 
"code".equalsIgnoreCase(value)) {
             return NodeLabelMode.CODE;

Reply via email to