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

davsclaus pushed a commit to branch tui-timeline-paused-badge
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c7327ba519ea77644decdc2eb462da5480a8e027
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 13:33:06 2026 +0100

    chore: camel-jbang - TUI Timeline says clearly whether it is paused or live
    
    Paused, the Timeline only said "paused, 10m44s ago" in plain text in the
    middle of its title, so a screen that stays at a moment looked like one that
    follows now. Paused on seconds with no data (Enter or Space pressed before
    the TUI read the first seconds of the integration), every route said "no
    data yet" and the screen looked like an integration that does nothing.
    
    - 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]>
---
 .../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