fix: v0.10.2 drag-select stays highlighted and copies
build-deb / Lint, test, package (push) Successful in 2m50s
build-deb / Deploy to CT 126 (push) Successful in 10s
build-deb / Notify on failure (push) Skipped

A left drag in the pane is turned into xterm's own selection (as Shift+drag
does) instead of tmux copy-mode, which dropped the highlight on release.
Copy on release with a toast; Ctrl+C copies when something is selected.
Wheel scroll and status-line clicks still reach tmux. Closes #51

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHRLCCdXYm41Qx5kNhehaP
This commit is contained in:
2026-10-12 16:35:26 -04:00
co-authored by Claude Opus 5.5
parent 3a0b099f02
commit 24d71131e8
5 changed files with 59 additions and 4 deletions
+2 -1
View File
@@ -71,7 +71,8 @@ The tmux server runs in its own unit, so every Claude process lives in **that**
| Ctrl-b 0 / 1 | Claude window / shell window |
| Ctrl-b s | Switch session (the URL follows) |
| Ctrl-b d | Detach (the session keeps running) |
| Shift+drag | Native browser selection (plain drag goes to tmux copy-mode, which copies via OSC 52) |
| Drag | Selects text; it stays highlighted and is copied when you let go. Double-click selects a word, triple-click a line. Clicks on the tmux status line still go to tmux. |
| Ctrl+C | Copies the selection when there is one; otherwise interrupts as usual |
| Ctrl+Shift+C / V | Copy selection / paste |
| Ctrl+Shift+P | Command palette (also the ⌕ button) |
| Ctrl+Shift+E | Files pane (Esc: back, then close) |
+10
View File
@@ -1,3 +1,13 @@
devdesk (0.10.2) bookworm; urgency=medium
* Selecting text in the terminal works like a normal terminal (#51). A
drag is a browser selection that stays highlighted and is copied when
you let go, with a short toast. Ctrl+C copies the selection and
interrupts only when nothing is selected. The wheel still scrolls tmux
history, and the tmux status line still takes clicks.
-- Jared Vititoe <jjvititoe1@gmail.com> Mon, 12 Oct 2026 16:36:00 -0400
devdesk (0.10.1) bookworm; urgency=medium
* Everything in 0.10.0, which was never published: its CI test for the
+1 -1
View File
@@ -176,7 +176,7 @@
<span class="lt-footer-sep">│</span>
<span class="lt-footer-hint"><span class="lt-footer-key">^b d</span>detach</span>
<span class="lt-footer-sep">│</span>
<span class="lt-footer-hint"><span class="lt-footer-key">⇧drag</span>select</span>
<span class="lt-footer-hint"><span class="lt-footer-key">drag</span>copy</span>
<span class="lt-footer-sep">│</span>
<span class="lt-footer-hint"><span class="lt-footer-key">drop / paste</span>attach files</span>
<span class="lt-footer-sep">│</span>
+3
View File
@@ -876,6 +876,8 @@ const KEY_HELP = [
['Ctrl+⇧ E', 'Files: browse, preview, diff, edit (Esc closes)'],
['Ctrl+P', 'Files: go to file'],
['Ctrl+S', 'Files: save the open file'],
['Drag', 'Select text; it is copied when you let go'],
['Ctrl+C', 'Copy the selection (interrupts only when nothing is selected)'],
['Ctrl+⇧ C / V', 'Copy selection / paste'],
['⇧ drag', 'Select text in the browser'],
['Drop or paste a file', 'Upload it and attach it for Claude'],
@@ -1199,6 +1201,7 @@ async function boot() {
fontSize,
theme: getTheme(),
onShiftEnter: () => state.conn?.sendInput('\n'),
onCopied: (text) => toast.success(`Copied ${text.length.toLocaleString()} character${text.length === 1 ? '' : 's'}`, 1500),
}));
onThemeChange((theme) => {
term.options.theme = THEMES[theme];
+43 -2
View File
@@ -90,7 +90,7 @@ export async function loadFonts(size) {
}
}
export function createTerminal(container, { fontSize, theme = 'dark', onShiftEnter }) {
export function createTerminal(container, { fontSize, theme = 'dark', onShiftEnter, onCopied }) {
const term = new Terminal({
fontFamily: FONT_FAMILY,
fontSize,
@@ -113,16 +113,28 @@ export function createTerminal(container, { fontSize, theme = 'dark', onShiftEnt
window.open(uri, '_blank', 'noopener');
}));
const copySelection = () => {
const text = term.getSelection();
navigator.clipboard?.writeText(text).then(() => onCopied?.(text), () => {});
};
term.attachCustomKeyEventHandler((event) => {
// Shift+Enter: newline in Claude's prompt (Ctrl+J), not submit.
if (event.key === 'Enter' && event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
if (event.type === 'keydown') onShiftEnter();
return false;
}
// Ctrl+C with text selected copies it instead of interrupting (devdesk#51).
if (event.type === 'keydown' && event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey
&& event.code === 'KeyC' && term.hasSelection()) {
copySelection();
term.clearSelection();
return false;
}
// Ctrl+Shift+C / Ctrl+Shift+V: browser clipboard, like desktop terminals.
if (event.type === 'keydown' && event.ctrlKey && event.shiftKey && !event.altKey) {
if (event.code === 'KeyC' && term.hasSelection()) {
navigator.clipboard?.writeText(term.getSelection()).catch(() => {});
copySelection();
return false;
}
if (event.code === 'KeyV') {
@@ -135,6 +147,35 @@ export function createTerminal(container, { fontSize, theme = 'dark', onShiftEnt
term.open(container);
// devdesk#51: tmux runs with the mouse on (wheel scrolls its history), so a
// drag would select inside tmux, which copies and then drops the highlight on
// release. A left-button drag in the terminal is made a browser selection
// instead (what Shift+drag does): it stays highlighted and is copied on
// release. Clicks on tmux's status line (the window tabs) still go to tmux.
const screen = () => container.querySelector('.xterm-screen');
container.addEventListener('mousedown', (event) => {
if (event.button !== 0 || event.shiftKey || event.ddForced || term.modes.mouseTrackingMode === 'none') return;
const rect = screen()?.getBoundingClientRect();
if (!rect) return;
const row = Math.floor(((event.clientY - rect.top) / rect.height) * term.rows);
if (row >= term.rows - 1) return; // status line
event.preventDefault();
event.stopImmediatePropagation();
const forced = new MouseEvent('mousedown', {
bubbles: true, cancelable: true, view: window, detail: event.detail, button: 0, buttons: event.buttons,
clientX: event.clientX, clientY: event.clientY, screenX: event.screenX, screenY: event.screenY,
shiftKey: true, ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey,
});
forced.ddForced = true;
event.target.dispatchEvent(forced);
}, true);
// Copy when the drag (or double/triple click) ends with something selected.
container.addEventListener('mouseup', () => {
setTimeout(() => {
if (term.hasSelection() && term.getSelection().trim()) copySelection();
}, 0);
});
// Touch devices: the DOM renderer is more reliable at high DPR than WebGL.
if (window.matchMedia?.('(pointer: coarse)').matches) return { term, fit };
try {