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

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

commit b6ae0f851f84d07f861f5b926c0ef506197f3433
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 17:41:59 2026 +0200

    chore: camel-jbang - layout polish: a summary when nothing is selected, 
wrapped lines keep their indent, a compact log, a waterfall that is a time 
line, and a screenshot with the menu open
    
    - Overview: an "All" panel (running, opened, totals, heap) instead of an 
empty box when no integration is selected, no >> marker without a selection, 
and TOTAL/FAIL wide enough for "(since last)"
    - a wrapped detail line continues under its value or indent (Inspect, 
Activity, Errors details, Architecture info)
    - Log: a compact view, on by default (c): time, level, logger and message
    - Inspect waterfall: each bar starts when its step ran
    - the view bar ends in … when a setting does not fit
    - run form: the web console path of the runtime (/actuator/camel, 
/q/camel/dev-console, /q/dev)
    - tui_action screenshot captures the screen as shown, an open menu included
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  6 +-
 .../jbang/core/commands/tui/ArchitectureView.java  |  8 +-
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 82 ++++++++++++++-----
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  | 65 +++++++++++++--
 .../dsl/jbang/core/commands/tui/OverviewTab.java   | 60 +++++++++++++-
 .../jbang/core/commands/tui/RunOptionsForm.java    | 13 ++-
 .../dsl/jbang/core/commands/tui/SubViewBar.java    | 17 ++++
 .../dsl/jbang/core/commands/tui/TuiHelper.java     | 92 ++++++++++++++++++++++
 .../src/main/resources/tui/help/log.md             |  1 +
 .../jbang/core/commands/tui/ActionsPopupTest.java  | 14 ++++
 .../jbang/core/commands/tui/HangingWrapTest.java   | 75 ++++++++++++++++++
 .../core/commands/tui/HistoryTabRenderTest.java    | 26 ++++++
 .../jbang/core/commands/tui/LogTabRenderTest.java  |  8 ++
 .../jbang/core/commands/tui/SubViewBarTest.java    | 15 ++++
 14 files changed, 448 insertions(+), 34 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index bf2db2b5c840..753147193168 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -1482,7 +1482,11 @@ class ActionsPopup {
                 Theme.toggle();
                 refreshTheme();
             }
