This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch tui-log-follow-catch-up
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 6b9752f3398949768b1ad158dff5a757c3ef2abf
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 20:15:44 2026 +0100

    chore: camel-jbang - the Log tab shows the newest lines at once after a 
while on another tab
    
    The log is read while the Log tab is on screen (or the log is pinned),
    at most 64KB at a refresh. After a while on another tab, such as browsing
    the Diagram, a busy log had grown well beyond that, and the tab played
    the lines it had missed for many seconds while it said it followed; the
    same on Space after a pause on another tab. Following, a log more than a
    read behind now starts again at its end, as when the tab opens; Home reads
    the lines it skipped. Paused, it reads on from where it was.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  | 42 ++++++++++++---
 .../jbang/core/commands/tui/LogTabTailTest.java    | 60 ++++++++++++++++++++++
 2 files changed, 95 insertions(+), 7 deletions(-)

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 fb01ffa8c93c..6365fecaaf0c 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
@@ -82,6 +82,8 @@ class LogTab extends AbstractTab {
     private volatile boolean loadAllRequested;
     /** How much of the end of a log the tab reads when it opens it. */
     static final int INITIAL_TAIL_BYTES = 32 * 1024;
+    /** The most of a log read at a refresh, so a refresh does not block on a 
log that grew a lot. */
+    static final int MAX_READ_BYTES = 64 * 1024;
 
     private long logFilePos = -1;
     private long logFileStartPos = -1;
@@ -642,6 +644,11 @@ class LogTab extends AbstractTab {
         frame.renderStatefulWidget(list, popup, logLevelListState);
     }
 
+    /** The entries the tab shows (tests). */
+    List<LogEntry> entriesForTesting() {
+        return filteredLogEntries;
+    }
+
     /** The entries of the tab, as read from the log file (tests). */
     void setEntriesForTesting(List<LogEntry> entries) {
         mutableFilteredEntries.clear();
@@ -680,6 +687,10 @@ class LogTab extends AbstractTab {
     }
 
     void refreshFromFile(String pid, String fileName) {
+        refreshFromFile(pid, 
CommandLineHelper.getCamelDir().resolve(fileName));
+    }
+
+    void refreshFromFile(String pid, Path logFile) {
         if (!pid.equals(logFilePid)) {
             mutableFilteredEntries.clear();
             logFilePos = -1;
@@ -688,13 +699,25 @@ class LogTab extends AbstractTab {
             logLoading = true;
         }
         boolean changed = false;
+        if (followMode && pid.equals(logFilePid) && logFilePos >= 0 && 
unread(logFile) > MAX_READ_BYTES) {
+            // far behind, as after a while on another tab (the log is read 
while it is on screen): it starts again at
+            // the end of the log, as when the tab opens, rather than play the 
lines it missed while it says it follows;
+            // Home reads them
+            mutableFilteredEntries.clear();
+            logFilePos = -1;
+            logFileStartPos = -1;
+            logTotalLinesRead = 0;
+            evictedSeen = 0;
+            logLineBuffer.setLength(0);
+            changed = true;
+        }
         int olderRead = 0;
         boolean loadAll = loadAllRequested;
         if (logFileStartPos > 0
                 && (loadAll || (!followMode && scroll == 0))) {
             loadAllRequested = false;
             List<String> olderLines = new ArrayList<>();
-            readOlderLogLines(fileName, loadAll, olderLines);
+            readOlderLogLines(logFile, loadAll, olderLines);
             if (!olderLines.isEmpty()) {
                 changed = true;
                 List<LogEntry> olderEntries = new ArrayList<>();
@@ -707,7 +730,7 @@ class LogTab extends AbstractTab {
             }
         }
         List<String> newRawLines = new ArrayList<>();
-        readNewLogLinesFromFile(pid, fileName, newRawLines);
+        readNewLogLines(pid, logFile, newRawLines);
         changed |= !newRawLines.isEmpty();
         if (changed) {
             logTotalLinesRead += newRawLines.size();
@@ -729,6 +752,15 @@ class LogTab extends AbstractTab {
         logLoading = false;
     }
 
+    /** How much of the log is written beyond what the tab read. */
+    private long unread(Path logFile) {
+        try {
+            return Files.size(logFile) - logFilePos;
+        } catch (IOException e) {
+            return 0;
+        }
+    }
+
     void readNewLogLines(String pid, List<String> newLines) {
         readNewLogLinesFromFile(pid, pid + ".log", newLines);
     }
@@ -758,7 +790,7 @@ class LogTab extends AbstractTab {
             }
             raf.seek(logFilePos);
             // Cap per-tick read to 64KB to avoid blocking the refresh cycle
-            byte[] buf = new byte[(int) Math.min(length - logFilePos, 64 * 
1024)];
+            byte[] buf = new byte[(int) Math.min(length - logFilePos, 
MAX_READ_BYTES)];
             raf.readFully(buf);
             int from = 0;
             if (skipPartialLine) {
@@ -802,10 +834,6 @@ class LogTab extends AbstractTab {
         return -1;
     }
 
-    void readOlderLogLines(String fileName, boolean loadAll, List<String> 
olderLines) {
-        readOlderLogLines(CommandLineHelper.getCamelDir().resolve(fileName), 
loadAll, olderLines);
-    }
-
     void readOlderLogLines(Path logFile, boolean loadAll, List<String> 
olderLines) {
         if (logFilePid == null || logFileStartPos <= 0) {
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
index bd030d623ae3..30398bcb0282 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
@@ -19,10 +19,13 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -66,6 +69,63 @@ class LogTabTailTest {
         assertThat(all).isEqualTo(written);
     }
 
+    @Test
+    void 
followingALogThatGrewALotWhileTheTabWasHiddenShowsItsNewestLinesAtOnce() throws 
Exception {
+        Path log = dir.resolve("1234.log");
+        List<String> written = new ArrayList<>();
+        append(log, written, 100);
+        LogTab tab = newTab();
+        tab.refreshFromFile("1234", log);
+
+        // another tab was shown: the log was not read while it grew
+        append(log, written, 4 * LogTab.MAX_READ_BYTES / 100);
+        tab.refreshFromFile("1234", log);
+
+        List<LogEntry> entries = tab.entriesForTesting();
+        assertThat(entries.get(entries.size() - 1).raw).as("the newest line, 
not one read since long ago")
+                .isEqualTo(written.get(written.size() - 1));
+
+        // the lines it skipped are still there for Home
+        List<String> older = new ArrayList<>();
+        tab.readOlderLogLines(log, true, older);
+        List<String> all = new ArrayList<>(older);
+        entries.forEach(e -> all.add(e.raw));
+        assertThat(all).isEqualTo(written);
+    }
+
+    @Test
+    void pausedTheLogIsReadOnFromWhereItWas() throws Exception {
+        Path log = dir.resolve("1234.log");
+        List<String> written = new ArrayList<>();
+        append(log, written, 100);
+        LogTab tab = newTab();
+        tab.refreshFromFile("1234", log);
+        tab.handleKeyEvent(KeyEvent.ofChar(' ', KeyModifiers.NONE));
+
+        append(log, written, 4 * LogTab.MAX_READ_BYTES / 100);
+        tab.refreshFromFile("1234", log);
+
+        List<LogEntry> entries = tab.entriesForTesting();
+        assertThat(entries.get(0).raw).as("the lines on screen 
stay").isEqualTo(written.get(0));
+        assertThat(entries.size()).as("read on a part at a 
time").isLessThan(written.size());
+    }
+
+    private static LogTab newTab() {
+        return new LogTab(new MonitorContext(new AtomicReference<>(List.of()), 
new AtomicReference<>(List.of())));
+    }
+
+    /** Appends about {@code count} lines of a hundred bytes each. */
+    private static void append(Path log, List<String> written, int count) 
throws Exception {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+            String line = "2026-11-14 20:08:17.646  WARN 718778 --- 
[timer://orders] TimerConsumer : Error processing "
+                          + written.size();
+            written.add(line);
+            sb.append(line).append('\n');
+        }
+        Files.writeString(log, sb, StandardCharsets.UTF_8, 
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+    }
+
     @Test
     void aShortLogIsReadWhole() throws Exception {
         Path log = dir.resolve("1234.log");

Reply via email to