This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/camel-tui-polish in repository https://gitbox.apache.org/repos/asf/camel.git
commit cd8f9d954383e58c1339468e0fecd752c0455ce8 Author: Claus Ibsen <[email protected]> AuthorDate: Sat May 16 21:01:29 2026 +0200 TUI: add 'c' key to show source code for selected route on Routes tab Press 'c' on the Routes tab to fetch and display the source code of the currently selected route. The view auto-scrolls to the route's start line, supports arrow/PgUp/PgDn/Home/End scrolling with both vertical and horizontal scrollbars, and shows the source filename and line number in the title. Press 'c' or Esc to close. Shows a message when no source is available (e.g. programmatic routes without a file location). Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 221 +++++++++++++++++++++ 1 file changed, 221 insertions(+) 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 a88e496e29b1..7e6bf1e6eb0e 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 @@ -254,6 +254,15 @@ public class CamelMonitor extends CamelCommand { private int diagramCropH = -1; private volatile long lastRefresh; + private boolean showSource; + private List<String> sourceLines = Collections.emptyList(); + private String sourceTitle; + private int sourceScroll; + private int sourceScrollX; + private final ScrollbarState sourceVScrollState = new ScrollbarState(); + private final ScrollbarState sourceHScrollState = new ScrollbarState(); + private final AtomicBoolean sourceLoading = new AtomicBoolean(false); + private final AtomicBoolean refreshInProgress = new AtomicBoolean(false); private final AtomicBoolean diagramLoading = new AtomicBoolean(false); private TuiRunner runner; @@ -296,6 +305,10 @@ public class CamelMonitor extends CamelCommand { if (event instanceof KeyEvent ke) { // Escape: navigate back if (ke.isCancel()) { + if (showSource) { + showSource = false; + return true; + } if (showDiagram) { showDiagram = false; diagramImageData = null; @@ -520,6 +533,37 @@ public class CamelMonitor extends CamelCommand { return true; } + if (tab == TAB_ROUTES && ke.isChar('c')) { + if (showSource) { + showSource = false; + } else { + loadSourceForSelectedRoute(); + } + return true; + } + if (tab == TAB_ROUTES && showSource) { + if (ke.isUp()) { + sourceScroll = Math.max(0, sourceScroll - 1); + } else if (ke.isDown()) { + sourceScroll++; + } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) { + sourceScroll = Math.max(0, sourceScroll - 20); + } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) { + sourceScroll += 20; + } else if (ke.isLeft()) { + sourceScrollX = Math.max(0, sourceScrollX - 1); + } else if (ke.isRight()) { + sourceScrollX++; + } else if (ke.isHome()) { + sourceScroll = 0; + sourceScrollX = 0; + } else if (ke.isEnd()) { + sourceScroll = Integer.MAX_VALUE; + } else { + return false; + } + return true; + } if (tab == TAB_ROUTES && showDiagram && ke.isCharIgnoreCase('m')) { diagramMetrics = !diagramMetrics; diagramLoading.set(false); @@ -747,6 +791,12 @@ public class CamelMonitor extends CamelCommand { // NOTE: When adding a new tab, reset its view state here too so switching integrations // on the Overview always shows a clean slate for the newly selected integration. private void resetIntegrationTabState() { + // Source (TAB_ROUTES) + showSource = false; + sourceLines = Collections.emptyList(); + sourceTitle = null; + sourceScroll = 0; + sourceScrollX = 0; // Diagram (TAB_ROUTES) showDiagram = false; diagramImageData = null; @@ -1280,6 +1330,16 @@ public class CamelMonitor extends CamelCommand { return; } + // Fullscreen source view + if (showSource) { + List<Rect> fullChunks = Layout.vertical() + .constraints(Constraint.length(4), Constraint.fill()) + .split(area); + renderRouteHeader(frame, fullChunks.get(0), info); + renderSource(frame, fullChunks.get(1)); + return; + } + // Fullscreen diagram mode if (showDiagram && (diagramTextMode ? !diagramLines.isEmpty() : diagramFullImageData != null)) { // Split: route info header (4 rows) + diagram (fill) @@ -2095,6 +2155,121 @@ public class CamelMonitor extends CamelCommand { }); } + private void loadSourceForSelectedRoute() { + if (selectedPid == null || runner == null) { + return; + } + if (!sourceLoading.compareAndSet(false, true)) { + return; + } + IntegrationInfo info = findSelectedIntegration(); + if (info == null || info.routes.isEmpty()) { + sourceLoading.set(false); + return; + } + List<RouteInfo> sortedRoutes = new ArrayList<>(info.routes); + sortedRoutes.sort(this::sortRoute); + Integer sel = routeTableState.selected(); + RouteInfo selectedRoute = (sel != null && sel >= 0 && sel < sortedRoutes.size()) + ? sortedRoutes.get(sel) : sortedRoutes.get(0); + + sourceLines = List.of("(Loading source...)"); + sourceTitle = selectedRoute.routeId; + sourceScroll = 0; + sourceScrollX = 0; + showSource = true; + + String pid = selectedPid; + String routeId = selectedRoute.routeId; + runner.scheduler().execute(() -> { + try { + loadSourceInBackground(pid, routeId); + } finally { + sourceLoading.set(false); + } + }); + } + + private void loadSourceInBackground(String pid, String routeId) { + Path outputFile = getOutputFile(pid); + PathUtils.deleteFile(outputFile); + + JsonObject root = new JsonObject(); + root.put("action", "source"); + root.put("filter", routeId); + + Path actionFile = getActionFile(pid); + org.apache.camel.dsl.jbang.core.common.PathUtils.writeTextSafely(root.toJson(), actionFile); + + JsonObject jo = pollJsonResponse(outputFile, 5000); + PathUtils.deleteFile(outputFile); + + if (jo == null) { + applySourceResult(routeId, null, List.of("(No response from integration)")); + return; + } + + JsonArray routes = (JsonArray) jo.get("routes"); + if (routes == null || routes.isEmpty()) { + applySourceResult(routeId, null, List.of("(No source available for route: " + routeId + ")")); + return; + } + + JsonObject routeObj = (JsonObject) routes.get(0); + String sourceLocation = objToString(routeObj.get("source")); + List<JsonObject> codeLines = routeObj.getCollection("code"); + if (codeLines == null || codeLines.isEmpty()) { + applySourceResult(routeId, sourceLocation, List.of("(No source code available)")); + return; + } + + List<String> lines = new ArrayList<>(); + int maxLineNum = 0; + for (JsonObject codeLine : codeLines) { + Integer lineNum = codeLine.getInteger("line"); + if (lineNum != null && lineNum > maxLineNum) { + maxLineNum = lineNum; + } + } + int lineNumWidth = String.valueOf(maxLineNum).length(); + int matchLine = -1; + int idx = 0; + for (JsonObject codeLine : codeLines) { + Integer lineNum = codeLine.getInteger("line"); + String code = Jsoner.unescape(objToString(codeLine.get("code"))); + Boolean match = codeLine.getBoolean("match"); + String prefix = lineNum != null + ? String.format("%" + lineNumWidth + "d ", lineNum) + : String.format("%" + lineNumWidth + "s ", ""); + lines.add(prefix + code); + if (Boolean.TRUE.equals(match) && matchLine < 0) { + matchLine = idx; + } + idx++; + } + + int scrollTo = matchLine > 0 ? Math.max(0, matchLine - 2) : 0; + applySourceResult(routeId, sourceLocation, lines, scrollTo); + } + + private void applySourceResult(String routeId, String location, List<String> lines) { + applySourceResult(routeId, location, lines, 0); + } + + private void applySourceResult(String routeId, String location, List<String> lines, int scrollTo) { + if (runner == null) { + return; + } + runner.runOnRenderThread(() -> { + if (!showSource) { + return; // user cancelled via Esc while loading + } + sourceTitle = location != null ? routeId + " " + location : routeId; + sourceLines = lines; + sourceScroll = scrollTo; + }); + } + private void loadDiagramInBackground(String pid, boolean textMode, String routeId, boolean metrics) { Path outputFile = getOutputFile(pid); PathUtils.deleteFile(outputFile); @@ -2372,6 +2547,46 @@ public class CamelMonitor extends CamelCommand { return a.compareToIgnoreCase(b); } + private void renderSource(Frame frame, Rect area) { + Block block = Block.builder() + .borderType(BorderType.ROUNDED) + .title(" Source [" + (sourceTitle != null ? sourceTitle : "") + "] ") + .build(); + Rect inner = block.inner(area); + frame.renderWidget(block, area); + + if (sourceLines.isEmpty()) { + return; + } + + int visibleLines = inner.height(); + int maxScroll = Math.max(0, sourceLines.size() - visibleLines); + sourceScroll = Math.min(sourceScroll, maxScroll); + + int maxLineWidth = sourceLines.stream().mapToInt(String::length).max().orElse(0); + int maxHScroll = Math.max(0, maxLineWidth - inner.width()); + sourceScrollX = Math.min(sourceScrollX, maxHScroll); + + int end = Math.min(sourceScroll + visibleLines, sourceLines.size()); + List<Line> visible = new ArrayList<>(); + for (int i = sourceScroll; i < end; i++) { + String raw = sourceLines.get(i); + visible.add(TuiHelper.ansiToLine(raw, sourceScrollX)); + } + frame.renderWidget(Paragraph.builder().text(Text.from(visible)).build(), inner); + + // Vertical scrollbar + if (sourceLines.size() > visibleLines) { + sourceVScrollState.contentLength(sourceLines.size()).viewportContentLength(visibleLines).position(sourceScroll); + frame.renderStatefulWidget(Scrollbar.builder().build(), inner, sourceVScrollState); + } + // Horizontal scrollbar + if (maxHScroll > 0) { + sourceHScrollState.contentLength(maxLineWidth).viewportContentLength(inner.width()).position(sourceScrollX); + frame.renderStatefulWidget(Scrollbar.horizontal(), inner, sourceHScrollState); + } + } + // ---- Tab 3: Health ---- private void renderHealth(Frame frame, Rect area) { @@ -3251,6 +3466,11 @@ public class CamelMonitor extends CamelCommand { hint(spans, "Esc", "unselect"); } hint(spans, "1-9", "tabs"); + } else if (tab == TAB_ROUTES && showSource) { + hint(spans, "c/Esc", "close"); + hint(spans, "\u2191\u2193\u2190\u2192", "scroll"); + hint(spans, "PgUp/PgDn", "page"); + hintLast(spans, "Home/End", "top/bottom"); } else if (tab == TAB_ROUTES && showDiagram) { String closeKey = diagramTextMode ? "D" : "d"; hint(spans, closeKey + "/Esc", "close"); @@ -3267,6 +3487,7 @@ public class CamelMonitor extends CamelCommand { hint(spans, "Esc", "back"); hint(spans, "\u2191\u2193", "navigate"); hint(spans, "s", "sort"); + hint(spans, "c", "source"); hint(spans, "d", "diagram"); hint(spans, "D", "text diagram"); hint(spans, "1-9", "tabs");
