This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/camel-tui-cosmetic in repository https://gitbox.apache.org/repos/asf/camel.git
commit 30a2d244d0a01304150eb1de65169c2ddd1c7655 Author: Claus Ibsen <[email protected]> AuthorDate: Fri May 15 19:45:12 2026 +0200 Rewrite TUI log tab as plain scrollable Paragraph dump Replace the master/detail Table layout with a single Paragraph widget scrolling through all log lines. Removes renderLogDetail(), colorStyleForLevel(), and the TableState — eliminating the root cause of the garbage-character corruption that occurred when stack-trace tab characters confused TamboUI's consecutive-cell cursor optimisation. --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 116 +++++++-------------- 1 file changed, 37 insertions(+), 79 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 4c019661522c..b7a87f45f86d 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 @@ -163,7 +163,8 @@ public class CamelMonitor extends CamelCommand { // Log state private List<String> logLines = new ArrayList<>(); private volatile List<LogEntry> filteredLogEntries = new ArrayList<>(); - private final TableState logTableState = new TableState(); + private int logScroll; + private final ScrollbarState logScrollState = new ScrollbarState(); private boolean logFollowMode = true; private boolean showLogTrace = true; private boolean showLogDebug = true; @@ -352,9 +353,7 @@ public class CamelMonitor extends CamelCommand { diagramScroll = Math.max(0, diagramScroll - 20); } else if (tab == TAB_LOG) { logFollowMode = false; - for (int i = 0; i < 20; i++) { - logTableState.selectPrevious(); - } + logScroll = Math.max(0, logScroll - 20); } else if (tab == TAB_HISTORY) { historyDetailScroll = Math.max(0, historyDetailScroll - 5); } else if (tab == TAB_TRACE && traceDetailView) { @@ -366,9 +365,7 @@ public class CamelMonitor extends CamelCommand { if (showDiagram && tab == TAB_ROUTES) { diagramScroll += 20; } else if (tab == TAB_LOG) { - for (int i = 0; i < 20; i++) { - logTableState.selectNext(filteredLogEntries.size()); - } + logScroll += 20; } else if (tab == TAB_HISTORY) { historyDetailScroll += 5; } else if (tab == TAB_TRACE && traceDetailView) { @@ -498,7 +495,7 @@ public class CamelMonitor extends CamelCommand { } if (ke.isHome()) { logFollowMode = false; - logTableState.select(0); + logScroll = 0; return true; } if (ke.isEnd()) { @@ -666,7 +663,7 @@ public class CamelMonitor extends CamelCommand { case TAB_ENDPOINTS -> endpointTableState.selectPrevious(); case TAB_LOG -> { logFollowMode = false; - logTableState.selectPrevious(); + logScroll = Math.max(0, logScroll - 1); } case TAB_TRACE -> { if (traceDetailView) { @@ -699,7 +696,7 @@ public class CamelMonitor extends CamelCommand { IntegrationInfo info = findSelectedIntegration(); endpointTableState.selectNext(info != null ? info.endpoints.size() : 0); } - case TAB_LOG -> logTableState.selectNext(filteredLogEntries.size()); + case TAB_LOG -> logScroll++; case TAB_TRACE -> { if (traceDetailView) { List<TraceEntry> steps = getTraceSteps(traceSelectedExchangeId); @@ -1906,84 +1903,45 @@ public class CamelMonitor extends CamelCommand { return; } - // Log data is refreshed in refreshData() tick handler + List<LogEntry> entries = filteredLogEntries; + int contentHeight = entries.size(); - // Auto-follow: select last entry - if (logFollowMode && !filteredLogEntries.isEmpty()) { - logTableState.select(filteredLogEntries.size() - 1); - } + Block block = Block.builder() + .borderType(BorderType.ROUNDED) + .title(" Log " + buildLevelFilterTitle()) + .build(); + frame.renderWidget(block, area); - // Split: log table (60%) + detail (40%) - List<Rect> chunks = Layout.vertical() - .constraints(Constraint.percentage(60), Constraint.fill()) - .split(area); + Rect inner = block.inner(area); + int visibleHeight = Math.max(1, inner.height()); - // Log table - List<Row> rows = new ArrayList<>(); - for (LogEntry entry : filteredLogEntries) { - rows.add(Row.from( - Cell.from(Span.raw(entry.time != null ? entry.time : "")), - Cell.from(Span.raw(entry.level != null ? entry.level : "")), - Cell.from(Span.raw(entry.logger != null ? entry.logger : "")), - Cell.from(Span.raw(entry.message != null ? entry.message : "")))); + if (logFollowMode) { + logScroll = Math.max(0, contentHeight - visibleHeight); } + logScroll = Math.min(logScroll, Math.max(0, contentHeight - visibleHeight)); - String levelTitle = buildLevelFilterTitle(); - Table logTable = Table.builder() - .rows(rows) - .header(Row.from( - Cell.from(Span.raw("TIME")), - Cell.from(Span.raw("LEVEL")), - Cell.from(Span.raw("LOGGER")), - Cell.from(Span.raw("MESSAGE")))) - .widths( - Constraint.length(12), - Constraint.length(6), - Constraint.length(20), - Constraint.fill()) - .highlightStyle(Style.EMPTY) - .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED) - .title(" Log " + levelTitle).build()) - .build(); + List<Line> lines = new ArrayList<>(); + for (LogEntry entry : entries) { + lines.add(Line.from(Span.raw(entry.raw != null ? entry.raw : ""))); + } - frame.renderStatefulWidget(logTable, chunks.get(0), logTableState); + List<Rect> hChunks = Layout.horizontal() + .constraints(Constraint.fill(), Constraint.length(1)) + .split(inner); - // Detail panel for selected log entry - renderLogDetail(frame, chunks.get(1)); - } + Paragraph para = Paragraph.builder() + .text(Text.from(lines)) + .overflow(Overflow.CLIP) + .scroll(logScroll) + .build(); + frame.renderWidget(para, hChunks.get(0)); - private void renderLogDetail(Frame frame, Rect area) { - Integer sel = logTableState.selected(); - if (sel == null || sel < 0 || sel >= filteredLogEntries.size()) { - frame.renderWidget( - Paragraph.builder() - .text(Text.from(Line.from(Span.raw(" Select a log entry")))) - .block(Block.builder().borderType(BorderType.ROUNDED) - .title(" Detail ").build()) - .build(), - area); - return; + if (contentHeight > visibleHeight) { + logScrollState.contentLength(contentHeight); + logScrollState.viewportContentLength(visibleHeight); + logScrollState.position(logScroll); + frame.renderStatefulWidget(Scrollbar.builder().build(), hChunks.get(1), logScrollState); } - - LogEntry entry = filteredLogEntries.get(sel); - frame.renderWidget( - Paragraph.builder() - .text(Text.from(Line.from(Span.raw(entry.raw != null ? entry.raw : "")))) - .overflow(Overflow.WRAP_WORD) - .block(Block.builder().borderType(BorderType.ROUNDED) - .title(" " + entry.time + " " + entry.level + " ").build()) - .build(), - area); - } - - private Style colorStyleForLevel(String level) { - return switch (level) { - case "ERROR", "FATAL" -> Style.EMPTY.fg(Color.RED); - case "WARN" -> Style.EMPTY.fg(Color.YELLOW); - case "DEBUG", "TRACE" -> Style.EMPTY.dim(); - default -> Style.EMPTY; - }; } private String buildLevelFilterTitle() {
