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);
