@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/dist/index.js
CHANGED
|
@@ -111,18 +111,120 @@ async function readClipboardText() {
|
|
|
111
111
|
}
|
|
112
112
|
return "";
|
|
113
113
|
}
|
|
114
|
+
var navigatorClipboard = {
|
|
115
|
+
writeText: (text) => writeClipboardText(text),
|
|
116
|
+
readText: () => readClipboardText()
|
|
117
|
+
};
|
|
118
|
+
function resolveClipboard(clipboard) {
|
|
119
|
+
if (clipboard === false) return { enabled: false, provider: navigatorClipboard };
|
|
120
|
+
if (clipboard && typeof clipboard === "object") return { enabled: true, provider: clipboard };
|
|
121
|
+
return { enabled: true, provider: navigatorClipboard };
|
|
122
|
+
}
|
|
123
|
+
async function providerWrite(provider, text) {
|
|
124
|
+
try {
|
|
125
|
+
await provider.writeText(text);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function providerRead(provider) {
|
|
132
|
+
try {
|
|
133
|
+
return await provider.readText() ?? "";
|
|
134
|
+
} catch {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
114
138
|
|
|
115
|
-
// src/copy-
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
// src/copy-paste-mode.ts
|
|
140
|
+
function copyPasteBehavior(mode) {
|
|
141
|
+
switch (mode) {
|
|
142
|
+
case "linux":
|
|
143
|
+
return { selectToCopy: true, middleClickPaste: true, keyCopy: false, keyPaste: false };
|
|
144
|
+
case "winmac":
|
|
145
|
+
return { selectToCopy: false, middleClickPaste: false, keyCopy: true, keyPaste: true };
|
|
146
|
+
case "contextmenu":
|
|
147
|
+
default:
|
|
148
|
+
return { selectToCopy: false, middleClickPaste: false, keyCopy: false, keyPaste: false };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function resolveKeyAction(e, hasSelection, behavior) {
|
|
152
|
+
if (e.type !== "keydown") return null;
|
|
153
|
+
const key = e.key.toLowerCase();
|
|
154
|
+
if (key === "c") {
|
|
155
|
+
if (e.ctrlKey && e.shiftKey && !e.metaKey) return "copy";
|
|
156
|
+
if (e.metaKey && !e.ctrlKey && hasSelection) return "copy";
|
|
157
|
+
if (behavior.keyCopy && (e.ctrlKey || e.metaKey) && !e.shiftKey && hasSelection) return "copy";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (key === "v" && behavior.keyPaste) {
|
|
161
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) return "paste";
|
|
162
|
+
if (e.ctrlKey && e.shiftKey) return "paste";
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/osc52.ts
|
|
169
|
+
function encodeBase64(text) {
|
|
170
|
+
const bytes = new TextEncoder().encode(text);
|
|
171
|
+
let binary = "";
|
|
172
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
173
|
+
return typeof btoa === "function" ? btoa(binary) : binary;
|
|
174
|
+
}
|
|
175
|
+
function decodeBase64(b64) {
|
|
176
|
+
try {
|
|
177
|
+
const binary = typeof atob === "function" ? atob(b64) : b64;
|
|
178
|
+
const bytes = new Uint8Array(binary.length);
|
|
179
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
180
|
+
return new TextDecoder().decode(bytes);
|
|
181
|
+
} catch {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function parseOsc52(payload) {
|
|
186
|
+
const semi = payload.indexOf(";");
|
|
187
|
+
if (semi < 0) return null;
|
|
188
|
+
const selection = payload.slice(0, semi);
|
|
189
|
+
const pd = payload.slice(semi + 1);
|
|
190
|
+
if (pd === "?") return { selection, kind: "read", data: "" };
|
|
191
|
+
return { selection, kind: "write", data: decodeBase64(pd) };
|
|
192
|
+
}
|
|
193
|
+
function osc52Response(selection, text) {
|
|
194
|
+
return `\x1B]52;${selection};${encodeBase64(text)}\x07`;
|
|
195
|
+
}
|
|
196
|
+
function registerOsc52(term, provider, mode) {
|
|
197
|
+
if (mode() === false) return null;
|
|
198
|
+
return term.parser.registerOscHandler(52, (payload) => {
|
|
199
|
+
const m = mode();
|
|
200
|
+
if (m === false) return true;
|
|
201
|
+
const req = parseOsc52(payload);
|
|
202
|
+
if (!req) return false;
|
|
203
|
+
if (req.kind === "write") {
|
|
204
|
+
if (m === "copy" || m === "both") {
|
|
205
|
+
void Promise.resolve(provider().writeText(req.data)).catch(() => {
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (m === "read" || m === "both") {
|
|
211
|
+
void providerRead(provider()).then((text) => {
|
|
212
|
+
term.write(osc52Response(req.selection, text));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
122
217
|
}
|
|
123
218
|
|
|
124
219
|
// src/hooks/use-terminal.ts
|
|
125
220
|
var DEFAULT_FONT = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
|
221
|
+
function makeDeferred() {
|
|
222
|
+
let resolve;
|
|
223
|
+
const promise = new Promise((r) => {
|
|
224
|
+
resolve = r;
|
|
225
|
+
});
|
|
226
|
+
return { promise, resolve };
|
|
227
|
+
}
|
|
126
228
|
function readBuffer(term) {
|
|
127
229
|
if (!term) return "";
|
|
128
230
|
const buf = term.buffer.active;
|
|
@@ -138,12 +240,16 @@ function useTerminal(containerRef, options = {}) {
|
|
|
138
240
|
const fitRef = useRef(null);
|
|
139
241
|
const optsRef = useRef(options);
|
|
140
242
|
optsRef.current = options;
|
|
243
|
+
const readyRef = useRef(makeDeferred());
|
|
141
244
|
const handleRef = useRef(null);
|
|
142
245
|
if (handleRef.current === null) {
|
|
143
246
|
handleRef.current = {
|
|
144
247
|
get xterm() {
|
|
145
248
|
return xtermRef.current;
|
|
146
249
|
},
|
|
250
|
+
get ready() {
|
|
251
|
+
return readyRef.current.promise;
|
|
252
|
+
},
|
|
147
253
|
write: (d) => xtermRef.current?.write(d),
|
|
148
254
|
writeln: (d) => xtermRef.current?.writeln(d),
|
|
149
255
|
clear: () => xtermRef.current?.clear(),
|
|
@@ -166,10 +272,10 @@ function useTerminal(containerRef, options = {}) {
|
|
|
166
272
|
copySelection: async () => {
|
|
167
273
|
const sel = xtermRef.current?.getSelection() ?? "";
|
|
168
274
|
if (!sel) return false;
|
|
169
|
-
return
|
|
275
|
+
return providerWrite(resolveClipboard(optsRef.current.clipboard).provider, sel);
|
|
170
276
|
},
|
|
171
277
|
paste: async (text) => {
|
|
172
|
-
const data = text ?? await
|
|
278
|
+
const data = text ?? await providerRead(resolveClipboard(optsRef.current.clipboard).provider);
|
|
173
279
|
if (data) xtermRef.current?.paste(data);
|
|
174
280
|
},
|
|
175
281
|
selectAll: () => xtermRef.current?.selectAll(),
|
|
@@ -211,17 +317,54 @@ function useTerminal(containerRef, options = {}) {
|
|
|
211
317
|
const resizeSub = term.onResize(
|
|
212
318
|
({ cols, rows }) => optsRef.current.onResize?.({ cols, rows })
|
|
213
319
|
);
|
|
320
|
+
const pasteFromClipboard = async () => {
|
|
321
|
+
const oo = optsRef.current;
|
|
322
|
+
if (oo.readOnly) return;
|
|
323
|
+
const text = await providerRead(resolveClipboard(oo.clipboard).provider);
|
|
324
|
+
if (text) xtermRef.current?.paste(text);
|
|
325
|
+
};
|
|
214
326
|
term.attachCustomKeyEventHandler((e) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
327
|
+
const oo = optsRef.current;
|
|
328
|
+
const clip = resolveClipboard(oo.clipboard);
|
|
329
|
+
if (!clip.enabled) return true;
|
|
330
|
+
const action = resolveKeyAction(e, term.hasSelection(), copyPasteBehavior(oo.copyPaste));
|
|
331
|
+
if (action === "copy") {
|
|
332
|
+
void providerWrite(clip.provider, term.getSelection());
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
if (action === "paste") {
|
|
336
|
+
void pasteFromClipboard();
|
|
218
337
|
return false;
|
|
219
338
|
}
|
|
220
339
|
return true;
|
|
221
340
|
});
|
|
341
|
+
const oscDisposable = registerOsc52(
|
|
342
|
+
term,
|
|
343
|
+
() => resolveClipboard(optsRef.current.clipboard).provider,
|
|
344
|
+
() => {
|
|
345
|
+
const oo = optsRef.current;
|
|
346
|
+
return resolveClipboard(oo.clipboard).enabled ? oo.osc52 ?? "copy" : false;
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
const selectionSub = term.onSelectionChange(() => {
|
|
350
|
+
const oo = optsRef.current;
|
|
351
|
+
const clip = resolveClipboard(oo.clipboard);
|
|
352
|
+
if (clip.enabled && copyPasteBehavior(oo.copyPaste).selectToCopy && term.hasSelection()) {
|
|
353
|
+
void providerWrite(clip.provider, term.getSelection());
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
const onMouseDown = (ev) => {
|
|
357
|
+
const oo = optsRef.current;
|
|
358
|
+
if (ev.button !== 1) return;
|
|
359
|
+
if (!resolveClipboard(oo.clipboard).enabled) return;
|
|
360
|
+
if (!copyPasteBehavior(oo.copyPaste).middleClickPaste) return;
|
|
361
|
+
ev.preventDefault();
|
|
362
|
+
void pasteFromClipboard();
|
|
363
|
+
};
|
|
364
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
222
365
|
const onPasteEvent = (e) => {
|
|
223
366
|
const o2 = optsRef.current;
|
|
224
|
-
if (o2.clipboard === false) return;
|
|
367
|
+
if (resolveClipboard(o2.clipboard).enabled === false) return;
|
|
225
368
|
if (o2.readOnly) {
|
|
226
369
|
e.preventDefault();
|
|
227
370
|
return;
|
|
@@ -232,13 +375,19 @@ function useTerminal(containerRef, options = {}) {
|
|
|
232
375
|
el.addEventListener("paste", onPasteEvent, true);
|
|
233
376
|
if (o.initialOutput) term.write(o.initialOutput);
|
|
234
377
|
if (o.fit ?? true) handle.fit();
|
|
378
|
+
optsRef.current.onReady?.(term);
|
|
379
|
+
readyRef.current.resolve(term);
|
|
235
380
|
return () => {
|
|
236
381
|
el.removeEventListener("paste", onPasteEvent, true);
|
|
382
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
383
|
+
oscDisposable?.dispose();
|
|
384
|
+
selectionSub.dispose();
|
|
237
385
|
dataSub.dispose();
|
|
238
386
|
resizeSub.dispose();
|
|
239
387
|
term.dispose();
|
|
240
388
|
xtermRef.current = null;
|
|
241
389
|
fitRef.current = null;
|
|
390
|
+
readyRef.current = makeDeferred();
|
|
242
391
|
};
|
|
243
392
|
}, [containerRef]);
|
|
244
393
|
useEffect(() => {
|
|
@@ -618,6 +767,12 @@ function resolveMenuItems(config, ctx, actions) {
|
|
|
618
767
|
const items = config === void 0 || config === true ? defaults : Array.isArray(config) ? config : config(ctx, defaults);
|
|
619
768
|
return items.length > 0 ? items : null;
|
|
620
769
|
}
|
|
770
|
+
|
|
771
|
+
// src/selection-snapshot.ts
|
|
772
|
+
function nextSelectionSnapshot(prev, event, liveSelection) {
|
|
773
|
+
if (event.type === "mousedown" && event.button === 0) return "";
|
|
774
|
+
return liveSelection || prev;
|
|
775
|
+
}
|
|
621
776
|
var Terminal = forwardRef(function Terminal2({
|
|
622
777
|
output,
|
|
623
778
|
theme,
|
|
@@ -638,6 +793,9 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
638
793
|
onShellChange,
|
|
639
794
|
showShellBar = false,
|
|
640
795
|
clipboard,
|
|
796
|
+
osc52,
|
|
797
|
+
copyPaste,
|
|
798
|
+
onReady,
|
|
641
799
|
onPaste,
|
|
642
800
|
contextMenu,
|
|
643
801
|
className,
|
|
@@ -674,12 +832,32 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
674
832
|
onData,
|
|
675
833
|
onResize,
|
|
676
834
|
clipboard,
|
|
835
|
+
osc52,
|
|
836
|
+
copyPaste,
|
|
837
|
+
onReady,
|
|
677
838
|
onPaste
|
|
678
839
|
});
|
|
679
840
|
useImperativeHandle(
|
|
680
841
|
ref,
|
|
681
842
|
() => ({
|
|
682
|
-
|
|
843
|
+
get xterm() {
|
|
844
|
+
return handle.xterm;
|
|
845
|
+
},
|
|
846
|
+
get ready() {
|
|
847
|
+
return handle.ready;
|
|
848
|
+
},
|
|
849
|
+
write: handle.write,
|
|
850
|
+
writeln: handle.writeln,
|
|
851
|
+
clear: handle.clear,
|
|
852
|
+
reset: handle.reset,
|
|
853
|
+
fit: handle.fit,
|
|
854
|
+
focus: handle.focus,
|
|
855
|
+
getBuffer: handle.getBuffer,
|
|
856
|
+
getSelection: handle.getSelection,
|
|
857
|
+
copySelection: handle.copySelection,
|
|
858
|
+
paste: handle.paste,
|
|
859
|
+
selectAll: handle.selectAll,
|
|
860
|
+
clearSelection: handle.clearSelection,
|
|
683
861
|
setShell: selectShell,
|
|
684
862
|
getShell: () => shellStateRef.current.shellId
|
|
685
863
|
}),
|
|
@@ -695,25 +873,42 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
695
873
|
written.current = output;
|
|
696
874
|
}, [output, handle]);
|
|
697
875
|
const [menu, setMenu] = useState(null);
|
|
876
|
+
const selectionSnapshot = useRef("");
|
|
877
|
+
const trackSelection = useCallback(
|
|
878
|
+
(e) => {
|
|
879
|
+
selectionSnapshot.current = nextSelectionSnapshot(
|
|
880
|
+
selectionSnapshot.current,
|
|
881
|
+
{ type: e.type, button: e.button },
|
|
882
|
+
handle.getSelection()
|
|
883
|
+
);
|
|
884
|
+
},
|
|
885
|
+
[handle]
|
|
886
|
+
);
|
|
698
887
|
const openMenu = useCallback(
|
|
699
888
|
(e) => {
|
|
700
889
|
if (contextMenu === false) return;
|
|
701
890
|
e.preventDefault();
|
|
702
|
-
const selection = handle.getSelection();
|
|
891
|
+
const selection = handle.getSelection() || selectionSnapshot.current;
|
|
703
892
|
const ctx = {
|
|
704
893
|
hasSelection: selection.length > 0,
|
|
705
894
|
selection,
|
|
706
895
|
readOnly: !!readOnly
|
|
707
896
|
};
|
|
708
897
|
const items = resolveMenuItems(contextMenu, ctx, {
|
|
709
|
-
|
|
898
|
+
// Copy writes the SNAPSHOTTED selection through the active clipboard
|
|
899
|
+
// provider — never a click-time re-read, which copies "" over a TUI
|
|
900
|
+
// (falling back to the live selection only when the snapshot is empty).
|
|
901
|
+
copy: (c) => {
|
|
902
|
+
const text = c.selection || handle.getSelection();
|
|
903
|
+
if (text) void providerWrite(resolveClipboard(clipboard).provider, text);
|
|
904
|
+
},
|
|
710
905
|
paste: () => void handle.paste(),
|
|
711
906
|
selectAll: () => handle.selectAll(),
|
|
712
907
|
clear: () => handle.clear()
|
|
713
908
|
});
|
|
714
909
|
if (items) setMenu({ at: { x: e.clientX, y: e.clientY }, items, ctx });
|
|
715
910
|
},
|
|
716
|
-
[contextMenu, handle, readOnly]
|
|
911
|
+
[contextMenu, handle, readOnly, clipboard]
|
|
717
912
|
);
|
|
718
913
|
const menuNode = menu ? /* @__PURE__ */ jsx(TerminalContextMenu, { at: menu.at, items: menu.items, ctx: menu.ctx, onClose: () => setMenu(null) }) : null;
|
|
719
914
|
const surface = /* @__PURE__ */ jsx(
|
|
@@ -721,6 +916,8 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
721
916
|
{
|
|
722
917
|
ref: containerRef,
|
|
723
918
|
"data-fancy-terminal": "",
|
|
919
|
+
onMouseDownCapture: trackSelection,
|
|
920
|
+
onMouseUpCapture: trackSelection,
|
|
724
921
|
onContextMenu: openMenu,
|
|
725
922
|
"data-readonly": readOnly ? "" : void 0,
|
|
726
923
|
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
@@ -814,6 +1011,15 @@ function useTerminalSession(options) {
|
|
|
814
1011
|
return { output, sendData, append, clear, shell, switchShell };
|
|
815
1012
|
}
|
|
816
1013
|
|
|
817
|
-
|
|
1014
|
+
// src/copy-keybinding.ts
|
|
1015
|
+
function shouldCopyEvent(e, hasSelection) {
|
|
1016
|
+
if (e.type !== "keydown") return false;
|
|
1017
|
+
if (e.key !== "c" && e.key !== "C") return false;
|
|
1018
|
+
if (e.ctrlKey && e.shiftKey && !e.metaKey) return true;
|
|
1019
|
+
if (e.metaKey && !e.ctrlKey && hasSelection) return true;
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export { BUILTIN_SHELLS, ShellSwitcher, Terminal, TerminalContextMenu, clampMenuPosition, copyPasteBehavior, decodeBase64, defaultMenuItems, encodeBase64, fancyDarkTheme, isImageFile, navigatorClipboard, nextSelectionSnapshot, osc52Response, parseOsc52, registerOsc52, resolveClipboard, resolveKeyAction, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
|
|
818
1024
|
//# sourceMappingURL=index.js.map
|
|
819
1025
|
//# sourceMappingURL=index.js.map
|