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 c474411cf4ec CAMEL-23648: camel-jbang - TUI add waterfall view and 
history tab fixes (#23649)
c474411cf4ec is described below

commit c474411cf4ec2236b8d46790cc34634051d19f39
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat May 30 08:18:34 2026 +0200

    CAMEL-23648: camel-jbang - TUI add waterfall view and history tab fixes 
(#23649)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  62 +++--
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 252 +++++++++++++++++++--
 .../dsl/jbang/core/commands/tui/StartupTab.java    |  16 +-
 .../dsl/jbang/core/commands/tui/TuiHelper.java     |  18 ++
 4 files changed, 287 insertions(+), 61 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index b51bb8b27a89..e70e6364e3fd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -835,6 +835,17 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
+    private List<Long> selectedPidAsList() {
+        if (ctx.selectedPid == null) {
+            return Collections.emptyList();
+        }
+        try {
+            return List.of(Long.parseLong(ctx.selectedPid));
+        } catch (NumberFormatException e) {
+            return Collections.emptyList();
+        }
+    }
+
     private void syncSelectedPid() {
         List<IntegrationInfo> infos = sortedOverviewInfos();
         List<InfraInfo> infras = infraData.get();
@@ -2273,18 +2284,21 @@ public class CamelMonitor extends CamelCommand {
                 }
             }
 
+            // Scope history/error/trace refresh to the selected integration 
only
+            List<Long> selectedPids = selectedPidAsList();
+
             // Refresh error data only when the Errors tab is visible
-            if (tabsState.selected() == TAB_ERRORS) {
-                refreshErrorData(pids);
+            if (tabsState.selected() == TAB_ERRORS && !selectedPids.isEmpty()) 
{
+                refreshErrorData(selectedPids);
             }
 
             // Refresh trace data only when the History tab is visible
-            if (tabsState.selected() == TAB_HISTORY) {
+            if (tabsState.selected() == TAB_HISTORY && 
!selectedPids.isEmpty()) {
                 if (historyTab.historyRefreshRequested) {
                     historyTab.historyRefreshRequested = false;
-                    refreshHistoryData(pids);
+                    refreshHistoryData(selectedPids);
                 }
-                refreshTraceData(pids);
+                refreshTraceData(selectedPids);
             }
         } catch (Exception e) {
             // ignore refresh errors
@@ -2670,21 +2684,6 @@ public class CamelMonitor extends CamelCommand {
         entry.last = json.getBooleanOrDefault("last", false);
         entry.nodeLevel = json.getIntegerOrDefault("nodeLevel", 0);
 
-        // timestamp is epoch millis (number)
-        Object tsObj = json.get("timestamp");
-        if (tsObj instanceof Number n) {
-            long epochMs = n.longValue();
-            entry.epochMs = epochMs;
-            entry.timestamp = Instant.ofEpochMilli(epochMs)
-                    .atZone(ZoneId.systemDefault())
-                    .toLocalTime().toString();
-            if (entry.timestamp.length() > 12) {
-                entry.timestamp = entry.timestamp.substring(0, 12);
-            }
-        } else if (tsObj != null) {
-            entry.timestamp = tsObj.toString();
-        }
-
         // Derive status from done/failed booleans
         boolean done = Boolean.TRUE.equals(json.get("done"));
         boolean failed = Boolean.TRUE.equals(json.get("failed"));
@@ -2708,6 +2707,24 @@ public class CamelMonitor extends CamelCommand {
             }
         }
 
+        // Timestamp — last entries carry the start time, so add elapsed to 
get completion time
+        Object tsObj = json.get("timestamp");
+        if (tsObj instanceof Number n) {
+            long epochMs = n.longValue();
+            if (entry.last && entry.elapsed > 0) {
+                epochMs += entry.elapsed;
+            }
+            entry.epochMs = epochMs;
+            entry.timestamp = Instant.ofEpochMilli(epochMs)
+                    .atZone(ZoneId.systemDefault())
+                    .toLocalTime().toString();
+            if (entry.timestamp.length() > 12) {
+                entry.timestamp = entry.timestamp.substring(0, 12);
+            }
+        } else if (tsObj != null) {
+            entry.timestamp = tsObj.toString();
+        }
+
         // Compute direction and processor label
         if (entry.first || entry.last) {
             entry.nodeLevel = Math.max(0, entry.nodeLevel - 1);
@@ -2841,10 +2858,13 @@ public class CamelMonitor extends CamelCommand {
             entry.processor = indent + (entry.nodeLabel != null ? 
entry.nodeLabel : "");
         }
 
-        // Timestamp
+        // Timestamp — last entries carry the start time, so add elapsed to 
get completion time
         Object tsObj = json.get("timestamp");
         if (tsObj instanceof Number n) {
             long epochMs = n.longValue();
+            if (entry.last && entry.elapsed > 0) {
+                epochMs += entry.elapsed;
+            }
             entry.epochMs = epochMs;
             entry.timestamp = Instant.ofEpochMilli(epochMs)
                     .atZone(ZoneId.systemDefault())
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 8fbfae4162af..4895759ceef9 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
@@ -80,6 +80,10 @@ class HistoryTab implements MonitorTab {
     private int traceDetailScroll;
     private int traceDetailHScroll;
 
+    private boolean showWaterfall;
+    private int waterfallScroll;
+    private final ScrollbarState waterfallScrollState = new ScrollbarState();
+
     private final DiagramSupport diagram = new DiagramSupport();
 
     volatile List<HistoryEntry> historyEntries = Collections.emptyList();
@@ -119,17 +123,33 @@ class HistoryTab implements MonitorTab {
 
         if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
             if (tracerActive && traceDetailView) {
-                traceDetailScroll = Math.max(0, traceDetailScroll - 5);
+                if (showWaterfall) {
+                    waterfallScroll = Math.max(0, waterfallScroll - 10);
+                } else {
+                    traceDetailScroll = Math.max(0, traceDetailScroll - 5);
+                }
             } else {
-                historyDetailScroll = Math.max(0, historyDetailScroll - 5);
+                if (showWaterfall) {
+                    waterfallScroll = Math.max(0, waterfallScroll - 10);
+                } else {
+                    historyDetailScroll = Math.max(0, historyDetailScroll - 5);
+                }
             }
             return true;
         }
         if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
             if (tracerActive && traceDetailView) {
-                traceDetailScroll += 5;
+                if (showWaterfall) {
+                    waterfallScroll += 10;
+                } else {
+                    traceDetailScroll += 5;
+                }
             } else {
-                historyDetailScroll += 5;
+                if (showWaterfall) {
+                    waterfallScroll += 10;
+                } else {
+                    historyDetailScroll += 5;
+                }
             }
             return true;
         }
@@ -175,6 +195,11 @@ class HistoryTab implements MonitorTab {
                 traceDetailHScroll = 0;
                 return true;
             }
+            if (ke.isCharIgnoreCase('g')) {
+                showWaterfall = !showWaterfall;
+                waterfallScroll = 0;
+                return true;
+            }
         } else if (tracerActive) {
             if (ke.isChar('s')) {
                 traceSortIndex = (traceSortIndex + 1) % 
TRACE_SORT_COLUMNS.length;
@@ -226,6 +251,11 @@ class HistoryTab implements MonitorTab {
                 historyDetailHScroll = 0;
                 return true;
             }
+            if (ke.isCharIgnoreCase('g')) {
+                showWaterfall = !showWaterfall;
+                waterfallScroll = 0;
+                return true;
+            }
             if (ke.isKey(KeyCode.F5)) {
                 historyEntries = Collections.emptyList();
                 historyDetailScroll = 0;
@@ -246,6 +276,8 @@ class HistoryTab implements MonitorTab {
             traceDetailView = false;
             traceSelectedExchangeId = null;
             traceDetailScroll = 0;
+            showWaterfall = false;
+            waterfallScroll = 0;
             return true;
         }
         return false;
@@ -349,16 +381,19 @@ class HistoryTab implements MonitorTab {
         if (tracerActive && traceDetailView) {
             hint(spans, "Esc", "back");
             hint(spans, "↑↓", "navigate");
-            hint(spans, "PgUp/PgDn", "scroll detail");
-            if (!traceWordWrap) {
+            hint(spans, "PgUp/PgDn", "scroll");
+            if (!showWaterfall && !traceWordWrap) {
                 hint(spans, "←→", "h-scroll");
             }
-            hint(spans, "d", "diagram");
-            hint(spans, "D", "text diagram");
-            hint(spans, "p", "properties" + (showTraceProperties ? " [on]" : " 
[off]"));
-            hint(spans, "v", "variables" + (showTraceVariables ? " [on]" : " 
[off]"));
-            hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : " 
[off]"));
-            hint(spans, "b", "body" + (showTraceBody ? " [on]" : " [off]"));
+            hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+            if (!showWaterfall) {
+                hint(spans, "d", "diagram");
+                hint(spans, "D", "text diagram");
+                hint(spans, "p", "properties" + (showTraceProperties ? " [on]" 
: " [off]"));
+                hint(spans, "v", "variables" + (showTraceVariables ? " [on]" : 
" [off]"));
+                hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : " 
[off]"));
+                hint(spans, "b", "body" + (showTraceBody ? " [on]" : " 
[off]"));
+            }
             hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : " 
[off]"));
         } else if (tracerActive) {
             hint(spans, "Esc", "back");
@@ -371,17 +406,20 @@ class HistoryTab implements MonitorTab {
         } else {
             hint(spans, "Esc", "back");
             hint(spans, "↑↓", "navigate");
-            hint(spans, "PgUp/PgDn", "scroll detail");
-            if (!historyWordWrap) {
+            hint(spans, "PgUp/PgDn", "scroll");
+            if (!showWaterfall && !historyWordWrap) {
                 hint(spans, "←→", "h-scroll");
             }
-            hint(spans, "d", "diagram");
-            hint(spans, "D", "text diagram");
-            hint(spans, "p", "properties" + (showHistoryProperties ? " [on]" : 
" [off]"));
-            hint(spans, "v", "variables" + (showHistoryVariables ? " [on]" : " 
[off]"));
-            hint(spans, "h", "headers" + (showHistoryHeaders ? " [on]" : " 
[off]"));
-            hint(spans, "b", "body" + (showHistoryBody ? " [on]" : " [off]"));
-            hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : " [off]"));
+            hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+            if (!showWaterfall) {
+                hint(spans, "d", "diagram");
+                hint(spans, "D", "text diagram");
+                hint(spans, "p", "properties" + (showHistoryProperties ? " 
[on]" : " [off]"));
+                hint(spans, "v", "variables" + (showHistoryVariables ? " [on]" 
: " [off]"));
+                hint(spans, "h", "headers" + (showHistoryHeaders ? " [on]" : " 
[off]"));
+                hint(spans, "b", "body" + (showHistoryBody ? " [on]" : " 
[off]"));
+                hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : " 
[off]"));
+            }
             hintLast(spans, "F5", "refresh");
         }
     }
@@ -568,7 +606,7 @@ class HistoryTab implements MonitorTab {
         List<TraceEntry> steps = getTraceSteps(traceSelectedExchangeId);
 
         List<Rect> chunks = Layout.vertical()
-                .constraints(Constraint.length(10), Constraint.fill())
+                .constraints(Constraint.length(10), Constraint.length(1), 
Constraint.fill())
                 .split(area);
 
         List<Row> rows = new ArrayList<>();
@@ -582,7 +620,11 @@ class HistoryTab implements MonitorTab {
         frame.renderStatefulWidget(
                 buildStepTable(rows, stepTitle), chunks.get(0), 
traceStepTableState);
 
-        renderTraceStepDetail(frame, chunks.get(1), steps);
+        if (showWaterfall) {
+            renderWaterfall(frame, chunks.get(2), 
steps.stream().map(WaterfallStep::fromTrace).toList());
+        } else {
+            renderTraceStepDetail(frame, chunks.get(2), steps);
+        }
     }
 
     private void renderTraceStepDetail(Frame frame, Rect area, 
List<TraceEntry> steps) {
@@ -626,6 +668,162 @@ class HistoryTab implements MonitorTab {
         traceDetailHScroll = hScroll[0];
     }
 
+    record WaterfallStep(String nodeId, String processor, String direction, 
boolean first, boolean last,
+            int nodeLevel, long elapsed) {
+
+        static WaterfallStep fromTrace(TraceEntry e) {
+            return new WaterfallStep(e.nodeId, e.processor, e.direction, 
e.first, e.last, e.nodeLevel, e.elapsed);
+        }
+
+        static WaterfallStep fromHistory(HistoryEntry e) {
+            return new WaterfallStep(e.nodeId, e.processor, e.direction, 
e.first, e.last, e.nodeLevel, e.elapsed);
+        }
+
+        WaterfallStep withElapsed(long newElapsed) {
+            return new WaterfallStep(nodeId, processor, direction, first, 
last, nodeLevel, newElapsed);
+        }
+
+        String label() {
+            if (nodeId != null && !nodeId.isEmpty()) {
+                return nodeId;
+            }
+            if (processor != null) {
+                return processor.stripLeading();
+            }
+            return "";
+        }
+    }
+
+    private void renderWaterfall(Frame frame, Rect area, List<WaterfallStep> 
allSteps) {
+        // Copy the elapsed from matching last entries onto first entries
+        // (first entries have elapsed=0, the total is on the last entry)
+        List<WaterfallStep> forward = new ArrayList<>();
+        for (WaterfallStep e : allSteps) {
+            if ("<--".equals(e.direction)) {
+                continue;
+            }
+            if (e.first) {
+                long totalElapsed = e.elapsed;
+                for (WaterfallStep other : allSteps) {
+                    if (other.last && nodeIdEquals(e.nodeId, other.nodeId)) {
+                        totalElapsed = other.elapsed;
+                        break;
+                    }
+                }
+                forward.add(totalElapsed != e.elapsed ? 
e.withElapsed(totalElapsed) : e);
+            } else {
+                forward.add(e);
+            }
+        }
+
+        if (forward.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled("  No steps to display.", 
Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Waterfall ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        long maxElapsed = 0;
+        long minDuration = Long.MAX_VALUE;
+        long maxDuration = 0;
+        for (WaterfallStep e : forward) {
+            maxElapsed = Math.max(maxElapsed, e.elapsed);
+            if (!e.first) {
+                minDuration = Math.min(minDuration, e.elapsed);
+                maxDuration = Math.max(maxDuration, e.elapsed);
+            }
+        }
+        if (minDuration == Long.MAX_VALUE) {
+            minDuration = 0;
+        }
+
+        String title = String.format(" Waterfall — %d steps ", forward.size());
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(title)
+                .build();
+        Rect inner = block.inner(area);
+        frame.renderWidget(block, area);
+
+        if (inner.height() < 1 || inner.width() < 10) {
+            return;
+        }
+
+        int visibleLines = inner.height();
+        int maxScroll = Math.max(0, forward.size() - visibleLines);
+        waterfallScroll = Math.min(waterfallScroll, maxScroll);
+
+        int labelWidth = 0;
+        for (WaterfallStep e : forward) {
+            int indent = e.nodeLevel * 2;
+            labelWidth = Math.max(labelWidth, indent + e.label().length());
+        }
+        labelWidth = Math.min(labelWidth + 2, inner.width() / 3);
+
+        int barMaxWidth = Math.max(10, inner.width() - labelWidth - 12);
+
+        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,
+                    maxElapsed, minDuration, maxDuration));
+        }
+
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), 
hChunks.get(0));
+
+        if (forward.size() > visibleLines) {
+            waterfallScrollState
+                    .contentLength(forward.size())
+                    .viewportContentLength(visibleLines)
+                    .position(waterfallScroll);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), waterfallScrollState);
+        }
+    }
+
+    private static Line renderWaterfallStep(
+            WaterfallStep entry, int labelWidth, int maxBarWidth,
+            long maxElapsed, long minDuration, long maxDuration) {
+        String indent = "  ".repeat(entry.nodeLevel);
+        String label = indent + entry.label();
+        if (label.length() > labelWidth) {
+            label = label.substring(0, labelWidth - 1) + "…";
+        } else {
+            label = String.format("%-" + labelWidth + "s", label);
+        }
+
+        boolean isRoute = entry.first;
+        Style bandStyle = isRoute ? Style.EMPTY.dim() : 
TuiHelper.colorForDuration(entry.elapsed, minDuration, maxDuration);
+
+        double ratio = maxElapsed > 0 ? (double) entry.elapsed / maxElapsed : 
0;
+        int barWidth = Math.max(1, (int) Math.round(ratio * maxBarWidth));
+        String bar = "█".repeat(barWidth);
+
+        String durationStr = entry.elapsed + "ms";
+        int pad = Math.max(1, 8 - durationStr.length());
+
+        return Line.from(
+                Span.styled(label, Style.EMPTY.fg(Color.CYAN)),
+                Span.styled(bar, bandStyle),
+                Span.raw(" ".repeat(pad)),
+                Span.styled(durationStr, isRoute ? Style.EMPTY.dim() : 
Style.EMPTY.fg(Color.WHITE).bold()));
+    }
+
+    private static boolean nodeIdEquals(String a, String b) {
+        if (a == null || b == null) {
+            return a == b;
+        }
+        return a.equals(b);
+    }
+
     // ---- History (Last) rendering ----
 
     private void renderHistory(Frame frame, Rect area) {
@@ -638,7 +836,7 @@ class HistoryTab implements MonitorTab {
         List<HistoryEntry> current = historyEntries;
 
         List<Rect> chunks = Layout.vertical()
-                .constraints(Constraint.length(10), Constraint.fill())
+                .constraints(Constraint.length(10), Constraint.length(1), 
Constraint.fill())
                 .split(area);
 
         List<Row> rows = new ArrayList<>();
@@ -652,7 +850,11 @@ class HistoryTab implements MonitorTab {
         frame.renderStatefulWidget(
                 buildStepTable(rows, historyTitle), chunks.get(0), 
historyTableState);
 
-        renderHistoryDetail(frame, chunks.get(1), current);
+        if (showWaterfall) {
+            renderWaterfall(frame, chunks.get(2), 
current.stream().map(WaterfallStep::fromHistory).toList());
+        } else {
+            renderHistoryDetail(frame, chunks.get(2), current);
+        }
     }
 
     private void renderHistoryDetail(Frame frame, Rect area, 
List<HistoryEntry> current) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
index 6854935115d7..5595f4a377be 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -50,14 +50,6 @@ class StartupTab implements MonitorTab {
     private static final Style VALUE = Style.EMPTY.fg(Color.WHITE).bold();
     private static final Style HEADER = Style.EMPTY.fg(Color.YELLOW).bold();
 
-    private static final Style[] BAND_STYLES = {
-            Style.EMPTY.fg(Color.GREEN),
-            Style.EMPTY.fg(Color.LIGHT_GREEN),
-            Style.EMPTY.fg(Color.YELLOW),
-            Style.EMPTY.fg(Color.rgb(0xFF, 0xA5, 0x00)),
-            Style.EMPTY.fg(Color.RED),
-    };
-
     private final MonitorContext ctx;
     private final ScrollbarState scrollbarState = new ScrollbarState();
     private final AtomicBoolean loading = new AtomicBoolean(false);
@@ -250,13 +242,7 @@ class StartupTab implements MonitorTab {
     }
 
     private Style colorForDuration(long duration) {
-        if (maxDurationColor <= minDurationColor) {
-            return BAND_STYLES[0];
-        }
-        double ratio = (Math.log1p(duration) - Math.log1p(minDurationColor))
-                       / (Math.log1p(maxDurationColor) - 
Math.log1p(minDurationColor));
-        int bandIndex = Math.min((int) (ratio * 5), 4);
-        return BAND_STYLES[bandIndex];
+        return TuiHelper.colorForDuration(duration, minDurationColor, 
maxDurationColor);
     }
 
     @Override
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 91f3d2b7630b..ea967b02d759 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
@@ -380,6 +380,24 @@ final class TuiHelper {
         return style;
     }
 
+    static final Style[] DURATION_BAND_STYLES = {
+            Style.EMPTY.fg(Color.GREEN),
+            Style.EMPTY.fg(Color.LIGHT_GREEN),
+            Style.EMPTY.fg(Color.YELLOW),
+            Style.EMPTY.fg(Color.rgb(0xFF, 0xA5, 0x00)),
+            Style.EMPTY.fg(Color.RED),
+    };
+
+    static Style colorForDuration(long duration, long minDuration, long 
maxDuration) {
+        if (maxDuration <= minDuration) {
+            return DURATION_BAND_STYLES[0];
+        }
+        double ratio = (Math.log1p(duration) - Math.log1p(minDuration))
+                       / (Math.log1p(maxDuration) - Math.log1p(minDuration));
+        int bandIndex = Math.min((int) (ratio * 5), 4);
+        return DURATION_BAND_STYLES[bandIndex];
+    }
+
     static String shortTypeName(String type) {
         if (type == null) {
             return "null";

Reply via email to