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;
