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 aa63c0a4cb94 chore: camel-jbang - mouse clicks on the +/-, f/F and
Space hints, and on the quit and kill confirm dialogs (#28458)
aa63c0a4cb94 is described below
commit aa63c0a4cb945f89be0e59f980622e33123b7911
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 20:45:44 2026 +0100
chore: camel-jbang - mouse clicks on the +/-, f/F and Space hints, and on
the quit and kill confirm dialogs (#28458)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 160 ++-------------
.../dsl/jbang/core/commands/tui/DialogHelper.java | 34 +++-
.../dsl/jbang/core/commands/tui/HintClicks.java | 222 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/PopupManager.java | 36 +++-
.../dsl/jbang/core/commands/tui/SubViewBar.java | 18 +-
.../jbang/core/commands/tui/CamelMonitorTest.java | 46 -----
.../jbang/core/commands/tui/HintClicksTest.java | 142 +++++++++++++
.../jbang/core/commands/tui/PopupManagerTest.java | 73 +++++++
.../jbang/core/commands/tui/SourceSizeTest.java | 2 +-
.../jbang/core/commands/tui/SubViewBarTest.java | 19 ++
10 files changed, 552 insertions(+), 200 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 d81249b92131..2c93309092fa 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
@@ -51,7 +51,6 @@ import dev.tamboui.tui.TuiRunner;
import dev.tamboui.tui.event.Event;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
-import dev.tamboui.tui.event.KeyModifiers;
import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.tui.event.MouseEventKind;
import dev.tamboui.tui.event.PasteEvent;
@@ -267,12 +266,8 @@ public class CamelMonitor extends CamelCommand {
private final DragSplit panelSplit = new DragSplit();
/** The views and view settings of the active tab, on the first row of its
content. */
private final SubViewBar subViewBar = new SubViewBar();
- // Footer key-binding hit-testing: each clickable hint records its
[startX, endX) column range on
- // the footer row and the KeyEvent to synthesize when clicked.
- private int footerRowY = -1;
- private int[] footerRegionStartX = new int[0];
- private int[] footerRegionEndX = new int[0];
- private KeyEvent[] footerRegionKey = new KeyEvent[0];
+ /** Where the key hints of the footer are, so a click on one presses its
key. */
+ private final HintClicks footerClicks = new HintClicks();
private final ClassLoader classLoader;
@@ -1563,12 +1558,17 @@ public class CamelMonitor extends CamelCommand {
}
return result;
}
+ // The confirm dialogs (quit, kill) too: the footer lists their keys
+ if (popupManager.isConfirmVisible() ||
popupManager.isKillConfirmVisible()) {
+ return me.isClick() && handleFooterClick(me, runner)
+ || popupManager.handleMouseEvent(me,
tabRegistry.selectedTabIndex(), TAB_LOG);
+ }
// Tab bar clicks: detect which tab was clicked and switch to it
if (me.isClick() && lastTabsArea != null && lastTabLabels != null) {
int tabsY = lastTabsArea.height() >= 2 ? lastTabsArea.y() + 1 :
lastTabsArea.y();
if (me.y() == tabsY && lastTabsArea.contains(me.x(), me.y())) {
- int clickedTab = findClickedTab(me.x() - lastTabsArea.x());
+ int clickedTab = HintClicks.tabAt(lastTabLabels,
lastTabDivider, me.x() - lastTabsArea.x());
if (clickedTab >= 0) {
if (isInfraSelected()) {
int realTab = clickedTab == 1 ? TAB_LOG : TAB_OVERVIEW;
@@ -1599,7 +1599,7 @@ public class CamelMonitor extends CamelCommand {
view.select().run();
return true;
}
- KeyEvent key = footerKeyEvent(subViewBar.keyAt(me.x(),
me.y()));
+ KeyEvent key = HintClicks.keyEvent(subViewBar.keyAt(me.x(),
me.y()));
if (key != null) {
return handleEvent(key, runner);
}
@@ -1631,142 +1631,10 @@ public class CamelMonitor extends CamelCommand {
return false;
}
- /**
- * Given a click x-offset within the tab bar, determine which tab index
was clicked. Returns -1 if the click falls
- * on a divider or outside tab labels.
- */
- private int findClickedTab(int xOffset) {
- if (lastTabLabels == null || lastTabDivider == null) {
- return -1;
- }
- int dividerW = CharWidth.of(lastTabDivider);
- int x = 0;
- for (int i = 0; i < lastTabLabels.length; i++) {
- if (i > 0) {
- // Skip divider region
- if (xOffset >= x && xOffset < x + dividerW) {
- return -1;
- }
- x += dividerW;
- }
- int tabW = lastTabLabels[i].width();
- if (xOffset >= x && xOffset < x + tabW) {
- return i;
- }
- x += tabW;
- }
- return -1;
- }
-
- /**
- * Records the clickable footer key-binding regions from the final list of
footer spans. A hint is drawn by
- * {@link TuiHelper#hint} as a key span styled with {@link
Theme#hintKey()} immediately followed by its label span,
- * so each key span styled that way (whose token maps to an unambiguous
single key) contributes a clickable region
- * covering both the key and its label. {@code area} is the single footer
row.
- */
- private void captureFooterRegions(Rect area, List<Span> spans) {
- List<int[]> bounds = new ArrayList<>();
- List<KeyEvent> keys = new ArrayList<>();
- Style hintKeyStyle = Theme.hintKey();
- int x = area.x();
- for (int i = 0; i < spans.size(); i++) {
- Span s = spans.get(i);
- int w = s.width();
- if (hintKeyStyle.equals(s.style())) {
- KeyEvent ke = footerKeyEvent(s.content());
- if (ke != null) {
- // Extend the region over the following label span so
clicking the label works too.
- int end = x + w + (i + 1 < spans.size() ? spans.get(i +
1).width() : 0);
- bounds.add(new int[] { x, end });
- keys.add(ke);
- }
- }
- x += w;
- }
- footerRowY = area.y();
- footerRegionStartX = bounds.stream().mapToInt(b -> b[0]).toArray();
- footerRegionEndX = bounds.stream().mapToInt(b -> b[1]).toArray();
- footerRegionKey = keys.toArray(new KeyEvent[0]);
- }
-
- /**
- * Hit-tests a click against the footer key-binding regions captured
during the last render and, when it lands on a
- * hint, feeds the synthesized key back through the normal key path so it
acts exactly like pressing that key.
- */
+ /** A click on a key hint of the footer presses its key, as if it was
typed. */
private boolean handleFooterClick(MouseEvent me, TuiRunner runner) {
- if (footerRowY < 0 || me.y() != footerRowY) {
- return false;
- }
- int idx = footerRegionAt(footerRegionStartX, footerRegionEndX, me.x());
- if (idx < 0) {
- return false;
- }
- return handleEvent(footerRegionKey[idx], runner);
- }
-
- /**
- * Returns the index of the footer hint region whose column range contains
{@code mouseX}, or {@code -1} when the
- * click is outside every region. Each region spans the half-open range
{@code [startX[i], endX[i])}.
- */
- static int footerRegionAt(int[] startX, int[] endX, int mouseX) {
- if (startX == null) {
- return -1;
- }
- for (int i = 0; i < startX.length; i++) {
- if (mouseX >= startX[i] && mouseX < endX[i]) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Maps a footer hint key token (the trimmed content of a {@link
Theme#hintKey()} span) to the {@link KeyEvent} that
- * pressing it would produce, or {@code null} when the token is not an
unambiguous single key. Recognizes the
- * function keys {@code F1}-{@code F12}, {@code Enter}, {@code Esc},
{@code Tab} and any single character; ambiguous
- * multi-key hints such as {@code Up/Down}, {@code PgUp/PgDn} or arrow
glyphs are left non-clickable.
- */
- static KeyEvent footerKeyEvent(String token) {
- if (token == null) {
- return null;
- }
- String t = token.trim();
- if (t.isEmpty()) {
- return null;
- }
- if (t.length() >= 2 && (t.charAt(0) == 'F' || t.charAt(0) == 'f')
- && t.chars().skip(1).allMatch(Character::isDigit)) {
- KeyCode fkey = functionKeyCode(Integer.parseInt(t.substring(1)));
- return fkey != null ? KeyEvent.ofKey(fkey) : null;
- }
- switch (t) {
- case "Enter":
- return KeyEvent.ofKey(KeyCode.ENTER);
- case "Esc":
- return KeyEvent.ofKey(KeyCode.ESCAPE);
- case "Tab":
- return KeyEvent.ofKey(KeyCode.TAB);
- default:
- return t.length() == 1 ? KeyEvent.ofChar(t.charAt(0),
KeyModifiers.NONE) : null;
- }
- }
-
- private static KeyCode functionKeyCode(int n) {
- return switch (n) {
- case 1 -> KeyCode.F1;
- case 2 -> KeyCode.F2;
- case 3 -> KeyCode.F3;
- case 4 -> KeyCode.F4;
- case 5 -> KeyCode.F5;
- case 6 -> KeyCode.F6;
- case 7 -> KeyCode.F7;
- case 8 -> KeyCode.F8;
- case 9 -> KeyCode.F9;
- case 10 -> KeyCode.F10;
- case 11 -> KeyCode.F11;
- case 12 -> KeyCode.F12;
- default -> null;
- };
+ KeyEvent key = footerClicks.keyAt(me.x(), me.y());
+ return key != null && handleEvent(key, runner);
}
private boolean handlePasteEvent(PasteEvent pe) {
@@ -2367,7 +2235,7 @@ public class CamelMonitor extends CamelCommand {
private void renderFooter(Frame frame, Rect area) {
// Disable footer key-binding clicks until the normal path below
re-captures the hint regions.
// Overlay/early-return states (screenshot flash, help, caption) leave
the footer non-clickable.
- footerRowY = -1;
+ footerClicks.clear();
// Show screenshot flash message briefly
String msg = recordingManager.screenshotFlashMessage();
@@ -2506,7 +2374,7 @@ public class CamelMonitor extends CamelCommand {
spans.addAll(rightSpans);
}
- captureFooterRegions(area, spans);
+ footerClicks.capture(area, spans);
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
index 9de34526dd7d..48e0f8ee4d6c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
@@ -104,6 +104,17 @@ final class DialogHelper {
static Rect renderConfirm(
Frame frame, Rect area, String title, String message, boolean
danger,
String acceptKey, String acceptLabel) {
+ return renderConfirm(frame, area, title, message, danger, acceptKey,
acceptLabel, null);
+ }
+
+ /**
+ * As above, recording where the keys of the dialog are drawn in {@code
clicks}, so a click on one presses it.
+ *
+ * @param clicks where the keys go, or null
+ */
+ static Rect renderConfirm(
+ Frame frame, Rect area, String title, String message, boolean
danger,
+ String acceptKey, String acceptLabel, HintClicks clicks) {
Style accent = danger ? Theme.error() : Theme.warning();
String titleText = " " + title + " ";
String msg = message.trim();
@@ -118,6 +129,7 @@ final class DialogHelper {
.build();
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
+ Line hints = TuiHelper.hintLine(acceptKey, acceptLabel, "Esc",
"cancel");
frame.renderWidget(
Paragraph.builder()
.centered()
@@ -125,12 +137,21 @@ final class DialogHelper {
Line.empty(),
Line.from(Span.styled(msg, accent.bold())),
Line.empty(),
- TuiHelper.hintLine(acceptKey, acceptLabel,
"Esc", "cancel")))
+ hints))
.build(),
inner);
+ captureHints(clicks, inner, 3, hints);
return popup;
}
+ /** Records where the centered hint line on the given row of the dialog
is, for its keys to be clicked. */
+ private static void captureHints(HintClicks clicks, Rect inner, int row,
Line hints) {
+ if (clicks != null) {
+ int x = inner.x() + Math.max(0, (inner.width() - hints.width()) /
2);
+ clicks.capture(new Rect(x, inner.y() + row,
Math.min(hints.width(), inner.width()), 1), hints.spans());
+ }
+ }
+
/**
* Renders a confirm dialog with a line that tells more and, when {@code
extraKey} is given, a second choice on its
* own key between Enter and Esc.
@@ -144,6 +165,13 @@ final class DialogHelper {
static Rect renderConfirm(
Frame frame, Rect area, String title, String message, String
detail, String acceptKey, String acceptLabel,
String extraKey, String extraLabel) {
+ return renderConfirm(frame, area, title, message, detail, acceptKey,
acceptLabel, extraKey, extraLabel, null);
+ }
+
+ /** As above, recording where the keys of the dialog are drawn in {@code
clicks} (when not null). */
+ static Rect renderConfirm(
+ Frame frame, Rect area, String title, String message, String
detail, String acceptKey, String acceptLabel,
+ String extraKey, String extraLabel, HintClicks clicks) {
Style accent = Theme.warning();
String titleText = " " + title + " ";
String msg = message.trim();
@@ -169,7 +197,9 @@ final class DialogHelper {
}
lines.add(Line.empty());
lines.add(hints);
-
frame.renderWidget(Paragraph.builder().centered().text(Text.from(lines)).build(),
block.inner(popup));
+ Rect inner = block.inner(popup);
+
frame.renderWidget(Paragraph.builder().centered().text(Text.from(lines)).build(),
inner);
+ captureHints(clicks, inner, lines.size() - 1, hints);
return popup;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HintClicks.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HintClicks.java
new file mode 100644
index 000000000000..8b04c2e49f5b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HintClicks.java
@@ -0,0 +1,222 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.CharWidth;
+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.tui.event.KeyModifiers;
+
+/**
+ * Where the key hints of the footer are, so a click on one presses its key. A
hint is drawn by {@link TuiHelper#hint}
+ * as a key span styled with {@link Theme#hintKey()} followed by its label; a
click on either presses the key. A hint of
+ * two keys ({@code +/-}, {@code f/F}, {@code c/Esc}) presses the key on the
side of the slash that was clicked, and so
+ * does a label naming both ({@code fold/expand all}); a label of one name
({@code zoom}, {@code close}) presses the
+ * first key.
+ */
+final class HintClicks {
+
+ private int row = -1;
+ private int[] startX = new int[0];
+ private int[] endX = new int[0];
+ private KeyEvent[] keys = new KeyEvent[0];
+
+ /** Records the hints of the spans drawn in the one-row {@code area}. */
+ void capture(Rect area, List<Span> spans) {
+ List<int[]> bounds = new ArrayList<>();
+ List<KeyEvent> found = new ArrayList<>();
+ Style hintKeyStyle = Theme.hintKey();
+ int x = area.x();
+ for (int i = 0; i < spans.size(); i++) {
+ Span s = spans.get(i);
+ if (hintKeyStyle.equals(s.style())) {
+ Span label = i + 1 < spans.size() &&
!hintKeyStyle.equals(spans.get(i + 1).style()) ? spans.get(i + 1) : null;
+ addHint(bounds, found, x, s, label);
+ }
+ x += s.width();
+ }
+ row = area.y();
+ startX = bounds.stream().mapToInt(b -> b[0]).toArray();
+ endX = bounds.stream().mapToInt(b -> b[1]).toArray();
+ keys = found.toArray(new KeyEvent[0]);
+ }
+
+ private static void addHint(List<int[]> bounds, List<KeyEvent> found, int
x, Span key, Span label) {
+ int keyEnd = x + key.width();
+ int labelEnd = keyEnd + (label != null ? label.width() : 0);
+ KeyEvent single = keyEvent(key.content());
+ if (single != null) {
+ bounds.add(new int[] { x, labelEnd });
+ found.add(single);
+ return;
+ }
+ KeyEvent[] pair = keyPair(key.content());
+ if (pair == null) {
+ return;
+ }
+ // the slash itself presses neither key
+ int slash = x + CharWidth.of(key.content().substring(0,
key.content().indexOf('/')));
+ bounds.add(new int[] { x, slash });
+ found.add(pair[0]);
+ bounds.add(new int[] { slash + 1, keyEnd });
+ found.add(pair[1]);
+ if (label == null) {
+ return;
+ }
+ int labelSlash = label.content().indexOf('/');
+ if (labelSlash < 0) {
+ bounds.add(new int[] { keyEnd, labelEnd });
+ found.add(pair[0]);
+ } else {
+ int split = keyEnd + CharWidth.of(label.content().substring(0,
labelSlash));
+ bounds.add(new int[] { keyEnd, split });
+ found.add(pair[0]);
+ bounds.add(new int[] { split + 1, labelEnd });
+ found.add(pair[1]);
+ }
+ }
+
+ /** Forgets the hints: the footer drawn now has none to click (a flash
message, the help). */
+ void clear() {
+ row = -1;
+ }
+
+ /** The key of the hint under a click, or null. */
+ KeyEvent keyAt(int x, int y) {
+ if (row < 0 || y != row) {
+ return null;
+ }
+ int idx = regionAt(startX, endX, x);
+ return idx >= 0 ? keys[idx] : null;
+ }
+
+ /**
+ * Returns the index of the region whose column range contains {@code
mouseX}, or {@code -1} when the click is
+ * outside every region. Each region spans the half-open range {@code
[startX[i], endX[i])}.
+ */
+ static int regionAt(int[] startX, int[] endX, int mouseX) {
+ if (startX == null) {
+ return -1;
+ }
+ for (int i = 0; i < startX.length; i++) {
+ if (mouseX >= startX[i] && mouseX < endX[i]) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * The tab of the tab bar under a click, or -1 on a divider or past the
last tab.
+ *
+ * @param labels the labels of the tabs, as drawn
+ * @param divider what is drawn between two tabs
+ * @param xOffset the column of the click, from the start of the bar
+ */
+ static int tabAt(Line[] labels, String divider, int xOffset) {
+ if (labels == null || divider == null) {
+ return -1;
+ }
+ int dividerW = CharWidth.of(divider);
+ int x = 0;
+ for (int i = 0; i < labels.length; i++) {
+ if (i > 0) {
+ if (xOffset >= x && xOffset < x + dividerW) {
+ return -1;
+ }
+ x += dividerW;
+ }
+ int tabW = labels[i].width();
+ if (xOffset >= x && xOffset < x + tabW) {
+ return i;
+ }
+ x += tabW;
+ }
+ return -1;
+ }
+
+ /**
+ * Maps a hint key token (the trimmed content of a {@link Theme#hintKey()}
span) to the {@link KeyEvent} that
+ * pressing it would produce, or {@code null} when the token is not one
key. Recognizes the function keys
+ * {@code F1}-{@code F12}, {@code Enter}, {@code Esc}, {@code Tab}, {@code
Space} and any single character; hints
+ * such as {@code Up/Down} or arrow glyphs are not one key.
+ */
+ static KeyEvent keyEvent(String token) {
+ if (token == null) {
+ return null;
+ }
+ String t = token.trim();
+ if (t.isEmpty()) {
+ return null;
+ }
+ if (t.length() >= 2 && (t.charAt(0) == 'F' || t.charAt(0) == 'f')
+ && t.chars().skip(1).allMatch(Character::isDigit)) {
+ KeyCode fkey = functionKeyCode(Integer.parseInt(t.substring(1)));
+ return fkey != null ? KeyEvent.ofKey(fkey) : null;
+ }
+ return switch (t) {
+ case "Enter" -> KeyEvent.ofKey(KeyCode.ENTER);
+ case "Esc" -> KeyEvent.ofKey(KeyCode.ESCAPE);
+ case "Tab" -> KeyEvent.ofKey(KeyCode.TAB);
+ case "Space" -> KeyEvent.ofChar(' ', KeyModifiers.NONE);
+ default -> t.length() == 1 ? KeyEvent.ofChar(t.charAt(0),
KeyModifiers.NONE) : null;
+ };
+ }
+
+ /**
+ * The two keys of a hint such as {@code +/-}, {@code f/F} or {@code
Esc/c}, or null when the token is not two keys
+ * around one slash.
+ */
+ static KeyEvent[] keyPair(String token) {
+ if (token == null) {
+ return null;
+ }
+ String t = token.trim();
+ int slash = t.indexOf('/');
+ if (slash <= 0 || slash != t.lastIndexOf('/')) {
+ return null;
+ }
+ KeyEvent first = keyEvent(t.substring(0, slash));
+ KeyEvent second = keyEvent(t.substring(slash + 1));
+ return first != null && second != null ? new KeyEvent[] { first,
second } : null;
+ }
+
+ private static KeyCode functionKeyCode(int n) {
+ return switch (n) {
+ case 1 -> KeyCode.F1;
+ case 2 -> KeyCode.F2;
+ case 3 -> KeyCode.F3;
+ case 4 -> KeyCode.F4;
+ case 5 -> KeyCode.F5;
+ case 6 -> KeyCode.F6;
+ case 7 -> KeyCode.F7;
+ case 8 -> KeyCode.F8;
+ case 9 -> KeyCode.F9;
+ case 10 -> KeyCode.F10;
+ case 11 -> KeyCode.F11;
+ case 12 -> KeyCode.F12;
+ default -> null;
+ };
+ }
+}
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 e298d86b8522..daa072555132 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
@@ -97,6 +97,9 @@ class PopupManager {
// Last rendered popup rects for mouse hit-testing
private Rect lastMorePopupRect;
+ /** Where the confirm dialog shown (quit, kill) and its keys were drawn,
for mouse clicks. */
+ private Rect lastConfirmRect;
+ private final HintClicks confirmClicks = new HintClicks();
private Rect lastSwitchPopupRect;
PopupManager(MonitorContext ctx, Supplier<List<IntegrationInfo>>
nonVanishingIntegrationsSupplier,
@@ -493,6 +496,9 @@ class PopupManager {
// ---- Mouse handling ----
boolean handleMouseEvent(MouseEvent me, int selectedTab, int tabLog) {
+ if (showKillConfirm || showConfirm) {
+ return handleConfirmMouse(me);
+ }
if (showMorePopup) {
return handleMorePopupMouse(me);
}
@@ -502,6 +508,28 @@ class PopupManager {
return false;
}
+ /**
+ * The confirm dialogs are modal: a click on one of their keys presses it,
a click outside cancels, and nothing
+ * reaches the screen under them.
+ */
+ private boolean handleConfirmMouse(MouseEvent me) {
+ if (!me.isClick()) {
+ return true;
+ }
+ KeyEvent key = confirmClicks.keyAt(me.x(), me.y());
+ if (key == null && lastConfirmRect != null &&
!lastConfirmRect.contains(me.x(), me.y())) {
+ key = KeyEvent.ofKey(KeyCode.ESCAPE);
+ }
+ if (key != null) {
+ if (showKillConfirm) {
+ handleKillConfirmKeys(key);
+ } else {
+ handleConfirmKeys(key);
+ }
+ }
+ return true;
+ }
+
private boolean handleMorePopupMouse(MouseEvent me) {
if (lastMorePopupRect == null) {
return false;
@@ -748,17 +776,17 @@ class PopupManager {
void renderKillConfirm(Frame frame, Rect area) {
String msg = "Kill " + ctx.selectedName() + " (PID: " +
ctx.selectedPid + ")?";
- DialogHelper.renderConfirm(frame, area, "Confirm Kill", msg, true);
+ lastConfirmRect = DialogHelper.renderConfirm(frame, area, "Confirm
Kill", msg, true, "Enter", "confirm", confirmClicks);
}
void renderConfirm(Frame frame, Rect area) {
String msg = confirmMessage != null ? confirmMessage : "";
String title = confirmTitle != null ? confirmTitle : "Confirm";
if (confirmDetail != null || confirmExtraCallback != null) {
- DialogHelper.renderConfirm(frame, area, title, msg, confirmDetail,
- confirmExtraCallback != null ?
String.valueOf(confirmExtraKey) : null, confirmExtraLabel);
+ lastConfirmRect = DialogHelper.renderConfirm(frame, area, title,
msg, confirmDetail, "Enter", "confirm",
+ confirmExtraCallback != null ?
String.valueOf(confirmExtraKey) : null, confirmExtraLabel, confirmClicks);
} else {
- DialogHelper.renderConfirm(frame, area, title, msg, false);
+ lastConfirmRect = DialogHelper.renderConfirm(frame, area, title,
msg, false, "Enter", "confirm", confirmClicks);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
index 7b6e01d4b5e3..9f8706dbd5b6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
@@ -152,12 +152,28 @@ final class SubViewBar {
spans.add(Span.raw(" ".repeat(area.width() - used - rightWidth)));
spans.addAll(right);
for (int i = 0; i < rightBoxes.size(); i++) {
- hits.add(new Hit(start + rightBoxes.get(i)[0], start +
rightBoxes.get(i)[1], null, rightKeys.get(i)));
+ addSettingHits(start + rightBoxes.get(i)[0], start +
rightBoxes.get(i)[1], rightKeys.get(i));
}
}
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(spans))).build(),
area);
}
+ /**
+ * A setting presses its key wherever it is clicked. A setting of two keys
({@code +/-} zoom) presses the key on the
+ * side of the slash that was clicked, and its label (the mode, such as
{@code compact 3/4}) presses neither.
+ */
+ private void addSettingHits(int from, int to, String key) {
+ int slash = key.indexOf('/');
+ if (slash < 0 || HintClicks.keyPair(key) == null) {
+ hits.add(new Hit(from, to, null, key));
+ return;
+ }
+ // the chip is the key between two spaces
+ int split = from + 1 + CharWidth.of(key.substring(0, slash));
+ hits.add(new Hit(from, split, null, key.substring(0, slash)));
+ hits.add(new Hit(split + 1, from + CharWidth.of(key) + 2, null,
key.substring(slash + 1)));
+ }
+
/** The view under a click on the bar, or null. */
View viewAt(int x, int y) {
Hit h = hitAt(x, y);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
index 10cb9f58b3f3..feac8ca43982 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
@@ -32,7 +32,6 @@ import picocli.CommandLine;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -88,51 +87,6 @@ class CamelMonitorTest {
// (F6 first, then F3, F2). The first F-key pair must survive: F1 (help)
when present, so the
// user can always reach help, even on a narrow terminal.
- // Footer key bindings are clickable only when the hint token maps to an
unambiguous single key.
- // footerKeyEvent parses the token; footerRegionAt maps a click x back to
the region under it.
-
- @Test
- void footerKeyEventParsesFunctionKeys() {
- assertTrue(CamelMonitor.footerKeyEvent("F1").isKey(KeyCode.F1), "F1
maps to the F1 key");
- assertTrue(CamelMonitor.footerKeyEvent(" F5 ").isKey(KeyCode.F5),
"surrounding spaces are trimmed");
- assertTrue(CamelMonitor.footerKeyEvent("F12").isKey(KeyCode.F12), "F12
is the highest function key");
- }
-
- @Test
- void footerKeyEventParsesNamedAndSingleCharKeys() {
- assertTrue(CamelMonitor.footerKeyEvent("Enter").isKey(KeyCode.ENTER),
"Enter maps to the Enter key");
- assertTrue(CamelMonitor.footerKeyEvent("Esc").isKey(KeyCode.ESCAPE),
"Esc maps to the Escape key");
- assertTrue(CamelMonitor.footerKeyEvent("Tab").isKey(KeyCode.TAB), "Tab
maps to the Tab key");
- assertTrue(CamelMonitor.footerKeyEvent("d").isChar('d'), "a single
letter maps to that character");
- assertTrue(CamelMonitor.footerKeyEvent("?").isChar('?'), "'?' (help)
maps to that character");
- assertTrue(CamelMonitor.footerKeyEvent("1").isChar('1'), "a digit maps
to that character");
- }
-
- @Test
- void footerKeyEventRejectsAmbiguousAndInvalidTokens() {
- assertNull(CamelMonitor.footerKeyEvent("Up/Down"), "a two-key hint is
not clickable");
- assertNull(CamelMonitor.footerKeyEvent("PgUp/PgDn"), "a paging hint is
not clickable");
- assertNull(CamelMonitor.footerKeyEvent(TuiIcons.HINT_SCROLL), "arrow
glyphs are not a single key");
- assertNull(CamelMonitor.footerKeyEvent("F13"), "there is no F13 key");
- assertNull(CamelMonitor.footerKeyEvent(""), "an empty token is not
clickable");
- assertNull(CamelMonitor.footerKeyEvent(null), "a null token is not
clickable");
- }
-
- @Test
- void footerRegionAtResolvesClicksToTheOwningRegion() {
- // Two half-open regions [0, 5) and [10, 18) with a gap between them.
- int[] startX = { 0, 10 };
- int[] endX = { 5, 18 };
- assertEquals(0, CamelMonitor.footerRegionAt(startX, endX, 0), "left
edge of the first region");
- assertEquals(0, CamelMonitor.footerRegionAt(startX, endX, 4), "last
cell of the first region");
- assertEquals(-1, CamelMonitor.footerRegionAt(startX, endX, 5), "column
5 is past the first region (exclusive end)");
- assertEquals(-1, CamelMonitor.footerRegionAt(startX, endX, 7), "column
7 is in the gap between regions");
- assertEquals(1, CamelMonitor.footerRegionAt(startX, endX, 10), "left
edge of the second region");
- assertEquals(1, CamelMonitor.footerRegionAt(startX, endX, 17), "last
cell of the second region");
- assertEquals(-1, CamelMonitor.footerRegionAt(startX, endX, 18),
"column 18 is past the second region");
- assertEquals(-1, CamelMonitor.footerRegionAt(null, endX, 3), "no
captured geometry yet");
- }
-
// Theme.applyBaseColors patches unset terminal cells with the active
theme canvas colors while
// preserving widget-specific foreground and background styles.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HintClicksTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HintClicksTest.java
new file mode 100644
index 000000000000..4d8d19988901
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HintClicksTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HintClicksTest {
+
+ @BeforeEach
+ void resetTheme() {
+ Theme.resetForTesting();
+ Theme.setMode("dark");
+ }
+
+ @Test
+ void keyEventParsesFunctionKeys() {
+ assertThat(HintClicks.keyEvent("F1").isKey(KeyCode.F1)).isTrue();
+ assertThat(HintClicks.keyEvent(" F5 ").isKey(KeyCode.F5)).as("spaces
around are trimmed").isTrue();
+ assertThat(HintClicks.keyEvent("F12").isKey(KeyCode.F12)).isTrue();
+ }
+
+ @Test
+ void keyEventParsesNamedAndSingleCharKeys() {
+ assertThat(HintClicks.keyEvent("Enter").isKey(KeyCode.ENTER)).isTrue();
+ assertThat(HintClicks.keyEvent("Esc").isKey(KeyCode.ESCAPE)).isTrue();
+ assertThat(HintClicks.keyEvent("Tab").isKey(KeyCode.TAB)).isTrue();
+ assertThat(HintClicks.keyEvent("Space").isChar(' ')).isTrue();
+ assertThat(HintClicks.keyEvent("d").isChar('d')).isTrue();
+ assertThat(HintClicks.keyEvent("?").isChar('?')).isTrue();
+ assertThat(HintClicks.keyEvent("+").isChar('+')).isTrue();
+ assertThat(HintClicks.keyEvent("F").isChar('F')).as("F alone is the
letter").isTrue();
+ }
+
+ @Test
+ void keyEventRejectsWhatIsNotOneKey() {
+ assertThat(HintClicks.keyEvent("+/-")).isNull();
+ assertThat(HintClicks.keyEvent("Up/Down")).isNull();
+ assertThat(HintClicks.keyEvent(TuiIcons.HINT_SCROLL)).isNull();
+ assertThat(HintClicks.keyEvent("F13")).isNull();
+ assertThat(HintClicks.keyEvent("")).isNull();
+ assertThat(HintClicks.keyEvent(null)).isNull();
+ }
+
+ @Test
+ void keyPairSplitsTwoKeysAroundASlash() {
+ KeyEvent[] zoom = HintClicks.keyPair(" +/- ");
+ assertThat(zoom[0].isChar('+')).isTrue();
+ assertThat(zoom[1].isChar('-')).isTrue();
+ KeyEvent[] fold = HintClicks.keyPair("f/F");
+ assertThat(fold[0].isChar('f')).isTrue();
+ assertThat(fold[1].isChar('F')).isTrue();
+ KeyEvent[] close = HintClicks.keyPair("Esc/c");
+ assertThat(close[0].isKey(KeyCode.ESCAPE)).isTrue();
+ assertThat(close[1].isChar('c')).isTrue();
+
+ assertThat(HintClicks.keyPair("Up/Down")).as("Up and Down are not keys
of a hint").isNull();
+ assertThat(HintClicks.keyPair("/")).isNull();
+ assertThat(HintClicks.keyPair("a/b/c")).isNull();
+ assertThat(HintClicks.keyPair(null)).isNull();
+ }
+
+ @Test
+ void regionAtResolvesClicksToTheOwningRegion() {
+ // two half-open regions [0, 5) and [10, 18) with a gap between them
+ int[] startX = { 0, 10 };
+ int[] endX = { 5, 18 };
+ assertThat(HintClicks.regionAt(startX, endX, 0)).isZero();
+ assertThat(HintClicks.regionAt(startX, endX, 4)).isZero();
+ assertThat(HintClicks.regionAt(startX, endX, 5)).as("the end is
exclusive").isEqualTo(-1);
+ assertThat(HintClicks.regionAt(startX, endX, 7)).as("the
gap").isEqualTo(-1);
+ assertThat(HintClicks.regionAt(startX, endX, 10)).isEqualTo(1);
+ assertThat(HintClicks.regionAt(startX, endX, 17)).isEqualTo(1);
+ assertThat(HintClicks.regionAt(startX, endX, 18)).isEqualTo(-1);
+ assertThat(HintClicks.regionAt(null, endX, 3)).as("nothing captured
yet").isEqualTo(-1);
+ }
+
+ @Test
+ void aClickOnTheDiagramFooterPressesTheKeyUnderIt() {
+ List<Span> spans = new ArrayList<>();
+ TuiHelper.hint(spans, "Esc", "back");
+ TuiHelper.hint(spans, "+/-", "zoom");
+ TuiHelper.hint(spans, "Space", "fold");
+ TuiHelper.hint(spans, "f/F", "fold/expand all");
+ TuiHelper.hint(spans, "Up/Down", "move");
+ String line = spans.stream().map(Span::content).reduce("",
String::concat);
+ HintClicks clicks = new HintClicks();
+ clicks.capture(new Rect(0, 9, line.length(), 1), spans);
+
+ assertThat(clicks.keyAt(line.indexOf("back"),
9).isKey(KeyCode.ESCAPE)).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("+/-"), 9).isChar('+')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("+/-") + 1, 9)).as("the
slash").isNull();
+ assertThat(clicks.keyAt(line.indexOf("+/-") + 2,
9).isChar('-')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("zoom"), 9).isChar('+')).as("a
one-name label: the first key").isTrue();
+ assertThat(clicks.keyAt(line.indexOf("Space"), 9).isChar('
')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf(" fold "), 9).isChar('
')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("f/F"), 9).isChar('f')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("f/F") + 2,
9).isChar('F')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("fold/expand"),
9).isChar('f')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("expand all"),
9).isChar('F')).isTrue();
+ assertThat(clicks.keyAt(line.indexOf("Up/Down"), 9)).as("not keys of a
hint").isNull();
+ assertThat(clicks.keyAt(line.indexOf("back"), 8)).as("another
row").isNull();
+
+ clicks.clear();
+ assertThat(clicks.keyAt(line.indexOf("back"), 9)).as("a footer without
hints").isNull();
+ }
+
+ @Test
+ void tabAtFindsTheTabUnderAClick() {
+ Line[] labels = { Line.from("1 Overview"), Line.from("2 Log") };
+ assertThat(HintClicks.tabAt(labels, " | ", 0)).isZero();
+ assertThat(HintClicks.tabAt(labels, " | ", 9)).isZero();
+ assertThat(HintClicks.tabAt(labels, " | ", 11)).as("the
divider").isEqualTo(-1);
+ assertThat(HintClicks.tabAt(labels, " | ", 13)).isEqualTo(1);
+ assertThat(HintClicks.tabAt(labels, " | ", 18)).as("past the last
tab").isEqualTo(-1);
+ assertThat(HintClicks.tabAt(null, " | ", 0)).isEqualTo(-1);
+ }
+}
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 489e8f61f667..720dc3d14d25 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
@@ -264,4 +264,77 @@ class PopupManagerTest {
popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE,
KeyModifiers.NONE), 0, 2);
assertFalse(popupManager.isKillConfirmVisible(), "Esc cancels the kill
confirm");
}
+
+ // The confirm dialogs are modal: a click on one of their keys presses it,
a click outside cancels, and no click
+ // reaches the screen under them.
+
+ @Test
+ void aClickOnAKeyOfTheConfirmDialogPressesIt() {
+ List<String> chosen = new ArrayList<>();
+ popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", "1
integration was started here",
+ () -> chosen.add("quit"), 's', "stop and quit", () ->
chosen.add("stop"));
+ String[] rows = renderConfirm();
+ int row = rowWith(rows, "stop and quit");
+
+ assertTrue(popupManager.handleMouseEvent(click(rows[row].indexOf("stop
and quit"), row), 0, 2));
+ assertEquals(List.of("stop"), chosen, "the second choice");
+ assertFalse(popupManager.isConfirmVisible());
+
+ popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () ->
chosen.add("quit"));
+ rows = renderConfirm();
+ row = rowWith(rows, "confirm");
+ popupManager.handleMouseEvent(click(rows[row].indexOf("Enter"), row),
0, 2);
+ assertEquals(List.of("stop", "quit"), chosen, "Enter, clicked");
+ assertFalse(popupManager.isConfirmVisible());
+ }
+
+ @Test
+ void aClickOutsideTheConfirmDialogCancelsIt() {
+ List<String> chosen = new ArrayList<>();
+ popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () ->
chosen.add("quit"), () -> chosen.add("cancel"));
+ String[] rows = renderConfirm();
+ int row = rowWith(rows, "Quit the TUI?");
+
+ assertTrue(popupManager.handleMouseEvent(click(rows[row].indexOf("Quit
the TUI?"), row), 0, 2),
+ "a click inside the dialog stays there");
+ assertTrue(popupManager.isConfirmVisible(), "the question is not a
key");
+ assertTrue(chosen.isEmpty());
+
+ assertTrue(popupManager.handleMouseEvent(click(1, 1), 0, 2), "the
screen under the dialog gets nothing");
+ assertEquals(List.of("cancel"), chosen);
+ assertFalse(popupManager.isConfirmVisible());
+ }
+
+ @Test
+ void aClickOnCancelClosesTheKillConfirm() {
+ popupManager.showKillConfirm();
+ Rect area = new Rect(0, 0, 100, 30);
+ Buffer buffer = Buffer.empty(area);
+ popupManager.renderKillConfirm(Frame.forTesting(buffer), area);
+ String[] rows = TuiTestHelper.bufferToString(buffer).split("\n");
+ int row = rowWith(rows, "cancel");
+
+ popupManager.handleMouseEvent(click(rows[row].indexOf("cancel"), row),
0, 2);
+ assertFalse(popupManager.isKillConfirmVisible());
+ }
+
+ private String[] renderConfirm() {
+ Rect area = new Rect(0, 0, 100, 30);
+ Buffer buffer = Buffer.empty(area);
+ popupManager.renderConfirm(Frame.forTesting(buffer), area);
+ return TuiTestHelper.bufferToString(buffer).split("\n");
+ }
+
+ private static int rowWith(String[] rows, String text) {
+ for (int i = 0; i < rows.length; i++) {
+ if (rows[i].contains(text)) {
+ return i;
+ }
+ }
+ throw new AssertionError(text + " not drawn: " + String.join("\n",
rows));
+ }
+
+ private static MouseEvent click(int x, int y) {
+ return MouseEvent.press(MouseButton.LEFT, x, y);
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index cd486eb9b083..fe089477f801 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -59,7 +59,7 @@ class SourceSizeTest {
AiPanel.java 3180
AiPanel.runAgentLoop 159
AiUsageStats.render 166
- CamelMonitor.java 2756
+ CamelMonitor.java 2624
DiagramSupport.java 2360
DiagramSupport.loadHighlightedNativeDiagramInBackground 200
EndpointsTab.renderContent 170
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
index 25f9eb92b933..5e67546c8c89 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
@@ -83,6 +83,25 @@ class SubViewBarTest {
assertThat(line).contains(" m ●");
}
+ @Test
+ void aSettingOfTwoKeysPressesTheKeyOnTheClickedSide() {
+ SubViewBar bar = new SubViewBar();
+ Spec spec = new Spec(
+ null, List.of(),
+ List.of(new Toggle("+/-", "", "compact 2/3"), new Toggle("m",
"metrics", "on")),
+ true);
+ String line = render(bar, spec, 2);
+ int chip = line.indexOf(" +/- ");
+
+ assertThat(bar.keyAt(chip, 2)).isEqualTo("+");
+ assertThat(bar.keyAt(chip + 1, 2)).isEqualTo("+");
+ assertThat(bar.keyAt(chip + 2, 2)).as("the slash").isNull();
+ assertThat(bar.keyAt(chip + 3, 2)).isEqualTo("-");
+ assertThat(bar.keyAt(chip + 4, 2)).isEqualTo("-");
+ assertThat(bar.keyAt(line.indexOf("compact"), 2)).as("the mode is not
a key").isNull();
+ assertThat(bar.keyAt(line.indexOf("metrics"), 2)).isEqualTo("m");
+ }
+
@Test
void settingsThatDoNotFitEndInAnEllipsis() {
SubViewBar bar = new SubViewBar();