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();

Reply via email to