This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23648-run-folder in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9d085652eea2e6e89ef4852c1befaa3ccd02c66c Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jun 1 09:04:43 2026 +0200 CAMEL-23648: camel-jbang - TUI Log tab find and highlight Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 20 +- .../camel/dsl/jbang/core/commands/tui/LogTab.java | 296 ++++++++++++++++++++- 2 files changed, 299 insertions(+), 17 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 dc8314a71d23..8649c1f52659 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 @@ -525,9 +525,11 @@ public class CamelMonitor extends CamelCommand { } return true; } - // Quit: q or Ctrl+c (skip when probe is editing text) + // Quit: q or Ctrl+c (skip when text input is active) boolean probeEditing = tabsState.selected() == TAB_HTTP && httpTab.isProbeMode(); - if (!probeEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) { + boolean logSearchActive = tabsState.selected() == TAB_LOG && logTab.isSearchInputActive(); + boolean textEditing = probeEditing || logSearchActive; + if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) { runner.quit(); return true; } @@ -535,9 +537,9 @@ public class CamelMonitor extends CamelCommand { runner.quit(); return true; } - // Tab switching with number keys (skip when probe is editing text) + // Tab switching with number keys (skip when text input is active) // When infra is selected, only Overview (1) and Log (2) are available - if (!probeEditing) { + if (!textEditing) { if (ke.isChar('1')) { return handleTabKey(TAB_OVERVIEW); } @@ -573,8 +575,8 @@ public class CamelMonitor extends CamelCommand { } // Tab cycling (check Shift+Tab before Tab since Tab binding also matches Shift+Tab) - // Skip tab cycling when HTTP probe is active (Tab navigates fields) - if (ke.isFocusPrevious() && !(tabsState.selected() == TAB_HTTP && httpTab.isProbeMode())) { + // Skip tab cycling when text input is active (Tab navigates fields) + if (ke.isFocusPrevious() && !textEditing) { if (isInfraSelected()) { // Cycle between Overview and Log only int prev = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; @@ -588,7 +590,7 @@ public class CamelMonitor extends CamelCommand { } return true; } - if (ke.isFocusNext() && !(tabsState.selected() == TAB_HTTP && httpTab.isProbeMode())) { + if (ke.isFocusNext() && !textEditing) { if (isInfraSelected()) { int next = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; tabsState.select(next); @@ -748,6 +750,10 @@ public class CamelMonitor extends CamelCommand { httpTab.handlePaste(pe.text()); return true; } + if (logTab.isSearchInputActive()) { + logTab.handlePaste(pe.text()); + return true; + } } if (event instanceof TickEvent) { long now = System.currentTimeMillis(); 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 c2ccdf95e6e1..9187e177e321 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,10 +38,12 @@ 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.input.TextInputState; import dev.tamboui.widgets.list.ListItem; import dev.tamboui.widgets.list.ListState; import dev.tamboui.widgets.list.ListWidget; @@ -87,12 +89,33 @@ 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); + LogTab(MonitorContext ctx) { this.ctx = ctx; } @Override public boolean handleKeyEvent(KeyEvent ke) { + if (findInputActive || highlightInputActive) { + return handleSearchInput(ke); + } + if (showLogLevelPopup) { if (ke.isUp()) { logLevelListState.selectPrevious(); @@ -113,6 +136,24 @@ 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(); + return true; + } if (ke.isChar('l') && !ctx.isInfraSelected()) { showLogLevelPopup = true; logLevelListState.select(2); @@ -160,12 +201,61 @@ 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; + return true; + } if (showLogLevelPopup) { showLogLevelPopup = false; return true; } + if (findTerm != null) { + findTerm = null; + findPattern = null; + findMatches = Collections.emptyList(); + findMatchIndex = -1; + return true; + } return false; } @@ -239,13 +329,14 @@ class LogTab implements MonitorTab { int hSkip = wordWrap ? 0 : hScroll; - if (entries != cachedLogEntries || hSkip != cachedLogHSkip) { + boolean entriesChanged = entries != cachedLogEntries; + if (entriesChanged || hSkip != cachedLogHSkip) { cachedLogEntries = entries; cachedLogHSkip = hSkip; List<Line> built = new ArrayList<>(entries.size()); int maxW = 0; - for (LogEntry entry : entries) { - String raw = entry.raw != null ? entry.raw : ""; + for (int i = 0; i < entries.size(); i++) { + String raw = entries.get(i).raw != null ? entries.get(i).raw : ""; if (!wordWrap) { maxW = Math.max(maxW, CharWidth.of(TuiHelper.stripAnsi(raw))); } @@ -260,10 +351,25 @@ class LogTab implements MonitorTab { hScroll = Math.min(hScroll, Math.max(0, cachedLogMaxWidth - visibleWidth)); } + if (findPattern != null && entriesChanged) { + buildFindMatches(); + } + 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; + List<Line> highlighted = new ArrayList<>(visibleLines.size()); + for (int i = 0; i < visibleLines.size(); i++) { + highlighted.add(applyHighlights(visibleLines.get(i), start + i, currentMatchLine)); + } + visibleLines = highlighted; + } + List<Rect> hChunks = Layout.horizontal() .constraints(Constraint.fill(), Constraint.length(1)) .split(inner); @@ -289,20 +395,43 @@ 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"); + return; + } if (showLogLevelPopup) { hint(spans, "↑↓", "navigate"); hint(spans, "Enter", "set level"); hintLast(spans, "Esc", "cancel"); return; } - hint(spans, "Esc", "back"); + + 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 + "] ")); + } else { + hint(spans, "Esc", "back"); + } hint(spans, "↑↓", "scroll"); - hint(spans, "PgUp/PgDn", "page"); - hint(spans, "Home/End", "top/end"); + hint(spans, "/", "find"); + hint(spans, "h", "highlight" + (highlightTerm != null ? " [" + highlightTerm + "]" : "")); hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]")); - if (!wordWrap) { - hint(spans, "←→", "h-scroll"); - } if (!ctx.isInfraSelected()) { hint(spans, "l", "level"); } @@ -347,6 +476,130 @@ 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; + } + + 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); + } + void readNewLogLines(String pid, List<String> newLines) { readNewLogLinesFromFile(pid, pid + ".log", newLines); } @@ -466,6 +719,23 @@ class LogTab implements MonitorTab { Filtering is useful when the log is noisy with INFO messages and you want to focus on warnings and errors. + ## Find and Highlight + + **Find** (`/`) — search for text in the log. Type a search term and + press Enter to jump to the first match. Use `n` to go to the next + match and `N` for the previous match. The current match is shown + with a green background, other matches with yellow. Press `Esc` + to clear the search. + + **Highlight** (`h`) — persistently highlight all occurrences of a + word in the log. Type a word and press Enter — all occurrences + are highlighted with a yellow background while the log continues + scrolling in follow mode. Press `h` again and submit an empty + term to clear the highlight. Both find and highlight can be + active at the same time. + + Both find and highlight are case-insensitive. + ## Thread Names The thread name in square brackets (e.g., `[Camel (camel-demo) thread #2]`) @@ -477,8 +747,14 @@ class LogTab implements MonitorTab { - `Up/Down` — scroll log - `PgUp/PgDn` — scroll by page - `Home/End` — jump to beginning/end of log + - `/` — find (search for text) + - `n` — next match + - `N` — previous match + - `h` — highlight a word - `l` — change log level filter - - `Esc` — back + - `f` — toggle follow mode + - `w` — toggle word wrap + - `Esc` — clear find / back """; } }
