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

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

commit 77116e8d9f958c59d8d1a53a5c07ec95bac4db69
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 09:58:08 2026 +0100

    chore: camel-jbang - Timeline: room around the panels, and taller charts 
and strips on a tall screen
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   4 +-
 .../dsl/jbang/core/commands/tui/TimelineTab.java   | 228 +++++++++++++++------
 .../src/main/resources/tui/help/timeline.md        |   2 +
 .../core/commands/tui/TimelineTabRenderTest.java   |  73 +++++++
 4 files changed, 238 insertions(+), 69 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 0e308f1b0fdf..d6599f431a84 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
@@ -53,8 +53,8 @@ camel-chaos, the faults of each chaos rule are a layer of 
their own, marked ๐ŸŽฒ
 apart. When no exchange failed, the WARN and ERROR lines of the log are the 
cause instead (`log ERROR`, `log WARN`),
 read from the log file of the integration: something that went wrong outside 
an exchange, such as a consumer that
 cannot connect. When nothing went wrong the band is one line.
-* *Routes* has a line per route. The height is the exchanges done that second, 
and the colour the worst that happened to
-them: completed, recovered by redelivery, handled by the error handler (such 
as the dead letter channel), or failed. A
+* *Routes* has a line per route (on a tall screen two rows high, with room 
around the panels and taller charts). The
+height is the exchanges done that second, and the colour the worst that 
happened to them: completed, recovered by redelivery, handled by the error 
handler (such as the dead letter channel), or failed. A
 second a chaos rule had the route down is shaded, and a mark says what 
happened to the route then: `โ–ถ` started, `โ– `
 stopped, `โ†ป` reloaded, `โœ•` removed. A `โ–’` is a second a circuit breaker of the 