-            case SCREENSHOT -> screenshotAction.run();
+            case SCREENSHOT -> {
+                // the screen as it is shown, with an open menu or popup (for 
docs): nothing is closed first
+                screenshotAction.run();
+                return true;
+            }
             case SHOW_KEYSTROKES -> toggleKeystrokes.run();
             case TAPE_RECORDING -> toggleTapeRecording.run();
             case DOCTOR -> doctorPopup.open();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index 33d7d5645917..7b5dad3d9d70 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -596,8 +596,8 @@ final class ArchitectureView {
             section(lines, "Routes", names(g.routes()), Set.of());
             section(lines, "Endpoints", g.entryPoints(), Set.of());
             frame.renderWidget(Paragraph.builder()
-                    .text(Text.from(lines))
-                    .overflow(Overflow.WRAP_WORD)
+                    .text(Text.from(TuiHelper.hangingWrap(lines, Math.max(1, 
area.width() - 2))))
+                    .overflow(Overflow.CLIP)
                     
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
 Info ").build())
                     .build(), area);
             return;
@@ -619,8 +619,8 @@ final class ArchitectureView {
                     Theme.warning())));
         }
         frame.renderWidget(Paragraph.builder()
-                .text(Text.from(lines))
-                .overflow(Overflow.WRAP_WORD)
+                .text(Text.from(TuiHelper.hangingWrap(lines, Math.max(1, 
area.width() - 2))))
+                .overflow(Overflow.CLIP)
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
 Info ").build())
                 .build(), area);
     }
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 5d41814776ac..8338d84aace9 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
@@ -1437,22 +1437,23 @@ class HistoryTab extends AbstractTab {
     }
 
     record WaterfallStep(String nodeId, String processor, String direction, 
boolean first, boolean last,
-            int nodeLevel, long elapsed, int inlineDepth) {
+            int nodeLevel, long elapsed, int inlineDepth, long startMs) {
 
         static WaterfallStep fromTrace(TraceEntry e) {
             return new WaterfallStep(
                     e.nodeId, e.processor, e.direction, e.first, e.last, 
e.nodeLevel, e.elapsed,
-                    e.inlineDepth);
+                    e.inlineDepth, e.epochMs);
         }
 
         static WaterfallStep fromHistory(HistoryEntry e) {
             return new WaterfallStep(
                     e.nodeId, e.processor, e.direction, e.first, e.last, 
e.nodeLevel, e.elapsed,
-                    e.inlineDepth);
+                    e.inlineDepth, e.epochMs);
         }
 
         WaterfallStep withElapsed(long newElapsed) {
-            return new WaterfallStep(nodeId, processor, direction, first, 
last, nodeLevel, newElapsed, inlineDepth);
+            return new WaterfallStep(
+                    nodeId, processor, direction, first, last, nodeLevel, 
newElapsed, inlineDepth, startMs);
         }
 
         String label() {
@@ -1507,11 +1508,14 @@ class HistoryTab extends AbstractTab {
             return;
         }
 
+        long[] offsets = waterfallOffsets(forward);
         long maxElapsed = 0;
         long minDuration = Long.MAX_VALUE;
         long maxDuration = 0;
-        for (WaterfallStep e : forward) {
-            maxElapsed = Math.max(maxElapsed, e.elapsed);
+        for (int i = 0; i < forward.size(); i++) {
+            WaterfallStep e = forward.get(i);
+            // the time line: as long as the step that ends last
+            maxElapsed = Math.max(maxElapsed, offsets[i] + e.elapsed);
             if (!e.first) {
                 minDuration = Math.min(minDuration, e.elapsed);
                 maxDuration = Math.max(maxDuration, e.elapsed);
@@ -1558,7 +1562,7 @@ class HistoryTab extends AbstractTab {
         int end = Math.min(waterfallScroll + visibleLines, forward.size());
         List<Line> lines = new ArrayList<>();
         for (int i = waterfallScroll; i < end; i++) {
-            lines.add(renderWaterfallStep(forward.get(i), labelWidth, 
barMaxWidth,
+            lines.add(renderWaterfallStep(forward.get(i), offsets[i], 
labelWidth, barMaxWidth,
                     maxElapsed, minDuration, maxDuration, i == 
selectedForwardIndex));
         }
 
@@ -1577,8 +1581,50 @@ class HistoryTab extends AbstractTab {
         }
     }
 
+    /**
+     * When each step started, in ms from the first step processed, so the 
bars of a waterfall stand where they ran. A
+     * route row starts with its first step (its own time is when the exchange 
was created, before it was processed).
+     * Without the times of the steps, every bar starts at 0.
+     */
+    static long[] waterfallOffsets(List<WaterfallStep> steps) {
+        long[] offsets = new long[steps.size()];
+        // the steps processed set the time line; a route's first and last 
rows carry when the exchange was created
+        long t0 = Long.MAX_VALUE;
+        for (WaterfallStep s : steps) {
+            if (!s.first() && !s.last()) {
+                if (s.startMs() <= 0) {
+                    return offsets;
+                }
+                t0 = Math.min(t0, s.startMs());
+            }
+        }
+        if (t0 == Long.MAX_VALUE) {
+            return offsets;
+        }
+        for (int i = 0; i < steps.size(); i++) {
+            WaterfallStep s = steps.get(i);
+            if (s.first()) {
+                // a route starts with its first step
+                long start = i + 1 < steps.size() && !steps.get(i + 1).last() 
? steps.get(i + 1).startMs() : t0;
+                offsets[i] = Math.max(0, start - t0);
+            } else if (s.last()) {
+                // the end of a route stands where the route started
+                offsets[i] = 0;
+                for (int j = i - 1; j >= 0; j--) {
+                    if (steps.get(j).first() && 
nodeIdEquals(steps.get(j).nodeId(), s.nodeId())) {
+                        offsets[i] = offsets[j];
+                        break;
+                    }
+                }
+            } else {
+                offsets[i] = Math.max(0, s.startMs() - t0);
+            }
+        }
+        return offsets;
+    }
+
     private static Line renderWaterfallStep(
-            WaterfallStep entry, int labelWidth, int maxBarWidth,
+            WaterfallStep entry, long offset, int labelWidth, int maxBarWidth,
             long maxElapsed, long minDuration, long maxDuration, boolean 
selected) {
         String indicator = selected ? ">> " : "   ";
         String indent = "  ".repeat(entry.nodeLevel);
@@ -1593,8 +1639,10 @@ class HistoryTab extends AbstractTab {
         Style bandStyle = isRoute ? Style.EMPTY.dim() : 
TuiHelper.colorForDuration(entry.elapsed, minDuration, maxDuration);
 
         double ratio = maxElapsed > 0 ? (double) entry.elapsed / maxElapsed : 
0;
+        int lead = maxElapsed > 0 ? (int) Math.round((double) offset / 
maxElapsed * maxBarWidth) : 0;
         int barWidth = Math.max(1, (int) Math.round(ratio * maxBarWidth));
-        String bar = "█".repeat(barWidth);
+        lead = Math.min(lead, Math.max(0, maxBarWidth - barWidth));
+        String bar = " ".repeat(lead) + "█".repeat(barWidth);
 
         String durationStr = entry.elapsed + "ms";
         int pad = Math.max(1, 8 - durationStr.length());
@@ -2327,19 +2375,11 @@ class HistoryTab extends AbstractTab {
         Rect inner = block.inner(area);
         int visibleHeight = Math.max(1, inner.height());
         int visibleWidth = Math.max(1, inner.width() - 1);
-        int contentHeight;
         if (wordWrap) {
-            contentHeight = 0;
-            for (Line l : lines) {
-                int w = l.width();
-                contentHeight += Math.max(1, (w + visibleWidth - 1) / 
visibleWidth);
-            }
-            // word-wrap breaks at word boundaries which can produce more lines
-            // than char-based math; add padding so last section is always 
reachable
-            contentHeight += visibleHeight;
-        } else {
-            contentHeight = lines.size();
+            // wrapped here, so a long header value continues under the value, 
not at the left edge
+            lines = TuiHelper.hangingWrap(lines, visibleWidth);
         }
+        int contentHeight = lines.size();
         int maxScroll = Math.max(0, contentHeight - visibleHeight);
         if (scroll[0] > maxScroll) {
             scroll[0] = maxScroll;
@@ -2357,7 +2397,7 @@ class HistoryTab extends AbstractTab {
         List<Line> visibleLines = (!wordWrap && hScroll[0] > 0) ? 
applyHSkip(lines, hScroll[0]) : lines;
         Paragraph detail = Paragraph.builder()
                 .text(Text.from(visibleLines))
-                .overflow(wordWrap ? Overflow.WRAP_WORD : Overflow.CLIP)
+                .overflow(Overflow.CLIP)
                 .scroll(scroll[0])
                 .build();
         frame.renderWidget(detail, hChunks.get(0));
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 838ed865fbee..1a1bd3d93d42 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
@@ -93,6 +93,9 @@ class LogTab extends AbstractTab {
     private long evictedSeen;
     private boolean followMode = true;
     private boolean wordWrap = true;
+    // time, level, logger and message only: the date, pid and thread take 
most of a row in a terminal
+    private boolean compact = true;
+    private boolean cachedCompact;
     private int hScroll;
     private boolean showLogLevelPopup;
 
@@ -178,6 +181,11 @@ class LogTab extends AbstractTab {
             hScroll = 0;
             return true;
         }
+        if (ke.isCharIgnoreCase('c')) {
+            compact = !compact;
+            hScroll = 0;
+            return true;
+        }
         if (!wordWrap) {
             if (ke.isLeft()) {
                 followMode = false;
@@ -340,19 +348,29 @@ class LogTab extends AbstractTab {
         int hSkip = wordWrap ? 0 : hScroll;
 
         boolean entriesChanged = entries != cachedLogEntries;
-        if (entriesChanged || hSkip != cachedLogHSkip) {
+        if (entriesChanged || hSkip != cachedLogHSkip || compact != 
cachedCompact) {
             cachedLogEntries = entries;
             cachedLogHSkip = hSkip;
+            cachedCompact = compact;
             List<Line> built = new ArrayList<>(entries.size());
             int maxW = 0;
             for (int i = 0; i < entries.size(); i++) {
                 LogEntry entry = entries.get(i);
                 String raw = entry.raw != null ? entry.raw : "";
-                if (!wordWrap) {
-                    maxW = Math.max(maxW, 
CharWidth.of(TuiHelper.stripAnsi(raw)));
+                Line line;
+                if (compact && !entry.time.isEmpty()) {
+                    line = compactLine(entry);
+                    if (!wordWrap) {
+                        maxW = Math.max(maxW, line.width());
+                        line = hSkip > 0 ? 
TuiHelper.ansiToLine(lineText(line), hSkip) : line;
+                    }
+                } else {
+                    if (!wordWrap) {
+                        maxW = Math.max(maxW, 
CharWidth.of(TuiHelper.stripAnsi(raw)));
+                    }
+                    line = raw.indexOf('\u001B') >= 0
+                            ? TuiHelper.ansiToLine(raw, hSkip) : 
colorizePlainLog(raw, entry);
                 }
-                Line line = raw.indexOf('\u001B') >= 0
-                        ? TuiHelper.ansiToLine(raw, hSkip) : 
colorizePlainLog(raw, entry);
                 if (entry.repeat > 1) {
                     line = line.append(Span.styled("  (x" + entry.repeat + 
")", Theme.warning().bold()));
                 }
@@ -430,6 +448,7 @@ class LogTab extends AbstractTab {
         }
         search.renderSearchHints(spans);
         hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
+        hint(spans, "c", "compact" + (compact ? " [on]" : " [off]"));
         if (!ctx.isInfraSelected()) {
             hint(spans, "l", "level");
         }
@@ -643,6 +662,42 @@ class LogTab extends AbstractTab {
                                                                + 
"\\[([^]]*)]\\s+"
                                                                + 
"(\\S+)\\s*:\\s*(.*)$");
 
+    /** A log line in the compact view: 17:24:30.257 INFO 
VertxPlatformHttpServer Vert.x HttpServer started ... */
+    static Line compactLine(LogEntry entry) {
+        String time = entry.time.length() > 12 ? 
entry.time.substring(entry.time.length() - 12) : entry.time;
+        // the time of day: the date is the same for every line of a run
+        int space = time.indexOf(' ');
+        if (space >= 0) {
+            time = time.substring(space + 1);
+        }
+        String logger = entry.logger != null ? 
TuiHelper.truncate(entry.logger, 24) : "";
+        return Line.from(
+                Span.styled(time, DIM),
+                Span.raw(" "),
+                Span.styled(String.format("%5s", entry.level), 
levelStyle(entry.level)),
+                Span.raw(" "),
+                Span.styled(String.format("%-24s", logger), 
Style.EMPTY.fg(Theme.accent())),
+                Span.raw(" "),
+                Span.raw(entry.message != null ? entry.message : ""));
+    }
+
+    private static String lineText(Line line) {
+        StringBuilder sb = new StringBuilder();
+        line.spans().forEach(sp -> sb.append(sp.content()));
+        return sb.toString();
+    }
+
+    private static Style levelStyle(String level) {
+        return switch (level) {
+            case "ERROR", "FATAL" -> Theme.error();
+            case "WARN" -> Theme.warning();
+            case "INFO" -> Theme.success();
+            case "DEBUG" -> Style.EMPTY.fg(Theme.accent());
+            case "TRACE" -> Style.EMPTY.dim();
+            default -> Style.EMPTY;
+        };
+    }
+
     private static Line colorizePlainLog(String raw, LogEntry entry) {
         String plain = TuiHelper.stripAnsi(raw);
         Matcher m = PID_PATTERN.matcher(plain);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 939b5e382764..5b9f4b10696b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -274,6 +274,10 @@ class OverviewTab extends AbstractTab {
             infraFocused = false;
         }
 
+        if (ctx.selectedPid == null && !infraFocused) {
+            // no integration selected: no row marked as if one were (the 
header and info panel say none is)
+            tableState.clearSelection();
+        }
         if (ctx.selectedPid != null) {
             for (int i = 0; i < infos.size(); i++) {
                 if (ctx.selectedPid.equals(infos.get(i).pid)) {
@@ -515,6 +519,9 @@ class OverviewTab extends AbstractTab {
         long maxFailed = infos.stream().mapToLong(i -> 
i.failed).max().orElse(0);
         int tw = Math.max(numWidth(maxTotal), 6);
         int fw = Math.max(numWidth(maxFailed), 6);
+        // TOTAL and FAIL are as wide as the longest "count (since last)", so 
the since part is not cut
+        int totalColWidth = 14;
+        int failColWidth = 14;
         for (IntegrationInfo info : infos) {
             boolean isEven = (rowIndex++ % 2 == 0);
             Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) : Style.EMPTY;
@@ -620,6 +627,8 @@ class OverviewTab extends AbstractTab {
                         ? Line.from(Span.styled(String.format("%" + fw + "d", 
info.failed), failStyle),
                                 Span.styled(" (" + info.sinceLastFailed + ")", 
Theme.muted()))
                         : Line.from(Span.styled(String.format("%" + fw + "d", 
info.failed), failStyle));
+                totalColWidth = Math.max(totalColWidth, totalCell.width() + 1);
+                failColWidth = Math.max(failColWidth, failCell.width() + 1);
 
                 rows.add(Row.from(
                         Cell.from(info.pid),
@@ -658,8 +667,8 @@ class OverviewTab extends AbstractTab {
                 Constraint.length(10),
                 Constraint.length(7),
                 Constraint.length(8),
-                Constraint.length(14),
-                Constraint.length(14),
+                Constraint.length(totalColWidth),
+                Constraint.length(failColWidth),
                 Constraint.min(20),
                 Constraint.length(0)
         };
@@ -829,6 +838,44 @@ class OverviewTab extends AbstractTab {
         renderInfoPanel(frame, infoArea);
     }
 
+    /** The info panel when no integration is selected: how many run, and 
their messages and memory together. */
+    static List<Line> summaryLines(List<IntegrationInfo> infos) {
+        Style dim = Theme.muted();
+        int running = 0;
+        int projects = 0;
+        long total = 0;
+        long failed = 0;
+        long heap = 0;
+        for (IntegrationInfo info : infos) {
+            if (info.vanishing) {
+                continue;
+            }
+            if (info.phantom) {
+                projects++;
+                continue;
+            }
+            running++;
+            total += info.exchangesTotal;
+            failed += info.failed;
+            heap += Math.max(0, info.heapMemUsed);
+        }
+        List<Line> lines = new ArrayList<>();
+        lines.add(Line.from(Span.styled("Running:  ", dim), 
Span.raw(String.valueOf(running))));
+        if (projects > 0) {
+            lines.add(Line.from(Span.styled("Opened:   ", dim), 
Span.raw(projects + " not running")));
+        }
+        lines.add(Line.from(Span.raw("")));
+        lines.add(Line.from(Span.styled("Total:    ", dim), 
Span.raw(String.valueOf(total))));
+        lines.add(Line.from(Span.styled("Failed:   ", dim),
+                Span.styled(String.valueOf(failed), failed > 0 ? Theme.error() 
: Style.EMPTY)));
+        if (heap > 0) {
+            lines.add(Line.from(Span.styled("Heap:     ", dim), 
Span.raw(TuiHelper.formatBytes(heap))));
+        }
+        lines.add(Line.from(Span.raw("")));
+        lines.add(Line.from(Span.styled("\u2191\u2193", Theme.hintKey()), 
Span.styled(" select one", dim)));
+        return lines;
+    }
+
     /** How long an opened project shows as Starting when its app does not 
show up. */
     static final long PROJECT_START_MS = 5 * 60_000;
 
@@ -890,6 +937,15 @@ class OverviewTab extends AbstractTab {
                 sel = active.get(0);
             }
         }
+        if (sel == null) {
+            // nothing selected: what runs, in numbers, rather than an empty 
box
+            Block allBlock = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                    .title(Title.from(Line.from(Span.styled(" All ", 
Theme.title())))).build();
+            frame.renderWidget(allBlock, area);
+            
frame.renderWidget(Paragraph.builder().text(Text.from(summaryLines(ctx.data.get()))).build(),
+                    allBlock.inner(area));
+            return;
+        }
         Block infoBlock = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).build();
         frame.renderWidget(infoBlock, area);
         Rect inner = infoBlock.inner(area);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index b463e69d6f4e..21d8db4f4784 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -77,6 +77,16 @@ class RunOptionsForm {
             TuiIcons.labeled(TuiIcons.JBANG, "JBang")
     };
     private static final String[] RUNTIME_VALUES = { "camel-main", 
"spring-boot", "quarkus", "jbang" };
+
+    /** Where each runtime serves the developer console: each keeps its own 
default path. */
+    static String consolePath(int runtime) {
+        return switch (runtime) {
+            case 1 -> "/actuator/camel";
+            case 2 -> "/q/camel/dev-console";
+            default -> "/q/dev";
+        };
+    }
+
     private static final String[] PROFILE_LABELS = {
             TuiIcons.labeled(TuiIcons.DEV_PROFILE, "dev"),
             TuiIcons.labeled(TuiIcons.PROD_PROFILE, "prod")
@@ -567,7 +577,8 @@ class RunOptionsForm {
         renderTextInput(frame, innerX + labelW, rowY, fieldW, maxInput, 
selectedRow == ROW_MAX);
         rowY++;
 
-        renderCheckbox(frame, innerX, rowY, innerW, "Web console (/q/dev)", 
webConsole, selectedRow == ROW_CONSOLE);
+        renderCheckbox(frame, innerX, rowY, innerW, "Web console (" + 
consolePath(runtimeMode) + ")", webConsole,
+                selectedRow == ROW_CONSOLE);
         rowY++;
 
         if (runtimeLocked) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
index 4867202f8045..c559fabd1e7b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
@@ -61,6 +61,8 @@ final class SubViewBar {
      * @param cycleKey the key that moves through the views, or null
      * @param levels   whether the views are levels of one another (separated 
by ›) rather than peers (│)
      */
+    private static final String MORE = "\u2026 ";
+
     record Spec(String cycleKey, List<View> views, List<Toggle> toggles, 
boolean levels) {
     }
 
@@ -108,6 +110,7 @@ final class SubViewBar {
         List<int[]> rightBoxes = new ArrayList<>();
         List<String> rightKeys = new ArrayList<>();
         int rightWidth = 0;
+        boolean dropped = false;
         for (Toggle t : spec.toggles()) {
             // an on/off setting shows its state as a mark, a setting with 
modes names the mode
             boolean onOff = "on".equals(t.state()) || "off".equals(t.state());
@@ -116,6 +119,7 @@ final class SubViewBar {
             String mark = (onOff ? (t.on() ? "●" : "○") : "") + "  ";
             int w = CharWidth.of(chip) + CharWidth.of(label) + 
CharWidth.of(mark);
             if (used + rightWidth + w + 2 > area.width()) {
+                dropped = true;
                 break;
             }
             right.add(Span.styled(chip, Theme.hintKey()));
@@ -125,6 +129,19 @@ final class SubViewBar {
             rightKeys.add(t.key());
             rightWidth += w;
         }
+        if (dropped) {
+            // a setting that does not fit is not dropped silently: a … says 
there is more (F1 lists them all)
+            while (!rightBoxes.isEmpty() && used + rightWidth + MORE.length() 
> area.width()) {
+                int[] last = rightBoxes.remove(rightBoxes.size() - 1);
+                rightKeys.remove(rightKeys.size() - 1);
+                right.subList(right.size() - 3, right.size()).clear();
+                rightWidth = last[0];
+            }
+            if (used + rightWidth + MORE.length() <= area.width()) {
+                right.add(Span.styled(MORE, Theme.muted()));
+                rightWidth += MORE.length();
+            }
+        }
         if (!right.isEmpty()) {
             int start = area.x() + area.width() - rightWidth;
             spans.add(Span.raw(" ".repeat(area.width() - used - rightWidth)));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 209997c8038c..e45a3810575d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -586,6 +586,98 @@ final class TuiHelper {
         return seconds + "s" + (ms % 1000) + "ms";
     }
 
+    /**
+     * Wraps lines to a width with a hanging indent: the continuation of a 
"key = value" line starts under the value,
+     * and of an indented line under its first character, where a plain word 
wrap would start it at the left edge.
+     */
+    static List<Line> hangingWrap(List<Line> lines, int width) {
+        List<Line> out = new ArrayList<>(lines.size());
+        for (Line line : lines) {
+            if (width < 8 || line.width() <= width) {
+                out.add(line);
+            } else {
+                wrapLine(line, width, out);
+            }
+        }
+        return out;
+    }
+
+    private record StyledChar(String text, Style style, int width) {
+    }
+
+    private static void wrapLine(Line line, int width, List<Line> out) {
+        List<StyledChar> chars = new ArrayList<>();
+        StringBuilder plain = new StringBuilder();
+        for (Span span : line.spans()) {
+            String content = span.content();
+            for (int i = 0; i < content.length();) {
+                int cp = content.codePointAt(i);
+                String ch = new String(Character.toChars(cp));
+                chars.add(new StyledChar(ch, span.style(), Math.max(0, 
CharWidth.of(cp))));
+                plain.append(ch);
+                i += Character.charCount(cp);
+            }
+        }
+        String text = plain.toString();
+        int indent = 0;
+        int eq = text.indexOf(" = ");
+        if (eq > 0 && eq + 3 < width * 3 / 5) {
+            indent = eq + 3;
+        } else {
+            while (indent < text.length() && text.charAt(indent) == ' ') {
+                indent++;
+            }
+        }
+        indent = Math.min(indent, width / 2);
+
+        int start = 0;
+        boolean first = true;
+        while (start < chars.size()) {
+            int room = first ? width : width - indent;
+            int used = 0;
+            int end = start;
+            int lastSpace = -1;
+            while (end < chars.size() && used + chars.get(end).width() <= 
room) {
+                if (" ".equals(chars.get(end).text())) {
+                    lastSpace = end;
+                }
+                used += chars.get(end).width();
+                end++;
+            }
+            if (end < chars.size() && lastSpace > start) {
+                // break after the last space that fits, so a word is not cut
+                end = lastSpace + 1;
+            } else if (end == start) {
+                end = start + 1;
+            }
+            List<Span> spans = new ArrayList<>();
+            if (!first && indent > 0) {
+                spans.add(Span.raw(" ".repeat(indent)));
+            }
+            StringBuilder run = new StringBuilder();
+            Style runStyle = null;
+            for (int i = start; i < end; i++) {
+                StyledChar c = chars.get(i);
+                if (runStyle != null && !runStyle.equals(c.style())) {
+                    spans.add(Span.styled(run.toString(), runStyle));
+                    run.setLength(0);
+                }
+                runStyle = c.style();
+                run.append(c.text());
+            }
+            if (!run.isEmpty()) {
+                spans.add(Span.styled(run.toString(), runStyle));
+            }
+            out.add(Line.from(spans));
+            start = end;
+            // a continuation does not start with the spaces it broke at
+            while (start < chars.size() && " 
".equals(chars.get(start).text())) {
+                start++;
+            }
+            first = false;
+        }
+    }
+
     static String formatLoad(String l1, String l5, String l15) {
         String s1 = l1 != null && !"0.00".equals(l1) ? l1 : "0";
         String s5 = l5 != null && !"0.00".equals(l5) ? l5 : "0";
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 881253076328..b789890600a0 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
@@ -93,5 +93,6 @@ with the mouse.
 - `l` — change log level
 - `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
 - `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)
 - `Esc` — clear find / back
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
index 8ebda7b7aed2..34bf2cd6bdde 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopupTest.java
@@ -51,6 +51,20 @@ class ActionsPopupTest {
         assertEquals(ActionsPopup.Action.QUIT, 
ActionsPopup.actionForLabel("exit"));
     }
 
+    @Test
+    void aScreenshotKeepsTheMenuOpenSoItIsInThePicture() {
+        AtomicBoolean shot = new AtomicBoolean();
+        ActionsPopup popup = new ActionsPopup(
+                Set::of, List::of, List::of, null, () -> shot.set(true), () -> 
{
+                }, () -> false, () -> {
+                }, () -> false, () -> {
+                }, new HashSet<>());
+        popup.open();
+        assertTrue(popup.executeActionByName("screenshot"));
+        assertTrue(shot.get());
+        assertTrue(popup.isVisible(), "the menu stays, as it was on the 
screen");
+    }
+
     @Test
     void quitIsTheLastMenuEntryAndRunsTheWiredAction() {
         ActionsPopup popup = new ActionsPopup(
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HangingWrapTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HangingWrapTest.java
new file mode 100644
index 000000000000..1c3b2486332f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HangingWrapTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A wrapped detail line continues under its value or its indent, not at the 
left edge.
+ */
+class HangingWrapTest {
+
+    @Test
+    void aHeaderValueContinuesUnderTheValue() {
+        Line header = Line.from(Span.styled("   CamelFileAbsolutePath", 
Theme.muted()),
+                Span.raw(" = 
/private/var/folders/33/4s84/T/camel-example-75917/orders/order-1003.json"));
+        List<String> rows = text(TuiHelper.hangingWrap(List.of(header), 60));
+
+        assertThat(rows).hasSizeGreaterThan(1);
+        int value = rows.get(0).indexOf("= ") + 2;
+        assertThat(rows.get(1)).startsWith(" 
".repeat(value)).doesNotStartWith(" ".repeat(value + 1));
+        assertThat(String.join("", rows).replace(" ", ""))
+                .isEqualTo(text(List.of(header)).get(0).replace(" ", ""));
+        assertThat(rows).allMatch(r -> r.length() <= 60);
+    }
+
+    @Test
+    void anIndentedLineContinuesUnderItsIndent() {
+        Line hint = Line.from(Span.raw(" not grouped yet: /overview groups the 
routes into capabilities"));
+        List<String> rows = text(TuiHelper.hangingWrap(List.of(hint), 30));
+        assertThat(rows.get(0)).startsWith(" not grouped");
+        assertThat(rows.subList(1, rows.size())).allMatch(r -> r.startsWith(" 
") && !r.startsWith("  "));
+    }
+
+    @Test
+    void stylesAreKeptAndShortLinesUntouched() {
+        Style red = Theme.error();
+        Line line = Line.from(Span.styled("error: ", red), Span.raw("a b c d e 
f g h i j k l m n o p q r s t u v w"));
+        List<Line> wrapped = TuiHelper.hangingWrap(List.of(line), 20);
+        assertThat(wrapped.get(0).spans().get(0).style()).isEqualTo(red);
+
+        Line shortLine = Line.from(Span.raw("short"));
+        assertThat(TuiHelper.hangingWrap(List.of(shortLine), 
20).get(0)).isSameAs(shortLine);
+    }
+
+    @Test
+    void aWordLongerThanTheRowIsCut() {
+        List<String> rows = 
text(TuiHelper.hangingWrap(List.of(Line.from(Span.raw("x".repeat(50)))), 20));
+        assertThat(rows).containsExactly("x".repeat(20), "x".repeat(20), 
"x".repeat(10));
+    }
+
+    private static List<String> text(List<Line> lines) {
+        return lines.stream().map(l -> 
l.spans().stream().map(Span::content).reduce("", String::concat)).toList();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
index ee5ebb15c425..76305b44c6f6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
@@ -207,4 +208,29 @@ class HistoryTabRenderTest {
         e.last = last;
         return e;
     }
+
+    @Test
+    void theWaterfallBarsStandWhereTheStepsRan() {
+        // the exchange was created 2s before it was processed: the route row 
starts with its first step
+        // the last row of a route carries the creation time too: it stands 
where its route started
+        List<HistoryTab.WaterfallStep> steps = List.of(
+                step("from1", true, false, 17, 1_000),
+                step("unmarshal1", false, false, 0, 3_000),
+                step("to2", false, false, 16, 3_001),
+                step("from2", true, false, 12, 1_000),
+                step("log3", false, false, 0, 3_005),
+                step("from2", false, true, 12, 1_000),
+                step("from1", false, true, 17, 1_000));
+        long[] offsets = HistoryTab.waterfallOffsets(steps);
+        assertTrue(Arrays.equals(new long[] { 0, 0, 1, 5, 5, 5, 0 }, offsets), 
Arrays.toString(offsets));
+
+        // without the times of the steps, every bar starts at 0
+        long[] none = HistoryTab.waterfallOffsets(
+                List.of(step("from1", true, false, 5, 0), step("log1", false, 
false, 5, 0)));
+        assertTrue(Arrays.equals(new long[] { 0, 0 }, none));
+    }
+
+    private static HistoryTab.WaterfallStep step(String id, boolean first, 
boolean last, long elapsed, long start) {
+        return new HistoryTab.WaterfallStep(id, id, "-->", first, last, 0, 
elapsed, 0, start);
+    }
 }
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 8d3b479a5bfb..ddb156189ecb 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
@@ -67,6 +67,14 @@ class LogTabRenderTest {
         assertEquals(1, entries.get(0).repeat, "a line that happened once is 
not a repeat");
     }
 
+    @Test
+    void theCompactViewShowsTimeLevelLoggerAndMessage() {
+        LogEntry entry = LogTab.parseLogLine("2026-10-01 17:24:30.257  INFO 
85013 --- [ntloop-thread-0]"
+                                             + " 
tform.http.vertx.VertxPlatformHttpServer : Vert.x HttpServer started on 
0.0.0.0:8080");
+        String text = 
LogTab.compactLine(entry).spans().stream().map(Span::content).reduce("", 
String::concat);
+        assertEquals("17:24:30.257  INFO VertxPlatformHttpServer  Vert.x 
HttpServer started on 0.0.0.0:8080", text);
+    }
+
     @Test
     void renderNoSelectionShowsPrompt() {
         ctx.selectedPid = null;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
index 7216366dc5f0..3ed6917aad1d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
@@ -68,6 +68,21 @@ class SubViewBarTest {
         assertThat(line).contains(" b  view: business ", " e  external: edges 
", " m  metrics ○");
     }
 
+    @Test
+    void settingsThatDoNotFitEndInAnEllipsis() {
+        SubViewBar bar = new SubViewBar();
+        Rect area = new Rect(0, 0, 90, 1);
+        Buffer buffer = Buffer.empty(area);
+        bar.render(Frame.forTesting(buffer), area, levels());
+        String line = TuiTestHelper.bufferToString(buffer).split("\n")[0];
+
+        assertThat(line).contains(" b  view: business 
").doesNotContain("metrics");
+        assertThat(line.stripTrailing()).endsWith("…");
+        assertThat(line.length()).isLessThanOrEqualTo(90);
+        // all fit on a wide bar: no ellipsis
+        assertThat(render(bar, levels(), 0)).doesNotContain("…");
+    }
+
     @Test
     void aClickGoesToAViewOrPressesTheKeyOfASetting() {
         SubViewBar bar = new SubViewBar();

Reply via email to