From bc2d7d04d0ae1975b2d421cf49d929abe6ef551d Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 13:17:35 +0200 Subject: [PATCH 1/7] CAMEL-23420: Add text diagram mode (Shift+D) to TUI monitor Co-Authored-By: Claude Opus 4.7 --- .../jbang/core/commands/tui/CamelMonitor.java | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) 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 7fcb1d958e2f2..e43b721c65a11 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 @@ -169,6 +169,7 @@ public class CamelMonitor extends CamelCommand { // Diagram state private boolean showDiagram; + private boolean diagramTextMode; private List diagramLines = Collections.emptyList(); private int diagramScroll; private String diagramRouteId; @@ -330,11 +331,22 @@ private boolean handleEvent(Event event, TuiRunner runner) { routeSort = ROUTE_SORT_COLUMNS[routeSortIndex]; return true; } - if (tab == TAB_ROUTES && ke.isCharIgnoreCase('d')) { + if (tab == TAB_ROUTES && ke.isChar('d')) { if (showDiagram) { showDiagram = false; diagramImageData = null; } else { + diagramTextMode = false; + loadDiagramForSelectedRoute(); + } + return true; + } + if (tab == TAB_ROUTES && ke.isChar('D')) { + if (showDiagram) { + showDiagram = false; + diagramImageData = null; + } else { + diagramTextMode = true; loadDiagramForSelectedRoute(); } return true; @@ -1000,60 +1012,43 @@ private void loadDiagramForSelectedRoute() { diagramRouteId = selectedRoute.routeId; diagramScroll = 0; - TerminalImageCapabilities caps = TerminalImageCapabilities.detect(); - if (caps.supportsNativeImages()) { - RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(); - List layoutRoutes = new ArrayList<>(); - int totalHeight = 0; - for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) { - RouteDiagramLayoutEngine.LayoutRoute lr = engine.layoutRoute(r, totalHeight); - layoutRoutes.add(lr); - totalHeight = lr.maxY; - } - RouteDiagramRenderer renderer = new RouteDiagramRenderer(); - RouteDiagramRenderer.DiagramColors colors = RouteDiagramRenderer.DiagramColors.parse("transparent"); - java.awt.image.BufferedImage image = renderer.renderDiagram(layoutRoutes, totalHeight, colors); - diagramImageData = ImageData.fromBufferedImage(image); - diagramProtocol = caps.bestProtocol(); - diagramLines = Collections.emptyList(); - } else { + if (diagramTextMode) { diagramImageData = null; diagramProtocol = null; - StringBuilder sb = new StringBuilder(); - org.apache.camel.dsl.jbang.core.common.Printer capturingPrinter - = new org.apache.camel.dsl.jbang.core.common.Printer() { - @Override - public void println() { - sb.append('\n'); - } - - @Override - public void println(String line) { - sb.append(line).append('\n'); - } - - @Override - public void print(String output) { - sb.append(output); - } - - @Override - public void printf(String format, Object... args) { - sb.append(String.format(format, args)); - } - }; - String ascii = renderAscii(diagramRoutes, RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, "CODE", true); - sb.append(ascii); List result = new ArrayList<>(); - for (String line : sb.toString().split("\n", -1)) { + for (String line : ascii.split("\n", -1)) { if (!line.isEmpty()) { result.add(line); } } diagramLines = result; + } else { + TerminalImageCapabilities caps = TerminalImageCapabilities.detect(); + if (caps.supportsNativeImages()) { + RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(); + List layoutRoutes = new ArrayList<>(); + int totalHeight = 0; + for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) { + RouteDiagramLayoutEngine.LayoutRoute lr = engine.layoutRoute(r, totalHeight); + layoutRoutes.add(lr); + totalHeight = lr.maxY; + } + RouteDiagramRenderer renderer = new RouteDiagramRenderer(); + RouteDiagramRenderer.DiagramColors colors = RouteDiagramRenderer.DiagramColors.parse("transparent"); + java.awt.image.BufferedImage image = renderer.renderDiagram(layoutRoutes, totalHeight, colors); + diagramImageData = ImageData.fromBufferedImage(image); + diagramProtocol = caps.bestProtocol(); + diagramLines = Collections.emptyList(); + } else { + diagramImageData = null; + diagramProtocol = null; + diagramLines = List.of( + "(Terminal does not support image rendering)", + "(Press Shift+D for text diagram)"); + } } showDiagram = true; @@ -1699,6 +1694,8 @@ private void renderFooter(Frame frame, Rect area) { Span.raw(" sort "), Span.styled("d", Style.create().fg(Color.YELLOW).bold()), Span.raw(" diagram "), + Span.styled("D", Style.create().fg(Color.YELLOW).bold()), + Span.raw(" text diagram "), Span.styled("1-6", Style.create().fg(Color.YELLOW).bold()), Span.raw(" tabs "), Span.styled("Refresh: " + refreshLabel, Style.create().dim())); From ed0c9e671419313cf3363d3567b886dcb81ad580 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 13:38:48 +0200 Subject: [PATCH 2/7] CAMEL-23420: Fullscreen text diagram with scrollbars in TUI monitor Co-Authored-By: Claude Opus 4.7 --- .../jbang/core/commands/tui/CamelMonitor.java | 86 +++++++++++++++++-- 1 file changed, 79 insertions(+), 7 deletions(-) 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 e43b721c65a11..2eea5136e72c0 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 @@ -66,6 +66,8 @@ import dev.tamboui.widgets.block.BorderType; import dev.tamboui.widgets.gauge.Gauge; import dev.tamboui.widgets.paragraph.Paragraph; +import dev.tamboui.widgets.scrollbar.Scrollbar; +import dev.tamboui.widgets.scrollbar.ScrollbarState; import dev.tamboui.widgets.table.Cell; import dev.tamboui.widgets.table.Row; import dev.tamboui.widgets.table.Table; @@ -172,6 +174,9 @@ public class CamelMonitor extends CamelCommand { private boolean diagramTextMode; private List diagramLines = Collections.emptyList(); private int diagramScroll; + private int diagramScrollX; + private final ScrollbarState diagramVScrollState = new ScrollbarState(); + private final ScrollbarState diagramHScrollState = new ScrollbarState(); private String diagramRouteId; private ImageData diagramImageData; private ImageProtocol diagramProtocol; @@ -315,6 +320,18 @@ private boolean handleEvent(Event event, TuiRunner runner) { } return true; } + if (ke.isLeft()) { + if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { + diagramScrollX = Math.max(0, diagramScrollX - 1); + return true; + } + } + if (ke.isRight()) { + if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { + diagramScrollX++; + return true; + } + } // Enter to drill into selected integration if (ke.isKey(KeyCode.ENTER) && tab == TAB_OVERVIEW) { @@ -694,6 +711,12 @@ private void renderRoutes(Frame frame, Rect area) { return; } + // Fullscreen text diagram mode + if (showDiagram && diagramTextMode && !diagramLines.isEmpty()) { + renderDiagram(frame, area); + return; + } + // Sort routes List sortedRoutes = new ArrayList<>(info.routes); sortedRoutes.sort(this::sortRoute); @@ -859,23 +882,71 @@ private void renderDiagram(Frame frame, Rect area) { return; } + // Compute max width for horizontal scrolling + int maxWidth = 0; + for (String line : diagramLines) { + maxWidth = Math.max(maxWidth, line.length()); + } + Rect inner = block.inner(area); - int visibleLines = inner.height(); - int maxScroll = Math.max(0, diagramLines.size() - visibleLines); - diagramScroll = Math.min(diagramScroll, maxScroll); + // Reserve 1 col for vertical scrollbar, 1 row for horizontal scrollbar + int visibleLines = Math.max(1, inner.height() - 1); + int visibleCols = Math.max(1, inner.width() - 1); + + int maxVScroll = Math.max(0, diagramLines.size() - visibleLines); + int maxHScroll = Math.max(0, maxWidth - visibleCols); + diagramScroll = Math.min(diagramScroll, maxVScroll); + diagramScrollX = Math.min(diagramScrollX, maxHScroll); + // Build visible lines with horizontal offset applied List lines = new ArrayList<>(); int end = Math.min(diagramScroll + visibleLines, diagramLines.size()); for (int i = diagramScroll; i < end; i++) { - lines.add(styleDiagramLine(diagramLines.get(i))); + String line = diagramLines.get(i); + if (diagramScrollX > 0) { + line = diagramScrollX < line.length() ? line.substring(diagramScrollX) : ""; + } + lines.add(styleDiagramLine(line)); } + // Layout: outer block wraps everything, inner splits content + scrollbars + frame.renderWidget(block, area); + + // Vertical layout inside the block: [content row (fill), horizontal scrollbar (1 row)] + List vChunks = Layout.vertical() + .constraints(Constraint.fill(), Constraint.length(1)) + .split(inner); + + // Horizontal layout for content row: [text (fill), vertical scrollbar (1 col)] + List hChunks = Layout.horizontal() + .constraints(Constraint.fill(), Constraint.length(1)) + .split(vChunks.get(0)); + + // Render diagram text Paragraph paragraph = Paragraph.builder() .text(Text.from(lines)) - .block(block) .build(); + frame.renderWidget(paragraph, hChunks.get(0)); + + // Render vertical scrollbar + diagramVScrollState.contentLength(diagramLines.size()); + diagramVScrollState.viewportContentLength(visibleLines); + diagramVScrollState.position(diagramScroll); + frame.renderStatefulWidget( + Scrollbar.builder() + .thumbStyle(Style.create().fg(Color.rgb(0xF6, 0x91, 0x23))) + .build(), + hChunks.get(1), diagramVScrollState); - frame.renderWidget(paragraph, area); + // Render horizontal scrollbar + if (maxWidth > visibleCols) { + diagramHScrollState.contentLength(maxWidth); + diagramHScrollState.viewportContentLength(visibleCols); + diagramHScrollState.position(diagramScrollX); + frame.renderStatefulWidget( + Scrollbar.horizontal(), + vChunks.get(1), diagramHScrollState); + } } private Line styleDiagramLine(String text) { @@ -1011,6 +1082,7 @@ private void loadDiagramForSelectedRoute() { diagramRouteId = selectedRoute.routeId; diagramScroll = 0; + diagramScrollX = 0; if (diagramTextMode) { diagramImageData = null; @@ -1680,7 +1752,7 @@ private void renderFooter(Frame frame, Rect area) { Span.raw("/"), Span.styled("Esc", Style.create().fg(Color.YELLOW).bold()), Span.raw(" close "), - Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()), + Span.styled("\u2191\u2193\u2190\u2192", Style.create().fg(Color.YELLOW).bold()), Span.raw(" scroll "), Span.styled("PgUp/PgDn", Style.create().fg(Color.YELLOW).bold()), Span.raw(" page")); From 0e660059afbcdc3c93d960fe1cd8a0416cd4fe46 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 13:41:17 +0200 Subject: [PATCH 3/7] CAMEL-23420: Show matching close key (d/D) in diagram footer Co-Authored-By: Claude Opus 4.7 --- .../apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 2eea5136e72c0..2d34377cee4fd 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 @@ -1747,8 +1747,9 @@ private void renderFooter(Frame frame, Rect area) { Span.styled("Refresh: " + refreshLabel, Style.create().dim())); } else if (tab == TAB_ROUTES) { if (showDiagram) { + String closeKey = diagramTextMode ? "D" : "d"; footer = Line.from( - Span.styled(" d", Style.create().fg(Color.YELLOW).bold()), + Span.styled(" " + closeKey, Style.create().fg(Color.YELLOW).bold()), Span.raw("/"), Span.styled("Esc", Style.create().fg(Color.YELLOW).bold()), Span.raw(" close "), From bd647c505de93eee598b147d52933ae51dbbabb8 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 13:44:45 +0200 Subject: [PATCH 4/7] CAMEL-23420: Show live route info header in fullscreen text diagram Co-Authored-By: Claude Opus 4.7 --- .../jbang/core/commands/tui/CamelMonitor.java | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) 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 2d34377cee4fd..4dbd995903532 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 @@ -713,7 +713,12 @@ private void renderRoutes(Frame frame, Rect area) { // Fullscreen text diagram mode if (showDiagram && diagramTextMode && !diagramLines.isEmpty()) { - renderDiagram(frame, area); + // Split: route info header (4 rows) + diagram (fill) + List fullChunks = Layout.vertical() + .constraints(Constraint.length(4), Constraint.fill()) + .split(area); + renderRouteHeader(frame, fullChunks.get(0), info); + renderDiagram(frame, fullChunks.get(1)); return; } @@ -865,6 +870,64 @@ private void renderProcessors(Frame frame, Rect area, RouteInfo route) { frame.renderStatefulWidget(table, area, new TableState()); } + private void renderRouteHeader(Frame frame, Rect area, IntegrationInfo info) { + RouteInfo route = null; + if (diagramRouteId != null) { + for (RouteInfo r : info.routes) { + if (diagramRouteId.equals(r.routeId)) { + route = r; + break; + } + } + } + + List rows = new ArrayList<>(); + if (route != null) { + Style stateStyle = "Started".equals(route.state) + ? Style.create().fg(Color.GREEN) + : Style.create().fg(Color.RED); + Style failStyle = route.failed > 0 + ? Style.create().fg(Color.RED).bold() + : Style.create(); + rows.add(Row.from( + Cell.from(Span.styled(route.routeId != null ? route.routeId : "", Style.create().fg(Color.CYAN))), + Cell.from(route.from != null ? route.from : ""), + Cell.from(Span.styled(route.state != null ? route.state : "", stateStyle)), + Cell.from(route.uptime != null ? route.uptime : ""), + Cell.from(route.throughput != null ? route.throughput : ""), + Cell.from(String.valueOf(route.total)), + Cell.from(Span.styled(String.valueOf(route.failed), failStyle)), + Cell.from(route.meanTime + "/" + route.maxTime))); + } + + Table table = Table.builder() + .rows(rows) + .header(Row.from( + Cell.from(Span.styled("ROUTE", Style.create().bold())), + Cell.from(Span.styled("FROM", Style.create().bold())), + Cell.from(Span.styled("STATE", Style.create().bold())), + Cell.from(Span.styled("UPTIME", Style.create().bold())), + Cell.from(Span.styled("THRUPUT", Style.create().bold())), + Cell.from(Span.styled("TOTAL", Style.create().bold())), + Cell.from(Span.styled("FAILED", Style.create().bold())), + Cell.from(Span.styled("MEAN/MAX", Style.create().bold())))) + .widths( + Constraint.length(12), + Constraint.fill(), + Constraint.length(10), + Constraint.length(8), + Constraint.length(10), + Constraint.length(8), + Constraint.length(8), + Constraint.length(12)) + .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue()) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" Route [" + info.name + "] ").build()) + .build(); + + frame.renderStatefulWidget(table, area, new TableState()); + } + private void renderDiagram(Frame frame, Rect area) { Block block = Block.builder() .borderType(BorderType.ROUNDED) From e5cc7d23d0591f3cd7e3954fe4ad71a0c298ce17 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 13:46:21 +0200 Subject: [PATCH 5/7] CAMEL-23420: Hide diagram block title in fullscreen text mode Co-Authored-By: Claude Opus 4.7 --- .../apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4dbd995903532..e8a9ad6bf1016 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 @@ -931,7 +931,7 @@ private void renderRouteHeader(Frame frame, Rect area, IntegrationInfo info) { private void renderDiagram(Frame frame, Rect area) { Block block = Block.builder() .borderType(BorderType.ROUNDED) - .title(" Diagram [" + diagramRouteId + "] ") + .title(diagramTextMode ? "" : " Diagram [" + diagramRouteId + "] ") .build(); if (diagramImageData != null) { From fa86aa9c41e0d38766c414c28e1c4969445d749a Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 14:14:58 +0200 Subject: [PATCH 6/7] CAMEL-23420: Fullscreen image diagram with scrollbars and flicker fix Co-Authored-By: Claude Opus 4.7 --- .../jbang/core/commands/tui/CamelMonitor.java | 141 +++++++++++++++--- 1 file changed, 118 insertions(+), 23 deletions(-) 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 e8a9ad6bf1016..56b62addaf4a8 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 @@ -179,7 +179,12 @@ public class CamelMonitor extends CamelCommand { private final ScrollbarState diagramHScrollState = new ScrollbarState(); private String diagramRouteId; private ImageData diagramImageData; + private ImageData diagramFullImageData; private ImageProtocol diagramProtocol; + private int diagramCropX = -1; + private int diagramCropY = -1; + private int diagramCropW = -1; + private int diagramCropH = -1; private volatile long lastRefresh; @@ -216,13 +221,13 @@ public Integer doCall() throws Exception { private boolean handleEvent(Event event, TuiRunner runner) { if (event instanceof MouseEvent me) { - if (showDiagram && diagramImageData == null && tabsState.selected() == TAB_ROUTES) { + if (showDiagram && tabsState.selected() == TAB_ROUTES) { if (me.kind() == MouseEventKind.SCROLL_UP) { diagramScroll = Math.max(0, diagramScroll - 3); return true; } if (me.kind() == MouseEventKind.SCROLL_DOWN) { - diagramScroll = Math.min(Math.max(0, diagramLines.size() - 1), diagramScroll + 3); + diagramScroll += 3; return true; } } @@ -233,6 +238,7 @@ private boolean handleEvent(Event event, TuiRunner runner) { if (showDiagram) { showDiagram = false; diagramImageData = null; + diagramFullImageData = null; return true; } // If in a detail tab, go back to overview first @@ -284,7 +290,7 @@ private boolean handleEvent(Event event, TuiRunner runner) { // Navigation (all tabs) if (ke.isUp()) { - if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { + if (showDiagram && tab == TAB_ROUTES) { diagramScroll = Math.max(0, diagramScroll - 1); } else { navigateUp(); @@ -292,15 +298,15 @@ private boolean handleEvent(Event event, TuiRunner runner) { return true; } if (ke.isDown()) { - if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { - diagramScroll = Math.min(Math.max(0, diagramLines.size() - 1), diagramScroll + 1); + if (showDiagram && tab == TAB_ROUTES) { + diagramScroll++; } else { navigateDown(); } return true; } if (ke.isKey(KeyCode.PAGE_UP)) { - if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { + if (showDiagram && tab == TAB_ROUTES) { diagramScroll = Math.max(0, diagramScroll - 20); } else if (tab == TAB_LOG) { logFollowMode = false; @@ -311,8 +317,8 @@ private boolean handleEvent(Event event, TuiRunner runner) { return true; } if (ke.isKey(KeyCode.PAGE_DOWN)) { - if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { - diagramScroll = Math.min(Math.max(0, diagramLines.size() - 1), diagramScroll + 20); + if (showDiagram && tab == TAB_ROUTES) { + diagramScroll += 20; } else if (tab == TAB_LOG) { for (int i = 0; i < 20; i++) { logTableState.selectNext(filteredLogEntries.size()); @@ -321,13 +327,13 @@ private boolean handleEvent(Event event, TuiRunner runner) { return true; } if (ke.isLeft()) { - if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { + if (showDiagram && tab == TAB_ROUTES) { diagramScrollX = Math.max(0, diagramScrollX - 1); return true; } } if (ke.isRight()) { - if (showDiagram && diagramImageData == null && tab == TAB_ROUTES) { + if (showDiagram && tab == TAB_ROUTES) { diagramScrollX++; return true; } @@ -352,6 +358,7 @@ private boolean handleEvent(Event event, TuiRunner runner) { if (showDiagram) { showDiagram = false; diagramImageData = null; + diagramFullImageData = null; } else { diagramTextMode = false; loadDiagramForSelectedRoute(); @@ -362,6 +369,7 @@ private boolean handleEvent(Event event, TuiRunner runner) { if (showDiagram) { showDiagram = false; diagramImageData = null; + diagramFullImageData = null; } else { diagramTextMode = true; loadDiagramForSelectedRoute(); @@ -441,10 +449,13 @@ private boolean handleEvent(Event event, TuiRunner runner) { } if (event instanceof TickEvent) { long now = System.currentTimeMillis(); - if (now - lastRefresh >= refreshInterval) { + long interval = showDiagram ? Math.max(refreshInterval, 1000) : refreshInterval; + if (now - lastRefresh >= interval) { refreshData(); + return true; } - return true; + // Skip re-render when showing image diagram to prevent flicker + return diagramFullImageData == null; } return false; } @@ -711,8 +722,8 @@ private void renderRoutes(Frame frame, Rect area) { return; } - // Fullscreen text diagram mode - if (showDiagram && diagramTextMode && !diagramLines.isEmpty()) { + // Fullscreen diagram mode + if (showDiagram && (diagramTextMode ? !diagramLines.isEmpty() : diagramFullImageData != null)) { // Split: route info header (4 rows) + diagram (fill) List fullChunks = Layout.vertical() .constraints(Constraint.length(4), Constraint.fill()) @@ -934,14 +945,8 @@ private void renderDiagram(Frame frame, Rect area) { .title(diagramTextMode ? "" : " Diagram [" + diagramRouteId + "] ") .build(); - if (diagramImageData != null) { - Image img = Image.builder() - .data(diagramImageData) - .protocol(diagramProtocol) - .scaling(ImageScaling.FIT) - .block(block) - .build(); - frame.renderWidget(img, area); + if (diagramFullImageData != null) { + renderImageDiagram(frame, area, block); return; } @@ -1012,6 +1017,88 @@ private void renderDiagram(Frame frame, Rect area) { } } + private void renderImageDiagram(Frame frame, Rect area, Block block) { + int imgW = diagramFullImageData.width(); + int imgH = diagramFullImageData.height(); + + Rect inner = block.inner(area); + // Convert cell area to pixel viewport using protocol resolution + int pxPerCol = diagramProtocol.resolution().widthMultiplier(); + int pxPerRow = diagramProtocol.resolution().heightMultiplier(); + // Reserve 1 col for vertical scrollbar, 1 row for horizontal scrollbar + int viewCols = Math.max(1, inner.width() - 1); + int viewRows = Math.max(1, inner.height() - 1); + int viewW = viewCols * pxPerCol; + int viewH = viewRows * pxPerRow; + + // Scroll units are in cells; convert to pixels for clamping + int maxScrollY = Math.max(0, (imgH - viewH + pxPerRow - 1) / pxPerRow); + int maxScrollX = Math.max(0, (imgW - viewW + pxPerCol - 1) / pxPerCol); + diagramScroll = Math.min(diagramScroll, maxScrollY); + diagramScrollX = Math.min(diagramScrollX, maxScrollX); + + int cropX = Math.min(diagramScrollX * pxPerCol, imgW); + int cropY = Math.min(diagramScroll * pxPerRow, imgH); + int cropW = Math.min(viewW, imgW - cropX); + int cropH = Math.min(viewH, imgH - cropY); + + if (cropW > 0 && cropH > 0) { + if (cropX != diagramCropX || cropY != diagramCropY + || cropW != diagramCropW || cropH != diagramCropH) { + diagramImageData = diagramFullImageData.crop(cropX, cropY, cropW, cropH); + diagramCropX = cropX; + diagramCropY = cropY; + diagramCropW = cropW; + diagramCropH = cropH; + } + } else if (diagramImageData != diagramFullImageData) { + diagramImageData = diagramFullImageData; + } + + // Render the outer block border + frame.renderWidget(block, area); + + // Vertical layout inside the block: [image+vscrollbar (fill), hscrollbar (1 row)] + List vChunks = Layout.vertical() + .constraints(Constraint.fill(), Constraint.length(1)) + .split(inner); + + // Horizontal layout: [image (fill), vertical scrollbar (1 col)] + List hChunks = Layout.horizontal() + .constraints(Constraint.fill(), Constraint.length(1)) + .split(vChunks.get(0)); + + // Render cropped image + Image img = Image.builder() + .data(diagramImageData) + .protocol(diagramProtocol) + .scaling(ImageScaling.FIT) + .build(); + frame.renderWidget(img, hChunks.get(0)); + + // Render vertical scrollbar + int totalRows = (imgH + pxPerRow - 1) / pxPerRow; + diagramVScrollState.contentLength(totalRows); + diagramVScrollState.viewportContentLength(viewRows); + diagramVScrollState.position(diagramScroll); + frame.renderStatefulWidget( + Scrollbar.builder() + .thumbStyle(Style.create().fg(Color.rgb(0xF6, 0x91, 0x23))) + .build(), + hChunks.get(1), diagramVScrollState); + + // Render horizontal scrollbar + if (imgW > viewW) { + int totalCols = (imgW + pxPerCol - 1) / pxPerCol; + diagramHScrollState.contentLength(totalCols); + diagramHScrollState.viewportContentLength(viewCols); + diagramHScrollState.position(diagramScrollX); + frame.renderStatefulWidget( + Scrollbar.horizontal(), + vChunks.get(1), diagramHScrollState); + } + } + private Line styleDiagramLine(String text) { List spans = new ArrayList<>(); int idx = 0; @@ -1146,9 +1233,14 @@ private void loadDiagramForSelectedRoute() { diagramRouteId = selectedRoute.routeId; diagramScroll = 0; diagramScrollX = 0; + diagramCropX = -1; + diagramCropY = -1; + diagramCropW = -1; + diagramCropH = -1; if (diagramTextMode) { diagramImageData = null; + diagramFullImageData = null; diagramProtocol = null; String ascii = renderAscii(diagramRoutes, RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, "CODE", true); @@ -1174,11 +1266,14 @@ private void loadDiagramForSelectedRoute() { RouteDiagramRenderer renderer = new RouteDiagramRenderer(); RouteDiagramRenderer.DiagramColors colors = RouteDiagramRenderer.DiagramColors.parse("transparent"); java.awt.image.BufferedImage image = renderer.renderDiagram(layoutRoutes, totalHeight, colors); - diagramImageData = ImageData.fromBufferedImage(image); + ImageData fullImage = ImageData.fromBufferedImage(image); + diagramFullImageData = fullImage.resize(fullImage.width() / 2, fullImage.height() / 2); + diagramImageData = diagramFullImageData; diagramProtocol = caps.bestProtocol(); diagramLines = Collections.emptyList(); } else { diagramImageData = null; + diagramFullImageData = null; diagramProtocol = null; diagramLines = List.of( "(Terminal does not support image rendering)", From 06d6b6eea19d230b58a7c2d33f111faaad50aabb Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 13 May 2026 15:09:03 +0200 Subject: [PATCH 7/7] CAMEL-23420: Add Home/End keys for diagram top/bottom scrolling Co-Authored-By: Claude Opus 4.7 --- .../jbang/core/commands/tui/CamelMonitor.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 56b62addaf4a8..2c20cbb0a26fa 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 @@ -338,6 +338,19 @@ private boolean handleEvent(Event event, TuiRunner runner) { return true; } } + if (ke.isKey(KeyCode.HOME)) { + if (showDiagram && tab == TAB_ROUTES) { + diagramScroll = 0; + diagramScrollX = 0; + return true; + } + } + if (ke.isKey(KeyCode.END)) { + if (showDiagram && tab == TAB_ROUTES) { + diagramScroll = Integer.MAX_VALUE; + return true; + } + } // Enter to drill into selected integration if (ke.isKey(KeyCode.ENTER) && tab == TAB_OVERVIEW) { @@ -1914,7 +1927,9 @@ private void renderFooter(Frame frame, Rect area) { Span.styled("\u2191\u2193\u2190\u2192", Style.create().fg(Color.YELLOW).bold()), Span.raw(" scroll "), Span.styled("PgUp/PgDn", Style.create().fg(Color.YELLOW).bold()), - Span.raw(" page")); + Span.raw(" page "), + Span.styled("Home/End", Style.create().fg(Color.YELLOW).bold()), + Span.raw(" top/bottom")); } else { footer = Line.from( Span.styled(" Esc", Style.create().fg(Color.YELLOW).bold()),