This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch camel-23485-ascii-art-renderer in repository https://gitbox.apache.org/repos/asf/camel.git
commit b4d492f02c8036d15ebfde7c9f07a6728a49cfa8 Author: Claus Ibsen <[email protected]> AuthorDate: Tue May 12 14:12:05 2026 +0200 CAMEL-23485: camel-diagram - Support --theme=ascii in route-diagram command Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- .../camel-jbang-cmd-route-diagram.adoc | 4 +- .../META-INF/camel-jbang-commands-metadata.json | 2 +- .../commands/action/CamelRouteDiagramAction.java | 101 ++++++++++++++------- 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc index d118b519b5c9..b5cd5154893d 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc @@ -25,8 +25,8 @@ camel cmd route-diagram [options] | `--ignore-loading-error` | Whether to ignore route loading and compilation errors (use this with care!) | false | boolean | `--metric` | Whether to include live metrics (only possible for running Camel application) | true | boolean | `--node-label` | What text to display in diagram nodes: code, description, or both (default) | both | String -| `--output` | Save diagram to a PNG file instead of displaying in terminal | | String -| `--theme` | Color theme preset (dark, light, transparent) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var. | transparent | String +| `--output` | Save diagram to a file (PNG for image themes, text for ascii theme) | | String +| `--theme` | Color theme preset (dark, light, transparent, ascii) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Use ascii for plain text output. Can also be set via DIAGRAM_COLORS env var. | transparent | String | `--watch` | Execute periodically and showing output fullscreen | | boolean | `--width` | Image width in pixels (0 = auto) | 0 | int | `-h,--help` | Display the help and sub-commands | | boolean diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index 9e7a209d62c0..bca47153fd15 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -2,7 +2,7 @@ "commands": [ { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To [...] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java index 165b53b1cd7e..c14733d80f07 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java @@ -26,6 +26,7 @@ import java.util.List; import javax.imageio.ImageIO; +import org.apache.camel.diagram.RouteDiagramAsciiRenderer; import org.apache.camel.diagram.RouteDiagramHelper; import org.apache.camel.diagram.RouteDiagramLayoutEngine; import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute; @@ -68,14 +69,15 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { int width; @CommandLine.Option(names = { "--output" }, - description = "Save diagram to a PNG file instead of displaying in terminal") + description = "Save diagram to a file (PNG for image themes, text for ascii theme)") String output; @CommandLine.Option(names = { "--theme" }, - description = "Color theme preset (dark, light, transparent) or custom colors " + description = "Color theme preset (dark, light, transparent, ascii) or custom colors " + "(e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or " + "ANSI color names (e.g. from=seagreen:to=steelblue). " - + "Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var.", + + "Use bg= for transparent. Use ascii for plain text output. " + + "Can also be set via DIAGRAM_COLORS env var.", defaultValue = "transparent") String theme; @@ -115,18 +117,26 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { public Integer doCall() throws Exception { System.setProperty("java.awt.headless", "true"); - // if output in terminal then ensure terminal supports this - if (output == null) { + boolean ascii = isAsciiTheme(); + if (!ascii) { String colorSpec = System.getenv("DIAGRAM_COLORS"); colors = DiagramColors.parse(colorSpec != null ? colorSpec : theme); + } + + // if output in terminal then set up terminal + if (output == null) { terminal = TerminalBuilder.builder().system(true).build(); - terminalGraphics = TerminalGraphicsManager.getBestProtocol(terminal).orElse(null); - if (terminalGraphics == null) { - printer().println("Terminal does not support graphics protocols (Kitty, iTerm2, or Sixel)."); - printer().println("Try running in a supported terminal: Kitty, iTerm2, WezTerm, Ghostty, or VS Code."); - return 1; - } lineReader = LineReaderBuilder.builder().terminal(terminal).build(); + if (!ascii) { + terminalGraphics = TerminalGraphicsManager.getBestProtocol(terminal).orElse(null); + if (terminalGraphics == null) { + printer().println("Terminal does not support graphics protocols (Kitty, iTerm2, or Sixel)."); + printer().println( + "Try running in a supported terminal: Kitty, iTerm2, WezTerm, Ghostty, or VS Code."); + printer().println("Or use --theme=ascii for plain text output."); + return 1; + } + } } try { @@ -190,9 +200,6 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel); RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(boxWidth, fontSize, labelMode); - RouteDiagramRenderer renderer = new RouteDiagramRenderer( - engine.getNodeWidth(), fontSize * RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(), - pid > 0 && metric); List<LayoutRoute> layoutRoutes = new ArrayList<>(); int currentY = RouteDiagramLayoutEngine.PADDING; @@ -202,27 +209,53 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP; } - BufferedImage image; - try { - image = renderer.renderDiagram(layoutRoutes, currentY, colors); - } catch (IllegalStateException e) { - printer().println(e.getMessage()); - return 1; - } - - if (output != null) { - File file = new File(output); - File parentDir = file.getParentFile(); - if (parentDir != null) { - parentDir.mkdirs(); + if (isAsciiTheme()) { + RouteDiagramAsciiRenderer asciiRenderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth()); + String ascii = asciiRenderer.renderDiagram(layoutRoutes, currentY); + + if (output != null) { + String fileName = output.endsWith(".png") + ? output.substring(0, output.length() - 4) + ".txt" : output; + File file = new File(fileName); + File parentDir = file.getParentFile(); + if (parentDir != null) { + parentDir.mkdirs(); + } + Files.writeString(file.toPath(), ascii); + printer().println("Diagram saved to: " + file.getAbsolutePath()); + } else { + if (watch) { + clearScreen(); + } + printer().println(ascii); } - ImageIO.write(image, "PNG", file); - printer().println("Diagram saved to: " + file.getAbsolutePath()); } else { - if (watch) { - clearScreen(); + RouteDiagramRenderer renderer = new RouteDiagramRenderer( + engine.getNodeWidth(), fontSize * RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(), + pid > 0 && metric); + + BufferedImage image; + try { + image = renderer.renderDiagram(layoutRoutes, currentY, colors); + } catch (IllegalStateException e) { + printer().println(e.getMessage()); + return 1; + } + + if (output != null) { + File file = new File(output); + File parentDir = file.getParentFile(); + if (parentDir != null) { + parentDir.mkdirs(); + } + ImageIO.write(image, "PNG", file); + printer().println("Diagram saved to: " + file.getAbsolutePath()); + } else { + if (watch) { + clearScreen(); + } + doDisplayDiagram(image); } - doDisplayDiagram(image); } return 0; @@ -319,6 +352,10 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { return RouteDiagramHelper.parseRoutes(jo); } + private boolean isAsciiTheme() { + return "ascii".equalsIgnoreCase(theme); + } + static NodeLabelMode parseNodeLabelMode(String value) { if (value == null || value.isBlank() || "code".equalsIgnoreCase(value)) { return NodeLabelMode.CODE;
