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) {
