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 b063abca876b chore: camel-jbang - Log tab: c switches between compact, 
wide and wide collapsed views (#29426)
b063abca876b is described below

commit b063abca876b23a7a35a942df1187fdff48b47f6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 8 21:16:30 2026 +0100

    chore: camel-jbang - Log tab: c switches between compact, wide and wide 
collapsed views (#29426)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   9 ++
 .../dsl/jbang/core/commands/tui/LogEntry.java      |  12 +++
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  | 104 ++++++++++++++++++---
 .../jbang/core/commands/tui/LogTabRenderTest.java  |  62 ++++++++++++
 4 files changed, 173 insertions(+), 14 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index 81f3e7c03a43..0124c1064b6b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -197,6 +197,15 @@ listed once.
 
 image::jbang/camel-tui-log-fix-with-ai-pick.png[Shift+F8 in the Log tab with 
two different ERRORs on the screen]
 
+The Log tab shows the log in one of three views, and *c* goes to the next one:
+
+* *compact* (the default): the time, level, logger and message of each line, 
as the date, pid and thread take most of
+a row in a terminal. The same line logged again and again is one line with a 
count, such as `Hello World  (x17)`.
+* *wide*: every line as it was logged, one by one, with the date, pid and 
thread.
+* *wide, collapsed*: the lines as they were logged, with the repeats as one 
line with a count.
+
+The line at the top of the screen stays there when you change the view.
+
 image::jbang/camel-tui-observe-error-diagram.png[A failed order: the path from 
checkout to payment-provider, and the onException handler that parked it]
 
 == OpenTelemetry Spans
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
index c051c50b45b9..220d3e097a49 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
@@ -24,4 +24,16 @@ class LogEntry {
     String message = "";
     /** How many identical lines in a row this entry stands for (CAMEL-24911); 
1 for a line that happened once. */
     int repeat = 1;
+    /** In a collapsed view: the index of the first line it stands for in the 
lines read, else -1. */
+    int line = -1;
+
+    LogEntry copy() {
+        LogEntry answer = new LogEntry();
+        answer.raw = raw;
+        answer.time = time;
+        answer.level = level;
+        answer.logger = logger;
+        answer.message = message;
+        return answer;
+    }
 }
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 165b6385ba07..f9d7e3b47a11 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
@@ -62,7 +62,8 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 
 class LogTab extends AbstractTab {
 
-    private static final int MAX_LOG_LINES = 3000;
+    // the lines read, one by one: the collapsed views fold them when they are 
shown
+    private static final int MAX_LOG_LINES = 10000;
     private static final int MOUSE_SCROLL_LINES = 3;
     private static final String[] LOG_LEVELS = { "ERROR", "WARN", "INFO", 
"DEBUG", "TRACE" };
 
@@ -94,8 +95,7 @@ class LogTab extends AbstractTab {
     private long evictedSeen;
     private boolean followMode = true;
     private boolean wordWrap = true;
-    // time, level, logger and message only: the date, pid and thread take 
most of a row in a terminal
-    private boolean compact = true;
+    private View view = View.COMPACT;
     private boolean cachedCompact;
     private int hScroll;
     private boolean showLogLevelPopup;
@@ -214,8 +214,7 @@ class LogTab extends AbstractTab {
             return true;
         }
         if (ke.isCharIgnoreCase('c')) {
-            compact = !compact;
-            hScroll = 0;
+            switchView(view.next());
             return true;
         }
         if (!wordWrap) {
@@ -384,17 +383,17 @@ class LogTab extends AbstractTab {
         int hSkip = wordWrap ? 0 : hScroll;
 
         boolean entriesChanged = entries != cachedLogEntries;
-        if (entriesChanged || hSkip != cachedLogHSkip || compact != 
cachedCompact) {
+        if (entriesChanged || hSkip != cachedLogHSkip || view.compact != 
cachedCompact) {
             cachedLogEntries = entries;
             cachedLogHSkip = hSkip;
-            cachedCompact = compact;
+            cachedCompact = view.compact;
             List<Line> built = new ArrayList<>(entries.size());
             int maxW = 0;
             for (int i = 0; i < entries.size(); i++) {
                 LogEntry entry = entries.get(i);
                 String raw = entry.raw != null ? entry.raw : "";
                 Line line;
-                if (compact && !entry.time.isEmpty()) {
+                if (view.compact && !entry.time.isEmpty()) {
                     line = compactLine(entry);
                     if (!wordWrap) {
                         maxW = Math.max(maxW, line.width());
@@ -505,7 +504,7 @@ class LogTab extends AbstractTab {
         }
         search.renderSearchHints(spans);
         hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
-        hint(spans, "c", "compact" + (compact ? " [on]" : " [off]"));
+        hint(spans, "c", "view [" + view.label + "]");
         if (!ctx.isInfraSelected()) {
             hint(spans, "l", "level");
         }
@@ -628,7 +627,9 @@ class LogTab extends AbstractTab {
 
     /** The entries of the tab, as read from the log file (tests). */
     void setEntriesForTesting(List<LogEntry> entries) {
-        filteredLogEntries = new ArrayList<>(entries);
+        mutableFilteredEntries.clear();
+        mutableFilteredEntries.addAll(entries);
+        filteredLogEntries = view.entries(mutableFilteredEntries);
         logLoading = false;
     }
 
@@ -670,6 +671,7 @@ class LogTab extends AbstractTab {
             logLoading = true;
         }
         boolean changed = false;
+        int olderRead = 0;
         boolean loadAll = loadAllRequested;
         if (logFileStartPos > 0
                 && (loadAll || (!followMode && scroll == 0))) {
@@ -680,11 +682,11 @@ class LogTab extends AbstractTab {
                 changed = true;
                 List<LogEntry> olderEntries = new ArrayList<>();
                 for (String line : olderLines) {
-                    addFolded(olderEntries, parseLogLine(line));
+                    olderEntries.add(parseLogLine(line));
                 }
                 mutableFilteredEntries.addAll(0, olderEntries);
                 logTotalLinesRead += olderLines.size();
-                scroll = olderEntries.size();
+                olderRead = olderEntries.size();
             }
         }
         List<String> newRawLines = new ArrayList<>();
@@ -693,7 +695,7 @@ class LogTab extends AbstractTab {
         if (changed) {
             logTotalLinesRead += newRawLines.size();
             for (String line : newRawLines) {
-                addFolded(mutableFilteredEntries, parseLogLine(line));
+                mutableFilteredEntries.add(parseLogLine(line));
             }
             if (mutableFilteredEntries.size() > MAX_LOG_LINES) {
                 mutableFilteredEntries.subList(0, 
mutableFilteredEntries.size() - MAX_LOG_LINES)
@@ -701,7 +703,11 @@ class LogTab extends AbstractTab {
             }
         }
         if (changed || logLoading) {
-            filteredLogEntries = new ArrayList<>(mutableFilteredEntries);
+            filteredLogEntries = view.entries(mutableFilteredEntries);
+            if (olderRead > 0) {
+                // the older lines go above: keep the line that was at the top 
of the screen there
+                scroll = shownIndexOf(filteredLogEntries, olderRead, 
view.collapse);
+            }
         }
         logLoading = false;
     }
@@ -919,12 +925,82 @@ class LogTab extends AbstractTab {
                 && !entry.message.isEmpty()) {
             // keep the newest line, so its timestamp is the last time this 
happened
             entry.repeat = last.repeat + 1;
+            entry.line = last.line;
             entries.set(entries.size() - 1, entry);
         } else {
             entries.add(entry);
         }
     }
 
+    /** The lines read, folded as {@link #addFolded}: copies, so the lines 
themselves stay one by one. */
+    static List<LogEntry> fold(List<LogEntry> lines) {
+        List<LogEntry> answer = new ArrayList<>();
+        for (int i = 0; i < lines.size(); i++) {
+            LogEntry entry = lines.get(i).copy();
+            entry.line = i;
+            addFolded(answer, entry);
+        }
+        return answer;
+    }
+
+    /** The index in the shown entries of the line read at {@code line}: the 
entry it is folded into, if collapsed. */
+    static int shownIndexOf(List<LogEntry> shown, int line, boolean collapsed) 
{
+        if (!collapsed) {
+            return Math.min(line, Math.max(0, shown.size() - 1));
+        }
+        int answer = 0;
+        for (int i = 0; i < shown.size() && shown.get(i).line <= line; i++) {
+            answer = i;
+        }
+        return answer;
+    }
+
+    /** The next view (c): the line at the top of the screen stays there. */
+    void switchView(View next) {
+        List<LogEntry> shown = filteredLogEntries;
+        int top = scroll < shown.size() ? scroll : Math.max(0, shown.size() - 
1);
+        int line = view.collapse && !shown.isEmpty() ? Math.max(0, 
shown.get(top).line) : top;
+        view = next;
+        hScroll = 0;
+        if (!mutableFilteredEntries.isEmpty()) {
+            filteredLogEntries = view.entries(mutableFilteredEntries);
+            scroll = shownIndexOf(filteredLogEntries, line, view.collapse);
+        }
+    }
+
+    View view() {
+        return view;
+    }
+
+    /**
+     * How the log is shown (c): compact is the time, level, logger and 
message, as the date, pid and thread take most
+     * of a row in a terminal, with the same line in a row collapsed into one 
with a count (CAMEL-24911); wide is the
+     * line as logged, one by one; wide and collapsed is the line as logged, 
with the repeats collapsed.
+     */
+    enum View {
+        COMPACT("compact", true, true),
+        WIDE("wide", false, false),
+        WIDE_COLLAPSED("wide, collapsed", false, true);
+
+        final String label;
+        final boolean compact;
+        final boolean collapse;
+
+        View(String label, boolean compact, boolean collapse) {
+            this.label = label;
+            this.compact = compact;
+            this.collapse = collapse;
+        }
+
+        View next() {
+            return values()[(ordinal() + 1) % values().length];
+        }
+
+        List<LogEntry> entries(List<LogEntry> lines) {
+            return collapse ? fold(lines) : new ArrayList<>(lines);
+        }
+    }
+
     static LogEntry parseLogLine(String line) {
         LogEntry entry = new LogEntry();
         entry.raw = line;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
index 34c7851a80a8..64e020e845a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -73,6 +74,67 @@ class LogTabRenderTest {
         assertEquals(1, entries.get(0).repeat, "a line that happened once is 
not a repeat");
     }
 
+    @Test
+    void cSwitchesBetweenCompactWideAndWideCollapsed() {
+        List<LogEntry> entries = new ArrayList<>();
+        for (int i = 0; i < 17; i++) {
+            entries.add(LogTab.parseLogLine("2026-11-08 10:00:" + 
String.format("%02d", i)
+                                            + ".000  INFO 42 --- [ - 
timer://java] route1 : Hello World"));
+        }
+        LogTab tab = new LogTab(ctx);
+        tab.setEntriesForTesting(entries);
+
+        // compact: the short heading, and the same line in a row is one line 
with a count
+        assertEquals(LogTab.View.COMPACT, tab.view());
+        String compact = TuiTestHelper.renderToString(tab, 140, 30);
+        assertTrue(compact.contains("10:00:16.000  INFO route1") && 
compact.contains("Hello World  (x17)"), compact);
+        assertTrue(footer(tab).contains("view [compact]"), footer(tab));
+
+        // wide: every line as logged, one by one
+        tab.handleKeyEvent(KeyEvent.ofChar('c'));
+        String wide = TuiTestHelper.renderToString(tab, 140, 30);
+        assertTrue(wide.contains("2026-11-08 10:00:00.000  INFO 42 --- [ - 
timer://java] route1 : Hello World"), wide);
+        assertTrue(wide.contains("2026-11-08 10:00:16.000"), wide);
+        assertFalse(wide.contains("(x"), wide);
+        assertTrue(footer(tab).contains("view [wide]"), footer(tab));
+
+        // wide and collapsed: the line as logged, with the repeats collapsed
+        tab.handleKeyEvent(KeyEvent.ofChar('c'));
+        String collapsed = TuiTestHelper.renderToString(tab, 140, 30);
+        assertTrue(collapsed.contains("2026-11-08 10:00:16.000  INFO 42 --- [ 
- timer://java] route1 : Hello World  (x17)"),
+                collapsed);
+        assertFalse(collapsed.contains("2026-11-08 10:00:00.000"), collapsed);
+        assertTrue(footer(tab).contains("view [wide, collapsed]"), 
footer(tab));
+
+        tab.handleKeyEvent(KeyEvent.ofChar('c'));
+        assertEquals(LogTab.View.COMPACT, tab.view());
+    }
+
+    private static String footer(LogTab tab) {
+        List<Span> spans = new ArrayList<>();
+        tab.renderFooter(spans);
+        return spans.stream().map(Span::content).reduce("", String::concat);
+    }
+
+    @Test
+    void switchingTheViewKeepsTheLineAtTheTop() {
+        List<LogEntry> lines = new ArrayList<>();
+        lines.add(LogTab.parseLogLine("2026-11-08 10:00:00.000  INFO 42 --- 
[main] route1 : Started"));
+        for (int i = 0; i < 5; i++) {
+            lines.add(LogTab.parseLogLine("2026-11-08 10:00:0" + (i + 1) + 
".000  INFO 42 --- [main] route1 : Hello"));
+        }
+        lines.add(LogTab.parseLogLine("2026-11-08 10:00:07.000  INFO 42 --- 
[main] route1 : Done"));
+        List<LogEntry> shown = LogTab.fold(lines);
+
+        assertEquals(3, shown.size());
+        assertEquals(1, shown.get(1).line, "the folded entry starts at the 
first of its lines");
+        assertEquals(1, lines.get(3).repeat, "the lines read stay one by one");
+        // line 4 (a Hello) is in the folded entry 1, and line 6 (Done) is 
entry 2
+        assertEquals(1, LogTab.shownIndexOf(shown, 4, true));
+        assertEquals(2, LogTab.shownIndexOf(shown, 6, true));
+        assertEquals(4, LogTab.shownIndexOf(lines, 4, false));
+    }
+
     @Test
     void theCompactViewShowsTimeLevelLoggerAndMessage() {
         LogEntry entry = LogTab.parseLogLine("2026-10-01 17:24:30.257  INFO 
85013 --- [ntloop-thread-0]"

Reply via email to