This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new cdcac334d395 CAMEL-24259: camel-jbang - TUI More popup use natural
mnemonic keys and cycle duplicates
cdcac334d395 is described below
commit cdcac334d39511bd33221138501a2f65bae48ee6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 22:17:56 2026 +0200
CAMEL-24259: camel-jbang - TUI More popup use natural mnemonic keys and
cycle duplicates
Rework More popup mnemonics to use natural first-letter keys instead of
forced unique letters. Duplicate mnemonics now cycle on repeated key
press and require Enter to confirm selection. Rename Browse to Browse
Endpoints, Heap Histogram to Heap Memory Histogram, Route Organizer to
Route Controller. Move Circuit Breaker from Routing to Observability group.
CAMEL-24259: camel-jbang - TUI Memory tab render time axis inside chart
border
Move the x-axis time labels (now, -30s, -1m, etc.) inside the block
border by reserving 1 row from the chart inner area. Previously the
axis rendered below the block border, visually overlapping with the
panel divider.
CAMEL-24259: camel-jbang - TUI rename Memory to Memory Usage in More popup
CAMEL-24259: camel-jbang - TUI show circuit breaker OPEN badge in More popup
When circuit breakers are in OPEN state, the More popup now shows a red
badge (e.g. "2 OPEN") next to the Circuit Breaker entry, matching the
badge already shown on the More tab itself.
CAMEL-24259: camel-jbang - TUI Source tab focus borders, word wrap scroll
fix, SqlTrace spacer
- Add focus-highlighted border to Source tab panels (file list, info,
viewer)
matching the SQL Query tab pattern
- Fix word wrap scroll-out-of-panel bug where cursor could scroll past the
visible area when word wrap was enabled on files with long lines
- Remove empty spacer line between SQL Trace tab panels
CAMEL-24259: camel-jbang - TUI Source viewer scroll improvements
- Reserve bottom row for horizontal scrollbar so cursor does not overlap
- Increase horizontal scroll step from 1 to 8 characters per key press
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/MemoryTab.java | 27 ++++-----
.../dsl/jbang/core/commands/tui/PopupManager.java | 51 ++++++++++++----
.../dsl/jbang/core/commands/tui/SourceTab.java | 8 +++
.../dsl/jbang/core/commands/tui/SourceViewer.java | 68 ++++++++++++++++------
.../dsl/jbang/core/commands/tui/SqlTraceTab.java | 4 +-
.../dsl/jbang/core/commands/tui/TabRegistry.java | 37 ++++++------
.../jbang/core/commands/tui/PopupManagerTest.java | 19 ++++--
.../jbang/core/commands/tui/TabRegistryTest.java | 21 +++----
8 files changed, 156 insertions(+), 79 deletions(-)
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 f100067731c6..63227d41e9d8 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
@@ -114,12 +114,7 @@ class MemoryTab extends AbstractTab {
renderStats(frame, chunks.get(0), info);
- List<Rect> vChunks = Layout.vertical()
- .constraints(Constraint.fill(), Constraint.length(1))
- .split(chunks.get(1));
-
- renderSparkline(frame, vChunks.get(0), info);
- renderTimeAxis(frame, vChunks.get(1), info);
+ renderSparkline(frame, chunks.get(1), info);
}
private void renderStats(Frame frame, Rect area, IntegrationInfo info) {
@@ -283,7 +278,10 @@ class MemoryTab extends AbstractTab {
}
int chartW = inner.width();
- int chartH = inner.height();
+ int chartH = inner.height() - 1;
+ if (chartH < 1) {
+ return;
+ }
// Build data array right-aligned: latest data on the right
long[] data = new long[chartW];
@@ -326,27 +324,26 @@ class MemoryTab extends AbstractTab {
}
}
}
+
+ // Render time axis on the bottom row of the inner area (inside the
block border)
+ renderTimeAxis(frame, inner, info, chartW);
}
- private void renderTimeAxis(Frame frame, Rect area, IntegrationInfo info) {
+ private void renderTimeAxis(Frame frame, Rect inner, IntegrationInfo info,
int chartW) {
LinkedList<Long> hist = heapMemHistory.get(info.pid);
int points = hist != null ? hist.size() : 0;
- int w = area.width();
- if (w < 10) {
+ if (chartW < 10) {
return;
}
- // The chart area has the same width; each column = 1 data point = 5
seconds
- // chartW columns map to the rightmost `chartW` points of history
- int chartW = w;
int totalPoints = Math.min(points, chartW);
long totalSeconds = totalPoints * 5L;
Buffer buf = frame.buffer();
Style dimStyle = Style.EMPTY.dim();
- int xAxisY = area.y();
- int startX = area.x();
+ int xAxisY = inner.y() + inner.height() - 1;
+ int startX = inner.x();
// "now" label at the right edge
int nowX = startX + chartW - 3;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index 3992063df94f..4d230b6c1497 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -278,10 +278,15 @@ class PopupManager {
}
return true;
}
- int shortcutSel = morePopupShortcut(ke);
+ int[] shortcutResult = morePopupShortcut(ke);
+ int shortcutSel = shortcutResult[0];
+ boolean uniqueShortcut = shortcutResult[1] == 1;
if (shortcutSel >= 0) {
int visualSel = moreToVisualIndex(shortcutSel);
morePopupState.select(visualSel);
+ if (!uniqueShortcut) {
+ return true;
+ }
}
if (ke.isConfirm() || shortcutSel >= 0) {
Integer visualSel = shortcutSel >= 0 ?
moreToVisualIndex(shortcutSel) : morePopupState.selected();
@@ -540,6 +545,17 @@ class PopupManager {
private ListItem[] morePopupItems(Style keyStyle) {
List<TabRegistry.MoreTab> visual = buildMoreVisualList();
ListItem[] items = new ListItem[visual.size()];
+
+ // Compute circuit breaker badge
+ long cbOpenCount = 0;
+ IntegrationInfo sel = ctx.findSelectedIntegration();
+ if (sel != null) {
+ cbOpenCount = sel.circuitBreakers.stream()
+ .filter(cb -> cb.state != null &&
(cb.state.equalsIgnoreCase("open")
+ || cb.state.equalsIgnoreCase("forced_open")))
+ .count();
+ }
+
String currentGroup = null;
for (int i = 0; i < visual.size(); i++) {
TabRegistry.MoreTab tab = visual.get(i);
@@ -558,14 +574,27 @@ class PopupManager {
String name = tab.displayName();
String prefix = TuiIcons.indent(tab.icon());
int keyPos = tab.mnemonicIndex();
+
+ // Append badge for Circuit Breaker when breakers are open
+ String badge = "";
+ Style badgeStyle = null;
+ if ("Circuit Breaker".equals(tab.name()) && cbOpenCount > 0) {
+ badge = " " + cbOpenCount + " OPEN";
+ badgeStyle = Theme.error();
+ }
+
+ List<Span> spans = new ArrayList<>();
if (keyPos >= 0 && keyPos < name.length()) {
- items[i] = ListItem.from(Line.from(
- Span.raw(prefix + name.substring(0, keyPos)),
- Span.styled(String.valueOf(name.charAt(keyPos)),
keyStyle),
- Span.raw(name.substring(keyPos + 1))));
+ spans.add(Span.raw(prefix + name.substring(0, keyPos)));
+ spans.add(Span.styled(String.valueOf(name.charAt(keyPos)),
keyStyle));
+ spans.add(Span.raw(name.substring(keyPos + 1)));
} else {
- items[i] = ListItem.from(prefix + name);
+ spans.add(Span.raw(prefix + name));
}
+ if (!badge.isEmpty()) {
+ spans.add(Span.styled(badge, badgeStyle));
+ }
+ items[i] = ListItem.from(Line.from(spans));
}
}
return items;
@@ -654,7 +683,7 @@ class PopupManager {
inner);
}
- int morePopupShortcut(KeyEvent ke) {
+ int[] morePopupShortcut(KeyEvent ke) {
List<TabRegistry.MoreTab> tabs = moreTabsSupplier.get();
List<Integer> matches = new ArrayList<>();
char pressedUpper = 0;
@@ -674,12 +703,12 @@ class PopupManager {
}
if (matches.isEmpty()) {
- return -1;
+ return new int[] { -1, 0 };
}
if (matches.size() == 1) {
lastShortcutLetter = pressedUpper;
lastShortcutIndex = matches.get(0);
- return matches.get(0);
+ return new int[] { matches.get(0), 1 };
}
// cycle through duplicate mnemonics
@@ -687,12 +716,12 @@ class PopupManager {
int pos = matches.indexOf(lastShortcutIndex);
int next = (pos + 1) % matches.size();
lastShortcutIndex = matches.get(next);
- return matches.get(next);
+ return new int[] { matches.get(next), matches.size() };
}
lastShortcutLetter = pressedUpper;
lastShortcutIndex = matches.get(0);
- return matches.get(0);
+ return new int[] { matches.get(0), matches.size() };
}
List<IntegrationInfo> getNonVanishingIntegrations() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index e71a8d621e58..42a7074c67ba 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -542,6 +542,7 @@ class SourceTab extends AbstractTab {
}
private void renderFileList(Frame frame, Rect area) {
+ Style fileBorderStyle = focusOnViewer ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
if (entries.isEmpty()) {
String noFilesMsg = rootDir == null ? "No source directory found"
: "No files found";
frame.renderWidget(
@@ -549,6 +550,7 @@ class SourceTab extends AbstractTab {
.text(Text.from(Line.from(Span.styled(" " +
noFilesMsg, Style.EMPTY.dim()))))
.block(Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(fileBorderStyle)
.title(Title.from(Line.from(
Span.styled(" Files ",
focusOnViewer ?
Style.EMPTY.fg(Theme.accent()) : Theme.title()))))
@@ -585,6 +587,7 @@ class SourceTab extends AbstractTab {
.scrollMode(ScrollMode.AUTO_SCROLL)
.block(Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(fileBorderStyle)
.title(Title.from(Line.from(Span.styled(panelTitle,
titleStyle))))
.build())
.build();
@@ -641,11 +644,13 @@ class SourceTab extends AbstractTab {
}
}
+ Style infoBorderStyle = focusOnViewer ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
frame.renderWidget(
Paragraph.builder()
.text(Text.from(lines))
.block(Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(infoBorderStyle)
.title(Title.from(Line.from(
Span.styled(" Info ", focusOnViewer ?
Style.EMPTY.fg(Theme.accent()) : Theme.title()))))
.build())
@@ -655,8 +660,10 @@ class SourceTab extends AbstractTab {
private void renderSourcePanel(Frame frame, Rect area) {
Style sourceTitleStyle = focusOnViewer ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+ Style sourceBorderStyle = focusOnViewer ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
if (sourceViewer.isVisible()) {
sourceViewer.setTitleStyle(sourceTitleStyle);
+ sourceViewer.setBorderStyle(sourceBorderStyle);
sourceViewer.setFocused(focusOnViewer);
sourceViewer.render(frame, area);
} else {
@@ -669,6 +676,7 @@ class SourceTab extends AbstractTab {
.text(Text.from(lines))
.block(Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(sourceBorderStyle)
.title(Title.from(Line.from(
Span.styled(" Source ",
sourceTitleStyle))))
.build())
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 cc8aadae919e..679e39ea674b 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
@@ -94,6 +94,7 @@ class SourceViewer {
private boolean quickDocEnabled;
private Map<Integer, List<String>> quickDocEntries =
Collections.emptyMap();
private Style titleStyle;
+ private Style borderStyle;
private boolean focused = true;
private record CachedSource(
@@ -109,6 +110,10 @@ class SourceViewer {
this.titleStyle = style;
}
+ void setBorderStyle(Style style) {
+ this.borderStyle = style;
+ }
+
void setFocused(boolean focused) {
this.focused = focused;
}
@@ -272,9 +277,9 @@ class SourceViewer {
selectedLine = Math.min(lines.size() - 1, selectedLine + page);
}
} else if (!wordWrap && ke.isLeft()) {
- scrollX = Math.max(0, scrollX - 1);
+ scrollX = Math.max(0, scrollX - 8);
} else if (!wordWrap && ke.isRight()) {
- scrollX++;
+ scrollX += 8;
} else if (ke.isHome()) {
selectedLine = 0;
scrollX = 0;
@@ -341,10 +346,13 @@ class SourceViewer {
void render(Frame frame, Rect area) {
if (markdownMode && rawMarkdownContent != null) {
- Block block = Block.builder()
+ Block.Builder bb = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(buildTitle())
- .build();
+ .title(buildTitle());
+ if (borderStyle != null) {
+ bb.borderStyle(borderStyle);
+ }
+ Block block = bb.build();
MarkdownView view = MarkdownView.builder()
.source(rawMarkdownContent)
.scroll(markdownScroll)
@@ -355,10 +363,13 @@ class SourceViewer {
return;
}
- Block block = Block.builder()
+ Block.Builder blockBuilder = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(buildTitle())
- .build();
+ .title(buildTitle());
+ if (borderStyle != null) {
+ blockBuilder.borderStyle(borderStyle);
+ }
+ Block block = blockBuilder.build();
Rect inner = block.inner(area);
lastInnerArea = inner;
frame.renderWidget(block, area);
@@ -368,6 +379,15 @@ class SourceViewer {
}
int visibleLines = inner.height();
+
+ // Reserve bottom row for horizontal scrollbar when content is wider
than viewport
+ if (!wordWrap) {
+ int cursorWidth = 3;
+ int maxLineWidth =
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
+ if (maxLineWidth > inner.width()) {
+ visibleLines = Math.max(1, visibleLines - 1);
+ }
+ }
lastVisibleLines = visibleLines;
// On initial load, position selected line at 2/3 of viewport
@@ -377,12 +397,15 @@ class SourceViewer {
pendingScroll = false;
}
- // Auto-scroll to keep selected line visible (accounting for inline
doc lines)
+ int contentWidth = inner.width() - 1;
+
+ // Auto-scroll to keep selected line visible (accounting for word wrap
and inline doc lines)
if (selectedLine >= 0) {
if (selectedLine < scrollY) {
scrollY = selectedLine;
- } else if (quickDocEnabled && !quickDocEntries.isEmpty()) {
- while (scrollY < selectedLine && countVisualRows(scrollY,
selectedLine) + 1 > visibleLines) {
+ } else if (wordWrap || (quickDocEnabled &&
!quickDocEntries.isEmpty())) {
+ while (scrollY < selectedLine
+ && countVisualRows(scrollY, selectedLine + 1,
contentWidth) > visibleLines) {
scrollY++;
}
} else if (selectedLine >= scrollY + visibleLines) {
@@ -391,14 +414,16 @@ class SourceViewer {
}
int maxScroll;
- if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+ if (wordWrap || (quickDocEnabled && !quickDocEntries.isEmpty())) {
maxScroll = 0;
int visualFromEnd = 0;
for (int i = lines.size() - 1; i >= 0; i--) {
- visualFromEnd++;
- List<String> docs = quickDocEntries.get(i);
- if (docs != null) {
- visualFromEnd += docs.size();
+ visualFromEnd += wrapRowCount(lines.get(i), contentWidth);
+ if (quickDocEnabled) {
+ List<String> docs = quickDocEntries.get(i);
+ if (docs != null) {
+ visualFromEnd += docs.size();
+ }
}
if (visualFromEnd >= visibleLines) {
maxScroll = i;
@@ -1118,10 +1143,10 @@ class SourceViewer {
return result;
}
- private int countVisualRows(int fromLine, int toLine) {
+ private int countVisualRows(int fromLine, int toLine, int contentWidth) {
int count = 0;
for (int i = fromLine; i < toLine && i < lines.size(); i++) {
- count++;
+ count += wrapRowCount(lines.get(i), contentWidth);
if (quickDocEnabled) {
List<String> docs = quickDocEntries.get(i);
if (docs != null) {
@@ -1132,6 +1157,13 @@ class SourceViewer {
return count;
}
+ private int wrapRowCount(String line, int contentWidth) {
+ if (!wordWrap || contentWidth <= 0 || line.length() <= contentWidth) {
+ return 1;
+ }
+ return (line.length() + contentWidth - 1) / contentWidth;
+ }
+
private static String objToString(Object o) {
return o != null ? o.toString() : "";
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
index 3da8e425e771..7e76f77e0750 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
@@ -218,14 +218,14 @@ class SqlTraceTab extends AbstractTableTab {
List<Rect> chunks = showDetail
? Layout.vertical()
- .constraints(Constraint.length(13),
Constraint.length(1), Constraint.fill())
+ .constraints(Constraint.length(13), Constraint.fill())
.split(area)
: List.of(area);
renderTable(frame, chunks.get(0), sorted);
if (showDetail) {
- renderDetail(frame, chunks.get(2), selected);
+ renderDetail(frame, chunks.get(1), selected);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index b88ec9ba9179..a5715381a39e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -166,41 +166,44 @@ class TabRegistry {
// The group field drives divider rendering in the More popup.
moreTabs = List.of(
// Routing
- new MoreTab(TuiIcons.TAB_BROWSE, "Browse", "Bro&wse",
browseTab, "Routing"),
- new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker",
"&Circuit Breaker", circuitBreakerTab, "Routing"),
- new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers",
consumersTab, "Routing"),
+ new MoreTab(TuiIcons.TAB_BROWSE, "Browse Endpoints", "&Browse
Endpoints", browseTab, "Routing"),
+ new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "&Consumers",
consumersTab, "Routing"),
new MoreTab(TuiIcons.TAB_HTTP, "HTTP", "&HTTP", httpTab,
"Routing"),
- new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "In&flight",
inflightTab, "Routing"),
- new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "Prod&ucers",
producersTab, "Routing"),
+ new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "&Inflight",
inflightTab, "Routing"),
+ new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "&Producers",
producersTab, "Routing"),
new MoreTab(
- TuiIcons.TAB_ROUTE_CONTROLLER, "Route Controller",
"Route Organi&zer", routeControllerTab,
+ TuiIcons.TAB_ROUTE_CONTROLLER, "Route Controller",
"&Route Controller", routeControllerTab,
"Routing"),
// Observability
- new MoreTab(TuiIcons.TAB_HEALTH, "Health", "H&ealth",
healthTab, "Observability"),
- new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "Metr&ics",
metricsTab, "Observability"),
+ new MoreTab(
+ TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker",
"&Circuit Breaker", circuitBreakerTab,
+ "Observability"),
+ new MoreTab(TuiIcons.TAB_HEALTH, "Health", "&Health",
healthTab, "Observability"),
+ new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "&Metrics",
metricsTab, "Observability"),
new MoreTab(TuiIcons.TAB_NETWORK, "Network Services",
"&Network Services", networkTab, "Observability"),
- new MoreTab(TuiIcons.TAB_EVENTS, "Events", "E&xchange Events",
eventTab, "Observability"),
+ new MoreTab(TuiIcons.TAB_EVENTS, "Events", "&Exchange Events",
eventTab, "Observability"),
new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans",
spansTab, "Observability"),
// Data
new MoreTab(TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC
DataSource", dataSourceTab, "Data"),
new MoreTab(TuiIcons.TAB_KAFKA, "Kafka", "&Kafka", kafkaTab,
"Data"),
new MoreTab(TuiIcons.TAB_SQL_QUERY, "SQL Query", "S&QL Query",
sqlQueryTab, "Data"),
- new MoreTab(TuiIcons.TAB_SQL_TRACE, "SQL Trace", "SQL T&race",
sqlTraceTab, "Data"),
+ new MoreTab(TuiIcons.TAB_SQL_TRACE, "SQL Trace", "S&QL Trace",
sqlTraceTab, "Data"),
// JVM
- new MoreTab(TuiIcons.TAB_CLASSPATH, "Classpath", "Cl&asspath",
classpathTab, "JVM"),
- new MoreTab(TuiIcons.TAB_HEAP, "Heap Histogram", "&Heap
Histogram", heapHistogramTab, "JVM"),
- new MoreTab(TuiIcons.TAB_MEMORY, "Memory", "&Memory",
memoryTab, "JVM"),
- new MoreTab(TuiIcons.TAB_MEMORY_LEAK, "Memory Leak", "Memor&y
Leak", memoryLeakTab, "JVM"),
+ new MoreTab(TuiIcons.TAB_CLASSPATH, "Classpath", "&Classpath",
classpathTab, "JVM"),
+ new MoreTab(TuiIcons.TAB_HEAP, "Heap Memory Histogram", "Heap
&Memory Histogram", heapHistogramTab, "JVM"),
+ new MoreTab(TuiIcons.TAB_MEMORY, "Memory Usage", "&Memory
Usage", memoryTab, "JVM"),
+ new MoreTab(TuiIcons.TAB_MEMORY_LEAK, "Memory Leak", "&Memory
Leak", memoryLeakTab, "JVM"),
new MoreTab(TuiIcons.TAB_PROCESS, "Process", "&Process",
processTab, "JVM"),
new MoreTab(TuiIcons.TAB_STARTUP, "Startup", "&Startup",
startupTab, "JVM"),
new MoreTab(TuiIcons.TAB_THREADS, "Threads", "&Threads",
threadsTab, "JVM"),
// Project
new MoreTab(TuiIcons.TAB_BEANS, "Beans", "&Beans", beansTab,
"Project"),
- new MoreTab(TuiIcons.TAB_CATALOG, "Catalog", "Cata&log",
catalogTab, "Project"),
- new MoreTab(TuiIcons.TAB_CONFIGURATION, "Configuration",
"Confi&guration", configurationTab, "Project"),
+ new MoreTab(TuiIcons.TAB_CATALOG, "Catalog", "&Catalog",
catalogTab, "Project"),
+ new MoreTab(TuiIcons.TAB_CONFIGURATION, "Configuration",
"&Configuration", configurationTab, "Project"),
new MoreTab(TuiIcons.TAB_CVE_AUDIT, "CVE Audit", "C&VE Audit",
cveAuditTab, "Project"),
new MoreTab(
- TuiIcons.TAB_MAVEN_DEPENDENCIES, "Maven Dependencies",
"Maven &Dependencies", mavenDependenciesTab,
+ TuiIcons.TAB_MAVEN_DEPENDENCIES, "Maven Dependencies",
"&Maven Dependencies",
+ mavenDependenciesTab,
"Project"));
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
index a2ad9984448a..9a8e7c4b9e32 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
@@ -25,6 +25,7 @@ import dev.tamboui.tui.event.KeyModifiers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -74,7 +75,7 @@ class PopupManagerTest {
ctx, () -> List.of(info),
() -> List.of(
new TabRegistry.MoreTab(TuiIcons.TAB_BEANS, "Beans",
"&Beans", null),
- new TabRegistry.MoreTab(TuiIcons.TAB_BROWSE, "Browse",
"Bro&wse", null)),
+ new TabRegistry.MoreTab(TuiIcons.TAB_BROWSE, "Browse",
"&Browse", null)),
new FilesBrowser(), callbacks);
}
@@ -126,10 +127,16 @@ class PopupManagerTest {
@Test
void morePopupShortcutMatchesEitherCase() {
- // 'w'/'W' both select Browse (index 1); Shift+letter must work too
- assertEquals(1, popupManager.morePopupShortcut(KeyEvent.ofChar('w',
KeyModifiers.NONE)));
- assertEquals(1, popupManager.morePopupShortcut(KeyEvent.ofChar('W',
KeyModifiers.NONE)));
- assertEquals(0, popupManager.morePopupShortcut(KeyEvent.ofChar('B',
KeyModifiers.NONE)));
- assertEquals(-1, popupManager.morePopupShortcut(KeyEvent.ofChar('z',
KeyModifiers.NONE)));
+ // Both Beans and Browse use 'B' — pressing 'b'/'B' cycles between
them (matchCount=2)
+ // morePopupShortcut returns int[] {selectedIndex, matchCount}
+ int[] bResult = popupManager.morePopupShortcut(KeyEvent.ofChar('b',
KeyModifiers.NONE));
+ assertEquals(0, bResult[0]);
+ assertEquals(2, bResult[1]);
+ // second press cycles to next match
+ int[] bUpperResult =
popupManager.morePopupShortcut(KeyEvent.ofChar('B', KeyModifiers.NONE));
+ assertEquals(1, bUpperResult[0]);
+ assertEquals(2, bUpperResult[1]);
+ // 'z' matches nothing
+ assertArrayEquals(new int[] { -1, 0 },
popupManager.morePopupShortcut(KeyEvent.ofChar('z', KeyModifiers.NONE)));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 8d6c332b8f4e..6122563554f2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -108,21 +108,22 @@ class TabRegistryTest {
// MORE_SHORTCUTS array carried before the MoreTab refactor. A label
edit that repoints a key must fail here.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
assertEquals(
- List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'N', 'X',
'O', 'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
- 'P', 'S', 'T', 'B', 'L', 'G', 'V', 'D'),
+ List.of('B', 'C', 'H', 'I', 'P', 'R', 'C', 'H', 'M', 'N', 'E',
'O', 'J', 'K', 'Q', 'Q', 'C', 'M', 'M', 'M',
+ 'P', 'S', 'T', 'B', 'C', 'C', 'V', 'M'),
shortcuts, "More tab shortcut letters must match the
historical sequence");
}
@Test
- void moreTabShortcutsHaveAtMostTwoDuplicates() {
+ void moreTabShortcutsHaveReasonableGroupSizes() {
// Duplicate mnemonics are allowed; the popup cycles through them on
repeated key press.
+ // Natural first-letter mnemonics create groups of up to 5 (e.g. C, M)
which is acceptable.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
java.util.Map<Character, Long> counts = shortcuts.stream()
.collect(java.util.stream.Collectors.groupingBy(c -> c,
java.util.stream.Collectors.counting()));
for (var entry : counts.entrySet()) {
- assertTrue(entry.getValue() <= 2,
+ assertTrue(entry.getValue() <= 5,
"More tab shortcut '" + entry.getKey() + "' appears " +
entry.getValue()
- + " times (max 2): " +
shortcuts);
+ + " times (max 5): " +
shortcuts);
}
}
@@ -173,13 +174,13 @@ class TabRegistryTest {
void browseAndConfigurationHighlightTheirShortcutLetter() {
// Regression guard: these two historically underlined the wrong
letter when a hand-maintained index array
// drifted. The '&' marker now pins the highlight to the actual
shortcut.
- TabRegistry.MoreTab browse = moreTabNamed("Browse");
- assertEquals(3, browse.mnemonicIndex(), "Should underline the 'w' in
Bro[w]se");
- assertEquals('W', browse.shortcut());
+ TabRegistry.MoreTab browse = moreTabNamed("Browse Endpoints");
+ assertEquals(0, browse.mnemonicIndex(), "Should underline the 'B' in
[B]rowse Endpoints");
+ assertEquals('B', browse.shortcut());
TabRegistry.MoreTab configuration = moreTabNamed("Configuration");
- assertEquals(5, configuration.mnemonicIndex(), "Should underline the
'g' in Confi[g]uration");
- assertEquals('G', configuration.shortcut());
+ assertEquals(0, configuration.mnemonicIndex(), "Should underline the
'C' in [C]onfiguration");
+ assertEquals('C', configuration.shortcut());
}
@Test