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

davsclaus pushed a commit to branch CAMEL-26864-tui-walk-ux
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d9caf573c5bf753d0be10ef3336b3b5e40fa6ff7
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 20:43:03 2026 +0100

    CAMEL-26864: camel-jbang - the Log says paused or live as the Timeline 
does, and a message without a body reads (no body)
    
    - Log: a green ● live or a reversed ‖ PAUSED badge in the title, a warning 
border while paused, and Space pauses and resumes as on the Timeline (f and End 
still work)
    - Source, Routes, Diagram and Inspect show the body type "null" as (no 
body), and the kind null as no body
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/BodyTypes.java     | 19 +++++++++-----
 .../dsl/jbang/core/commands/tui/HistoryTab.java    |  2 +-
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  | 26 ++++++++++++++-----
 .../src/main/resources/tui/help/log.md             |  2 +-
 .../core/commands/tui/BodyTypesViewsTest.java      | 18 ++++++++++---
 .../jbang/core/commands/tui/LogTabRenderTest.java  | 30 ++++++++++++++++++++++
 6 files changed, 79 insertions(+), 18 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
index 47034b59dbdd..bb9cb78482b3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
@@ -32,6 +32,12 @@ public final class BodyTypes {
     /** The mark of a step reached with bodies of more than one kind. */
     public static final String MARK = TuiIcons.WARN;
 
+    /**
+     * A message without a body, which the app gives as the type "null" 
(MessageHelper): the word null alone reads as a
+     * class name.
+     */
+    public static final String NO_BODY = "(no body)";
+
     private BodyTypes() {
     }
 
@@ -40,9 +46,9 @@ public final class BodyTypes {
         return kinds != null && kinds.size() > 1;
     }
 
-    /** The simple class name of a type: LinkedHashMap, byte[]. */
+    /** The simple class name of a type: LinkedHashMap, byte[], or (no body). 
*/
     public static String simpleName(String type) {
-        return type.substring(type.lastIndexOf('.') + 1);
+        return "null".equals(type) ? NO_BODY : 
type.substring(type.lastIndexOf('.') + 1);
     }
 
     /** The types by their simple class names: String | LinkedHashMap. */
@@ -54,12 +60,13 @@ public final class BodyTypes {
         return sj.toString();
     }
 
-    /** The kinds in words: payload and data, or null, payload and data. */
+    /** The kinds in words: payload and data, or no body, payload and data. */
     public static String kinds(List<String> kinds) {
-        if (kinds.size() < 2) {
-            return String.join("", kinds);
+        List<String> words = kinds.stream().map(k -> "null".equals(k) ? "no 
body" : k).toList();
+        if (words.size() < 2) {
+            return String.join("", words);
         }
-        return String.join(", ", kinds.subList(0, kinds.size() - 1)) + " and " 
+ kinds.get(kinds.size() - 1);
+        return String.join(", ", words.subList(0, words.size() - 1)) + " and " 
+ words.get(words.size() - 1);
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 6345bae4b9dd..45c82982f621 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -2152,7 +2152,7 @@ class HistoryTab extends AbstractTab {
                 lines.add(Line.from(Span.raw("   " + stripControlChars(bl))));
             }
         } else {
-            lines.add(Line.from(Span.styled(" Body is null", headerStyle)));
+            lines.add(Line.from(Span.styled(" Body: ", headerStyle), 
Span.styled(BodyTypes.NO_BODY, Style.EMPTY.dim())));
         }
         lines.add(Line.from(Span.raw("")));
     }
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 8442ece204b3..19c27f0e66e9 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
@@ -209,7 +209,8 @@ class LogTab extends AbstractTab {
             logLevelListState.select(2);
             return true;
         }
-        if (ke.isCharIgnoreCase('f')) {
+        if (ke.isChar(' ') || ke.isCharIgnoreCase('f')) {
+            // Space pauses and resumes, as on the Timeline
             followMode = !followMode;
             return true;
         }
@@ -355,18 +356,17 @@ class LogTab extends AbstractTab {
         } else {
             titleSpans.add(Span.raw(" Log "));
         }
-        if (hasNew) {
-            // follow mode is off (a scroll or Home/PgUp switches it off) and 
lines arrived below the view
-            titleSpans.add(Span.styled("(paused, End follows)", 
Theme.warning()));
-            titleSpans.add(Span.raw(" "));
-        }
+        titleSpans.add(followBadge(hasNew));
+        titleSpans.add(Span.raw(" "));
         if (ctx.logPinned && ctx.logPinPercent > 0 && ctx.logPinVisible) {
             titleSpans.add(Span.styled(" Ctrl+L ", Theme.hintKey()));
             titleSpans.add(Span.raw(" pin (" + ctx.logPinPercent + "%)  "));
         }
         titleLine = Line.from(titleSpans);
+        // paused, the border is in the warning colour, so a log that stays 
put does not pass for one that follows
         Block block = Block.builder()
                 .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .borderStyle(followMode ? Style.EMPTY : Theme.warning())
                 .title(Title.from(titleLine))
                 .build();
         frame.renderWidget(block, area);
@@ -485,6 +485,18 @@ class LogTab extends AbstractTab {
         }
     }
 
+    /**
+     * The badge of the title, the same as on the Timeline: a green {@code ● 
live} while the log follows the newest
+     * lines, a reversed {@code ‖ PAUSED} when it stays put (Space, f, a 
scroll, Home or PgUp), which says so when there
+     * are lines below the view.
+     */
+    Span followBadge(boolean hasNew) {
+        if (followMode) {
+            return Span.styled("● live", Theme.success().bold());
+        }
+        return Span.styled(" ‖ PAUSED" + (hasNew ? ", more below " : " "), 
Theme.warning().bold().reversed());
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         search.renderFooterHints(spans);
@@ -513,7 +525,7 @@ class LogTab extends AbstractTab {
         if (!ctx.isInfraSelected()) {
             hint(spans, "l", "level");
         }
-        hint(spans, "f", "follow" + (followMode ? " [on]" : " [off]"));
+        hint(spans, "Space", followMode ? "pause" : "resume");
         if (ctx.logPinned) {
             hint(spans, "Ctrl+L", "pin (" + ctx.logPinPercent + "%)");
         } else {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/log.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/log.md
index ea077b993d5a..973071a63f48 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/log.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/log.md
@@ -102,7 +102,7 @@ newest first; the same error repeated is listed once with 
its count.
 - `h` — highlight a word
 - `l` — change log level
 - `Shift+F8` — fix with AI: the ERROR on the screen (a list to pick from when 
there are several)
-- `f` — toggle follow mode (`End` turns it on). Scrolling with the mouse 
wheel, `Up`, `PgUp` or `Home` turns it off; the title then shows `(paused, End 
follows)` when new lines arrive below the view
+- `Space` — pause or resume, as on the Timeline (`f` does the same, and `End` 
resumes). Scrolling with the mouse wheel, `Up`, `PgUp` or `Home` pauses too. 
The title says `● live` while the log follows the newest lines, and a reversed 
`‖ PAUSED` in a warning border while it stays put, with `more below` when there 
are lines below the view
 - `w` — toggle word wrap
 - `c` — toggle the compact view (on by default): the time, level, logger and 
message of each line, without the date, pid and thread
 - `Ctrl+L` — pin/cycle/unpin log panel (works from any tab)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
index a27775637353..633408affbd9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
 import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -113,11 +114,22 @@ class BodyTypesViewsTest {
         assertThat(BodyTypes.label(List.of("java.lang.String", "byte[]"), 
List.of("payload")))
                 .isEqualTo("String | byte[]");
         assertThat(BodyTypes.label(List.of("null", "java.util.LinkedHashMap"), 
List.of("null", "data")))
-                .isEqualTo(TuiIcons.WARN + " null | LinkedHashMap (null and 
data)");
+                .isEqualTo(TuiIcons.WARN + " (no body) | LinkedHashMap (no 
body and data)");
         assertThat(BodyTypes.label(List.of(), List.of())).isEmpty();
         assertThat(BodyTypes.shortLabel(List.of("null", 
"java.util.LinkedHashMap"), List.of("null", "data")))
-                .isEqualTo(TuiIcons.WARN + " null | LinkedHashMap");
-        assertThat(BodyTypes.kinds(List.of("null", "payload", 
"data"))).isEqualTo("null, payload and data");
+                .isEqualTo(TuiIcons.WARN + " (no body) | LinkedHashMap");
+        assertThat(BodyTypes.kinds(List.of("null", "payload", 
"data"))).isEqualTo("no body, payload and data");
+        // CAMEL-26864: a step reached without a body, as the setBody after a 
timer, says so, not "null"
+        assertThat(BodyTypes.label(List.of("null"), 
List.of("null"))).isEqualTo("(no body)");
+        assertThat(BodyTypes.fit(List.of("null"), List.of("null"), 
20)).isEqualTo("(no body)");
+    }
+
+    @Test
+    void inspectSaysAMessageHasNoBody() {
+        List<Line> lines = new ArrayList<>();
+        HistoryTab.addBodyLines(lines, null, "null", -1, false);
+        String text = 
lines.get(0).spans().stream().map(Span::content).reduce("", String::concat);
+        assertThat(text).isEqualTo(" Body: (no body)");
     }
 
     @Test
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 64e020e845a6..e2704850699f 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
@@ -110,6 +110,36 @@ class LogTabRenderTest {
         assertEquals(LogTab.View.COMPACT, tab.view());
     }
 
+    @Test
+    void spacePausesAndResumesWithTheBadgeOfTheTimeline() {
+        List<LogEntry> entries = new ArrayList<>();
+        for (int i = 0; i < 40; i++) {
+            entries.add(LogTab.parseLogLine("2026-11-11 10:00:" + 
String.format("%02d", i)
+                                            + ".000  INFO 42 --- [main] route1 
: Line " + i));
+        }
+        LogTab tab = new LogTab(ctx);
+        tab.setEntriesForTesting(entries);
+
+        String live = TuiTestHelper.renderToString(tab, 140, 12);
+        assertTrue(live.contains("● live"), live);
+        assertTrue(footer(tab).matches(".*Space +pause.*"), footer(tab));
+
+        // CAMEL-26864: Space pauses, as on the Timeline; the badge says so, 
not only the footer
+        assertTrue(tab.handleKeyEvent(KeyEvent.ofChar(' ')));
+        String paused = TuiTestHelper.renderToString(tab, 140, 12);
+        assertTrue(paused.contains("‖ PAUSED") && !paused.contains("● live"), 
paused);
+        assertTrue(footer(tab).matches(".*Space +resume.*"), footer(tab));
+
+        tab.pageUp();
+        String above = TuiTestHelper.renderToString(tab, 140, 12);
+        assertTrue(above.contains("‖ PAUSED, more below"), above);
+
+        // End (and f) still resume
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.END));
+        String resumed = TuiTestHelper.renderToString(tab, 140, 12);
+        assertTrue(resumed.contains("● live") && resumed.contains("Line 39"), 
resumed);
+    }
+
     private static String footer(LogTab tab) {
         List<Span> spans = new ArrayList<>();
         tab.renderFooter(spans);

Reply via email to