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

davsclaus pushed a commit to branch CAMEL-23514-ascii-diagram-metrics
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 0f3fdfed08eee91e6ba8283349c59cd311519a31
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 14 10:15:25 2026 +0200

    CAMEL-23514: Add colored counters to text and image diagrams
    
    Add counter position tracking to RouteDiagramAsciiRenderer so callers
    can colorize success (green) and failure (red) counters.
    
    - Renderer records CounterPos (row, col, length, type) for each counter
    - renderDiagramAnsi() applies ANSI color codes for CLI terminal output
    - TUI styleDiagramLine() uses positions to create colored Spans
    - CLI route-diagram command uses ANSI-colored output
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/diagram/RouteDiagramAsciiRenderer.java   |  44 ++++++++-
 .../commands/action/CamelRouteDiagramAction.java   |   2 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 107 ++++++++++++++++-----
 3 files changed, 124 insertions(+), 29 deletions(-)

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 6bf83259298a..5d0f9bb0173a 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
@@ -58,6 +58,15 @@ public class RouteDiagramAsciiRenderer {
     private final int boxWidth;
     private final boolean unicode;
     private final boolean metrics;
+    private final List<CounterPos> counterPositions = new ArrayList<>();
+
+    public enum CounterType {
+        OK,
+        FAIL
+    }
+
+    public record CounterPos(int row, int col, int length, CounterType type) {
+    }
 
     public RouteDiagramAsciiRenderer(int nodeWidth) {
         this(nodeWidth, false, false);
@@ -78,7 +87,33 @@ public class RouteDiagramAsciiRenderer {
         return boxWidth;
     }
 
+    public List<CounterPos> getCounterPositions() {
+        return counterPositions;
+    }
+
+    public String renderDiagramAnsi(List<LayoutRoute> layoutRoutes, int 
totalHeight) {
+        String plain = renderDiagram(layoutRoutes, totalHeight);
+        if (counterPositions.isEmpty()) {
+            return plain;
+        }
+        String[] lines = plain.split("\n", -1);
+        for (CounterPos cp : counterPositions) {
+            if (cp.row >= 0 && cp.row < lines.length) {
+                String line = lines[cp.row];
+                if (cp.col >= 0 && cp.col + cp.length <= line.length()) {
+                    String before = line.substring(0, cp.col);
+                    String counter = line.substring(cp.col, cp.col + 
cp.length);
+                    String after = line.substring(cp.col + cp.length);
+                    String color = cp.type == CounterType.OK ? "\033[32m" : 
"\033[31m";
+                    lines[cp.row] = before + color + counter + "\033[0m" + 
after;
+                }
+            }
+        }
+        return String.join("\n", lines);
+    }
+
     public String renderDiagram(List<LayoutRoute> layoutRoutes, int 
totalHeight) {
+        counterPositions.clear();
         int maxPixelX = layoutRoutes.stream()
                 .mapToInt(lr -> lr.maxX).max().orElse(nodeWidth) + PADDING;
         int gridWidth = toCol(maxPixelX) + boxWidth + 4;
@@ -215,11 +250,16 @@ public class RouteDiagramAsciiRenderer {
         long failed = stat.exchangesFailed;
         long ok = total - failed;
         if (ok > 0) {
-            drawText(grid, toTop - 1, toCx + 2, "" + ok);
+            String okStr = "" + ok;
+            int col = toCx + 2;
+            drawText(grid, toTop - 1, col, okStr);
+            counterPositions.add(new CounterPos(toTop - 1, col, 
okStr.length(), CounterType.OK));
         }
         if (failed > 0) {
             String failStr = "" + failed;
-            drawText(grid, toTop - 1, toCx - 1 - failStr.length(), failStr);
+            int col = toCx - 1 - failStr.length();
+            drawText(grid, toTop - 1, col, failStr);
+            counterPositions.add(new CounterPos(toTop - 1, col, 
failStr.length(), CounterType.FAIL));
         }
     }
 
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 61084a5e9c63..c29cb7b8df37 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
@@ -212,7 +212,7 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
             if (isTextTheme()) {
                 RouteDiagramAsciiRenderer asciiRenderer
                         = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme(), pid > 0 && metric);
-                String ascii = asciiRenderer.renderDiagram(layoutRoutes, 
currentY);
+                String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, 
currentY);
 
                 if (output != null) {
                     String fileName = output.endsWith(".png")
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 33bb7dd581ba..fe6d9cc64e20 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -178,6 +178,7 @@ public class CamelMonitor extends CamelCommand {
     private boolean showDiagram;
     private boolean diagramTextMode;
     private boolean diagramMetrics = true;
+    private List<RouteDiagramAsciiRenderer.CounterPos> diagramCounterPositions 
= Collections.emptyList();
     private List<String> diagramLines = Collections.emptyList();
     private int diagramScroll;
     private int diagramScrollX;
@@ -1054,7 +1055,7 @@ public class CamelMonitor extends CamelCommand {
             if (diagramScrollX > 0) {
                 line = diagramScrollX < line.length() ? 
line.substring(diagramScrollX) : "";
             }
-            lines.add(styleDiagramLine(line));
+            lines.add(styleDiagramLine(line, i, diagramScrollX));
         }
 
         // Layout: outer block wraps everything, inner splits content + 
scrollbars
@@ -1175,10 +1176,36 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
-    private Line styleDiagramLine(String text) {
+    private Line styleDiagramLine(String text, int row, int scrollX) {
+        // Build counter color ranges for this row
+        List<int[]> counterRanges = new ArrayList<>();
+        for (RouteDiagramAsciiRenderer.CounterPos cp : 
diagramCounterPositions) {
+            if (cp.row() == row) {
+                int start = cp.col() - scrollX;
+                int end = start + cp.length();
+                if (end > 0 && start < text.length()) {
+                    start = Math.max(0, start);
+                    end = Math.min(end, text.length());
+                    int colorFlag = cp.type() == 
RouteDiagramAsciiRenderer.CounterType.OK ? 1 : 2;
+                    counterRanges.add(new int[] { start, end, colorFlag });
+                }
+            }
+        }
+
         List<Span> spans = new ArrayList<>();
         int idx = 0;
         while (idx < text.length()) {
+            // Check if current position is a counter
+            int[] counterRange = findCounterRange(counterRanges, idx);
+            if (counterRange != null) {
+                // Flush any text before the counter
+                Color counterColor = counterRange[2] == 1 ? Color.GREEN : 
Color.RED;
+                spans.add(Span.styled(text.substring(counterRange[0], 
counterRange[1]),
+                        Style.EMPTY.fg(counterColor).bold()));
+                idx = counterRange[1];
+                continue;
+            }
+
             int open = text.indexOf('[', idx);
             if (open < 0) {
                 spans.add(Span.styled(text.substring(idx), 
Style.EMPTY.fg(Color.WHITE)));
@@ -1190,7 +1217,7 @@ public class CamelMonitor extends CamelCommand {
                 break;
             }
             if (open > idx) {
-                spans.add(Span.styled(text.substring(idx, open), 
Style.EMPTY.fg(Color.GRAY)));
+                addStyledSegment(spans, text, idx, open, counterRanges);
             }
             String tag = text.substring(open + 1, close);
             Color tagColor = getDiagramNodeColor(tag);
@@ -1201,13 +1228,41 @@ public class CamelMonitor extends CamelCommand {
             int nextNewline = text.length();
             int labelEnd = nextOpen >= 0 ? nextOpen : nextNewline;
             if (afterTag < labelEnd) {
-                spans.add(Span.styled(text.substring(afterTag, labelEnd), 
Style.EMPTY.fg(Color.WHITE)));
+                addStyledSegment(spans, text, afterTag, labelEnd, 
counterRanges);
             }
             idx = labelEnd;
         }
         return Line.from(spans);
     }
 
+    private void addStyledSegment(List<Span> spans, String text, int from, int 
to, List<int[]> counterRanges) {
+        int pos = from;
+        while (pos < to) {
+            int[] cr = findCounterRange(counterRanges, pos);
+            if (cr != null && cr[0] < to) {
+                if (pos < cr[0]) {
+                    spans.add(Span.styled(text.substring(pos, cr[0]), 
Style.EMPTY.fg(Color.GRAY)));
+                }
+                int counterEnd = Math.min(cr[1], to);
+                Color counterColor = cr[2] == 1 ? Color.GREEN : Color.RED;
+                spans.add(Span.styled(text.substring(cr[0], counterEnd), 
Style.EMPTY.fg(counterColor).bold()));
+                pos = counterEnd;
+            } else {
+                spans.add(Span.styled(text.substring(pos, to), 
Style.EMPTY.fg(Color.GRAY)));
+                pos = to;
+            }
+        }
+    }
+
+    private static int[] findCounterRange(List<int[]> ranges, int pos) {
+        for (int[] range : ranges) {
+            if (pos >= range[0] && pos < range[1]) {
+                return range;
+            }
+        }
+        return null;
+    }
+
     private Color getDiagramNodeColor(String type) {
         if (type == null) {
             return Color.GRAY;
@@ -1339,14 +1394,27 @@ public class CamelMonitor extends CamelCommand {
         }
 
         if (textMode) {
-            String ascii = renderAscii(diagramRoutes, 
RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, "CODE", true, metrics);
+            RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
+                    RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, 
RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
+                    RouteDiagramLayoutEngine.NodeLabelMode.CODE);
+            List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new 
ArrayList<>();
+            int currentY = RouteDiagramLayoutEngine.PADDING;
+            for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) {
+                RouteDiagramLayoutEngine.LayoutRoute lr = 
engine.layoutRoute(r, currentY);
+                layoutRoutes.add(lr);
+                currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
+            }
+            RouteDiagramAsciiRenderer asciiRenderer = new 
RouteDiagramAsciiRenderer(
+                    RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH * 
RouteDiagramLayoutEngine.SCALE, true, metrics);
+            String ascii = asciiRenderer.renderDiagram(layoutRoutes, currentY);
+            List<RouteDiagramAsciiRenderer.CounterPos> positions = new 
ArrayList<>(asciiRenderer.getCounterPositions());
             List<String> result = new ArrayList<>();
             for (String line : ascii.split("\n", -1)) {
                 if (!line.isEmpty()) {
                     result.add(line);
                 }
             }
-            applyDiagramResult(routeId, result, null, null, null);
+            applyDiagramResult(routeId, result, null, null, null, positions);
         } else {
             TerminalImageCapabilities caps = 
TerminalImageCapabilities.detect();
             if (caps.supportsNativeImages()) {
@@ -1377,6 +1445,12 @@ public class CamelMonitor extends CamelCommand {
 
     private void applyDiagramResult(
             String routeId, List<String> lines, ImageData imageData, ImageData 
fullImageData, ImageProtocol protocol) {
+        applyDiagramResult(routeId, lines, imageData, fullImageData, protocol, 
Collections.emptyList());
+    }
+
+    private void applyDiagramResult(
+            String routeId, List<String> lines, ImageData imageData, ImageData 
fullImageData, ImageProtocol protocol,
+            List<RouteDiagramAsciiRenderer.CounterPos> positions) {
         if (runner == null) {
             return;
         }
@@ -1384,6 +1458,7 @@ public class CamelMonitor extends CamelCommand {
             boolean wasShowing = showDiagram;
             diagramRouteId = routeId;
             diagramLines = lines;
+            diagramCounterPositions = positions;
             diagramImageData = imageData;
             diagramFullImageData = fullImageData;
             diagramProtocol = protocol;
@@ -1399,26 +1474,6 @@ public class CamelMonitor extends CamelCommand {
         });
     }
 
-    private static String renderAscii(
-            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, 
String nodeLabel, boolean unicode,
-            boolean metrics) {
-        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, metrics);
-        return renderer.renderDiagram(layoutRoutes, currentY);
-    }
-
     private static JsonObject pollJsonResponse(Path outputFile, long timeout) {
         long start = System.currentTimeMillis();
         while (System.currentTimeMillis() - start < timeout) {

Reply via email to