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

davsclaus pushed a commit to branch feature/CAMEL-23672-tui-refactor
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 50bfaf0210e8c0dad3493ab0ab4c036a1922ec57
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jun 6 13:17:36 2026 +0200

    CAMEL-23672: TUI - Extract SearchHighlighter and MetricsCollector utilities
    
    Extract two shared utility classes from the TUI to improve maintainability:
    
    - SearchHighlighter: consolidates duplicated find/highlight/wrap logic
      from SourceViewer and LogTab into a single reusable class
    - MetricsCollector: moves 30+ ConcurrentHashMap fields and all
      sliding-window update/cleanup/reset logic out of CamelMonitor
    
    CamelMonitor reduced from 3,256 to 2,936 lines. Tab constructors
    simplified (EndpointsTab from 10 map params to 1 MetricsCollector).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 348 +----------------
 .../jbang/core/commands/tui/CircuitBreakerTab.java |   8 +-
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |  32 +-
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  | 270 ++-----------
 .../dsl/jbang/core/commands/tui/MemoryTab.java     |   4 +-
 .../jbang/core/commands/tui/MetricsCollector.java  | 418 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  10 +-
 .../jbang/core/commands/tui/SearchHighlighter.java | 299 +++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 250 ++----------
 9 files changed, 814 insertions(+), 825 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 cf8f472d3f03..bf82eb000b0d 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
@@ -22,15 +22,12 @@ import java.io.RandomAccessFile;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -97,10 +94,7 @@ public class CamelMonitor extends CamelCommand {
 
     private static final long VANISH_DURATION_MS = 6000;
     private static final long DEFAULT_REFRESH_MS = 100;
-    private static final int MAX_SPARKLINE_POINTS = 60;
-    private static final int MAX_ENDPOINT_CHART_POINTS = 60;
-    private static final int MAX_HEAP_HISTORY_POINTS = 120;
-    private static final long HEAP_SAMPLE_INTERVAL_MS = 5000;
+
     private static final int MAX_LOG_LINES = 3000;
     private static final int MAX_TRACES = 200;
     private static final int NUM_TABS = 10;
@@ -146,57 +140,8 @@ public class CamelMonitor extends CamelCommand {
     private final Map<String, VanishingInfraInfo> vanishingInfra = new 
ConcurrentHashMap<>();
     private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
 
-    // Sparkline: throughput history per PID (one point per second)
-    private final Map<String, LinkedList<Long>> throughputHistory = new 
ConcurrentHashMap<>();
-    // Sparkline: failed throughput history per PID (one point per second)
-    private final Map<String, LinkedList<Long>> failedHistory = new 
ConcurrentHashMap<>();
-    // Sliding window of [timestamp, exchangesTotal, exchangesFailed] samples 
for smoothing
-    private final Map<String, LinkedList<long[]>> throughputSamples = new 
ConcurrentHashMap<>();
-    // Track last time a sparkline point was recorded
-    private final Map<String, Long> previousExchangesTime = new 
ConcurrentHashMap<>();
-
-    // Endpoint in/out sliding window history per PID — all endpoints
-    private final Map<String, LinkedList<Long>> endpointInHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<Long>> endpointOutHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<long[]>> endpointSamples = new 
ConcurrentHashMap<>();
-    private final Map<String, Long> previousEndpointTime = new 
ConcurrentHashMap<>();
-
-    // Endpoint in/out sliding window history per PID — remote endpoints only
-    private final Map<String, LinkedList<Long>> endpointRemoteInHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<Long>> endpointRemoteOutHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<long[]>> endpointRemoteSamples = new 
ConcurrentHashMap<>();
-    private final Map<String, Long> previousEndpointRemoteTime = new 
ConcurrentHashMap<>();
-
-    // Endpoint in/out sliding window history per PID — remote+stub endpoints
-    private final Map<String, LinkedList<Long>> endpointRemoteStubInHistory = 
new ConcurrentHashMap<>();
-    private final Map<String, LinkedList<Long>> endpointRemoteStubOutHistory = 
new ConcurrentHashMap<>();
-    private final Map<String, LinkedList<long[]>> endpointRemoteStubSamples = 
new ConcurrentHashMap<>();
-    private final Map<String, Long> previousEndpointRemoteStubTime = new 
ConcurrentHashMap<>();
-
-    // Endpoint payload size (mean body size) history per PID — for sparkline
-    private final Map<String, LinkedList<Long>> endpointInSizeHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<Long>> endpointOutSizeHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, Long> previousEndpointSizeTime = new 
ConcurrentHashMap<>();
-
-    // Per-endpoint in/out rate history — keyed by pid + "|" + uri
-    private final Map<String, LinkedList<Long>> perEndpointInHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<Long>> perEndpointOutHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<long[]>> perEndpointSamples = new 
ConcurrentHashMap<>();
-    private final Map<String, Long> previousPerEndpointTime = new 
ConcurrentHashMap<>();
-
-    // Circuit breaker throughput history per PID/cbId (success + fail, one 
point per second)
-    private final Map<String, LinkedList<Long>> cbSuccessHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<Long>> cbFailHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, LinkedList<long[]>> cbThroughputSamples = new 
ConcurrentHashMap<>();
-    private final Map<String, Long> previousCbTime = new ConcurrentHashMap<>();
-
-    // Heap memory usage history per PID (one point per 5 seconds, in bytes)
-    private final Map<String, LinkedList<Long>> heapMemHistory = new 
ConcurrentHashMap<>();
-    private final Map<String, Long> previousHeapTime = new 
ConcurrentHashMap<>();
-
-    // Load averages (EWMA) — CPU%, per PID (inflight EWMA is read from the 
management JSON)
-    private final Map<String, LoadAvg> cpuLoadAvg = new ConcurrentHashMap<>();
-    private final Map<String, long[]> prevCpuSample = new 
ConcurrentHashMap<>();
+    // Sparkline/chart history for all metric families
+    private final MetricsCollector metrics = new MetricsCollector();
 
     // Cached PID list — full process scan throttled to every 2 seconds (1 
second in burst mode)
     private volatile List<Long> cachedPids = Collections.emptyList();
