@particle-academy/fancy-term 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -2
- package/dist/index.cjs +233 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -7
- package/dist/index.d.ts +169 -7
- package/dist/index.js +224 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,10 @@ fancy-query's `useFancyStream`.
|
|
|
77
77
|
| `activeShell` | `string` | controlled selected shell id (omit for uncontrolled) |
|
|
78
78
|
| `onShellChange` | `(id, profile) => void` | fired when the user / `setShell` switches |
|
|
79
79
|
| `showShellBar` | `boolean` | render the `<ShellSwitcher>` toolbar above the surface (default `false`) |
|
|
80
|
-
| `clipboard` | `boolean` |
|
|
80
|
+
| `clipboard` | `boolean \| { writeText, readText }` | clipboard wiring: `true`/omit (navigator), `false` (off), or an **injectable provider** for sandboxed Electron hosts (default `true`) |
|
|
81
|
+
| `osc52` | `"copy" \| "read" \| "both" \| false` | OSC 52 policy — let terminal programs (Claude Code, tmux, vim) set/read the clipboard (default `"copy"`, write-only) |
|
|
82
|
+
| `copyPaste` | `"contextmenu" \| "linux" \| "winmac"` | copy/paste convention (see below); omit for the default chord |
|
|
83
|
+
| `onReady` | `(xterm) => void` | called once xterm is opened + measured (twin of `handle.ready`) |
|
|
81
84
|
| `onPaste` | `(payload) => void \| boolean` | every paste; receives `{ text, files, images }` — handle pasted **images** here. Return `false` to suppress the native text paste |
|
|
82
85
|
| `contextMenu` | `false \| Item[] \| (ctx, defaults) => Item[]` | the right-click selection menu (see [Clipboard & context menu](#clipboard--context-menu)) |
|
|
83
86
|
|
|
@@ -93,7 +96,8 @@ const term = useRef<TerminalHandle>(null);
|
|
|
93
96
|
// term.current.selectAll() / clearSelection()
|
|
94
97
|
// term.current.setShell("pwsh") → switch the active shell (fires onShellChange)
|
|
95
98
|
// term.current.getShell() → the active shell id
|
|
96
|
-
// term.current.xterm → the raw xterm.js instance (escape hatch)
|
|
99
|
+
// term.current.xterm → the raw xterm.js instance (escape hatch; null before mount)
|
|
100
|
+
// term.current.ready → Promise<XTerm>, resolves once opened + measured
|
|
97
101
|
```
|
|
98
102
|
|
|
99
103
|
## Clipboard & context menu
|
|
@@ -138,6 +142,54 @@ Customize it with the `contextMenu` prop:
|
|
|
138
142
|
Each item is `{ id, label?, icon?, disabled?, separator?, onSelect?(ctx) }`. Set
|
|
139
143
|
`clipboard={false}` to turn off the copy chord + image-paste interception entirely.
|
|
140
144
|
|
|
145
|
+
`ctx.selection` is **snapshotted before the right-click lands** and the built-in
|
|
146
|
+
Copy writes that snapshot (through the active clipboard provider) — so it still
|
|
147
|
+
copies over a mouse-reporting TUI (Claude Code, tmux, vim with mouse mode),
|
|
148
|
+
where the app's redraw clears xterm's live selection before you can click Copy.
|
|
149
|
+
|
|
150
|
+
### OSC 52 (terminal-program clipboard)
|
|
151
|
+
|
|
152
|
+
Modern TUIs — Claude Code, tmux, vim/neovim — copy their own selection with an
|
|
153
|
+
**OSC 52** escape (`ESC ] 52`). xterm.js drops these by default, so the app shows
|
|
154
|
+
"copied" but nothing reaches the system clipboard. fancy-term handles OSC 52 out
|
|
155
|
+
of the box (`osc52="copy"`), routing it through the active clipboard provider:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<Terminal osc52="copy" … /> // default — programs may WRITE the clipboard
|
|
159
|
+
<Terminal osc52="both" … /> // also answer read (?) requests — an exfiltration risk, opt in
|
|
160
|
+
<Terminal osc52={false} … /> // disable OSC 52
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Copy/paste conventions
|
|
164
|
+
|
|
165
|
+
`copyPaste` picks the UX to match your platform expectations:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<Terminal copyPaste="winmac" … /> // Ctrl/Cmd+C copies selection, Ctrl/Cmd+V pastes
|
|
169
|
+
<Terminal copyPaste="linux" … /> // highlight-to-copy + middle-click paste
|
|
170
|
+
<Terminal copyPaste="contextmenu" … /> // menu only (+ Ctrl+Shift+C)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Omit it for the historical default (Ctrl+Shift+C + Cmd+C-with-selection).
|
|
174
|
+
**Ctrl+Shift+C always copies** and plain **Ctrl+C stays SIGINT** (unless `winmac`
|
|
175
|
+
+ a selection) in every mode.
|
|
176
|
+
|
|
177
|
+
### Injectable clipboard (Electron)
|
|
178
|
+
|
|
179
|
+
In a sandboxed Electron renderer `navigator.clipboard` silently no-ops. Supply a
|
|
180
|
+
**provider** so every copy/paste path — the chord, the menu, OSC 52,
|
|
181
|
+
`handle.copySelection` / `handle.paste` — bridges to your main-process clipboard:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
<Terminal
|
|
185
|
+
clipboard={{
|
|
186
|
+
writeText: (text) => window.electron.clipboard.write(text), // → IPC
|
|
187
|
+
readText: () => window.electron.clipboard.read(),
|
|
188
|
+
}}
|
|
189
|
+
osc52="copy"
|
|
190
|
+
/>
|
|
191
|
+
```
|
|
192
|
+
|
|
141
193
|
## Switching shells
|
|
142
194
|
|
|
143
195
|
fancy-term is a **frontend wrapper** — it never spawns a shell. It owns the
|
package/dist/index.cjs
CHANGED
|
@@ -113,18 +113,120 @@ async function readClipboardText() {
|
|
|
113
113
|
}
|
|
114
114
|
return "";
|
|
115
115
|
}
|
|
116
|
+
var navigatorClipboard = {
|
|
117
|
+
writeText: (text) => writeClipboardText(text),
|
|
118
|
+
readText: () => readClipboardText()
|
|
119
|
+
};
|
|
120
|
+
function resolveClipboard(clipboard) {
|
|
121
|
+
if (clipboard === false) return { enabled: false, provider: navigatorClipboard };
|
|
122
|
+
if (clipboard && typeof clipboard === "object") return { enabled: true, provider: clipboard };
|
|
123
|
+
return { enabled: true, provider: navigatorClipboard };
|
|
124
|
+
}
|
|
125
|
+
async function providerWrite(provider, text) {
|
|
126
|
+
try {
|
|
127
|
+
await provider.writeText(text);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function providerRead(provider) {
|
|
134
|
+
try {
|
|
135
|
+
return await provider.readText() ?? "";
|
|
136
|
+
} catch {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
116
140
|
|
|
117
|
-
// src/copy-
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
// src/copy-paste-mode.ts
|
|
142
|
+
function copyPasteBehavior(mode) {
|
|
143
|
+
switch (mode) {
|
|
144
|
+
case "linux":
|
|
145
|
+
return { selectToCopy: true, middleClickPaste: true, keyCopy: false, keyPaste: false };
|
|
146
|
+
case "winmac":
|
|
147
|
+
return { selectToCopy: false, middleClickPaste: false, keyCopy: true, keyPaste: true };
|
|
148
|
+
case "contextmenu":
|
|
149
|
+
default:
|
|
150
|
+
return { selectToCopy: false, middleClickPaste: false, keyCopy: false, keyPaste: false };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function resolveKeyAction(e, hasSelection, behavior) {
|
|
154
|
+
if (e.type !== "keydown") return null;
|
|
155
|
+
const key = e.key.toLowerCase();
|
|
156
|
+
if (key === "c") {
|
|
157
|
+
if (e.ctrlKey && e.shiftKey && !e.metaKey) return "copy";
|
|
158
|
+
if (e.metaKey && !e.ctrlKey && hasSelection) return "copy";
|
|
159
|
+
if (behavior.keyCopy && (e.ctrlKey || e.metaKey) && !e.shiftKey && hasSelection) return "copy";
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (key === "v" && behavior.keyPaste) {
|
|
163
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) return "paste";
|
|
164
|
+
if (e.ctrlKey && e.shiftKey) return "paste";
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/osc52.ts
|
|
171
|
+
function encodeBase64(text) {
|
|
172
|
+
const bytes = new TextEncoder().encode(text);
|
|
173
|
+
let binary = "";
|
|
174
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
175
|
+
return typeof btoa === "function" ? btoa(binary) : binary;
|
|
176
|
+
}
|
|
177
|
+
function decodeBase64(b64) {
|
|
178
|
+
try {
|
|
179
|
+
const binary = typeof atob === "function" ? atob(b64) : b64;
|
|
180
|
+
const bytes = new Uint8Array(binary.length);
|
|
181
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
182
|
+
return new TextDecoder().decode(bytes);
|
|
183
|
+
} catch {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function parseOsc52(payload) {
|
|
188
|
+
const semi = payload.indexOf(";");
|
|
189
|
+
if (semi < 0) return null;
|
|
190
|
+
const selection = payload.slice(0, semi);
|
|
191
|
+
const pd = payload.slice(semi + 1);
|
|
192
|
+
if (pd === "?") return { selection, kind: "read", data: "" };
|
|
193
|
+
return { selection, kind: "write", data: decodeBase64(pd) };
|
|
194
|
+
}
|
|
195
|
+
function osc52Response(selection, text) {
|
|
196
|
+
return `\x1B]52;${selection};${encodeBase64(text)}\x07`;
|
|
197
|
+
}
|
|
198
|
+
function registerOsc52(term, provider, mode) {
|
|
199
|
+
if (mode() === false) return null;
|
|
200
|
+
return term.parser.registerOscHandler(52, (payload) => {
|
|
201
|
+
const m = mode();
|
|
202
|
+
if (m === false) return true;
|
|
203
|
+
const req = parseOsc52(payload);
|
|
204
|
+
if (!req) return false;
|
|
205
|
+
if (req.kind === "write") {
|
|
206
|
+
if (m === "copy" || m === "both") {
|
|
207
|
+
void Promise.resolve(provider().writeText(req.data)).catch(() => {
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (m === "read" || m === "both") {
|
|
213
|
+
void providerRead(provider()).then((text) => {
|
|
214
|
+
term.write(osc52Response(req.selection, text));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
124
219
|
}
|
|
125
220
|
|
|
126
221
|
// src/hooks/use-terminal.ts
|
|
127
222
|
var DEFAULT_FONT = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
|
223
|
+
function makeDeferred() {
|
|
224
|
+
let resolve;
|
|
225
|
+
const promise = new Promise((r) => {
|
|
226
|
+
resolve = r;
|
|
227
|
+
});
|
|
228
|
+
return { promise, resolve };
|
|
229
|
+
}
|
|
128
230
|
function readBuffer(term) {
|
|
129
231
|
if (!term) return "";
|
|
130
232
|
const buf = term.buffer.active;
|
|
@@ -140,12 +242,16 @@ function useTerminal(containerRef, options = {}) {
|
|
|
140
242
|
const fitRef = react.useRef(null);
|
|
141
243
|
const optsRef = react.useRef(options);
|
|
142
244
|
optsRef.current = options;
|
|
245
|
+
const readyRef = react.useRef(makeDeferred());
|
|
143
246
|
const handleRef = react.useRef(null);
|
|
144
247
|
if (handleRef.current === null) {
|
|
145
248
|
handleRef.current = {
|
|
146
249
|
get xterm() {
|
|
147
250
|
return xtermRef.current;
|
|
148
251
|
},
|
|
252
|
+
get ready() {
|
|
253
|
+
return readyRef.current.promise;
|
|
254
|
+
},
|
|
149
255
|
write: (d) => xtermRef.current?.write(d),
|
|
150
256
|
writeln: (d) => xtermRef.current?.writeln(d),
|
|
151
257
|
clear: () => xtermRef.current?.clear(),
|
|
@@ -168,10 +274,10 @@ function useTerminal(containerRef, options = {}) {
|
|
|
168
274
|
copySelection: async () => {
|
|
169
275
|
const sel = xtermRef.current?.getSelection() ?? "";
|
|
170
276
|
if (!sel) return false;
|
|
171
|
-
return
|
|
277
|
+
return providerWrite(resolveClipboard(optsRef.current.clipboard).provider, sel);
|
|
172
278
|
},
|
|
173
279
|
paste: async (text) => {
|
|
174
|
-
const data = text ?? await
|
|
280
|
+
const data = text ?? await providerRead(resolveClipboard(optsRef.current.clipboard).provider);
|
|
175
281
|
if (data) xtermRef.current?.paste(data);
|
|
176
282
|
},
|
|
177
283
|
selectAll: () => xtermRef.current?.selectAll(),
|
|
@@ -213,17 +319,54 @@ function useTerminal(containerRef, options = {}) {
|
|
|
213
319
|
const resizeSub = term.onResize(
|
|
214
320
|
({ cols, rows }) => optsRef.current.onResize?.({ cols, rows })
|
|
215
321
|
);
|
|
322
|
+
const pasteFromClipboard = async () => {
|
|
323
|
+
const oo = optsRef.current;
|
|
324
|
+
if (oo.readOnly) return;
|
|
325
|
+
const text = await providerRead(resolveClipboard(oo.clipboard).provider);
|
|
326
|
+
if (text) xtermRef.current?.paste(text);
|
|
327
|
+
};
|
|
216
328
|
term.attachCustomKeyEventHandler((e) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
329
|
+
const oo = optsRef.current;
|
|
330
|
+
const clip = resolveClipboard(oo.clipboard);
|
|
331
|
+
if (!clip.enabled) return true;
|
|
332
|
+
const action = resolveKeyAction(e, term.hasSelection(), copyPasteBehavior(oo.copyPaste));
|
|
333
|
+
if (action === "copy") {
|
|
334
|
+
void providerWrite(clip.provider, term.getSelection());
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (action === "paste") {
|
|
338
|
+
void pasteFromClipboard();
|
|
220
339
|
return false;
|
|
221
340
|
}
|
|
222
341
|
return true;
|
|
223
342
|
});
|
|
343
|
+
const oscDisposable = registerOsc52(
|
|
344
|
+
term,
|
|
345
|
+
() => resolveClipboard(optsRef.current.clipboard).provider,
|
|
346
|
+
() => {
|
|
347
|
+
const oo = optsRef.current;
|
|
348
|
+
return resolveClipboard(oo.clipboard).enabled ? oo.osc52 ?? "copy" : false;
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
const selectionSub = term.onSelectionChange(() => {
|
|
352
|
+
const oo = optsRef.current;
|
|
353
|
+
const clip = resolveClipboard(oo.clipboard);
|
|
354
|
+
if (clip.enabled && copyPasteBehavior(oo.copyPaste).selectToCopy && term.hasSelection()) {
|
|
355
|
+
void providerWrite(clip.provider, term.getSelection());
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
const onMouseDown = (ev) => {
|
|
359
|
+
const oo = optsRef.current;
|
|
360
|
+
if (ev.button !== 1) return;
|
|
361
|
+
if (!resolveClipboard(oo.clipboard).enabled) return;
|
|
362
|
+
if (!copyPasteBehavior(oo.copyPaste).middleClickPaste) return;
|
|
363
|
+
ev.preventDefault();
|
|
364
|
+
void pasteFromClipboard();
|
|
365
|
+
};
|
|
366
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
224
367
|
const onPasteEvent = (e) => {
|
|
225
368
|
const o2 = optsRef.current;
|
|
226
|
-
if (o2.clipboard === false) return;
|
|
369
|
+
if (resolveClipboard(o2.clipboard).enabled === false) return;
|
|
227
370
|
if (o2.readOnly) {
|
|
228
371
|
e.preventDefault();
|
|
229
372
|
return;
|
|
@@ -234,13 +377,19 @@ function useTerminal(containerRef, options = {}) {
|
|
|
234
377
|
el.addEventListener("paste", onPasteEvent, true);
|
|
235
378
|
if (o.initialOutput) term.write(o.initialOutput);
|
|
236
379
|
if (o.fit ?? true) handle.fit();
|
|
380
|
+
optsRef.current.onReady?.(term);
|
|
381
|
+
readyRef.current.resolve(term);
|
|
237
382
|
return () => {
|
|
238
383
|
el.removeEventListener("paste", onPasteEvent, true);
|
|
384
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
385
|
+
oscDisposable?.dispose();
|
|
386
|
+
selectionSub.dispose();
|
|
239
387
|
dataSub.dispose();
|
|
240
388
|
resizeSub.dispose();
|
|
241
389
|
term.dispose();
|
|
242
390
|
xtermRef.current = null;
|
|
243
391
|
fitRef.current = null;
|
|
392
|
+
readyRef.current = makeDeferred();
|
|
244
393
|
};
|
|
245
394
|
}, [containerRef]);
|
|
246
395
|
react.useEffect(() => {
|
|
@@ -620,6 +769,12 @@ function resolveMenuItems(config, ctx, actions) {
|
|
|
620
769
|
const items = config === void 0 || config === true ? defaults : Array.isArray(config) ? config : config(ctx, defaults);
|
|
621
770
|
return items.length > 0 ? items : null;
|
|
622
771
|
}
|
|
772
|
+
|
|
773
|
+
// src/selection-snapshot.ts
|
|
774
|
+
function nextSelectionSnapshot(prev, event, liveSelection) {
|
|
775
|
+
if (event.type === "mousedown" && event.button === 0) return "";
|
|
776
|
+
return liveSelection || prev;
|
|
777
|
+
}
|
|
623
778
|
var Terminal = react.forwardRef(function Terminal2({
|
|
624
779
|
output,
|
|
625
780
|
theme,
|
|
@@ -640,6 +795,9 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
640
795
|
onShellChange,
|
|
641
796
|
showShellBar = false,
|
|
642
797
|
clipboard,
|
|
798
|
+
osc52,
|
|
799
|
+
copyPaste,
|
|
800
|
+
onReady,
|
|
643
801
|
onPaste,
|
|
644
802
|
contextMenu,
|
|
645
803
|
className,
|
|
@@ -676,12 +834,32 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
676
834
|
onData,
|
|
677
835
|
onResize,
|
|
678
836
|
clipboard,
|
|
837
|
+
osc52,
|
|
838
|
+
copyPaste,
|
|
839
|
+
onReady,
|
|
679
840
|
onPaste
|
|
680
841
|
});
|
|
681
842
|
react.useImperativeHandle(
|
|
682
843
|
ref,
|
|
683
844
|
() => ({
|
|
684
|
-
|
|
845
|
+
get xterm() {
|
|
846
|
+
return handle.xterm;
|
|
847
|
+
},
|
|
848
|
+
get ready() {
|
|
849
|
+
return handle.ready;
|
|
850
|
+
},
|
|
851
|
+
write: handle.write,
|
|
852
|
+
writeln: handle.writeln,
|
|
853
|
+
clear: handle.clear,
|
|
854
|
+
reset: handle.reset,
|
|
855
|
+
fit: handle.fit,
|
|
856
|
+
focus: handle.focus,
|
|
857
|
+
getBuffer: handle.getBuffer,
|
|
858
|
+
getSelection: handle.getSelection,
|
|
859
|
+
copySelection: handle.copySelection,
|
|
860
|
+
paste: handle.paste,
|
|
861
|
+
selectAll: handle.selectAll,
|
|
862
|
+
clearSelection: handle.clearSelection,
|
|
685
863
|
setShell: selectShell,
|
|
686
864
|
getShell: () => shellStateRef.current.shellId
|
|
687
865
|
}),
|
|
@@ -697,25 +875,42 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
697
875
|
written.current = output;
|
|
698
876
|
}, [output, handle]);
|
|
699
877
|
const [menu, setMenu] = react.useState(null);
|
|
878
|
+
const selectionSnapshot = react.useRef("");
|
|
879
|
+
const trackSelection = react.useCallback(
|
|
880
|
+
(e) => {
|
|
881
|
+
selectionSnapshot.current = nextSelectionSnapshot(
|
|
882
|
+
selectionSnapshot.current,
|
|
883
|
+
{ type: e.type, button: e.button },
|
|
884
|
+
handle.getSelection()
|
|
885
|
+
);
|
|
886
|
+
},
|
|
887
|
+
[handle]
|
|
888
|
+
);
|
|
700
889
|
const openMenu = react.useCallback(
|
|
701
890
|
(e) => {
|
|
702
891
|
if (contextMenu === false) return;
|
|
703
892
|
e.preventDefault();
|
|
704
|
-
const selection = handle.getSelection();
|
|
893
|
+
const selection = handle.getSelection() || selectionSnapshot.current;
|
|
705
894
|
const ctx = {
|
|
706
895
|
hasSelection: selection.length > 0,
|
|
707
896
|
selection,
|
|
708
897
|
readOnly: !!readOnly
|
|
709
898
|
};
|
|
710
899
|
const items = resolveMenuItems(contextMenu, ctx, {
|
|
711
|
-
|
|
900
|
+
// Copy writes the SNAPSHOTTED selection through the active clipboard
|
|
901
|
+
// provider — never a click-time re-read, which copies "" over a TUI
|
|
902
|
+
// (falling back to the live selection only when the snapshot is empty).
|
|
903
|
+
copy: (c) => {
|
|
904
|
+
const text = c.selection || handle.getSelection();
|
|
905
|
+
if (text) void providerWrite(resolveClipboard(clipboard).provider, text);
|
|
906
|
+
},
|
|
712
907
|
paste: () => void handle.paste(),
|
|
713
908
|
selectAll: () => handle.selectAll(),
|
|
714
909
|
clear: () => handle.clear()
|
|
715
910
|
});
|
|
716
911
|
if (items) setMenu({ at: { x: e.clientX, y: e.clientY }, items, ctx });
|
|
717
912
|
},
|
|
718
|
-
[contextMenu, handle, readOnly]
|
|
913
|
+
[contextMenu, handle, readOnly, clipboard]
|
|
719
914
|
);
|
|
720
915
|
const menuNode = menu ? /* @__PURE__ */ jsxRuntime.jsx(TerminalContextMenu, { at: menu.at, items: menu.items, ctx: menu.ctx, onClose: () => setMenu(null) }) : null;
|
|
721
916
|
const surface = /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -723,6 +918,8 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
723
918
|
{
|
|
724
919
|
ref: containerRef,
|
|
725
920
|
"data-fancy-terminal": "",
|
|
921
|
+
onMouseDownCapture: trackSelection,
|
|
922
|
+
onMouseUpCapture: trackSelection,
|
|
726
923
|
onContextMenu: openMenu,
|
|
727
924
|
"data-readonly": readOnly ? "" : void 0,
|
|
728
925
|
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
@@ -816,14 +1013,33 @@ function useTerminalSession(options) {
|
|
|
816
1013
|
return { output, sendData, append, clear, shell, switchShell };
|
|
817
1014
|
}
|
|
818
1015
|
|
|
1016
|
+
// src/copy-keybinding.ts
|
|
1017
|
+
function shouldCopyEvent(e, hasSelection) {
|
|
1018
|
+
if (e.type !== "keydown") return false;
|
|
1019
|
+
if (e.key !== "c" && e.key !== "C") return false;
|
|
1020
|
+
if (e.ctrlKey && e.shiftKey && !e.metaKey) return true;
|
|
1021
|
+
if (e.metaKey && !e.ctrlKey && hasSelection) return true;
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
819
1025
|
exports.BUILTIN_SHELLS = BUILTIN_SHELLS;
|
|
820
1026
|
exports.ShellSwitcher = ShellSwitcher;
|
|
821
1027
|
exports.Terminal = Terminal;
|
|
822
1028
|
exports.TerminalContextMenu = TerminalContextMenu;
|
|
823
1029
|
exports.clampMenuPosition = clampMenuPosition;
|
|
1030
|
+
exports.copyPasteBehavior = copyPasteBehavior;
|
|
1031
|
+
exports.decodeBase64 = decodeBase64;
|
|
824
1032
|
exports.defaultMenuItems = defaultMenuItems;
|
|
1033
|
+
exports.encodeBase64 = encodeBase64;
|
|
825
1034
|
exports.fancyDarkTheme = fancyDarkTheme;
|
|
826
1035
|
exports.isImageFile = isImageFile;
|
|
1036
|
+
exports.navigatorClipboard = navigatorClipboard;
|
|
1037
|
+
exports.nextSelectionSnapshot = nextSelectionSnapshot;
|
|
1038
|
+
exports.osc52Response = osc52Response;
|
|
1039
|
+
exports.parseOsc52 = parseOsc52;
|
|
1040
|
+
exports.registerOsc52 = registerOsc52;
|
|
1041
|
+
exports.resolveClipboard = resolveClipboard;
|
|
1042
|
+
exports.resolveKeyAction = resolveKeyAction;
|
|
827
1043
|
exports.resolveMenuItems = resolveMenuItems;
|
|
828
1044
|
exports.resolveShell = resolveShell;
|
|
829
1045
|
exports.shouldCopyEvent = shouldCopyEvent;
|