From b25b99be4d0f8351f43cd9103ba820cc784ef6df Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 4 Sep 2026 14:23:58 +0200 Subject: [PATCH] kde/gui: add Acme-style mouse chording to prompt input Implement proper Plan 9/Acme mouse chording in the input area: - B1 = select (TextArea default) - B2 = execute selection as prompt - B3 = plumb selection or context menu - B1+B2 = cut (chord while selecting) - B1+B3 = paste (chord while selecting) Uses MouseArea overlay that detects when B1 is held while B2/B3 is pressed to trigger chord actions. Context menu items now show the chord shortcuts (Cut B1+B2, Paste B1+B3). Placeholder text shows the mouse action reference. --- kde/gui/ChatPane.qml | 85 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/kde/gui/ChatPane.qml b/kde/gui/ChatPane.qml index 3598631..fc3214a 100644 --- a/kde/gui/ChatPane.qml +++ b/kde/gui/ChatPane.qml @@ -307,7 +307,7 @@ Item { ToolTip.text: "Clear input" } - // TextArea in ScrollView for overflow, with context menu overlay + // TextArea in ScrollView for overflow, with mouse chording Item { Layout.fillWidth: true Layout.maximumHeight: 200 @@ -327,7 +327,7 @@ Item { if (!ollie.activeSessionId) return "No session" if (!ollie.activeAgentId) return "No agent" if (ollie.isSessionPaused(ollie.activeSessionId)) return "Session paused" - return "Message... (B2=execute selection, B3=plumb)" + return "B1=sel B2=exec B3=plumb B1+B2=cut B1+B3=paste" } enabled: ollie.daemonConnected && ollie.activeSessionId !== "" && ollie.activeAgentId !== "" && !ollie.isSessionPaused(ollie.activeSessionId) wrapMode: TextEdit.Wrap @@ -350,23 +350,76 @@ Item { event.accepted = true } } + } + } - // Acme-style mouse handling in input - TapHandler { - acceptedButtons: Qt.MiddleButton - onTapped: { - // B2: Execute selection as prompt immediately - var sel = promptInput.selectedText - if (sel && sel.trim() !== "") { - ollie.submit(sel.trim()) - } + // Acme-style mouse chording overlay + // B1 = select (handled by TextArea) + // B2 = execute selection + // B3 = plumb selection / context menu + // B1+B2 = cut (snarf) + // B1+B3 = paste + MouseArea { + id: chordArea + anchors.fill: parent + acceptedButtons: Qt.MiddleButton | Qt.RightButton + propagateComposedEvents: true + cursorShape: Qt.IBeamCursor + + // Track if B1 (left) is currently held for chord detection + property bool leftHeld: false + + // We can't directly detect B1 here since TextArea handles it, + // but we can check mouse.buttons in the handlers + onPressed: function(mouse) { + // Check if this is a chord (B1 already held) + if (mouse.buttons & Qt.LeftButton) { + leftHeld = true + if (mouse.button === Qt.MiddleButton) { + // B1+B2 = cut + promptInput.cut() + mouse.accepted = true + return + } else if (mouse.button === Qt.RightButton) { + // B1+B3 = paste + promptInput.paste() + mouse.accepted = true + return } } + leftHeld = false + mouse.accepted = false // Let it propagate for non-chord + } - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: promptContextMenu.popup() + onClicked: function(mouse) { + // Only handle non-chord clicks + if (leftHeld) { + leftHeld = false + return } + + if (mouse.button === Qt.MiddleButton) { + // B2: Execute selection as prompt + var sel = promptInput.selectedText + if (sel && sel.trim() !== "") { + ollie.submit(sel.trim()) + } else { + // No selection - paste from clipboard and execute? + // Or just ignore. For now, ignore. + } + } else if (mouse.button === Qt.RightButton) { + // B3: Plumb selection or show context menu + var sel = promptInput.selectedText + if (sel && sel.trim() !== "" && plumber.available()) { + plumber.plumb(sel.trim()) + } else { + promptContextMenu.popup() + } + } + } + + onReleased: { + leftHeld = false } } @@ -390,7 +443,7 @@ Item { } MenuSeparator {} MenuItem { - text: "Cut" + text: "Cut (B1+B2)" enabled: promptInput.selectedText.length > 0 onTriggered: promptInput.cut() } @@ -400,7 +453,7 @@ Item { onTriggered: promptInput.copy() } MenuItem { - text: "Paste" + text: "Paste (B1+B3)" enabled: promptInput.canPaste onTriggered: promptInput.paste() }