route was open (the Circuit Breaker EIP
 with camel-resilience4j or camel-microprofile-fault-tolerance, or the 
throttling exception route policy), and the route
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 6875f44e8452..3de12c0904b1 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
@@ -25,6 +25,7 @@ import java.util.function.LongSupplier;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Padding;
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
@@ -62,6 +63,12 @@ class TimelineTab extends AbstractTableTab {
     private static final int DETAIL_LINES = DETAIL_ROWS + 5;
     private static final int SUFFIX = 30;
     private static final int MAX_LABEL = 18;
+    // a tall screen (this many rows to spare) has room around the panels, and 
its charts and strips grow
+    static final int ROOMY_ROWS = 12;
+    // the rows the room around takes: the padding of both panels, a blank 
line in each, and the line between them
+    private static final int ROOMY_EXTRA = 9;
+    private static final int MAX_CHART_ROWS = 6;
+    private static final int MAX_STRIP_ROWS = 2;
 
     private final LongSupplier clock;
     // the seconds a column has, and the characters a column is wide, as the 
last render worked them out
@@ -72,7 +79,11 @@ class TimelineTab extends AbstractTableTab {
     private long pausedAt = -1;
     // the column of the cursor while paused
     private int cursor = -1;
-    private int routeLinesY = -1;
+    // where the strips of the routes are, for the clicks: the first line, the 
lines from one route to the next, the
+    // lines of a strip
+    private int routeTop = -1;
+    private int routePitch = 1;
+    private int routeStrip = 1;
     private final TimelineExchanges drill = new TimelineExchanges();
 
     TimelineTab(MonitorContext ctx) {
@@ -285,10 +296,50 @@ class TimelineTab extends AbstractTableTab {
 
     @Override
     public boolean handleMouseEvent(MouseEvent me, Rect area) {
-        if (routeLinesY < 0 || lastTableArea == null) {
+        if (routeTop < 0 || !me.isClick() || lastTableArea == null || 
!lastTableArea.contains(me.x(), me.y())) {
             return false;
         }
-        return handleTableClick(me, lastTableArea, tableState, getRowCount());
+        // a click on a strip of a route selects it, not on the blank line 
between two routes
+        int dy = me.y() - routeTop;
+        int i = dy / routePitch;
+        if (dy >= 0 && dy % routePitch < routeStrip && i < getRowCount()) {
+            tableState.select(i);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * How the screen is laid out: compact, or on a tall screen with room 
around the panels, the charts this many rows
+     * high, the strips of the routes this many rows high and a blank line 
between them.
+     */
+    record Room(boolean roomy, int causeRows, int detailRows, int strip, 
boolean spaced) {
+        static final Room COMPACT = new Room(false, CAUSE_ROWS, DETAIL_ROWS, 
1, false);
+    }
+
+    /**
+     * The room the screen has: the rows to spare go first to the room around 
the panels, then a blank line between the
+     * routes, then taller strips, and the rest to the charts (a line kept 
free at the bottom).
+     */
+    static Room room(int height, int routes, boolean anyCause) {
+        int compact = (anyCause ? CAUSE_ROWS + 1 : 1) + routes + 4 + 
DETAIL_LINES + 2;
+        int spare = height - compact;
+        if (spare < ROOMY_ROWS) {
+            return Room.COMPACT;
+        }
+        spare -= ROOMY_EXTRA + 1;
+        boolean spaced = routes > 1 && spare >= routes - 1;
+        if (spaced) {
+            spare -= routes - 1;
+        }
+        int strip = 1;
+        if (routes > 0) {
+            strip = Math.min(MAX_STRIP_ROWS, 1 + spare / routes);
+            spare -= (strip - 1) * routes;
+        }
+        int cause = anyCause ? Math.min(MAX_CHART_ROWS - CAUSE_ROWS, spare / 
2) : 0;
+        int detail = Math.min(MAX_CHART_ROWS - DETAIL_ROWS, spare - cause);
+        return new Room(true, CAUSE_ROWS + cause, DETAIL_ROWS + detail, strip, 
spaced);
     }
 
     int seconds() {
@@ -323,20 +374,27 @@ class TimelineTab extends AbstractTableTab {
         List<Timeline.RouteSeries> routes = routes(info);
         List<Timeline.CauseSeries> causes = ctx.timeline.causes(info.pid, 
endSecond(), seconds());
         boolean anyCause = causes.stream().anyMatch(c -> sum(c.values()) > 0);
-        int causeHeight = anyCause ? CAUSE_ROWS + 1 : 1;
-        // the cause, the legend of the routes, a line per route and the time 
axis, in a border
-        int detailHeight = DETAIL_LINES + 2;
-        int topHeight = Math.max(6, Math.min(area.height() - detailHeight, 
causeHeight + routes.size() + 4));
+        Room room = room(area.height(), routes.size(), anyCause);
+        // on a tall screen: padding in the panels, a blank line after the 
cause, after the legend and in the detail,
+        // and a line between the panels
+        int gap = room.roomy() ? 1 : 0;
+        int causeHeight = anyCause ? room.causeRows() + 1 : 1;
+        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));
         List<Rect> rows = Layout.vertical()
-                .constraints(Constraint.length(topHeight), 
Constraint.length(detailHeight), Constraint.fill())
+                .constraints(Constraint.length(topHeight), 
Constraint.length(gap), Constraint.length(detailHeight),
+                        Constraint.fill())
                 .split(area);
-        int inner = Math.max(20, area.width() - 2);
+        int inner = Math.max(20, area.width() - 2 - 2 * gap);
         int label = Math.min(MAX_LABEL,
                 Math.max(11, routes.stream().mapToInt(r -> 
r.route().length()).max().orElse(0))) + 4;
         // a second is 1 to 3 characters wide as the screen allows, or a 
character is several seconds
-        int room = Math.max(10, inner - label - SUFFIX - 1);
-        int per = room >= seconds() ? 1 : (int) Math.ceil((double) seconds() / 
room);
-        width = per == 1 ? Math.max(1, Math.min(3, room / seconds())) : 1;
+        int space = Math.max(10, inner - label - SUFFIX - 1);
+        int per = space >= seconds() ? 1 : (int) Math.ceil((double) seconds() 
/ space);
+        width = per == 1 ? Math.max(1, Math.min(3, space / seconds())) : 1;
         this.per = per;
         int columns = (seconds() + per - 1) / per * width;
         if (cursor >= 0) {
@@ -345,42 +403,60 @@ class TimelineTab extends AbstractTableTab {
         }
 
         List<Line> lines = new ArrayList<>();
-        renderCauses(lines, causes, anyCause, info, columns, per, label);
-        boolean anyOpen = routes.stream().anyMatch(r -> r.total(r.open()) > 0);
-        lines.add(Line.from(Span.raw(pad("", label)), Span.styled("Routes  ", 
Theme.label()),
-                Span.styled("โ–ˆ", Theme.success()), Span.raw(" completed  "),
-                Span.styled("โ–ˆ", Theme.warning()), Span.raw(" recovered  "),
-                Span.styled("โ–ˆ", handledStyle()), Span.raw(" handled  "),
-                Span.styled("โ–ˆ", Theme.error()), Span.raw(" failed  "),
-                Span.styled("โ–‘", Theme.error()), Span.raw(" down"),
-                Span.raw(anyOpen ? "  " : ""), Span.styled(anyOpen ? "โ–’" : "", 
Theme.notice()),
-                Span.raw(anyOpen ? " circuit breaker open" : ""),
-                Span.raw(routes.stream().anyMatch(r -> 
Arrays.stream(r.events()).anyMatch(e -> e != null))
-                        ? "   โ–ถ started  โ–  stopped  โ†ป reloaded" : "")));
+        renderCauses(lines, causes, anyCause, info, columns, room.causeRows(), 
label);
+        blank(lines, room.roomy());
+        lines.add(legend(routes, label));
+        blank(lines, room.roomy());
         Integer sel = tableState.selected();
-        routeLinesY = lines.size();
+        int routeLinesY = lines.size();
         for (int i = 0; i < routes.size(); i++) {
-            lines.add(routeLine(routes.get(i), sel != null && sel == i, 
columns, per, label));
+            blank(lines, room.spaced() && i > 0);
+            lines.addAll(routeLines(routes.get(i), sel != null && sel == i, 
columns, per, label, room.strip()));
         }
         if (routes.isEmpty()) {
             lines.add(Line.from(Span.styled(pad("", label) + "no routes yet", 
Theme.muted())));
         }
         lines.add(axis(columns, per, label));
         Rect top = rows.get(0);
-        // the rows of the routes, as a table with a header line of two would 
have them, for the clicks
-        lastTableArea = new Rect(top.x(), top.y() + 1 + routeLinesY - 2, 
top.width(), routes.size() + 2);
-        
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title(info))).build(),
 top);
+        // the lines of the routes, for the clicks
+        routeTop = top.y() + 1 + gap + routeLinesY;
+        routePitch = room.strip() + (room.spaced() ? 1 : 0);
+        routeStrip = room.strip();
+        lastTableArea = new Rect(top.x(), routeTop, top.width(), Math.max(1, 
routeHeight));
+        Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE;
+        
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title(info),
 padding)).build(), top);
 
         if (drill.isOpen()) {
             List<Timeline.Kept> kept = keptAtCursor();
-            
frame.renderWidget(Paragraph.builder().text(Text.from(drill.lines(kept, 
DETAIL_LINES)))
-                    .block(block(drill.title(kept, ctx.timeline.done(info.pid, 
drill.from(), drill.to())))).build(),
-                    rows.get(1));
+            
frame.renderWidget(Paragraph.builder().text(Text.from(drill.lines(kept, 
detailHeight - 2 - 2 * gap)))
+                    .block(block(drill.title(kept, ctx.timeline.done(info.pid, 
drill.from(), drill.to())), padding))
+                    .build(),
+                    rows.get(2));
             return;
         }
         Timeline.RouteSeries selected = sel != null && sel >= 0 && sel < 
routes.size() ? routes.get(sel) : null;
-        renderDetail(frame, rows.get(1), selected, columns, per, label, info);
+        renderDetail(frame, rows.get(2), selected, columns, label, info, room);
+    }
+
+    private static void blank(List<Line> lines, boolean when) {
+        if (when) {
+            lines.add(Line.from(Span.raw("")));
+        }
+    }
 
+    /** The legend of the strips of the routes. */
+    private static Line legend(List<Timeline.RouteSeries> routes, int label) {
+        boolean anyOpen = routes.stream().anyMatch(r -> r.total(r.open()) > 0);
+        return Line.from(Span.raw(pad("", label)), Span.styled("Routes  ", 
Theme.label()),
+                Span.styled("โ–ˆ", Theme.success()), Span.raw(" completed  "),
+                Span.styled("โ–ˆ", Theme.warning()), Span.raw(" recovered  "),
+                Span.styled("โ–ˆ", handledStyle()), Span.raw(" handled  "),
+                Span.styled("โ–ˆ", Theme.error()), Span.raw(" failed  "),
+                Span.styled("โ–‘", Theme.error()), Span.raw(" down"),
+                Span.raw(anyOpen ? "  " : ""), Span.styled(anyOpen ? "โ–’" : "", 
Theme.notice()),
+                Span.raw(anyOpen ? " circuit breaker open" : ""),
+                Span.raw(routes.stream().anyMatch(r -> 
Arrays.stream(r.events()).anyMatch(e -> e != null))
+                        ? "   โ–ถ started  โ–  stopped  โ†ป reloaded" : ""));
     }
 
     private String title(IntegrationInfo info) {
@@ -406,7 +482,7 @@ class TimelineTab extends AbstractTableTab {
 
     private void renderCauses(
             List<Line> lines, List<Timeline.CauseSeries> causes, boolean 
anyCause, IntegrationInfo info, int columns,
-            int per, int label) {
+            int rows, int label) {
         List<Span> legend = new ArrayList<>();
         legend.add(Span.raw(pad("", label)));
         legend.add(Span.styled("Errors/s  ", Theme.label()));
@@ -433,7 +509,7 @@ class TimelineTab extends AbstractTableTab {
             return;
         }
         lines.add(Line.from(legend));
-        lines.addAll(stacked(values, styles, CAUSE_ROWS, columns, label));
+        lines.addAll(stacked(values, styles, rows, columns, label));
     }
 
     private static Style causeStyle(int i) {
@@ -445,12 +521,12 @@ class TimelineTab extends AbstractTableTab {
 
     // ---- the routes ----
 
-    private Line routeLine(Timeline.RouteSeries r, boolean selected, int 
columns, int per, int label) {
-        List<Span> spans = new ArrayList<>();
+    /**
+     * The strip of a route, {@code strip} rows high (the blocks stacked, so a 
strip of 2 rows has 16 levels), its name
+     * and its summary on the bottom row, where the blocks stand.
+     */
+    private List<Line> routeLines(Timeline.RouteSeries r, boolean selected, 
int columns, int per, int label, int strip) {
         String name = r.route().length() > label - 3 ? r.route().substring(0, 
label - 4) + "โ€ฆ" : r.route();
-        spans.add(selected
-                ? Span.styled(pad(">> " + name, label), Theme.selectionBg())
-                : Span.raw(pad("   " + name, label)));
         long[] completed = cols(r.completed(), false);
         long[] recovered = cols(r.recovered(), false);
         long[] handled = cols(r.handled(), false);
@@ -463,32 +539,47 @@ class TimelineTab extends AbstractTableTab {
             max = Math.max(max, total(completed[c], recovered[c], handled[c], 
failed[c]));
         }
         String[] events = eventsOf(r.events(), per);
+        List<List<Span>> rows = new ArrayList<>();
+        for (int row = 0; row < strip; row++) {
+            boolean bottom = row == strip - 1;
+            List<Span> spans = new ArrayList<>();
+            String text = bottom ? (selected ? ">> " : "   ") + name : "";
+            spans.add(selected ? Span.styled(pad(text, label), 
Theme.selectionBg()) : Span.raw(pad(text, label)));
+            rows.add(spans);
+        }
         for (int c = 0; c < columns; c++) {
             long t = total(completed[c], recovered[c], handled[c], failed[c]);
             String event = c / width < events.length && c % width == 0 ? 
events[c / width] : null;
-            if (event != null) {
-                // what happened to the route then, in place of its exchanges
-                spans.add(eventMark(event));
-            } else if (down[c] > 0) {
-                spans.add(Span.styled("โ–‘", Theme.error()));
-            } else if (open[c] > 0) {
-                // the circuit breaker was open (half open dim): the calls 
went to the fallback, or failed fast
-                spans.add(Span.styled("โ–’", open[c] >= 2 ? Theme.notice() : 
Theme.muted()));
-            } else if (completed[c] < 0) {
-                spans.add(Span.raw(" "));
-            } else if (t == 0) {
-                spans.add(Span.styled("ยท", Theme.muted()));
-            } else {
-                int level = (int) Math.max(1, Math.min(8, Math.ceil(t * 8.0 / 
max)));
-                Style style = failed[c] > 0 ? Theme.error()
-                        : handled[c] > 0 ? handledStyle()
-                        : recovered[c] > 0 || attempts[c] > 0 ? 
Theme.warning() : Theme.success();
-                spans.add(Span.styled(String.valueOf(BLOCKS.charAt(level - 
1)), style));
+            Style style = failed[c] > 0 ? Theme.error()
+                    : handled[c] > 0 ? handledStyle()
+                    : recovered[c] > 0 || attempts[c] > 0 ? Theme.warning() : 
Theme.success();
+            int level = t == 0 ? 0 : (int) Math.max(1, Math.min(8L * strip, 
Math.ceil(t * 8.0 * strip / max)));
+            for (int row = 0; row < strip; row++) {
+                boolean bottom = row == strip - 1;
+                Span cell;
+                if (event != null) {
+                    // what happened to the route then, in place of its 
exchanges
+                    cell = bottom ? eventMark(event) : Span.raw(" ");
+                } else if (down[c] > 0) {
+                    cell = Span.styled("โ–‘", Theme.error());
+                } else if (open[c] > 0) {
+                    // the circuit breaker was open (half open dim): the calls 
went to the fallback, or failed fast
+                    cell = Span.styled("โ–’", open[c] >= 2 ? Theme.notice() : 
Theme.muted());
+                } else if (completed[c] < 0) {
+                    cell = Span.raw(" ");
+                } else if (t == 0) {
+                    cell = bottom ? Span.styled("ยท", Theme.muted()) : 
Span.raw(" ");
+                } else {
+                    int here = Math.max(0, Math.min(8, level - (strip - 1 - 
row) * 8));
+                    cell = here == 0 ? Span.raw(" ") : 
Span.styled(String.valueOf(BLOCKS.charAt(here - 1)), style);
+                }
+                rows.get(row).add(cell);
             }
         }
-        spans.add(Span.raw("  "));
-        spans.addAll(summary(r));
-        return Line.from(spans);
+        List<Span> last = rows.get(strip - 1);
+        last.add(Span.raw("  "));
+        last.addAll(summary(r));
+        return rows.stream().map(Line::from).toList();
     }
 
     /** The event of each column of several seconds: the last in the column. */
@@ -609,18 +700,20 @@ class TimelineTab extends AbstractTableTab {
     // ---- the selected route ----
 
     private void renderDetail(
-            Frame frame, Rect area, Timeline.RouteSeries r, int columns, int 
per, int label, IntegrationInfo info) {
+            Frame frame, Rect area, Timeline.RouteSeries r, int columns, int 
label, IntegrationInfo info, Room room) {
         List<Line> lines = new ArrayList<>();
+        Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE;
         if (r == null) {
             
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(Span.raw(""))))
-                    .block(block(" Route ")).build(), area);
+                    .block(block(" Route ", padding)).build(), area);
             return;
         }
         List<long[]> values = List.of(cols(r.completed(), false), 
cols(r.recovered(), false),
                 cols(r.handled(), false), cols(r.failed(), false));
         List<Style> styles = List.of(Theme.success(), Theme.warning(), 
handledStyle(), Theme.error());
-        int rows = Math.max(2, Math.min(DETAIL_ROWS, area.height() - 7));
+        int rows = Math.max(2, Math.min(room.detailRows(), area.height() - 7 - 
(room.roomy() ? 4 : 0)));
         lines.addAll(stacked(values, styles, rows, columns, label));
+        blank(lines, room.roomy());
 
         long[] mean = cols(r.mean(), true);
         long maxMean = 0;
@@ -685,6 +778,7 @@ class TimelineTab extends AbstractTableTab {
             callLine.addAll(calls);
         }
         lines.add(Line.from(callLine));
+        blank(lines, room.roomy());
 
         List<Span> totals = new ArrayList<>();
         totals.add(Span.raw(pad("", label)));
@@ -694,7 +788,7 @@ class TimelineTab extends AbstractTableTab {
         addTotal(totals, r.total(r.failed()), "failed", Theme.error());
         lines.add(Line.from(totals));
         String title = " " + r.route() + " โ”€ per second ";
-        
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title)).build(),
 area);
+        
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title,
 padding)).build(), area);
     }
 
     private static void addTotal(List<Span> spans, long count, String words, 
Style style) {
@@ -823,8 +917,8 @@ class TimelineTab extends AbstractTableTab {
         return s.length() >= width ? s : " ".repeat(width - s.length()) + s;
     }
 
-    private static Block block(String title) {
-        return 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build();
+    private static Block block(String title, Padding padding) {
+        return 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).padding(padding).build();
     }
 
     @Override
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 c430256bfaac..59ce27f2c213 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
@@ -40,6 +40,8 @@ When nothing went wrong, the band is one line: `Errors/s 
none`.
 
 A line per route, one character per second. The height is the number of
 exchanges done that second, relative to the busiest second of the route.
+On a tall screen the lines are two rows high with a blank line between
+them, the charts are taller, and there is room around the panels.
 The colour is the worst that happened to them that second:
 
 - green: completed
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 257389c061fd..ee381eead6d1 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
@@ -22,10 +22,13 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
+import dev.tamboui.layout.Rect;
 import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
+import dev.tamboui.tui.event.MouseButton;
+import dev.tamboui.tui.event.MouseEvent;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.BeforeEach;
@@ -319,4 +322,74 @@ class TimelineTabRenderTest {
         spans.forEach(s -> sb.append(s.content()));
         return sb.toString();
     }
+
+    @Test
+    void aTallScreenHasRoomAroundThePanelsAndTallerStrips() {
+        TimelineTab tab = new TimelineTab(ctx, now::get);
+        List<String> lines = TuiTestHelper.renderToString(tab, 198, 
48).lines().toList();
+
+        // a line between the panels
+        int bottom = indexOf(lines, "โ•ฐ");
+        assertThat(lines.get(bottom + 1)).isBlank();
+        assertThat(lines.get(bottom + 2)).contains("orders โ”€ per second");
+        // a strip is 2 rows: the blocks of a busy second stand on the row 
with the name
+        int orders = indexOf(lines, ">> orders");
+        assertThat(lines.get(orders - 1)).contains("โ–ˆโ–ˆโ–ˆโ–ˆ");
+        // and a blank line between the routes
+        int shipping = indexOf(lines, "   shipping");
+        assertThat(shipping).isEqualTo(orders + 3);
+        assertThat(lines.get(orders + 1).replace("โ”‚", "")).isBlank();
+        // the charts grow: the scale of the errors per second is 6 rows from 
top to bottom
+        int top = indexOf(lines, "2 โ”ค");
+        assertThat(lines.get(top + 5)).contains("0 โ”ค");
+    }
+
+    @Test
+    void aSmallScreenStaysCompact() {
+        TimelineTab tab = new TimelineTab(ctx, now::get);
+        List<String> lines = TuiTestHelper.renderToString(tab, 160, 
30).lines().toList();
+
+        int bottom = indexOf(lines, "โ•ฐ");
+        assertThat(lines.get(bottom + 1)).contains("orders โ”€ per second");
+        int orders = indexOf(lines, ">> orders");
+        assertThat(lines.get(orders + 1)).contains("   shipping");
+    }
+
+    @Test
+    void theRoomGoesToTheSpaceAroundThenTheRoutesThenTheCharts() {
+        assertThat(TimelineTab.room(30, 3, 
true)).isEqualTo(TimelineTab.Room.COMPACT);
+        assertThat(TimelineTab.room(48, 3, true)).isEqualTo(new 
TimelineTab.Room(true, 6, 6, 2, true));
+        // the routes need the rows: one row each, no blank line between them
+        TimelineTab.Room many = TimelineTab.room(48, 10, true);
+        assertThat(many.roomy()).isTrue();
+        assertThat(many.strip()).isEqualTo(1);
+        assertThat(many.spaced()).isFalse();
+        // no error: the detail has the rows the errors would have
+        assertThat(TimelineTab.room(48, 3, false).causeRows()).isEqualTo(4);
+    }
+
+    @Test
+    void aClickOnTheStripOfARouteSelectsIt() {
+        TimelineTab tab = new TimelineTab(ctx, now::get);
+        Rect area = new Rect(0, 0, 198, 48);
+        List<String> lines = TuiTestHelper.renderToString(tab, 198, 
48).lines().toList();
+        int shipping = indexOf(lines, "   shipping");
+
+        // the blank line between orders and shipping is no route
+        assertThat(tab.handleMouseEvent(MouseEvent.press(MouseButton.LEFT, 40, 
shipping - 2), area)).isFalse();
+        // the upper row of the strip of shipping is shipping
+        assertThat(tab.handleMouseEvent(MouseEvent.press(MouseButton.LEFT, 40, 
shipping - 1), area)).isTrue();
+        
assertThat(tab.getTableDataAsJson().getInteger("selectedIndex")).isEqualTo(1);
+        assertThat(tab.handleMouseEvent(MouseEvent.press(MouseButton.LEFT, 40, 
shipping + 3), area)).isTrue();
+        
assertThat(tab.getTableDataAsJson().getInteger("selectedIndex")).isEqualTo(2);
+    }
+
+    private static int indexOf(List<String> lines, String text) {
+        for (int i = 0; i < lines.size(); i++) {
+            if (lines.get(i).contains(text)) {
+                return i;
+            }
+        }
+        throw new AssertionError("no line with " + text);
+    }
 }

Reply via email to