feat: terminal copy and paste by keys off a Mac - Ctrl+C copies a selection (interrupts without one), Ctrl+Shift+C copies and stays from the browser, Ctrl+V and Ctrl+Shift+V paste

This commit is contained in:
redtea committed 2026-10-15 10:45:58 +03:00
1 parent 1297ab36d0
commit 36dc12b69d
1 file changed
+44
+44
View File
@@ -28,6 +28,49 @@
return u;
}
// ---------- copy and paste by keys ----------
// Off a Mac, where Cmd does it, Ctrl+C copies when there is a selection and
// interrupts as ever when there is none; Ctrl+Shift+C always copies (and is
// kept from the browser, whose developer tools it would open); Ctrl+V and
// Ctrl+Shift+V paste. Keys go by position, so a Russian layout works too.
const mac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
function copyText(text) {
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);
// Plain http: the old way, through a field that is there for a moment.
const ta = document.createElement('textarea'), back = document.activeElement;
ta.value = text;
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
ta.remove();
if (back && back.focus) back.focus();
return ok ? Promise.resolve() : Promise.reject(new Error('нет доступа к буферу'));
}
function clipKeys(s, ev) {
if (mac || !ev.ctrlKey || ev.altKey || ev.metaKey) return true;
const c = ev.code === 'KeyC', v = ev.code === 'KeyV';
if (!c && !v) return true;
if (c && !ev.shiftKey && !s.term.hasSelection()) return true; // ^C to the shell
// From here the key is ours: xterm does not turn it into a control code.
if (ev.type === 'keydown' && c) {
ev.preventDefault();
if (s.term.hasSelection()) {
const text = s.term.getSelection();
s.term.clearSelection();
copyText(text).then(() => flashStatus('скопировано', 'st-ok'), e => flashStatus('не скопировалось: ' + e.message, 'st-err'));
}
}
// Ctrl+V: the browser's paste goes on into the terminal's text field.
return false;
}
function flashStatus(t, cls) {
statusEl.textContent = t;
statusEl.className = cls;
clearTimeout(flashStatus.t);
flashStatus.t = setTimeout(showStatus, 1500);
}
// ---------- one shell ----------
class Session {
// ssh, when given, is where the agent connects by SSH for this shell:
@@ -53,6 +96,7 @@
this.fitter = new FitAddon.FitAddon();
this.term.loadAddon(this.fitter);
this.term.open(this.el.querySelector('.pane-body'));
this.term.attachCustomKeyEventHandler(ev => clipKeys(this, ev));
this.term.onData(d => this.send(TermKeys.withMods(d)));
this.term.onResize(() => this.queueIn({ t: 'r', cols: this.term.cols, rows: this.term.rows }));
this.term.onSelectionChange(() => TermKeys.selectionChanged(this));