This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 050af78ca32c TUI: overview info panel, chart toggle, source view and
UX improvements (#23262)
050af78ca32c is described below
commit 050af78ca32c383d30d2f7f4aa78d666f043fd60
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat May 16 22:24:42 2026 +0200
TUI: overview info panel, chart toggle, source view and UX improvements
(#23262)
* TUI: add info panel to diagram view with heap and thread stats
Shows a 22-col Info panel to the right of the diagram with:
- HEAP: used/max (percent)
- THREADS: current count and peak
Removes the HEAP column from the overview integrations table since
the information is now visible on the diagram tab.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* TUI: fix info panel not visible in diagram view
Add missing renderDiagramInfoPanel call at the end of the text diagram
path, and extract helper method shared by both image and text paths.
Panel renders last so it draws on top of any diagram overflow.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* TUI: enrich overview info panel with runtime, JVM, heap and thread details
Add an info panel beside the throughput chart on the Overview tab showing:
- Runtime platform and Camel version
- JVM version, vendor, and VM name (new fields added to LocalCliConnector)
- Uptime, heap (used/max/%), non-heap (metaspace), and thread counts
Also emits javaVendor and javaVmName from LocalCliConnector via
RuntimeMXBean.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* TUI: auto-select integration on overview scroll and reset tab state on
switch
- Scrolling up/down on the Overview table now immediately updates the
selected integration (same behaviour as the Routes tab)
- Switching to a different integration resets all tab-specific view state:
diagram, route table, log scroll/follow, and trace detail view
- Fix Esc race: diagram no longer re-shows if user presses Esc while the
background load is still in flight
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* TUI: add chart toggle between all integrations and selected only
Press 'a' on the Overview tab to switch the throughput bar chart between
aggregated stats for all running integrations [All] and stats for the
currently selected integration only, with the name shown in yellow.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* 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]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
.../camel/cli/connector/LocalCliConnector.java | 2 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 435 +++++++++++++++++++--
2 files changed, 411 insertions(+), 26 deletions(-)
diff --git
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index aa98e238b689..438e7d426006 100644
---
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1098,6 +1098,8 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
RuntimeMXBean mb = ManagementFactory.getRuntimeMXBean();
if (mb != null) {
rc.put("javaVersion", mb.getVmVersion());
+ rc.put("javaVendor", mb.getVmVendor());
+ rc.put("javaVmName", mb.getVmName());
}
root.put("runtime", rc);
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 6cba4ed88161..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
@@ -232,6 +232,7 @@ public class CamelMonitor extends CamelCommand {
private String selectedPid;
// Diagram state
+ private boolean chartAllIntegrations = true;
private boolean showDiagram;
private boolean diagramTextMode;
private boolean diagramMetrics = true;
@@ -253,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;
@@ -295,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;
@@ -459,6 +473,11 @@ public class CamelMonitor extends CamelCommand {
overviewSort = OVERVIEW_SORT_COLUMNS[overviewSortIndex];
return true;
}
+ // Overview tab: toggle chart between all integrations and
selected only
+ if (tab == TAB_OVERVIEW && ke.isCharIgnoreCase('a')) {
+ chartAllIntegrations = !chartAllIntegrations;
+ return true;
+ }
// Consumers tab: sort
if (tab == TAB_CONSUMERS && ke.isCharIgnoreCase('s')) {
@@ -514,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);
@@ -718,9 +768,59 @@ public class CamelMonitor extends CamelCommand {
}
}
+ private void syncSelectedPidFromOverview() {
+ List<IntegrationInfo> infos = data.get().stream().filter(i ->
!i.vanishing).toList();
+ Integer sel = overviewTableState.selected();
+ String newPid = null;
+ if (sel != null && sel >= 0 && sel < infos.size()) {
+ newPid = infos.get(sel).pid;
+ } else if (infos.size() == 1) {
+ newPid = infos.get(0).pid;
+ }
+ if (newPid != null && !newPid.equals(selectedPid)) {
+ selectedPid = newPid;
+ resetIntegrationTabState();
+ List<Long> pids = List.of(Long.parseLong(selectedPid));
+ refreshHistoryData(pids);
+ traceFilePositions.clear();
+ traces.set(Collections.emptyList());
+ refreshTraceData(pids);
+ }
+ }
+
+ // 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;
+ diagramFullImageData = null;
+ diagramLines = Collections.emptyList();
+ diagramScroll = 0;
+ diagramScrollX = 0;
+ // Routes (TAB_ROUTES)
+ routeTableState.select(0);
+ // Log (TAB_LOG)
+ logScroll = 0;
+ logFollowMode = true;
+ // Trace (TAB_TRACE)
+ traceDetailView = false;
+ traceSelectedExchangeId = null;
+ traceDetailScroll = 0;
+ }
+
private void navigateUp() {
switch (tabsState.selected()) {
- case TAB_OVERVIEW -> overviewTableState.selectPrevious();
+ case TAB_OVERVIEW -> {
+ overviewTableState.selectPrevious();
+ syncSelectedPidFromOverview();
+ }
case TAB_ROUTES -> routeTableState.selectPrevious();
case TAB_CONSUMERS -> consumerTableState.selectPrevious();
case TAB_HEALTH -> healthTableState.selectPrevious();
@@ -748,7 +848,10 @@ public class CamelMonitor extends CamelCommand {
private void navigateDown() {
List<IntegrationInfo> infos = data.get().stream().filter(i ->
!i.vanishing).toList();
switch (tabsState.selected()) {
- case TAB_OVERVIEW -> overviewTableState.selectNext(infos.size());
+ case TAB_OVERVIEW -> {
+ overviewTableState.selectNext(infos.size());
+ syncSelectedPidFromOverview();
+ }
case TAB_ROUTES -> {
IntegrationInfo info = findSelectedIntegration();
routeTableState.selectNext(info != null ? info.routes.size() :
0);
@@ -933,7 +1036,6 @@ public class CamelMonitor extends CamelCommand {
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle))));
} else {
Style statusStyle = switch (extractState(info.state)) {
@@ -958,8 +1060,7 @@ public class CamelMonitor extends CamelCommand {
rightCell(String.valueOf(info.exchangesTotal), 8),
rightCell(String.valueOf(info.failed), 6, failStyle),
rightCell(String.valueOf(info.inflight), 8),
- Cell.from(sinceLastDisplay),
- Cell.from(formatMemory(info.heapMemUsed,
info.heapMemMax))));
+ Cell.from(sinceLastDisplay)));
}
}
@@ -975,8 +1076,7 @@ public class CamelMonitor extends CamelCommand {
rightCell(overviewSortLabel("TOTAL", "total"), 8,
overviewSortStyle("total")),
rightCell(overviewSortLabel("FAIL", "fail"), 6,
overviewSortStyle("fail")),
rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
- Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())),
- Cell.from(Span.styled("HEAP", Style.EMPTY.bold())));
+ Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())));
Table table = Table.builder()
.rows(rows)
@@ -993,8 +1093,7 @@ public class CamelMonitor extends CamelCommand {
Constraint.length(8),
Constraint.length(6),
Constraint.length(8),
- Constraint.length(12),
- Constraint.length(14))
+ Constraint.length(12))
.highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).title("
Integrations ").build())
@@ -1006,10 +1105,17 @@ public class CamelMonitor extends CamelCommand {
if (hasSparkline && chunks.size() > 1) {
Rect chartTotalArea = chunks.get(1);
+ // Split chart area horizontally: bar chart (fill) + info panel
(30 cols)
+ List<Rect> chartHSplit = Layout.horizontal()
+ .constraints(Constraint.fill(), Constraint.length(30))
+ .split(chartTotalArea);
+ Rect chartArea = chartHSplit.get(0);
+ Rect infoArea = chartHSplit.get(1);
+
// Split chart area: chart rows (13) + x-axis label row (1)
List<Rect> vChunks = Layout.vertical()
.constraints(Constraint.fill(), Constraint.length(1))
- .split(chartTotalArea);
+ .split(chartArea);
// Split chart rows: y-axis labels (4 cols) + bar chart (fill)
List<Rect> hChunks = Layout.horizontal()
@@ -1022,20 +1128,25 @@ public class CamelMonitor extends CamelCommand {
int innerBarCols = Math.max(2, barChartArea.width() - 2); // minus
block borders
int renderPoints = Math.min(MAX_SPARKLINE_POINTS, innerBarCols /
2);
- // Merge throughput histories across all PIDs
+ // Merge throughput histories: all PIDs or selected only
long[] mergedTotal = new long[renderPoints];
long[] mergedFailed = new long[renderPoints];
+ String chartPid = (!chartAllIntegrations && selectedPid != null) ?
selectedPid : null;
for (int i = 0; i < renderPoints; i++) {
- for (LinkedList<Long> hist : throughputHistory.values()) {
- int idx = hist.size() - renderPoints + i;
- if (idx >= 0) {
- mergedTotal[i] += hist.get(idx);
+ for (Map.Entry<String, LinkedList<Long>> e :
throughputHistory.entrySet()) {
+ if (chartPid == null || chartPid.equals(e.getKey())) {
+ int idx = e.getValue().size() - renderPoints + i;
+ if (idx >= 0) {
+ mergedTotal[i] += e.getValue().get(idx);
+ }
}
}
- for (LinkedList<Long> hist : failedHistory.values()) {
- int idx = hist.size() - renderPoints + i;
- if (idx >= 0) {
- mergedFailed[i] += hist.get(idx);
+ for (Map.Entry<String, LinkedList<Long>> e :
failedHistory.entrySet()) {
+ if (chartPid == null || chartPid.equals(e.getKey())) {
+ int idx = e.getValue().size() - renderPoints + i;
+ if (idx >= 0) {
+ mergedFailed[i] += e.getValue().get(idx);
+ }
}
}
}
@@ -1049,12 +1160,26 @@ public class CamelMonitor extends CamelCommand {
long curOk = Math.max(0, curTp - curFailed);
// Styled legend in chart title
- Line titleLine = Line.from(
- Span.raw(String.format(" Throughput: %d msg/s ", curTp)),
- Span.styled("■", Style.EMPTY.fg(Color.GREEN)),
- Span.raw(String.format(" ok:%d ", curOk)),
- Span.styled("■", Style.EMPTY.fg(Color.RED)),
- Span.raw(String.format(" fail:%d ", curFailed)));
+ Line titleLine;
+ if (!chartAllIntegrations && selectedPid != null) {
+ IntegrationInfo chartSel = findSelectedIntegration();
+ String chartName = chartSel != null ?
TuiHelper.truncate(chartSel.name, 12) : selectedPid;
+ titleLine = Line.from(
+ Span.raw(" ["),
+ Span.styled(chartName, Style.EMPTY.fg(Color.YELLOW)),
+ Span.raw(String.format("] Throughput: %d msg/s ",
curTp)),
+ Span.styled("■", Style.EMPTY.fg(Color.GREEN)),
+ Span.raw(String.format(" ok:%d ", curOk)),
+ Span.styled("■", Style.EMPTY.fg(Color.RED)),
+ Span.raw(String.format(" fail:%d ", curFailed)));
+ } else {
+ titleLine = Line.from(
+ Span.raw(String.format(" [All] Throughput: %d msg/s
", curTp)),
+ Span.styled("■", Style.EMPTY.fg(Color.GREEN)),
+ Span.raw(String.format(" ok:%d ", curOk)),
+ Span.styled("■", Style.EMPTY.fg(Color.RED)),
+ Span.raw(String.format(" fail:%d ", curFailed)));
+ }
// Build bar groups (ok=green, failed=red), no bar value labels
List<BarGroup> groups = new ArrayList<>();
@@ -1118,7 +1243,82 @@ public class CamelMonitor extends CamelCommand {
}
}
}
+
+ // Info panel: heap and threads for the selected integration
+ renderOverviewInfoPanel(frame, infoArea);
+ }
+ }
+
+ private void renderOverviewInfoPanel(Frame frame, Rect area) {
+ IntegrationInfo sel = findSelectedIntegration();
+ // Fall back to the single active integration when nothing is
explicitly selected
+ if (sel == null) {
+ List<IntegrationInfo> active = data.get().stream().filter(i ->
!i.vanishing).toList();
+ if (active.size() == 1) {
+ sel = active.get(0);
+ }
+ }
+ Block infoBlock =
Block.builder().borderType(BorderType.ROUNDED).build();
+ frame.renderWidget(infoBlock, area);
+ Rect inner = infoBlock.inner(area);
+ List<Line> lines = new ArrayList<>();
+ Style dim = Style.EMPTY.dim();
+ if (sel != null) {
+ // Identity
+ if (sel.platform != null) {
+ String plat = sel.platformVersion != null
+ ? sel.platform + "/" + sel.platformVersion
+ : sel.platform;
+ lines.add(Line.from(
+ Span.styled("Runtime: ", dim),
+ Span.raw(TuiHelper.truncate(plat, inner.width() -
9))));
+ }
+ if (sel.camelVersion != null) {
+ lines.add(Line.from(
+ Span.styled("Version: ", dim),
+ Span.raw(TuiHelper.truncate(sel.camelVersion,
inner.width() - 9))));
+ }
+ lines.add(Line.from(Span.raw("")));
+ // Resources
+ if (sel.javaVersion != null) {
+ lines.add(Line.from(
+ Span.styled("JVM: ", dim),
+ Span.raw(TuiHelper.truncate(sel.javaVersion,
inner.width() - 6))));
+ }
+ if (sel.javaVendor != null) {
+ lines.add(Line.from(
+ Span.styled(" ", dim),
+ Span.raw(TuiHelper.truncate(sel.javaVendor,
inner.width() - 6))));
+ }
+ if (sel.javaVmName != null) {
+ lines.add(Line.from(
+ Span.styled(" ", dim),
+ Span.raw(TuiHelper.truncate(sel.javaVmName,
inner.width() - 6))));
+ }
+ lines.add(Line.from(
+ Span.styled("Uptime: ", dim),
+ Span.raw(sel.ago != null ? sel.ago : "-")));
+ if (sel.heapMemUsed > 0) {
+ String heap = formatMemory(sel.heapMemUsed, sel.heapMemMax);
+ long pct = sel.heapMemMax > 0 ? sel.heapMemUsed * 100 /
sel.heapMemMax : 0;
+ lines.add(Line.from(
+ Span.styled("Heap: ", dim),
+ Span.raw(heap + " " + pct + "%")));
+ }
+ if (sel.nonHeapMemUsed > 0) {
+ lines.add(Line.from(
+ Span.styled("Meta: ", dim),
+ Span.raw(formatMemory(sel.nonHeapMemUsed, 0))));
+ }
+ if (sel.threadCount > 0) {
+ lines.add(Line.from(
+ Span.styled("Thds: ", dim),
+ Span.raw(sel.threadCount + " / " +
sel.peakThreadCount)));
+ }
+ } else {
+ lines.add(Line.from(Span.raw("-")));
}
+ frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(),
inner);
}
// ---- Tab 2: Routes ----
@@ -1130,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)
@@ -1945,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);
@@ -2094,7 +2419,10 @@ public class CamelMonitor extends CamelCommand {
diagramCropW = -1;
diagramCropH = -1;
}
- showDiagram = true;
+ // Only restore showDiagram if user hasn't cancelled via Esc while
loading
+ if (wasShowing) {
+ showDiagram = true;
+ }
});
}
@@ -2219,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) {
@@ -3092,11 +3460,17 @@ public class CamelMonitor extends CamelCommand {
hint(spans, "q", "quit");
hint(spans, "\u2191\u2193", "navigate");
hint(spans, "s", "sort");
+ hint(spans, "a", "chart " + (chartAllIntegrations ? "[all]" :
"[single]"));
hint(spans, "Enter", "details");
if (selectedPid != null) {
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");
@@ -3113,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");
@@ -3821,6 +4196,9 @@ public class CamelMonitor extends CamelCommand {
JsonObject runtime = (JsonObject) root.get("runtime");
info.platform = runtime != null ? runtime.getString("platform") : null;
info.platformVersion = runtime != null ?
runtime.getString("platformVersion") : null;
+ info.javaVersion = runtime != null ? runtime.getString("javaVersion")
: null;
+ info.javaVendor = runtime != null ? runtime.getString("javaVendor") :
null;
+ info.javaVmName = runtime != null ? runtime.getString("javaVmName") :
null;
Map<String, ?> stats = context.getMap("statistics");
if (stats != null) {
@@ -3855,6 +4233,7 @@ public class CamelMonitor extends CamelCommand {
if (mem != null) {
info.heapMemUsed = mem.getLongOrDefault("heapMemoryUsed", 0L);
info.heapMemMax = mem.getLongOrDefault("heapMemoryMax", 0L);
+ info.nonHeapMemUsed = mem.getLongOrDefault("nonHeapMemoryUsed",
0L);
}
JsonObject threads = (JsonObject) root.get("threads");
@@ -4157,6 +4536,9 @@ public class CamelMonitor extends CamelCommand {
String camelVersion;
String platform;
String platformVersion;
+ String javaVersion;
+ String javaVendor;
+ String javaVmName;
String profile;
String ready;
int state;
@@ -4176,6 +4558,7 @@ public class CamelMonitor extends CamelCommand {
int routeTotal;
long heapMemUsed;
long heapMemMax;
+ long nonHeapMemUsed;
int threadCount;
int peakThreadCount;
boolean vanishing;