This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23648-run-folder in repository https://gitbox.apache.org/repos/asf/camel.git
commit e7aa3229ce86ad7eccc1205b24c568d6cf46ddbf Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jun 1 10:00:46 2026 +0200 CAMEL-23648: camel-jbang - TUI Log tab loading and scroll improvements Co-Authored-By: Claude <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 22 +++- .../camel/dsl/jbang/core/commands/tui/LogTab.java | 116 ++++++++++++++++++--- 2 files changed, 124 insertions(+), 14 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 8649c1f52659..b40053bf55db 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 @@ -1872,8 +1872,27 @@ public class CamelMonitor extends CamelCommand { logTab.logFilePos = -1; logTab.logTotalLinesRead = 0; logTab.logLineBuffer.setLength(0); + logTab.logLoading = true; } List<String> newRawLines = new ArrayList<>(); + // Load older lines when scrolled to the top or Home pressed + boolean loadAll = logTab.loadAllRequested; + if (logTab.logFileStartPos > 0 + && (loadAll || (!logTab.followMode && logTab.scroll == 0))) { + logTab.loadAllRequested = false; + List<String> olderLines = new ArrayList<>(); + logTab.readOlderLogLines(logFileName, loadAll, olderLines); + if (!olderLines.isEmpty()) { + List<LogEntry> olderEntries = new ArrayList<>(); + for (String line : olderLines) { + olderEntries.add(LogTab.parseLogLine(line)); + } + logTab.mutableFilteredEntries.addAll(0, olderEntries); + logTab.logTotalLinesRead += olderLines.size(); + // Adjust scroll to keep the same content visible + logTab.scroll = olderEntries.size(); + } + } logTab.readNewLogLinesFromFile(logPid, logFileName, newRawLines); if (!newRawLines.isEmpty()) { logTab.logTotalLinesRead += newRawLines.size(); @@ -1884,8 +1903,9 @@ public class CamelMonitor extends CamelCommand { logTab.mutableFilteredEntries.subList(0, logTab.mutableFilteredEntries.size() - MAX_LOG_LINES) .clear(); } - logTab.filteredLogEntries = new ArrayList<>(logTab.mutableFilteredEntries); } + logTab.filteredLogEntries = new ArrayList<>(logTab.mutableFilteredEntries); + logTab.logLoading = false; } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java index 9187e177e321..bb2bb659dfb9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java @@ -43,6 +43,7 @@ import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.widgets.Clear; import dev.tamboui.widgets.block.Block; import dev.tamboui.widgets.block.BorderType; +import dev.tamboui.widgets.block.Title; import dev.tamboui.widgets.input.TextInputState; import dev.tamboui.widgets.list.ListItem; import dev.tamboui.widgets.list.ListState; @@ -72,7 +73,10 @@ class LogTab implements MonitorTab { private final ListState logLevelListState = new ListState(); volatile List<LogEntry> filteredLogEntries = new ArrayList<>(); + volatile boolean logLoading; + volatile boolean loadAllRequested; long logFilePos = -1; + long logFileStartPos = -1; long logTotalLinesRead; String logFilePid; final StringBuilder logLineBuffer = new StringBuilder(); @@ -82,9 +86,9 @@ class LogTab implements MonitorTab { private int cachedLogHSkip = -1; private int cachedLogMaxWidth; private List<Line> cachedLogLines = Collections.emptyList(); - private int scroll; + int scroll; private long evictedSeen; - private boolean followMode = true; + boolean followMode = true; private boolean wordWrap = true; private int hScroll; private boolean showLogLevelPopup; @@ -110,6 +114,21 @@ class LogTab implements MonitorTab { this.ctx = ctx; } + @Override + public void onIntegrationChanged() { + filteredLogEntries = new ArrayList<>(); + logLoading = true; + loadAllRequested = false; + logFilePid = null; + logFilePos = -1; + logFileStartPos = -1; + logTotalLinesRead = 0; + logLineBuffer.setLength(0); + mutableFilteredEntries.clear(); + cachedLogEntries = null; + cachedLogLines = Collections.emptyList(); + } + @Override public boolean handleKeyEvent(KeyEvent ke) { if (findInputActive || highlightInputActive) { @@ -192,6 +211,7 @@ class LogTab implements MonitorTab { followMode = false; scroll = 0; hScroll = 0; + loadAllRequested = true; return true; } if (ke.isEnd()) { @@ -292,24 +312,44 @@ class LogTab implements MonitorTab { return; } + if (logLoading && filteredLogEntries.isEmpty()) { + Block loadingBlock = Block.builder() + .borderType(BorderType.ROUNDED) + .title(" Log ") + .build(); + frame.renderWidget(loadingBlock, area); + Rect loadingInner = loadingBlock.inner(area); + frame.renderWidget( + Paragraph.builder().text(Text.from(Line.from(Span.raw("(Loading logs...)")))) + .build(), + loadingInner); + return; + } + List<LogEntry> entries = filteredLogEntries; int contentHeight = entries.size(); - long totalRead = logTotalLinesRead; - String chunkSuffix = totalRead > entries.size() - ? " #" + (totalRead - entries.size() + 1) + "-" + totalRead - : ""; - String logTitle; + boolean hasNew = !followMode && scroll < contentHeight - Math.max(1, area.height() - 2); + String logLabel; if (infraSel != null) { - logTitle = " Log [" + infraSel.alias + "]" + chunkSuffix + " "; + logLabel = " Log [" + infraSel.alias + "]"; } else if (info != null && info.rootLogLevel != null) { - logTitle = " Log level:" + info.rootLogLevel + chunkSuffix + " "; + logLabel = " Log level:" + info.rootLogLevel; + } else { + logLabel = " Log"; + } + Line titleLine; + if (hasNew) { + titleLine = Line.from( + Span.raw(logLabel + " "), + Span.styled("(*)", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.raw(" ")); } else { - logTitle = " Log" + chunkSuffix + " "; + titleLine = Line.from(Span.raw(logLabel + " ")); } Block block = Block.builder() .borderType(BorderType.ROUNDED) - .title(logTitle) + .title(Title.from(titleLine)) .build(); frame.renderWidget(block, area); @@ -615,14 +655,17 @@ class LogTab implements MonitorTab { try (RandomAccessFile raf = new RandomAccessFile(logFile.toFile(), "r")) { long length = raf.length(); if (logFilePos < 0 || logFilePos > length) { - logFilePos = Math.max(0, length - 1024 * 1024); + // Initial load: only read last 8KB for fast first render + logFilePos = Math.max(0, length - 8 * 1024); + logFileStartPos = logFilePos; logLineBuffer.setLength(0); } if (logFilePos >= length) { return; } raf.seek(logFilePos); - byte[] buf = new byte[(int) Math.min(length - logFilePos, 4 * 1024 * 1024)]; + // Cap per-tick read to 64KB to avoid blocking the refresh cycle + byte[] buf = new byte[(int) Math.min(length - logFilePos, 64 * 1024)]; raf.readFully(buf); logFilePos += buf.length; @@ -645,6 +688,53 @@ class LogTab implements MonitorTab { } } + void readOlderLogLines(String fileName, boolean loadAll, List<String> olderLines) { + if (logFilePid == null || logFileStartPos <= 0) { + return; + } + Path logFile = CommandLineHelper.getCamelDir().resolve(fileName); + if (!Files.exists(logFile)) { + return; + } + try (RandomAccessFile raf = new RandomAccessFile(logFile.toFile(), "r")) { + long readEnd = logFileStartPos; + // Load all: read from start of file (cap at 2MB), otherwise 64KB chunk + long readStart; + if (loadAll) { + readStart = Math.max(0, readEnd - 2 * 1024 * 1024); + } else { + readStart = Math.max(0, readEnd - 64 * 1024); + } + if (readStart >= readEnd) { + return; + } + raf.seek(readStart); + byte[] buf = new byte[(int) (readEnd - readStart)]; + raf.readFully(buf); + logFileStartPos = readStart; + + String chunk = new String(buf, StandardCharsets.UTF_8); + // If we didn't read from the start of the file, skip the first partial line + int start = 0; + if (readStart > 0) { + int firstNewline = chunk.indexOf('\n'); + if (firstNewline >= 0) { + start = firstNewline + 1; + } + } + int end; + while ((end = chunk.indexOf('\n', start)) >= 0) { + String line = TuiHelper.fixControlChars(chunk.substring(start, end)); + if (!line.isEmpty()) { + olderLines.add(line); + } + start = end + 1; + } + } catch (IOException e) { + // ignore + } + } + static LogEntry parseLogLine(String line) { LogEntry entry = new LogEntry(); entry.raw = line;