@@ -334,17 +279,11 @@ public class CamelMonitor extends CamelCommand {
         diagramTab = new DiagramTab(ctx);
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
-        endpointsTab = new EndpointsTab(
-                ctx,
-                endpointInHistory, endpointOutHistory,
-                endpointRemoteInHistory, endpointRemoteOutHistory,
-                endpointRemoteStubInHistory, endpointRemoteStubOutHistory,
-                endpointInSizeHistory, endpointOutSizeHistory,
-                perEndpointInHistory, perEndpointOutHistory);
+        endpointsTab = new EndpointsTab(ctx, metrics);
         httpTab = new HttpTab(ctx);
         healthTab = new HealthTab(ctx);
         historyTab = new HistoryTab(ctx, traces, traceFilePositions);
-        circuitBreakerTab = new CircuitBreakerTab(ctx, cbSuccessHistory, 
cbFailHistory);
+        circuitBreakerTab = new CircuitBreakerTab(ctx, metrics);
         errorsTab = new ErrorsTab(ctx);
         metricsTab = new MetricsTab(ctx);
         startupTab = new StartupTab(ctx);
@@ -353,10 +292,10 @@ public class CamelMonitor extends CamelCommand {
         browseTab = new BrowseTab(ctx);
         classpathTab = new ClasspathTab(ctx);
         inflightTab = new InflightTab(ctx);
-        memoryTab = new MemoryTab(ctx, heapMemHistory);
+        memoryTab = new MemoryTab(ctx, metrics);
         threadsTab = new ThreadsTab(ctx);
         overviewTab = new OverviewTab(
-                ctx, throughputHistory, failedHistory, cpuLoadAvg, 
stoppingPids,
+                ctx, metrics, stoppingPids,
                 this::resetIntegrationTabState);
 
         // Initial data load (synchronous before TUI starts)
@@ -1826,35 +1765,7 @@ public class CamelMonitor extends CamelCommand {
         root.put("action", "reset-stats");
         Path actionFile = ctx.getActionFile(pid);
         PathUtils.writeTextSafely(root.toJson(), actionFile);
-        // Clear local sparkline history — overview
-        throughputHistory.remove(pid);
-        failedHistory.remove(pid);
-        throughputSamples.remove(pid);
-        previousExchangesTime.remove(pid);
-        // Clear local sparkline history — endpoints
-        endpointInHistory.remove(pid);
-        endpointOutHistory.remove(pid);
-        endpointSamples.remove(pid);
-        previousEndpointTime.remove(pid);
-        endpointRemoteInHistory.remove(pid);
-        endpointRemoteOutHistory.remove(pid);
-        endpointRemoteSamples.remove(pid);
-        previousEndpointRemoteTime.remove(pid);
-        endpointRemoteStubInHistory.remove(pid);
-        endpointRemoteStubOutHistory.remove(pid);
-        endpointRemoteStubSamples.remove(pid);
-        previousEndpointRemoteStubTime.remove(pid);
-        endpointInSizeHistory.remove(pid);
-        endpointOutSizeHistory.remove(pid);
-        previousEndpointSizeTime.remove(pid);
-        String perEpPrefix = pid + "|";
-        perEndpointInHistory.keySet().removeIf(k -> k.startsWith(perEpPrefix));
-        perEndpointOutHistory.keySet().removeIf(k -> 
k.startsWith(perEpPrefix));
-        perEndpointSamples.keySet().removeIf(k -> k.startsWith(perEpPrefix));
-        previousPerEndpointTime.keySet().removeIf(k -> 
k.startsWith(perEpPrefix));
-        // Clear local sparkline history — heap memory
-        heapMemHistory.remove(pid);
-        previousHeapTime.remove(pid);
+        metrics.resetStats(pid);
     }
 
     private void sendRouteCommand(String pid, String routeId, String command) {
@@ -2175,11 +2086,11 @@ public class CamelMonitor extends CamelCommand {
                     IntegrationInfo info = StatusParser.parseIntegration(ph, 
root);
                     if (info != null) {
                         infos.add(info);
-                        updateThroughputHistory(info);
-                        updateEndpointHistory(info);
-                        updateCbHistory(info);
-                        updateHeapHistory(info);
-                        updateLoadMetrics(ph, info);
+                        metrics.updateThroughputHistory(info);
+                        metrics.updateEndpointHistory(info);
+                        metrics.updateCbHistory(info);
+                        metrics.updateHeapHistory(info);
+                        metrics.updateLoadMetrics(ph, info);
                     }
                 }
             }
@@ -2209,38 +2120,7 @@ public class CamelMonitor extends CamelCommand {
                 Map.Entry<String, VanishingInfo> entry = it.next();
                 if (now - entry.getValue().startTime > VANISH_DURATION_MS) {
                     it.remove();
-                    throughputHistory.remove(entry.getKey());
-                    failedHistory.remove(entry.getKey());
-                    endpointInHistory.remove(entry.getKey());
-                    endpointOutHistory.remove(entry.getKey());
-                    endpointSamples.remove(entry.getKey());
-                    previousEndpointTime.remove(entry.getKey());
-                    endpointRemoteInHistory.remove(entry.getKey());
-                    endpointRemoteOutHistory.remove(entry.getKey());
-                    endpointRemoteSamples.remove(entry.getKey());
-                    previousEndpointRemoteTime.remove(entry.getKey());
-                    endpointRemoteStubInHistory.remove(entry.getKey());
-                    endpointRemoteStubOutHistory.remove(entry.getKey());
-                    endpointRemoteStubSamples.remove(entry.getKey());
-
-                    endpointInSizeHistory.remove(entry.getKey());
-                    endpointOutSizeHistory.remove(entry.getKey());
-                    previousEndpointSizeTime.remove(entry.getKey());
-                    previousEndpointRemoteStubTime.remove(entry.getKey());
-                    heapMemHistory.remove(entry.getKey());
-                    previousHeapTime.remove(entry.getKey());
-                    cpuLoadAvg.remove(entry.getKey());
-                    prevCpuSample.remove(entry.getKey());
-                    String vanishCbPrefix = entry.getKey() + "/";
-                    cbSuccessHistory.keySet().removeIf(k -> 
k.startsWith(vanishCbPrefix));
-                    cbFailHistory.keySet().removeIf(k -> 
k.startsWith(vanishCbPrefix));
-                    cbThroughputSamples.keySet().removeIf(k -> 
k.startsWith(vanishCbPrefix));
-                    previousCbTime.keySet().removeIf(k -> 
k.startsWith(vanishCbPrefix));
-                    String vanishEpPrefix = entry.getKey() + "|";
-                    perEndpointInHistory.keySet().removeIf(k -> 
k.startsWith(vanishEpPrefix));
-                    perEndpointOutHistory.keySet().removeIf(k -> 
k.startsWith(vanishEpPrefix));
-                    perEndpointSamples.keySet().removeIf(k -> 
k.startsWith(vanishEpPrefix));
-                    previousPerEndpointTime.keySet().removeIf(k -> 
k.startsWith(vanishEpPrefix));
+                    metrics.removeVanished(entry.getKey());
                 } else if (!livePids.contains(entry.getKey())) {
                     IntegrationInfo ghost = entry.getValue().info;
                     ghost.vanishing = true;
@@ -2415,170 +2295,6 @@ public class CamelMonitor extends CamelCommand {
         infraData.set(infraInfos);
     }
 
-    private void updateThroughputHistory(IntegrationInfo info) {
-        // Track exchangesTotal and exchangesFailed over a 1-second sliding 
window
-        long currentTotal = info.exchangesTotal;
-        long currentFailed = info.failed;
-        long now = System.currentTimeMillis();
-
-        String pid = info.pid;
-        LinkedList<long[]> samples = throughputSamples.computeIfAbsent(pid, k 
-> new LinkedList<>());
-        samples.add(new long[] { now, currentTotal, currentFailed });
-
-        // Remove samples older than 1 second
-        while (!samples.isEmpty() && now - samples.get(0)[0] > 1000) {
-            samples.remove(0);
-        }
-
-        // Compute throughput over the window
-        if (samples.size() >= 2) {
-            long[] oldest = samples.get(0);
-            long[] newest = samples.get(samples.size() - 1);
-            long deltaTotal = newest[1] - oldest[1];
-            long deltaFailed = newest[2] - oldest[2];
-            long deltaTimeMs = newest[0] - oldest[0];
-            long tp = deltaTimeMs > 0 ? (deltaTotal * 1000) / deltaTimeMs : 0;
-            long fp = deltaTimeMs > 0 ? (deltaFailed * 1000) / deltaTimeMs : 0;
-
-            // Only add one point per second to keep the sparkline meaningful
-            Long lastTime = previousExchangesTime.get(pid);
-            if (lastTime == null || now - lastTime >= 1000) {
-                previousExchangesTime.put(pid, now);
-                LinkedList<Long> hist = throughputHistory.computeIfAbsent(pid, 
k -> new LinkedList<>());
-                hist.add(tp);
-                while (hist.size() > MAX_SPARKLINE_POINTS) {
-                    hist.remove(0);
-                }
-                LinkedList<Long> fhist = failedHistory.computeIfAbsent(pid, k 
-> new LinkedList<>());
-                fhist.add(fp);
-                while (fhist.size() > MAX_SPARKLINE_POINTS) {
-                    fhist.remove(0);
-                }
-            }
-        }
-    }
-
-    private void updateEndpointHistory(IntegrationInfo info) {
-        long inTotal = info.endpoints.stream()
-                .filter(ep -> "in".equals(ep.direction))
-                .mapToLong(ep -> ep.hits).sum();
-        long outTotal = info.endpoints.stream()
-                .filter(ep -> "out".equals(ep.direction))
-                .mapToLong(ep -> ep.hits).sum();
-        long inRemote = info.endpoints.stream()
-                .filter(ep -> "in".equals(ep.direction) && ep.remote)
-                .mapToLong(ep -> ep.hits).sum();
-        long outRemote = info.endpoints.stream()
-                .filter(ep -> "out".equals(ep.direction) && ep.remote)
-                .mapToLong(ep -> ep.hits).sum();
-        long inRemoteStub = info.endpoints.stream()
-                .filter(ep -> "in".equals(ep.direction) && (ep.remote || 
ep.stub))
-                .mapToLong(ep -> ep.hits).sum();
-        long outRemoteStub = info.endpoints.stream()
-                .filter(ep -> "out".equals(ep.direction) && (ep.remote || 
ep.stub))
-                .mapToLong(ep -> ep.hits).sum();
-
-        long now = System.currentTimeMillis();
-        String pid = info.pid;
-
-        recordEndpointSample(pid, now, inTotal, outTotal,
-                endpointSamples, previousEndpointTime, endpointInHistory, 
endpointOutHistory);
-        recordEndpointSample(pid, now, inRemote, outRemote,
-                endpointRemoteSamples, previousEndpointRemoteTime, 
endpointRemoteInHistory, endpointRemoteOutHistory);
-        recordEndpointSample(pid, now, inRemoteStub, outRemoteStub,
-                endpointRemoteStubSamples, previousEndpointRemoteStubTime,
-                endpointRemoteStubInHistory, endpointRemoteStubOutHistory);
-
-        // Record payload size snapshots (mean body size per direction)
-        long inMeanSize = info.endpoints.stream()
-                .filter(ep -> "in".equals(ep.direction) && ep.meanBodySize >= 
0)
-                .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
-        long outMeanSize = info.endpoints.stream()
-                .filter(ep -> "out".equals(ep.direction) && ep.meanBodySize >= 
0)
-                .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
-        Long lastSizeTime = previousEndpointSizeTime.get(pid);
-        if (lastSizeTime == null || now - lastSizeTime >= 1000) {
-            previousEndpointSizeTime.put(pid, now);
-            LinkedList<Long> inSizeHist = 
endpointInSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
-            inSizeHist.add(inMeanSize);
-            while (inSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
-                inSizeHist.remove(0);
-            }
-            LinkedList<Long> outSizeHist = 
endpointOutSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
-            outSizeHist.add(outMeanSize);
-            while (outSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
-                outSizeHist.remove(0);
-            }
-        }
-
-        // Per-endpoint rate history (keyed by pid|uri)
-        Map<String, long[]> perUri = new LinkedHashMap<>();
-        for (EndpointInfo ep : info.endpoints) {
-            if (ep.uri == null) {
-                continue;
-            }
-            long[] inOut = perUri.computeIfAbsent(ep.uri, k -> new long[2]);
-            if ("in".equals(ep.direction)) {
-                inOut[0] += ep.hits;
-            } else if ("out".equals(ep.direction)) {
-                inOut[1] += ep.hits;
-            }
-        }
-        for (Map.Entry<String, long[]> entry : perUri.entrySet()) {
-            String key = pid + "|" + entry.getKey();
-            long[] inOut = entry.getValue();
-            recordEndpointSample(key, now, inOut[0], inOut[1],
-                    perEndpointSamples, previousPerEndpointTime,
-                    perEndpointInHistory, perEndpointOutHistory);
-        }
-    }
-
-    private void recordEndpointSample(
-            String pid, long now, long inTotal, long outTotal,
-            Map<String, LinkedList<long[]>> samplesMap, Map<String, Long> 
prevTimeMap,
-            Map<String, LinkedList<Long>> inHistMap, Map<String, 
LinkedList<Long>> outHistMap) {
-        LinkedList<long[]> samples = samplesMap.computeIfAbsent(pid, k -> new 
LinkedList<>());
-        samples.add(new long[] { now, inTotal, outTotal });
-        while (!samples.isEmpty() && now - samples.get(0)[0] > 1000) {
-            samples.remove(0);
-        }
-        if (samples.size() >= 2) {
-            long[] oldest = samples.get(0);
-            long[] newest = samples.get(samples.size() - 1);
-            long deltaMs = newest[0] - oldest[0];
-            long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 / 
deltaMs : 0;
-            long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 / 
deltaMs : 0;
-            Long lastTime = prevTimeMap.get(pid);
-            if (lastTime == null || now - lastTime >= 1000) {
-                prevTimeMap.put(pid, now);
-                LinkedList<Long> inHist = inHistMap.computeIfAbsent(pid, k -> 
new LinkedList<>());
-                inHist.add(Math.max(0, inRate));
-                while (inHist.size() > MAX_ENDPOINT_CHART_POINTS) {
-                    inHist.remove(0);
-                }
-                LinkedList<Long> outHist = outHistMap.computeIfAbsent(pid, k 
-> new LinkedList<>());
-                outHist.add(Math.max(0, outRate));
-                while (outHist.size() > MAX_ENDPOINT_CHART_POINTS) {
-                    outHist.remove(0);
-                }
-            }
-        }
-    }
-
-    private void updateCbHistory(IntegrationInfo info) {
-        long now = System.currentTimeMillis();
-        for (CircuitBreakerInfo cb : info.circuitBreakers) {
-            if (cb.id == null) {
-                continue;
-            }
-            String key = info.pid + "/" + cb.id;
-            long success = cb.successfulCalls;
-            long failed = cb.failedCalls;
-            recordEndpointSample(key, now, success, failed,
-                    cbThroughputSamples, previousCbTime, cbSuccessHistory, 
cbFailHistory);
-        }
-    }
-
     // ---- Trace Data Loading ----
 
     private void refreshTraceData(List<Long> pids) {
@@ -2610,42 +2326,6 @@ public class CamelMonitor extends CamelCommand {
         traces.set(allTraces);
     }
 
-    private void updateHeapHistory(IntegrationInfo info) {
-        if (info.heapMemUsed > 0) {
-            long now = System.currentTimeMillis();
-            Long lastTime = previousHeapTime.get(info.pid);
-            if (lastTime == null || now - lastTime >= HEAP_SAMPLE_INTERVAL_MS) 
{
-                previousHeapTime.put(info.pid, now);
-                LinkedList<Long> hist = 
heapMemHistory.computeIfAbsent(info.pid, k -> new LinkedList<>());
-                hist.add(info.heapMemUsed);
-                while (hist.size() > MAX_HEAP_HISTORY_POINTS) {
-                    hist.remove(0);
-                }
-            }
-        }
-    }
-
-    private void updateLoadMetrics(ProcessHandle ph, IntegrationInfo info) {
-        String pid = info.pid;
-
-        // CPU EWMA — compute % from ProcessHandle CPU duration delta
-        Optional<Duration> durOpt = ph.info().totalCpuDuration();
-        if (durOpt.isPresent()) {
-            long cpuNanos = durOpt.get().toNanos();
-            long wallMs = System.currentTimeMillis();
-            long[] prev = prevCpuSample.get(pid);
-            if (prev != null) {
-                long deltaCpuNanos = cpuNanos - prev[0];
-                long deltaWallNanos = (wallMs - prev[1]) * 1_000_000L;
-                if (deltaWallNanos > 0) {
-                    double cpuPct = (double) deltaCpuNanos / deltaWallNanos * 
100.0;
-                    cpuLoadAvg.computeIfAbsent(pid, k -> new 
LoadAvg()).update(Math.max(0, cpuPct));
-                }
-            }
-            prevCpuSample.put(pid, new long[] { cpuNanos, wallMs });
-        }
-    }
-
     @SuppressWarnings("unchecked")
     private void readTraceFile(String pid, List<TraceEntry> allTraces) {
         Path traceFile = CommandLineHelper.getCamelDir().resolve(pid + 
"-trace.json");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
index 200b5046c510..ac683ae3563c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
@@ -58,12 +58,10 @@ class CircuitBreakerTab implements MonitorTab {
     private int sortIndex;
     private boolean sortReversed;
 
-    CircuitBreakerTab(MonitorContext ctx,
-                      Map<String, LinkedList<Long>> cbSuccessHistory,
-                      Map<String, LinkedList<Long>> cbFailHistory) {
+    CircuitBreakerTab(MonitorContext ctx, MetricsCollector metrics) {
         this.ctx = ctx;
-        this.cbSuccessHistory = cbSuccessHistory;
-        this.cbFailHistory = cbFailHistory;
+        this.cbSuccessHistory = metrics.getCbSuccessHistory();
+        this.cbFailHistory = metrics.getCbFailHistory();
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index db5b29d7b7a3..d21e43723292 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -73,28 +73,18 @@ class EndpointsTab implements MonitorTab {
     private int filter;
     private int chartMode = CHART_ALL;
 
-    EndpointsTab(MonitorContext ctx,
-                 Map<String, LinkedList<Long>> endpointInHistory,
-                 Map<String, LinkedList<Long>> endpointOutHistory,
-                 Map<String, LinkedList<Long>> endpointRemoteInHistory,
-                 Map<String, LinkedList<Long>> endpointRemoteOutHistory,
-                 Map<String, LinkedList<Long>> endpointRemoteStubInHistory,
-                 Map<String, LinkedList<Long>> endpointRemoteStubOutHistory,
-                 Map<String, LinkedList<Long>> endpointInSizeHistory,
-                 Map<String, LinkedList<Long>> endpointOutSizeHistory,
-                 Map<String, LinkedList<Long>> perEndpointInHistory,
-                 Map<String, LinkedList<Long>> perEndpointOutHistory) {
+    EndpointsTab(MonitorContext ctx, MetricsCollector metrics) {
         this.ctx = ctx;
-        this.endpointInHistory = endpointInHistory;
-        this.endpointOutHistory = endpointOutHistory;
-        this.endpointRemoteInHistory = endpointRemoteInHistory;
-        this.endpointRemoteOutHistory = endpointRemoteOutHistory;
-        this.endpointRemoteStubInHistory = endpointRemoteStubInHistory;
-        this.endpointRemoteStubOutHistory = endpointRemoteStubOutHistory;
-        this.endpointInSizeHistory = endpointInSizeHistory;
-        this.endpointOutSizeHistory = endpointOutSizeHistory;
-        this.perEndpointInHistory = perEndpointInHistory;
-        this.perEndpointOutHistory = perEndpointOutHistory;
+        this.endpointInHistory = metrics.getEndpointInHistory();
+        this.endpointOutHistory = metrics.getEndpointOutHistory();
+        this.endpointRemoteInHistory = metrics.getEndpointRemoteInHistory();
+        this.endpointRemoteOutHistory = metrics.getEndpointRemoteOutHistory();
+        this.endpointRemoteStubInHistory = 
metrics.getEndpointRemoteStubInHistory();
+        this.endpointRemoteStubOutHistory = 
metrics.getEndpointRemoteStubOutHistory();
+        this.endpointInSizeHistory = metrics.getEndpointInSizeHistory();
+        this.endpointOutSizeHistory = metrics.getEndpointOutSizeHistory();
+        this.perEndpointInHistory = metrics.getPerEndpointInHistory();
+        this.perEndpointOutHistory = metrics.getPerEndpointOutHistory();
     }
 
     @Override
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 ba3a88a420e4..f176af756b43 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
@@ -38,13 +38,11 @@ import dev.tamboui.text.CharWidth;
 import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
-import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.widgets.Clear;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Title;
-import dev.tamboui.widgets.input.TextInputState;
 import dev.tamboui.widgets.list.ListItem;
 import dev.tamboui.widgets.list.ListState;
 import dev.tamboui.widgets.list.ListWidget;
@@ -94,22 +92,7 @@ class LogTab implements MonitorTab {
     private int hScroll;
     private boolean showLogLevelPopup;
 
-    // Highlight mode: persistent keyword highlighting
-    private String highlightTerm;
-    private Pattern highlightPattern;
-
-    // Find mode: search with next/prev navigation
-    private boolean findInputActive;
-    private boolean highlightInputActive;
-    private TextInputState searchInputState = new TextInputState("");
-    private String findTerm;
-    private Pattern findPattern;
-    private int findMatchIndex = -1;
-    private List<Integer> findMatches = Collections.emptyList();
-
-    private static final Style HIGHLIGHT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
-    private static final Style FIND_MATCH_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
-    private static final Style FIND_CURRENT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.LIGHT_GREEN);
+    private final SearchHighlighter search = new SearchHighlighter();
 
     LogTab(MonitorContext ctx) {
         this.ctx = ctx;
@@ -132,8 +115,21 @@ class LogTab implements MonitorTab {
 
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
-        if (findInputActive || highlightInputActive) {
-            return handleSearchInput(ke);
+        if (search.isSearchInputActive()) {
+            boolean handled = search.handleKeyEvent(ke);
+            if (handled && !search.isSearchInputActive() && 
search.hasFindTerm()) {
+                List<String> plainLines = new 
ArrayList<>(filteredLogEntries.size());
+                for (LogEntry e : filteredLogEntries) {
+                    plainLines.add(TuiHelper.stripAnsi(e.raw != null ? e.raw : 
""));
+                }
+                search.buildFindMatches(plainLines);
+                int newPos = search.jumpToNearestMatch(scroll);
+                if (newPos != scroll) {
+                    followMode = false;
+                    scroll = newPos;
+                }
+            }
+            return handled;
         }
 
         if (showLogLevelPopup) {
@@ -156,22 +152,12 @@ class LogTab implements MonitorTab {
             return true;
         }
 
-        if (ke.isChar('/')) {
-            findInputActive = true;
-            searchInputState = new TextInputState("");
-            return true;
-        }
-        if (ke.isChar('h')) {
-            highlightInputActive = true;
-            searchInputState = new TextInputState("");
-            return true;
-        }
-        if (ke.isChar('n') && findTerm != null) {
-            navigateToNextMatch();
-            return true;
-        }
-        if (ke.isChar('N') && findTerm != null) {
-            navigateToPrevMatch();
+        if (search.handleKeyEvent(ke)) {
+            int matchLine = search.currentMatchLine();
+            if (matchLine >= 0) {
+                followMode = false;
+                scroll = matchLine;
+            }
             return true;
         }
         if (ke.isChar('l') && !ctx.isInfraSelected()) {
@@ -222,61 +208,15 @@ class LogTab implements MonitorTab {
         return false;
     }
 
-    private boolean handleSearchInput(KeyEvent ke) {
-        if (ke.isKey(KeyCode.ESCAPE)) {
-            findInputActive = false;
-            highlightInputActive = false;
-            return true;
-        }
-        if (ke.isConfirm()) {
-            String text = searchInputState.text().trim();
-            if (findInputActive) {
-                if (text.isEmpty()) {
-                    findTerm = null;
-                    findPattern = null;
-                    findMatches = Collections.emptyList();
-                    findMatchIndex = -1;
-                } else {
-                    findTerm = text;
-                    findPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
-                    buildFindMatches();
-                    jumpToNearestMatch();
-                }
-                findInputActive = false;
-            } else if (highlightInputActive) {
-                if (text.isEmpty()) {
-                    highlightTerm = null;
-                    highlightPattern = null;
-                } else {
-                    highlightTerm = text;
-                    highlightPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
-                }
-                highlightInputActive = false;
-            }
-            return true;
-        }
-        FormHelper.handleTextInput(ke, searchInputState);
-        return true;
-    }
-
     @Override
     public boolean handleEscape() {
-        if (findInputActive || highlightInputActive) {
-            findInputActive = false;
-            highlightInputActive = false;
+        if (search.handleEscape()) {
             return true;
         }
         if (showLogLevelPopup) {
             showLogLevelPopup = false;
             return true;
         }
-        if (findTerm != null) {
-            findTerm = null;
-            findPattern = null;
-            findMatches = Collections.emptyList();
-            findMatchIndex = -1;
-            return true;
-        }
         return false;
     }
 
@@ -392,21 +332,23 @@ class LogTab implements MonitorTab {
             hScroll = Math.min(hScroll, Math.max(0, cachedLogMaxWidth - 
visibleWidth));
         }
 
-        if (findPattern != null && entriesChanged) {
-            buildFindMatches();
+        if (search.hasFindTerm() && entriesChanged) {
+            List<String> plainLines = new ArrayList<>(entries.size());
+            for (LogEntry e : entries) {
+                plainLines.add(TuiHelper.stripAnsi(e.raw != null ? e.raw : 
""));
+            }
+            search.buildFindMatches(plainLines);
         }
 
         List<Line> allLines = cachedLogLines;
         int start = Math.min(scroll, Math.max(0, allLines.size() - 
visibleHeight));
         List<Line> visibleLines = allLines.subList(start, 
Math.min(allLines.size(), start + visibleHeight));
 
-        // Apply highlights only to visible lines
-        if (highlightPattern != null || findPattern != null) {
-            int currentMatchLine = findMatchIndex >= 0 && findMatchIndex < 
findMatches.size()
-                    ? findMatches.get(findMatchIndex) : -1;
+        int currentMatchLine = search.currentMatchLine();
+        if (currentMatchLine >= 0 || search.hasFindTerm()) {
             List<Line> highlighted = new ArrayList<>(visibleLines.size());
             for (int i = 0; i < visibleLines.size(); i++) {
-                highlighted.add(applyHighlights(visibleLines.get(i), start + 
i, currentMatchLine));
+                highlighted.add(search.applyHighlights(visibleLines.get(i), 
start + i, currentMatchLine));
             }
             visibleLines = highlighted;
         }
@@ -436,18 +378,8 @@ class LogTab implements MonitorTab {
 
     @Override
     public void renderFooter(List<Span> spans) {
-        if (findInputActive) {
-            spans.add(Span.styled(" /", HINT_KEY_STYLE));
-            spans.add(Span.raw(searchInputState.text() + "█  "));
-            hint(spans, "Enter", "search");
-            hintLast(spans, "Esc", "cancel");
-            return;
-        }
-        if (highlightInputActive) {
-            spans.add(Span.styled(" h:", HINT_KEY_STYLE));
-            spans.add(Span.raw(searchInputState.text() + "█  "));
-            hint(spans, "Enter", "set");
-            hintLast(spans, "Esc", "cancel");
+        search.renderFooterHints(spans);
+        if (search.isSearchInputActive()) {
             return;
         }
         if (showLogLevelPopup) {
@@ -457,21 +389,13 @@ class LogTab implements MonitorTab {
             return;
         }
 
-        if (findTerm != null) {
-            hint(spans, "Esc", "clear find");
-            hint(spans, "n", "next");
-            hint(spans, "N", "prev");
-            String pos = findMatches.isEmpty()
-                    ? "0/0"
-                    : (findMatchIndex + 1) + "/" + findMatches.size();
-            spans.add(Span.styled("  /", HINT_KEY_STYLE));
-            spans.add(Span.raw("\"" + findTerm + "\" [" + pos + "]  "));
+        if (search.hasFindTerm()) {
+            search.renderFindStatus(spans);
         } else {
             hint(spans, "Esc", "back");
         }
         hint(spans, "↑↓", "scroll");
-        hint(spans, "/", "find");
-        hint(spans, "h", "highlight" + (highlightTerm != null ? " [" + 
highlightTerm + "]" : ""));
+        search.renderSearchHints(spans);
         hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
         if (!ctx.isInfraSelected()) {
             hint(spans, "l", "level");
@@ -523,128 +447,12 @@ class LogTab implements MonitorTab {
         
org.apache.camel.dsl.jbang.core.common.PathUtils.writeTextSafely(root.toJson(), 
actionFile);
     }
 
-    private void buildFindMatches() {
-        List<Integer> matches = new ArrayList<>();
-        List<LogEntry> entries = filteredLogEntries;
-        for (int i = 0; i < entries.size(); i++) {
-            String plain = TuiHelper.stripAnsi(entries.get(i).raw != null ? 
entries.get(i).raw : "");
-            if (findPattern.matcher(plain).find()) {
-                matches.add(i);
-            }
-        }
-        findMatches = matches;
-    }
-
-    private void jumpToNearestMatch() {
-        if (findMatches.isEmpty()) {
-            findMatchIndex = -1;
-            return;
-        }
-        for (int i = 0; i < findMatches.size(); i++) {
-            if (findMatches.get(i) >= scroll) {
-                findMatchIndex = i;
-                scrollToMatch();
-                return;
-            }
-        }
-        findMatchIndex = 0;
-        scrollToMatch();
-    }
-
-    private void navigateToNextMatch() {
-        if (findMatches.isEmpty()) {
-            return;
-        }
-        findMatchIndex = (findMatchIndex + 1) % findMatches.size();
-        scrollToMatch();
-    }
-
-    private void navigateToPrevMatch() {
-        if (findMatches.isEmpty()) {
-            return;
-        }
-        findMatchIndex = findMatchIndex <= 0 ? findMatches.size() - 1 : 
findMatchIndex - 1;
-        scrollToMatch();
-    }
-
-    private void scrollToMatch() {
-        if (findMatchIndex >= 0 && findMatchIndex < findMatches.size()) {
-            followMode = false;
-            scroll = findMatches.get(findMatchIndex);
-        }
-    }
-
     boolean isSearchInputActive() {
-        return findInputActive || highlightInputActive;
+        return search.isSearchInputActive();
     }
 
     void handlePaste(String text) {
-        if (findInputActive || highlightInputActive) {
-            FormHelper.handlePaste(text, searchInputState);
-        }
-    }
-
-    private Line applyHighlights(Line line, int entryIndex, int 
currentMatchLine) {
-        String fullText = line.rawContent();
-        if (fullText.isEmpty()) {
-            return line;
-        }
-
-        // Collect all match ranges with their styles
-        List<int[]> ranges = new ArrayList<>();
-        List<Style> styles = new ArrayList<>();
-        if (highlightPattern != null) {
-            Matcher m = highlightPattern.matcher(fullText);
-            while (m.find()) {
-                ranges.add(new int[] { m.start(), m.end() });
-                styles.add(HIGHLIGHT_STYLE);
-            }
-        }
-        if (findPattern != null) {
-            boolean isCurrentLine = entryIndex == currentMatchLine;
-            Matcher m = findPattern.matcher(fullText);
-            while (m.find()) {
-                ranges.add(new int[] { m.start(), m.end() });
-                styles.add(isCurrentLine ? FIND_CURRENT_STYLE : 
FIND_MATCH_STYLE);
-            }
-        }
-        if (ranges.isEmpty()) {
-            return line;
-        }
-
-        // Rebuild spans with highlights applied
-        List<Span> original = line.spans();
-        List<Span> result = new ArrayList<>();
-        int charPos = 0;
-
-        for (Span span : original) {
-            String content = span.content();
-            Style baseStyle = span.style();
-            int spanStart = charPos;
-            int spanEnd = charPos + content.length();
-            int cursor = 0;
-
-            for (int r = 0; r < ranges.size(); r++) {
-                int matchStart = ranges.get(r)[0];
-                int matchEnd = ranges.get(r)[1];
-                if (matchEnd <= spanStart || matchStart >= spanEnd) {
-                    continue;
-                }
-                int localStart = Math.max(0, matchStart - spanStart);
-                int localEnd = Math.min(content.length(), matchEnd - 
spanStart);
-
-                if (localStart > cursor) {
-                    result.add(Span.styled(content.substring(cursor, 
localStart), baseStyle));
-                }
-                result.add(Span.styled(content.substring(localStart, 
localEnd), styles.get(r)));
-                cursor = localEnd;
-            }
-            if (cursor < content.length()) {
-                result.add(Span.styled(content.substring(cursor), baseStyle));
-            }
-            charPos = spanEnd;
-        }
-        return Line.from(result);
+        search.handlePaste(text);
     }
 
     void readNewLogLines(String pid, List<String> newLines) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
index 3e752839756b..8b6c72923883 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
@@ -51,9 +51,9 @@ class MemoryTab implements MonitorTab {
     private final MonitorContext ctx;
     private final Map<String, LinkedList<Long>> heapMemHistory;
 
-    MemoryTab(MonitorContext ctx, Map<String, LinkedList<Long>> 
heapMemHistory) {
+    MemoryTab(MonitorContext ctx, MetricsCollector metrics) {
         this.ctx = ctx;
-        this.heapMemHistory = heapMemHistory;
+        this.heapMemHistory = metrics.getHeapMemHistory();
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
new file mode 100644
index 000000000000..c342c77fe1ee
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -0,0 +1,418 @@
+/*
+ * 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.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Collects and maintains sparkline/chart history data for all metric 
families. Extracted from CamelMonitor to
+ * consolidate the 30+ sliding-window maps and their update/cleanup/reset 
logic.
+ */
+class MetricsCollector {
+
+    static final int MAX_SPARKLINE_POINTS = 60;
+    static final int MAX_ENDPOINT_CHART_POINTS = 60;
+    static final int MAX_HEAP_HISTORY_POINTS = 120;
+    static final long HEAP_SAMPLE_INTERVAL_MS = 5000;
+
+    // Throughput history per PID (one point per second)
+    private final Map<String, LinkedList<Long>> throughputHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> failedHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> throughputSamples = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousExchangesTime = new 
ConcurrentHashMap<>();
+
+    // Endpoint in/out sliding window history per PID — all endpoints
+    private final Map<String, LinkedList<Long>> endpointInHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> endpointOutHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> endpointSamples = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousEndpointTime = new 
ConcurrentHashMap<>();
+
+    // Endpoint in/out sliding window history per PID — remote endpoints only
+    private final Map<String, LinkedList<Long>> endpointRemoteInHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> endpointRemoteOutHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> endpointRemoteSamples = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousEndpointRemoteTime = new 
ConcurrentHashMap<>();
+
+    // Endpoint in/out sliding window history per PID — remote+stub endpoints
+    private final Map<String, LinkedList<Long>> endpointRemoteStubInHistory = 
new ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> endpointRemoteStubOutHistory = 
new ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> endpointRemoteStubSamples = 
new ConcurrentHashMap<>();
+    private final Map<String, Long> previousEndpointRemoteStubTime = new 
ConcurrentHashMap<>();
+
+    // Endpoint payload size (mean body size) history per PID
+    private final Map<String, LinkedList<Long>> endpointInSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> endpointOutSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousEndpointSizeTime = new 
ConcurrentHashMap<>();
+
+    // Per-endpoint in/out rate history — keyed by pid + "|" + uri
+    private final Map<String, LinkedList<Long>> perEndpointInHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> perEndpointOutHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> perEndpointSamples = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousPerEndpointTime = new 
ConcurrentHashMap<>();
+
+    // Circuit breaker throughput history per PID/cbId
+    private final Map<String, LinkedList<Long>> cbSuccessHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> cbFailHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> cbThroughputSamples = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousCbTime = new ConcurrentHashMap<>();
+
+    // Heap memory usage history per PID (one point per 5 seconds, in bytes)
+    private final Map<String, LinkedList<Long>> heapMemHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousHeapTime = new 
ConcurrentHashMap<>();
+
+    // Load averages (EWMA) — CPU%, per PID
+    private final Map<String, LoadAvg> cpuLoadAvg = new ConcurrentHashMap<>();
+    private final Map<String, long[]> prevCpuSample = new 
ConcurrentHashMap<>();
+
+    // --- Getters for tab read access ---
+
+    Map<String, LinkedList<Long>> getThroughputHistory() {
+        return throughputHistory;
+    }
+
+    Map<String, LinkedList<Long>> getFailedHistory() {
+        return failedHistory;
+    }
+
+    Map<String, LoadAvg> getCpuLoadAvg() {
+        return cpuLoadAvg;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointInHistory() {
+        return endpointInHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointOutHistory() {
+        return endpointOutHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointRemoteInHistory() {
+        return endpointRemoteInHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointRemoteOutHistory() {
+        return endpointRemoteOutHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointRemoteStubInHistory() {
+        return endpointRemoteStubInHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointRemoteStubOutHistory() {
+        return endpointRemoteStubOutHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointInSizeHistory() {
+        return endpointInSizeHistory;
+    }
+
+    Map<String, LinkedList<Long>> getEndpointOutSizeHistory() {
+        return endpointOutSizeHistory;
+    }
+
+    Map<String, LinkedList<Long>> getPerEndpointInHistory() {
+        return perEndpointInHistory;
+    }
+
+    Map<String, LinkedList<Long>> getPerEndpointOutHistory() {
+        return perEndpointOutHistory;
+    }
+
+    Map<String, LinkedList<Long>> getCbSuccessHistory() {
+        return cbSuccessHistory;
+    }
+
+    Map<String, LinkedList<Long>> getCbFailHistory() {
+        return cbFailHistory;
+    }
+
+    Map<String, LinkedList<Long>> getHeapMemHistory() {
+        return heapMemHistory;
+    }
+
+    // --- Update methods ---
+
+    void updateThroughputHistory(IntegrationInfo info) {
+        long currentTotal = info.exchangesTotal;
+        long currentFailed = info.failed;
+        long now = System.currentTimeMillis();
+
+        String pid = info.pid;
+        LinkedList<long[]> samples = throughputSamples.computeIfAbsent(pid, k 
-> new LinkedList<>());
+        samples.add(new long[] { now, currentTotal, currentFailed });
+
+        while (!samples.isEmpty() && now - samples.get(0)[0] > 1000) {
+            samples.remove(0);
+        }
+
+        if (samples.size() >= 2) {
+            long[] oldest = samples.get(0);
+            long[] newest = samples.get(samples.size() - 1);
+            long deltaTotal = newest[1] - oldest[1];
+            long deltaFailed = newest[2] - oldest[2];
+            long deltaTimeMs = newest[0] - oldest[0];
+            long tp = deltaTimeMs > 0 ? (deltaTotal * 1000) / deltaTimeMs : 0;
+            long fp = deltaTimeMs > 0 ? (deltaFailed * 1000) / deltaTimeMs : 0;
+
+            Long lastTime = previousExchangesTime.get(pid);
+            if (lastTime == null || now - lastTime >= 1000) {
+                previousExchangesTime.put(pid, now);
+                LinkedList<Long> hist = throughputHistory.computeIfAbsent(pid, 
k -> new LinkedList<>());
+                hist.add(tp);
+                while (hist.size() > MAX_SPARKLINE_POINTS) {
+                    hist.remove(0);
+                }
+                LinkedList<Long> fhist = failedHistory.computeIfAbsent(pid, k 
-> new LinkedList<>());
+                fhist.add(fp);
+                while (fhist.size() > MAX_SPARKLINE_POINTS) {
+                    fhist.remove(0);
+                }
+            }
+        }
+    }
+
+    void updateEndpointHistory(IntegrationInfo info) {
+        long inTotal = info.endpoints.stream()
+                .filter(ep -> "in".equals(ep.direction))
+                .mapToLong(ep -> ep.hits).sum();
+        long outTotal = info.endpoints.stream()
+                .filter(ep -> "out".equals(ep.direction))
+                .mapToLong(ep -> ep.hits).sum();
+        long inRemote = info.endpoints.stream()
+                .filter(ep -> "in".equals(ep.direction) && ep.remote)
+                .mapToLong(ep -> ep.hits).sum();
+        long outRemote = info.endpoints.stream()
+                .filter(ep -> "out".equals(ep.direction) && ep.remote)
+                .mapToLong(ep -> ep.hits).sum();
+        long inRemoteStub = info.endpoints.stream()
+                .filter(ep -> "in".equals(ep.direction) && (ep.remote || 
ep.stub))
+                .mapToLong(ep -> ep.hits).sum();
+        long outRemoteStub = info.endpoints.stream()
+                .filter(ep -> "out".equals(ep.direction) && (ep.remote || 
ep.stub))
+                .mapToLong(ep -> ep.hits).sum();
+
+        long now = System.currentTimeMillis();
+        String pid = info.pid;
+
+        recordEndpointSample(pid, now, inTotal, outTotal,
+                endpointSamples, previousEndpointTime, endpointInHistory, 
endpointOutHistory);
+        recordEndpointSample(pid, now, inRemote, outRemote,
+                endpointRemoteSamples, previousEndpointRemoteTime, 
endpointRemoteInHistory, endpointRemoteOutHistory);
+        recordEndpointSample(pid, now, inRemoteStub, outRemoteStub,
+                endpointRemoteStubSamples, previousEndpointRemoteStubTime,
+                endpointRemoteStubInHistory, endpointRemoteStubOutHistory);
+
+        // Record payload size snapshots (mean body size per direction)
+        long inMeanSize = info.endpoints.stream()
+                .filter(ep -> "in".equals(ep.direction) && ep.meanBodySize >= 
0)
+                .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
+        long outMeanSize = info.endpoints.stream()
+                .filter(ep -> "out".equals(ep.direction) && ep.meanBodySize >= 
0)
+                .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
+        Long lastSizeTime = previousEndpointSizeTime.get(pid);
+        if (lastSizeTime == null || now - lastSizeTime >= 1000) {
+            previousEndpointSizeTime.put(pid, now);
+            LinkedList<Long> inSizeHist = 
endpointInSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+            inSizeHist.add(inMeanSize);
+            while (inSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+                inSizeHist.remove(0);
+            }
+            LinkedList<Long> outSizeHist = 
endpointOutSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+            outSizeHist.add(outMeanSize);
+            while (outSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+                outSizeHist.remove(0);
+            }
+        }
+
+        // Per-endpoint rate history (keyed by pid|uri)
+        Map<String, long[]> perUri = new LinkedHashMap<>();
+        for (EndpointInfo ep : info.endpoints) {
+            if (ep.uri == null) {
+                continue;
+            }
+            long[] inOut = perUri.computeIfAbsent(ep.uri, k -> new long[2]);
+            if ("in".equals(ep.direction)) {
+                inOut[0] += ep.hits;
+            } else if ("out".equals(ep.direction)) {
+                inOut[1] += ep.hits;
+            }
+        }
+        for (Map.Entry<String, long[]> entry : perUri.entrySet()) {
+            String key = pid + "|" + entry.getKey();
+            long[] inOut = entry.getValue();
+            recordEndpointSample(key, now, inOut[0], inOut[1],
+                    perEndpointSamples, previousPerEndpointTime,
+                    perEndpointInHistory, perEndpointOutHistory);
+        }
+    }
+
+    private void recordEndpointSample(
+            String pid, long now, long inTotal, long outTotal,
+            Map<String, LinkedList<long[]>> samplesMap, Map<String, Long> 
prevTimeMap,
+            Map<String, LinkedList<Long>> inHistMap, Map<String, 
LinkedList<Long>> outHistMap) {
+        LinkedList<long[]> samples = samplesMap.computeIfAbsent(pid, k -> new 
LinkedList<>());
+        samples.add(new long[] { now, inTotal, outTotal });
+        while (!samples.isEmpty() && now - samples.get(0)[0] > 1000) {
+            samples.remove(0);
+        }
+        if (samples.size() >= 2) {
+            long[] oldest = samples.get(0);
+            long[] newest = samples.get(samples.size() - 1);
+            long deltaMs = newest[0] - oldest[0];
+            long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 / 
deltaMs : 0;
+            long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 / 
deltaMs : 0;
+            Long lastTime = prevTimeMap.get(pid);
+            if (lastTime == null || now - lastTime >= 1000) {
+                prevTimeMap.put(pid, now);
+                LinkedList<Long> inHist = inHistMap.computeIfAbsent(pid, k -> 
new LinkedList<>());
+                inHist.add(Math.max(0, inRate));
+                while (inHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+                    inHist.remove(0);
+                }
+                LinkedList<Long> outHist = outHistMap.computeIfAbsent(pid, k 
-> new LinkedList<>());
+                outHist.add(Math.max(0, outRate));
+                while (outHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+                    outHist.remove(0);
+                }
+            }
+        }
+    }
+
+    void updateCbHistory(IntegrationInfo info) {
+        long now = System.currentTimeMillis();
+        for (CircuitBreakerInfo cb : info.circuitBreakers) {
+            if (cb.id == null) {
+                continue;
+            }
+            String key = info.pid + "/" + cb.id;
+            long success = cb.successfulCalls;
+            long failed = cb.failedCalls;
+            recordEndpointSample(key, now, success, failed,
+                    cbThroughputSamples, previousCbTime, cbSuccessHistory, 
cbFailHistory);
+        }
+    }
+
+    void updateHeapHistory(IntegrationInfo info) {
+        if (info.heapMemUsed > 0) {
+            long now = System.currentTimeMillis();
+            Long lastTime = previousHeapTime.get(info.pid);
+            if (lastTime == null || now - lastTime >= HEAP_SAMPLE_INTERVAL_MS) 
{
+                previousHeapTime.put(info.pid, now);
+                LinkedList<Long> hist = 
heapMemHistory.computeIfAbsent(info.pid, k -> new LinkedList<>());
+                hist.add(info.heapMemUsed);
+                while (hist.size() > MAX_HEAP_HISTORY_POINTS) {
+                    hist.remove(0);
+                }
+            }
+        }
+    }
+
+    void updateLoadMetrics(ProcessHandle ph, IntegrationInfo info) {
+        String pid = info.pid;
+
+        Optional<Duration> durOpt = ph.info().totalCpuDuration();
+        if (durOpt.isPresent()) {
+            long cpuNanos = durOpt.get().toNanos();
+            long wallMs = System.currentTimeMillis();
+            long[] prev = prevCpuSample.get(pid);
+            if (prev != null) {
+                long deltaCpuNanos = cpuNanos - prev[0];
+                long deltaWallNanos = (wallMs - prev[1]) * 1_000_000L;
+                if (deltaWallNanos > 0) {
+                    double cpuPct = (double) deltaCpuNanos / deltaWallNanos * 
100.0;
+                    cpuLoadAvg.computeIfAbsent(pid, k -> new 
LoadAvg()).update(Math.max(0, cpuPct));
+                }
+            }
+            prevCpuSample.put(pid, new long[] { cpuNanos, wallMs });
+        }
+    }
+
+    // --- Cleanup methods ---
+
+    void resetStats(String pid) {
+        throughputHistory.remove(pid);
+        failedHistory.remove(pid);
+        throughputSamples.remove(pid);
+        previousExchangesTime.remove(pid);
+
+        endpointInHistory.remove(pid);
+        endpointOutHistory.remove(pid);
+        endpointSamples.remove(pid);
+        previousEndpointTime.remove(pid);
+        endpointRemoteInHistory.remove(pid);
+        endpointRemoteOutHistory.remove(pid);
+        endpointRemoteSamples.remove(pid);
+        previousEndpointRemoteTime.remove(pid);
+        endpointRemoteStubInHistory.remove(pid);
+        endpointRemoteStubOutHistory.remove(pid);
+        endpointRemoteStubSamples.remove(pid);
+        previousEndpointRemoteStubTime.remove(pid);
+        endpointInSizeHistory.remove(pid);
+        endpointOutSizeHistory.remove(pid);
+        previousEndpointSizeTime.remove(pid);
+
+        removeByPrefix(pid + "|", perEndpointInHistory, perEndpointOutHistory,
+                perEndpointSamples, previousPerEndpointTime);
+
+        heapMemHistory.remove(pid);
+        previousHeapTime.remove(pid);
+    }
+
+    void removeVanished(String pid) {
+        throughputHistory.remove(pid);
+        failedHistory.remove(pid);
+
+        endpointInHistory.remove(pid);
+        endpointOutHistory.remove(pid);
+        endpointSamples.remove(pid);
+        previousEndpointTime.remove(pid);
+        endpointRemoteInHistory.remove(pid);
+        endpointRemoteOutHistory.remove(pid);
+        endpointRemoteSamples.remove(pid);
+        previousEndpointRemoteTime.remove(pid);
+        endpointRemoteStubInHistory.remove(pid);
+        endpointRemoteStubOutHistory.remove(pid);
+        endpointRemoteStubSamples.remove(pid);
+
+        endpointInSizeHistory.remove(pid);
+        endpointOutSizeHistory.remove(pid);
+        previousEndpointSizeTime.remove(pid);
+        previousEndpointRemoteStubTime.remove(pid);
+
+        heapMemHistory.remove(pid);
+        previousHeapTime.remove(pid);
+        cpuLoadAvg.remove(pid);
+        prevCpuSample.remove(pid);
+
+        removeByPrefix(pid + "/", cbSuccessHistory, cbFailHistory,
+                cbThroughputSamples, previousCbTime);
+        removeByPrefix(pid + "|", perEndpointInHistory, perEndpointOutHistory,
+                perEndpointSamples, previousPerEndpointTime);
+    }
+
+    @SafeVarargs
+    private void removeByPrefix(String prefix, Map<String, ?>... maps) {
+        for (Map<String, ?> map : maps) {
+            map.keySet().removeIf(k -> k.startsWith(prefix));
+        }
+    }
+}
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 979eae7e9842..f7507415cfb6 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
@@ -77,15 +77,13 @@ class OverviewTab implements MonitorTab {
 
     OverviewTab(
                 MonitorContext ctx,
-                Map<String, LinkedList<Long>> throughputHistory,
-                Map<String, LinkedList<Long>> failedHistory,
-                Map<String, LoadAvg> cpuLoadAvg,
+                MetricsCollector metrics,
                 Set<String> stoppingPids,
                 Runnable onPidChanged) {
         this.ctx = ctx;
-        this.throughputHistory = throughputHistory;
-        this.failedHistory = failedHistory;
-        this.cpuLoadAvg = cpuLoadAvg;
+        this.throughputHistory = metrics.getThroughputHistory();
+        this.failedHistory = metrics.getFailedHistory();
+        this.cpuLoadAvg = metrics.getCpuLoadAvg();
         this.stoppingPids = stoppingPids;
         this.onPidChanged = onPidChanged;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
new file mode 100644
index 000000000000..72a8d7379724
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
@@ -0,0 +1,299 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.input.TextInputState;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+/**
+ * Shared find/highlight search logic used by LogTab and SourceViewer.
+ */
+class SearchHighlighter {
+
+    static final Style HIGHLIGHT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
+    static final Style FIND_MATCH_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
+    static final Style FIND_CURRENT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.LIGHT_GREEN);
+
+    private boolean findInputActive;
+    private boolean highlightInputActive;
+    private TextInputState searchInputState = new TextInputState("");
+    private String findTerm;
+    private Pattern findPattern;
+    private int findMatchIndex = -1;
+    private List<Integer> findMatches = Collections.emptyList();
+    private String highlightTerm;
+    private Pattern highlightPattern;
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (findInputActive || highlightInputActive) {
+            return handleSearchInput(ke);
+        }
+        if (ke.isChar('/')) {
+            findInputActive = true;
+            searchInputState = new TextInputState("");
+            return true;
+        }
+        if (ke.isChar('h')) {
+            highlightInputActive = true;
+            searchInputState = new TextInputState("");
+            return true;
+        }
+        if (ke.isChar('n') && findTerm != null) {
+            navigateToNextMatch();
+            return true;
+        }
+        if (ke.isChar('N') && findTerm != null) {
+            navigateToPrevMatch();
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handleSearchInput(KeyEvent ke) {
+        if (ke.isKey(KeyCode.ESCAPE)) {
+            findInputActive = false;
+            highlightInputActive = false;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            String text = searchInputState.text().trim();
+            if (findInputActive) {
+                if (text.isEmpty()) {
+                    findTerm = null;
+                    findPattern = null;
+                    findMatches = Collections.emptyList();
+                    findMatchIndex = -1;
+                } else {
+                    findTerm = text;
+                    findPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
+                }
+                findInputActive = false;
+            } else if (highlightInputActive) {
+                if (text.isEmpty()) {
+                    highlightTerm = null;
+                    highlightPattern = null;
+                } else {
+                    highlightTerm = text;
+                    highlightPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
+                }
+                highlightInputActive = false;
+            }
+            return true;
+        }
+        FormHelper.handleTextInput(ke, searchInputState);
+        return true;
+    }
+
+    boolean handleEscape() {
+        if (findInputActive || highlightInputActive) {
+            findInputActive = false;
+            highlightInputActive = false;
+            return true;
+        }
+        if (findTerm != null) {
+            findTerm = null;
+            findPattern = null;
+            findMatches = Collections.emptyList();
+            findMatchIndex = -1;
+            return true;
+        }
+        return false;
+    }
+
+    boolean isSearchInputActive() {
+        return findInputActive || highlightInputActive;
+    }
+
+    void handlePaste(String text) {
+        if (findInputActive || highlightInputActive) {
+            FormHelper.handlePaste(text, searchInputState);
+        }
+    }
+
+    void buildFindMatches(List<String> plainTextLines) {
+        if (findPattern == null) {
+            findMatches = Collections.emptyList();
+            findMatchIndex = -1;
+            return;
+        }
+        List<Integer> matches = new ArrayList<>();
+        for (int i = 0; i < plainTextLines.size(); i++) {
+            if (findPattern.matcher(plainTextLines.get(i)).find()) {
+                matches.add(i);
+            }
+        }
+        findMatches = matches;
+    }
+
+    int jumpToNearestMatch(int currentPosition) {
+        if (findMatches.isEmpty()) {
+            findMatchIndex = -1;
+            return currentPosition;
+        }
+        for (int i = 0; i < findMatches.size(); i++) {
+            if (findMatches.get(i) >= currentPosition) {
+                findMatchIndex = i;
+                return findMatches.get(findMatchIndex);
+            }
+        }
+        findMatchIndex = 0;
+        return findMatches.get(0);
+    }
+
+    void navigateToNextMatch() {
+        if (findMatches.isEmpty()) {
+            return;
+        }
+        findMatchIndex = (findMatchIndex + 1) % findMatches.size();
+    }
+
+    void navigateToPrevMatch() {
+        if (findMatches.isEmpty()) {
+            return;
+        }
+        findMatchIndex = findMatchIndex <= 0 ? findMatches.size() - 1 : 
findMatchIndex - 1;
+    }
+
+    int currentMatchLine() {
+        if (findMatchIndex >= 0 && findMatchIndex < findMatches.size()) {
+            return findMatches.get(findMatchIndex);
+        }
+        return -1;
+    }
+
+    Line applyHighlights(Line line, int lineIndex, int currentMatchLine) {
+        String fullText = line.rawContent();
+        if (fullText.isEmpty()) {
+            return line;
+        }
+
+        List<int[]> ranges = new ArrayList<>();
+        List<Style> rangeStyles = new ArrayList<>();
+        if (highlightPattern != null) {
+            Matcher m = highlightPattern.matcher(fullText);
+            while (m.find()) {
+                ranges.add(new int[] { m.start(), m.end() });
+                rangeStyles.add(HIGHLIGHT_STYLE);
+            }
+        }
+        if (findPattern != null) {
+            boolean isCurrentLine = lineIndex == currentMatchLine;
+            Matcher m = findPattern.matcher(fullText);
+            while (m.find()) {
+                ranges.add(new int[] { m.start(), m.end() });
+                rangeStyles.add(isCurrentLine ? FIND_CURRENT_STYLE : 
FIND_MATCH_STYLE);
+            }
+        }
+        if (ranges.isEmpty()) {
+            return line;
+        }
+
+        List<Span> original = line.spans();
+        List<Span> result = new ArrayList<>();
+        int charPos = 0;
+        for (Span span : original) {
+            String content = span.content();
+            Style baseStyle = span.style();
+            int spanStart = charPos;
+            int spanEnd = charPos + content.length();
+            int cursor = 0;
+            for (int r = 0; r < ranges.size(); r++) {
+                int matchStart = ranges.get(r)[0];
+                int matchEnd = ranges.get(r)[1];
+                if (matchEnd <= spanStart || matchStart >= spanEnd) {
+                    continue;
+                }
+                int localStart = Math.max(0, matchStart - spanStart);
+                int localEnd = Math.min(content.length(), matchEnd - 
spanStart);
+                if (localStart > cursor) {
+                    result.add(Span.styled(content.substring(cursor, 
localStart), baseStyle));
+                }
+                result.add(Span.styled(content.substring(localStart, 
localEnd), rangeStyles.get(r)));
+                cursor = localEnd;
+            }
+            if (cursor < content.length()) {
+                result.add(Span.styled(content.substring(cursor), baseStyle));
+            }
+            charPos = spanEnd;
+        }
+        return Line.from(result);
+    }
+
+    void renderFooterHints(List<Span> spans) {
+        if (findInputActive) {
+            spans.add(Span.styled(" /", HINT_KEY_STYLE));
+            spans.add(Span.raw(searchInputState.text() + "█  "));
+            hint(spans, "Enter", "search");
+            hintLast(spans, "Esc", "cancel");
+            return;
+        }
+        if (highlightInputActive) {
+            spans.add(Span.styled(" h:", HINT_KEY_STYLE));
+            spans.add(Span.raw(searchInputState.text() + "█  "));
+            hint(spans, "Enter", "set");
+            hintLast(spans, "Esc", "cancel");
+            return;
+        }
+    }
+
+    void renderFindStatus(List<Span> spans) {
+        if (findTerm != null) {
+            hint(spans, "Esc", "clear find");
+            hint(spans, "n", "next");
+            hint(spans, "N", "prev");
+            String pos = findMatches.isEmpty()
+                    ? "0/0"
+                    : (findMatchIndex + 1) + "/" + findMatches.size();
+            spans.add(Span.styled("  /", HINT_KEY_STYLE));
+            spans.add(Span.raw("\"" + findTerm + "\" [" + pos + "]  "));
+        }
+    }
+
+    void renderSearchHints(List<Span> spans) {
+        hint(spans, "/", "find");
+        hint(spans, "h", "highlight" + (highlightTerm != null ? " [" + 
highlightTerm + "]" : ""));
+    }
+
+    boolean hasFindTerm() {
+        return findTerm != null;
+    }
+
+    void reset() {
+        findInputActive = false;
+        highlightInputActive = false;
+        searchInputState = new TextInputState("");
+        findTerm = null;
+        findPattern = null;
+        findMatchIndex = -1;
+        findMatches = Collections.emptyList();
+        highlightTerm = null;
+        highlightPattern = null;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 273a0a0d5c17..8a10d434c6a3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -24,8 +24,6 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.IntConsumer;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
@@ -41,7 +39,6 @@ import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
-import dev.tamboui.widgets.input.TextInputState;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.scrollbar.Scrollbar;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
@@ -76,23 +73,7 @@ class SourceViewer {
     private IntConsumer onLineSelected;
     private final Map<String, CachedSource> sourceCache = new 
ConcurrentHashMap<>();
     private boolean wordWrap;
-
-    // Find mode
-    private boolean findInputActive;
-    private boolean highlightInputActive;
-    private TextInputState searchInputState = new TextInputState("");
-    private String findTerm;
-    private Pattern findPattern;
-    private int findMatchIndex = -1;
-    private List<Integer> findMatches = Collections.emptyList();
-
-    // Highlight mode
-    private String highlightTerm;
-    private Pattern highlightPattern;
-
-    private static final Style HIGHLIGHT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
-    private static final Style FIND_MATCH_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
-    private static final Style FIND_CURRENT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.LIGHT_GREEN);
+    private final SearchHighlighter search = new SearchHighlighter();
 
     private record CachedSource(
             List<String> lines, List<JsonObject> codeData,
@@ -120,14 +101,7 @@ class SourceViewer {
         onLineSelected = null;
         sourceCache.clear();
         wordWrap = false;
-        findInputActive = false;
-        highlightInputActive = false;
-        findTerm = null;
-        findPattern = null;
-        findMatchIndex = -1;
-        findMatches = Collections.emptyList();
-        highlightTerm = null;
-        highlightPattern = null;
+        search.reset();
     }
 
     void setOnLineSelected(IntConsumer callback) {
@@ -138,15 +112,16 @@ class SourceViewer {
         if (!visible) {
             return false;
         }
-        if (findInputActive || highlightInputActive) {
-            return handleSearchInput(ke);
+        if (search.isSearchInputActive()) {
+            boolean handled = search.handleKeyEvent(ke);
+            if (handled && !search.isSearchInputActive() && 
search.hasFindTerm()) {
+                search.buildFindMatches(lines);
+                selectedLine = search.jumpToNearestMatch(selectedLine);
+            }
+            return handled;
         }
         if (ke.isCancel()) {
-            if (findTerm != null) {
-                findTerm = null;
-                findPattern = null;
-                findMatches = Collections.emptyList();
-                findMatchIndex = -1;
+            if (search.handleEscape()) {
                 return true;
             }
             visible = false;
@@ -158,22 +133,11 @@ class SourceViewer {
             onLineSelected = null;
             return true;
         }
-        if (ke.isChar('/')) {
-            findInputActive = true;
-            searchInputState = new TextInputState("");
-            return true;
-        }
-        if (ke.isChar('h')) {
-            highlightInputActive = true;
-            searchInputState = new TextInputState("");
-            return true;
-        }
-        if (ke.isChar('n') && findTerm != null) {
-            navigateToNextMatch();
-            return true;
-        }
-        if (ke.isChar('N') && findTerm != null) {
-            navigateToPrevMatch();
+        if (search.handleKeyEvent(ke)) {
+            int matchLine = search.currentMatchLine();
+            if (matchLine >= 0) {
+                selectedLine = matchLine;
+            }
             return true;
         }
         if (ke.isChar('w')) {
@@ -224,51 +188,12 @@ class SourceViewer {
         return true;
     }
 
-    private boolean handleSearchInput(KeyEvent ke) {
-        if (ke.isKey(KeyCode.ESCAPE)) {
-            findInputActive = false;
-            highlightInputActive = false;
-            return true;
-        }
-        if (ke.isConfirm()) {
-            String text = searchInputState.text().trim();
-            if (findInputActive) {
-                if (text.isEmpty()) {
-                    findTerm = null;
-                    findPattern = null;
-                    findMatches = Collections.emptyList();
-                    findMatchIndex = -1;
-                } else {
-                    findTerm = text;
-                    findPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
-                    buildFindMatches();
-                    jumpToNearestMatch();
-                }
-                findInputActive = false;
-            } else if (highlightInputActive) {
-                if (text.isEmpty()) {
-                    highlightTerm = null;
-                    highlightPattern = null;
-                } else {
-                    highlightTerm = text;
-                    highlightPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
-                }
-                highlightInputActive = false;
-            }
-            return true;
-        }
-        FormHelper.handleTextInput(ke, searchInputState);
-        return true;
-    }
-
     boolean isSearchInputActive() {
-        return findInputActive || highlightInputActive;
+        return search.isSearchInputActive();
     }
 
     void handlePaste(String text) {
-        if (findInputActive || highlightInputActive) {
-            FormHelper.handlePaste(text, searchInputState);
-        }
+        search.handlePaste(text);
     }
 
     void render(Frame frame, Rect area) {
@@ -312,8 +237,7 @@ class SourceViewer {
             scrollX = Math.min(scrollX, maxHScroll);
         }
 
-        int currentMatchLine = findMatchIndex >= 0 && findMatchIndex < 
findMatches.size()
-                ? findMatches.get(findMatchIndex) : -1;
+        int currentMatchLine = search.currentMatchLine();
 
         int end = Math.min(scrollY + visibleLines, lines.size());
         List<Line> visible = new ArrayList<>();
@@ -321,9 +245,7 @@ class SourceViewer {
             String raw = lines.get(i);
             boolean isSelected = (i == selectedLine);
             Line line = highlightSourceLine(raw, hSkip, isSelected, 
inner.width());
-            if (highlightPattern != null || findPattern != null) {
-                line = applySearchHighlights(line, i, currentMatchLine);
-            }
+            line = search.applyHighlights(line, i, currentMatchLine);
             visible.add(line);
         }
 
@@ -350,35 +272,17 @@ class SourceViewer {
     }
 
     void renderFooter(List<Span> spans) {
-        if (findInputActive) {
-            spans.add(Span.styled(" /", MonitorContext.HINT_KEY_STYLE));
-            spans.add(Span.raw(searchInputState.text() + "█  "));
-            MonitorContext.hint(spans, "Enter", "search");
-            MonitorContext.hintLast(spans, "Esc", "cancel");
-            return;
-        }
-        if (highlightInputActive) {
-            spans.add(Span.styled(" h:", MonitorContext.HINT_KEY_STYLE));
-            spans.add(Span.raw(searchInputState.text() + "█  "));
-            MonitorContext.hint(spans, "Enter", "set");
-            MonitorContext.hintLast(spans, "Esc", "cancel");
+        search.renderFooterHints(spans);
+        if (search.isSearchInputActive()) {
             return;
         }
-        if (findTerm != null) {
-            MonitorContext.hint(spans, "Esc", "clear find");
-            MonitorContext.hint(spans, "n", "next");
-            MonitorContext.hint(spans, "N", "prev");
-            String pos = findMatches.isEmpty()
-                    ? "0/0"
-                    : (findMatchIndex + 1) + "/" + findMatches.size();
-            spans.add(Span.styled("  /", MonitorContext.HINT_KEY_STYLE));
-            spans.add(Span.raw("\"" + findTerm + "\" [" + pos + "]  "));
+        if (search.hasFindTerm()) {
+            search.renderFindStatus(spans);
         } else {
             MonitorContext.hint(spans, "Esc/c", "close");
         }
         MonitorContext.hint(spans, "↑↓", "navigate");
-        MonitorContext.hint(spans, "/", "find");
-        MonitorContext.hint(spans, "h", "highlight" + (highlightTerm != null ? 
" [" + highlightTerm + "]" : ""));
+        search.renderSearchHints(spans);
         MonitorContext.hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " 
[off]"));
         if (!wordWrap) {
             MonitorContext.hint(spans, "←→", "horizontal");
@@ -671,112 +575,6 @@ class SourceViewer {
         return full;
     }
 
-    private void buildFindMatches() {
-        List<Integer> matches = new ArrayList<>();
-        for (int i = 0; i < lines.size(); i++) {
-            if (findPattern.matcher(lines.get(i)).find()) {
-                matches.add(i);
-            }
-        }
-        findMatches = matches;
-    }
-
-    private void jumpToNearestMatch() {
-        if (findMatches.isEmpty()) {
-            findMatchIndex = -1;
-            return;
-        }
-        for (int i = 0; i < findMatches.size(); i++) {
-            if (findMatches.get(i) >= selectedLine) {
-                findMatchIndex = i;
-                scrollToMatch();
-                return;
-            }
-        }
-        findMatchIndex = 0;
-        scrollToMatch();
-    }
-
-    private void navigateToNextMatch() {
-        if (findMatches.isEmpty()) {
-            return;
-        }
-        findMatchIndex = (findMatchIndex + 1) % findMatches.size();
-        scrollToMatch();
-    }
-
-    private void navigateToPrevMatch() {
-        if (findMatches.isEmpty()) {
-            return;
-        }
-        findMatchIndex = findMatchIndex <= 0 ? findMatches.size() - 1 : 
findMatchIndex - 1;
-        scrollToMatch();
-    }
-
-    private void scrollToMatch() {
-        if (findMatchIndex >= 0 && findMatchIndex < findMatches.size()) {
-            selectedLine = findMatches.get(findMatchIndex);
-        }
-    }
-
-    private Line applySearchHighlights(Line line, int lineIndex, int 
currentMatchLine) {
-        String fullText = line.rawContent();
-        if (fullText.isEmpty()) {
-            return line;
-        }
-
-        List<int[]> ranges = new ArrayList<>();
-        List<Style> rangeStyles = new ArrayList<>();
-        if (highlightPattern != null) {
-            Matcher m = highlightPattern.matcher(fullText);
-            while (m.find()) {
-                ranges.add(new int[] { m.start(), m.end() });
-                rangeStyles.add(HIGHLIGHT_STYLE);
-            }
-        }
-        if (findPattern != null) {
-            boolean isCurrentLine = lineIndex == currentMatchLine;
-            Matcher m = findPattern.matcher(fullText);
-            while (m.find()) {
-                ranges.add(new int[] { m.start(), m.end() });
-                rangeStyles.add(isCurrentLine ? FIND_CURRENT_STYLE : 
FIND_MATCH_STYLE);
-            }
-        }
-        if (ranges.isEmpty()) {
-            return line;
-        }
-
-        List<Span> original = line.spans();
-        List<Span> result = new ArrayList<>();
-        int charPos = 0;
-        for (Span span : original) {
-            String content = span.content();
-            Style baseStyle = span.style();
-            int spanStart = charPos;
-            int spanEnd = charPos + content.length();
-            int cursor = 0;
-            for (int r = 0; r < ranges.size(); r++) {
-                int matchStart = ranges.get(r)[0];
-                int matchEnd = ranges.get(r)[1];
-                if (matchEnd <= spanStart || matchStart >= spanEnd) {
-                    continue;
-                }
-                int localStart = Math.max(0, matchStart - spanStart);
-                int localEnd = Math.min(content.length(), matchEnd - 
spanStart);
-                if (localStart > cursor) {
-                    result.add(Span.styled(content.substring(cursor, 
localStart), baseStyle));
-                }
-                result.add(Span.styled(content.substring(localStart, 
localEnd), rangeStyles.get(r)));
-                cursor = localEnd;
-            }
-            if (cursor < content.length()) {
-                result.add(Span.styled(content.substring(cursor), baseStyle));
-            }
-            charPos = spanEnd;
-        }
-        return Line.from(result);
-    }
-
     static int findLicenseHeaderEnd(List<JsonObject> codeLines) {
         boolean inBlock = false;
         int lastCommentLine = -1;


Reply via email to