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 3bf571f4928f chore: camel-jbang - TUI Timeline says clearly whether it 
is paused or live (#29634)
3bf571f4928f is described below

commit 3bf571f4928f8ae22fd8f1e26fe566dfbda58df5
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 15:00:11 2026 +0100

    chore: camel-jbang - TUI Timeline says clearly whether it is paused or live 
(#29634)
    
    Paused, the Timeline only said "paused, 10m44s ago" in plain text in its 
title, so a paused screen looked like one that follows now.
    
    - the title has a badge: a reversed "‖ PAUSED, 10m44s ago" or a green "● 
live"
    - the border of the timeline is in the warning colour while paused
    - the time of the cursor under the axis is in the warning colour
    - the footer says "Space resume" while paused (was "follow")
    - paused on seconds with no data, a line says so, with Space to go back to 
now
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/TimelineTab.java   | 65 ++++++++++++++++++----
 .../src/main/resources/tui/help/timeline.md        |  5 +-
 .../core/commands/tui/TimelineTabRenderTest.java   | 28 +++++++++-
 3 files changed, 85 insertions(+), 13 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
index 2c0f488e1fab..4410914b712e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
@@ -38,6 +38,7 @@ import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -401,8 +402,9 @@ class TimelineTab extends AbstractTableTab {
         int routeHeight = routes.size() * room.strip() + (room.spaced() ? 
Math.max(0, routes.size() - 1) : 0);
         // the cause, the legend of the routes, a strip per route and the time 
axis, in a border
         int detailHeight = DETAIL_LINES + 2 + room.detailRows() - DETAIL_ROWS 
+ 4 * gap;
-        int topHeight = Math.max(6,
-                Math.min(area.height() - detailHeight - gap, causeHeight + 
Math.max(1, routeHeight) + 4 + 4 * gap));
+        boolean noData = pausedOnNoData(routes);
+        int topHeight = Math.max(6, Math.min(area.height() - detailHeight - 
gap,
+                causeHeight + Math.max(1, routeHeight) + 4 + 4 * gap + (noData 
? 1 : 0)));
         List<Rect> rows = Layout.vertical()
                 .constraints(Constraint.length(topHeight), 
Constraint.length(gap), Constraint.length(detailHeight),
                         Constraint.fill())
@@ -436,6 +438,14 @@ class TimelineTab extends AbstractTableTab {
             lines.add(Line.from(Span.styled(pad("", label) + "no routes yet", 
Theme.muted())));
         }
         lines.add(axis(columns, per, label));
+        if (noData) {
+            // a screen paused before there was anything to see looks like an 
integration that does nothing
+            List<Span> hint = new ArrayList<>();
+            hint.add(Span.raw(pad("", label)));
+            hint.add(Span.styled("‖ paused on seconds with no data  ", 
Theme.warning()));
+            hint(hint, "Space", "back to now");
+            lines.add(Line.from(hint));
+        }
         Rect top = rows.get(0);
         // the lines of the routes, for the clicks
         routeTop = top.y() + 1 + gap + routeLinesY;
@@ -446,7 +456,9 @@ class TimelineTab extends AbstractTableTab {
         drillArea = null;
         Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE;
         frame.renderWidget(
-                
Paragraph.builder().text(Text.from(lines)).block(block(title(info), padding, 
!drill.isOpen())).build(), top);
+                Paragraph.builder().text(Text.from(lines))
+                        .block(block(title(info), padding, !drill.isOpen(), 
pausedAt >= 0)).build(),
+                top);
 
         if (drill.isOpen()) {
             List<Timeline.Kept> kept = keptAtCursor();
@@ -485,7 +497,11 @@ class TimelineTab extends AbstractTableTab {
                         ? "   ▶ started  ■ stopped  ↻ reloaded" : ""));
     }
 
-    private String title(IntegrationInfo info) {
+    /**
+     * The title, with a badge that says at a glance whether the screen 
follows now ({@code ● live}) or stays at a
+     * moment ({@code ‖ PAUSED}, reversed, with how long ago that moment is).
+     */
+    private Title title(IntegrationInfo info) {
         String span = seconds() >= 120 ? (seconds() / 60) + "m" : seconds() + 
"s";
         StringBuilder sb = new StringBuilder(" Timeline ─ ").append(info.name 
!= null ? info.name : info.pid)
                 .append(" ─ last ").append(span).append(' ');
@@ -497,11 +513,31 @@ class TimelineTab extends AbstractTableTab {
         } else if (pausedAt < 0 && last < 0 && 
ctx.timeline.endSecond(info.pid) >= 0) {
             sb.append("─ no exchange in the last 5m ");
         }
+        sb.append("─ ");
+        Span badge;
         if (pausedAt >= 0) {
             long ago = nowSecond() - pausedAt;
-            sb.append("─ paused").append(ago > 0 ? ", " + 
ChaosTab.shortTime(ago * 1000) + " ago " : " ");
+            badge = Span.styled(" ‖ PAUSED" + (ago > 0 ? ", " + 
ChaosTab.shortTime(ago * 1000) + " ago " : " "),
+                    Theme.warning().bold().reversed());
+        } else {
+            badge = Span.styled("● live", Theme.success().bold());
         }
-        return sb.toString();
+        return Title.from(Line.from(Span.raw(sb.toString()), badge, Span.raw(" 
")));
+    }
+
+    /** Paused on seconds the integration did not report (before it started, 
or before the TUI read it). */
+    private boolean pausedOnNoData(List<Timeline.RouteSeries> routes) {
+        if (pausedAt < 0 || routes.isEmpty()) {
+            return false;
+        }
+        for (Timeline.RouteSeries r : routes) {
+            for (long v : r.completed()) {
+                if (v >= 0) {
+                    return false;
+                }
+            }
+        }
+        return true;
     }
 
     // ---- the cause ----
@@ -703,7 +739,8 @@ class TimelineTab extends AbstractTableTab {
         if (cursor >= 0) {
             long ago = nowSecond() - cursorTo();
             return Line.from(Span.raw(pad("", label)), Span.styled(text, 
Theme.muted()),
-                    Span.styled("  ▲ " + (ago > 0 ? ChaosTab.shortTime(ago * 
1000) + " ago" : "now"), Theme.label()),
+                    Span.styled("  ▲ " + (ago > 0 ? ChaosTab.shortTime(ago * 
1000) + " ago" : "now"),
+                            ago > 0 ? Theme.warning() : Theme.label()),
                     Span.styled(drill.isOpen() ? "" : "  Enter: its 
exchanges", Theme.muted()));
         }
         return Line.from(Span.raw(pad("", label)), Span.styled(text, 
Theme.muted()));
@@ -954,9 +991,17 @@ class TimelineTab extends AbstractTableTab {
         return s.length() >= width ? s : " ".repeat(width - s.length()) + s;
     }
 
-    /** A panel: the one the keys go to has the focus border (the routes, or 
the exchanges of a second when open). */
+    /**
+     * A panel: the one the keys go to has the focus border (the routes, or 
the exchanges of a second when open). The
+     * timeline paused has a warning border, so a screen that stays at a 
moment does not pass for one that follows now.
+     */
     private Block block(String title, Padding padding, boolean focused) {
-        return 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(focused))
+        return block(Title.from(title), padding, focused, false);
+    }
+
+    private Block block(Title title, Padding padding, boolean focused, boolean 
paused) {
+        Style border = paused ? Theme.warning() : ctx.paneBorder(focused);
+        return 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(border)
                 .title(title).padding(padding).build();
     }
 
@@ -972,7 +1017,7 @@ class TimelineTab extends AbstractTableTab {
     public void renderFooter(List<Span> spans) {
         hint(spans, "↑↓", "route");
         hint(spans, "w", "window " + (seconds() >= 120 ? seconds() / 60 + "m" 
: seconds() + "s"));
-        hint(spans, "Space", pausedAt >= 0 ? "follow" : "pause");
+        hint(spans, "Space", pausedAt >= 0 ? "resume" : "pause");
         if (pausedAt >= 0) {
             hint(spans, "←→", "second");
             hint(spans, "Shift+←→", "back and forth");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
index 00487643cc10..d0d10a71aabe 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
@@ -88,7 +88,10 @@ were done then, of which these were kept.
 - **w** — The window: 1 minute, 2 minutes or 5 minutes (several seconds
   a column when the screen is too narrow)
 - **Space** — Pause, to read a moment while it scrolls away; Space
-  follows now again
+  resumes, following now again. The title says `● live` or `‖ PAUSED`
+  (with how long ago the moment is), and the border is in the warning
+  colour while paused. Paused on seconds with no data (before the
+  integration started), the screen says so
 - **Left/Right** — While paused, move the cursor a second; at the edge
   the window moves
 - **Shift+Left/Right** — While paused, go back and forth in the 5
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
index af5c0ed1348b..31811a98514a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
@@ -142,13 +142,37 @@ class TimelineTabRenderTest {
         now.addAndGet(10_000);
         ctx.timeline.merge("1234", outage(T0 / 1000 + 69, 10, 60).json(), 
false);
         String screen = TuiTestHelper.renderToString(tab, 160, 40);
-        assertThat(screen).contains("paused, 10s ago").contains("▲ 10s ago");
+        assertThat(screen).contains("‖ PAUSED, 10s ago").contains("▲ 10s 
ago").doesNotContain("● live")
+                .doesNotContain("paused on seconds with no data");
         List<Span> hints = new ArrayList<>();
         tab.renderFooter(hints);
-        assertThat(text(hints)).contains("follow").contains("back and 
forth").contains("exchanges");
+        assertThat(text(hints)).contains("resume").contains("back and 
forth").contains("exchanges");
 
         assertThat(tab.handleEscape()).isTrue();
         assertThat(tab.isPaused()).isFalse();
+        assertThat(TuiTestHelper.renderToString(tab, 160, 40)).contains("● 
live").doesNotContain("PAUSED");
+    }
+
+    @Test
+    void pausedOnSecondsWithNoDataSaysSoAndHowToGoBackToNow() {
+        // the integration has just started, and Enter pauses before the TUI 
has read a second of it
+        ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        ctx.selectedPid = "1234";
+        TimelineTab tab = new TimelineTab(ctx, now::get);
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER));
+        assertThat(tab.isPaused()).isTrue();
+        tab.handleEscape();
+        assertThat(tab.isPaused()).isTrue();
+        // minutes later the integration has worked, but the screen stays at 
the moment it had no data
+        now.addAndGet(150_000);
+        ctx.timeline.merge("1234", outage(T0 / 1000 + 209, 60, 0).json(), 
true);
+        String screen = TuiTestHelper.renderToString(tab, 160, 40);
+        assertThat(screen).contains("‖ PAUSED, 2m30s ago").contains("no data 
yet")
+                .contains("paused on seconds with no data").contains("back to 
now");
+
+        tab.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertThat(tab.isPaused()).isFalse();
+        assertThat(TuiTestHelper.renderToString(tab, 160, 
40)).doesNotContain("paused on seconds with no data");
     }
 
     @Test

Reply via email to