@particle-academy/fancy-term 0.2.2 → 0.3.0
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 -0
- package/dist/index.cjs +283 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -2
- package/dist/index.d.ts +146 -2
- package/dist/index.js +280 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,69 @@ import * as react from 'react';
|
|
|
2
2
|
import { HTMLAttributes, CSSProperties, RefObject } from 'react';
|
|
3
3
|
import { ITheme, Terminal as Terminal$1 } from '@xterm/xterm';
|
|
4
4
|
|
|
5
|
+
/** What a paste / drop carried: plain text plus any files (e.g. pasted images). */
|
|
6
|
+
interface ClipboardPayload {
|
|
7
|
+
/** Plain text from the clipboard (or pasted selection). May be "". */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Files present on the clipboard / drop — typically pasted/dropped images. */
|
|
10
|
+
files: File[];
|
|
11
|
+
/** Convenience: the subset of `files` whose MIME type is `image/*`. */
|
|
12
|
+
images: File[];
|
|
13
|
+
}
|
|
14
|
+
/** True when a file looks like an image (by MIME type). */
|
|
15
|
+
declare function isImageFile(f: {
|
|
16
|
+
type?: string;
|
|
17
|
+
}): boolean;
|
|
18
|
+
|
|
19
|
+
/** Context handed to the menu at right-click time + to custom item builders. */
|
|
20
|
+
interface TerminalContextMenuContext {
|
|
21
|
+
/** Whether there is a non-empty text selection. */
|
|
22
|
+
hasSelection: boolean;
|
|
23
|
+
/** The selected text (or ""). */
|
|
24
|
+
selection: string;
|
|
25
|
+
/** Whether the terminal is read-only (paste / clear are dropped by default). */
|
|
26
|
+
readOnly: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A single menu entry. `onSelect` runs on click; set `separator` for a divider. */
|
|
29
|
+
interface TerminalContextMenuItem {
|
|
30
|
+
/** Stable id (used as the React key + a `data-menu-item` handle). */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Visible label. Ignored for separators. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Optional leading glyph / emoji / short string (JSON-friendly — no ReactNode). */
|
|
35
|
+
icon?: string;
|
|
36
|
+
/** Greyed-out + non-interactive. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Render a divider instead of a clickable item. */
|
|
39
|
+
separator?: boolean;
|
|
40
|
+
/** Invoked on click with the menu context. */
|
|
41
|
+
onSelect?: (ctx: TerminalContextMenuContext) => void;
|
|
42
|
+
}
|
|
43
|
+
/** The actions the built-in items dispatch to (wired by the component). */
|
|
44
|
+
interface TerminalMenuActions {
|
|
45
|
+
copy: () => void;
|
|
46
|
+
paste: () => void;
|
|
47
|
+
selectAll: () => void;
|
|
48
|
+
clear: () => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The `contextMenu` prop shape:
|
|
52
|
+
* - `false` — disable the menu entirely.
|
|
53
|
+
* - `true` / omitted — the default Copy / Paste / Select all / Clear menu.
|
|
54
|
+
* - an array — replace the default items wholesale.
|
|
55
|
+
* - a function — receives the context **and the default items** so you can
|
|
56
|
+
* add/remove/reorder (e.g. append a "Send to agent" item).
|
|
57
|
+
*/
|
|
58
|
+
type TerminalContextMenuConfig = boolean | TerminalContextMenuItem[] | ((ctx: TerminalContextMenuContext, defaults: TerminalContextMenuItem[]) => TerminalContextMenuItem[]);
|
|
59
|
+
/** The built-in Copy / Paste / Select all / Clear items, contextual to selection + readOnly. */
|
|
60
|
+
declare function defaultMenuItems(ctx: TerminalContextMenuContext, actions: TerminalMenuActions): TerminalContextMenuItem[];
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the configured menu into the final item list, or `null` when disabled.
|
|
63
|
+
* Empty results (e.g. a function that filtered everything out) also resolve to
|
|
64
|
+
* `null` so the caller can skip opening an empty menu.
|
|
65
|
+
*/
|
|
66
|
+
declare function resolveMenuItems(config: TerminalContextMenuConfig | undefined, ctx: TerminalContextMenuContext, actions: TerminalMenuActions): TerminalContextMenuItem[] | null;
|
|
67
|
+
|
|
5
68
|
/** xterm.js color theme (re-exported for ergonomic typing). */
|
|
6
69
|
type TerminalTheme = ITheme;
|
|
7
70
|
type CursorStyle = "block" | "underline" | "bar";
|
|
@@ -49,6 +112,14 @@ interface TerminalHandle {
|
|
|
49
112
|
getBuffer: () => string;
|
|
50
113
|
/** The current text selection, or "" when nothing is selected. */
|
|
51
114
|
getSelection: () => string;
|
|
115
|
+
/** Copy the current selection to the system clipboard. Resolves false when there's nothing selected or the clipboard is unavailable. */
|
|
116
|
+
copySelection: () => Promise<boolean>;
|
|
117
|
+
/** Paste text into the terminal. With no argument, reads the system clipboard. Honors xterm's bracketed-paste mode. */
|
|
118
|
+
paste: (text?: string) => Promise<void>;
|
|
119
|
+
/** Select the entire buffer. */
|
|
120
|
+
selectAll: () => void;
|
|
121
|
+
/** Clear the current selection. */
|
|
122
|
+
clearSelection: () => void;
|
|
52
123
|
/**
|
|
53
124
|
* Switch the active shell by id. Resolves the matching {@link ShellProfile}
|
|
54
125
|
* from `shells` and fires `onShellChange`; in uncontrolled mode it also updates
|
|
@@ -88,8 +159,22 @@ interface TerminalOptions {
|
|
|
88
159
|
cols: number;
|
|
89
160
|
rows: number;
|
|
90
161
|
}) => void;
|
|
162
|
+
/**
|
|
163
|
+
* Enable clipboard wiring — the Ctrl+Shift+C / Cmd+C copy chord and the paste
|
|
164
|
+
* interceptor (which surfaces pasted images via {@link onPaste}). Default true.
|
|
165
|
+
* Text paste works regardless; this gates the *enhanced* clipboard behavior.
|
|
166
|
+
*/
|
|
167
|
+
clipboard?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Fired on every paste with the clipboard payload — `{ text, files, images }`.
|
|
170
|
+
* Plain text still pastes into the terminal natively; this is where a host
|
|
171
|
+
* receives pasted **images** (a shell can't render them) to upload / hand to an
|
|
172
|
+
* agent / write a path. Return `false` to fully consume the paste (suppress the
|
|
173
|
+
* default text paste — e.g. to transform it, then call `handle.paste(...)`).
|
|
174
|
+
*/
|
|
175
|
+
onPaste?: (payload: ClipboardPayload) => void | boolean;
|
|
91
176
|
}
|
|
92
|
-
interface TerminalProps extends TerminalOptions, Omit<HTMLAttributes<HTMLDivElement>, "onInput" | "onResize" | "children"> {
|
|
177
|
+
interface TerminalProps extends TerminalOptions, Omit<HTMLAttributes<HTMLDivElement>, "onInput" | "onResize" | "onPaste" | "contextMenu" | "children"> {
|
|
93
178
|
/**
|
|
94
179
|
* The controlled output buffer. The component diffs against the previous value
|
|
95
180
|
* and writes only the appended delta, so a host can drive the terminal from
|
|
@@ -112,6 +197,13 @@ interface TerminalProps extends TerminalOptions, Omit<HTMLAttributes<HTMLDivElem
|
|
|
112
197
|
* `shells` is provided. Default `false` — opt-in so existing layout is unchanged.
|
|
113
198
|
*/
|
|
114
199
|
showShellBar?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* The right-click selection context menu. `false` disables it; `true` / omitted
|
|
202
|
+
* gives the default Copy / Paste / Select all / Clear menu; an array replaces
|
|
203
|
+
* the items; a `(ctx, defaults) => items` function lets you add/remove/reorder
|
|
204
|
+
* (e.g. append a "Send to agent" item). See {@link TerminalContextMenuConfig}.
|
|
205
|
+
*/
|
|
206
|
+
contextMenu?: TerminalContextMenuConfig;
|
|
115
207
|
}
|
|
116
208
|
/**
|
|
117
209
|
* Resolve a shell id to its {@link ShellProfile} within a list. Pure helper —
|
|
@@ -169,6 +261,22 @@ interface ShellSwitcherProps {
|
|
|
169
261
|
*/
|
|
170
262
|
declare function ShellSwitcher({ shells, value, onChange, className, style, disabled, }: ShellSwitcherProps): react.JSX.Element;
|
|
171
263
|
|
|
264
|
+
/**
|
|
265
|
+
* The terminal selection context menu. Deliberately plain React + inline styles
|
|
266
|
+
* (fancy-term has no react-fancy dependency) styled to match the shell bar. It
|
|
267
|
+
* closes on click-outside + Escape, and clamps itself inside the viewport on
|
|
268
|
+
* mount so a right-click near an edge never spills off-screen.
|
|
269
|
+
*/
|
|
270
|
+
declare function TerminalContextMenu({ at, items, ctx, onClose, }: {
|
|
271
|
+
at: {
|
|
272
|
+
x: number;
|
|
273
|
+
y: number;
|
|
274
|
+
};
|
|
275
|
+
items: TerminalContextMenuItem[];
|
|
276
|
+
ctx: TerminalContextMenuContext;
|
|
277
|
+
onClose: () => void;
|
|
278
|
+
}): react.JSX.Element;
|
|
279
|
+
|
|
172
280
|
/**
|
|
173
281
|
* Create + own an xterm.js instance for a container, returning a stable
|
|
174
282
|
* {@link TerminalHandle}. The headless engine layer behind `<Terminal>` — use it
|
|
@@ -257,4 +365,40 @@ declare function useTerminalSession(options: UseTerminalSessionOptions): Termina
|
|
|
257
365
|
*/
|
|
258
366
|
declare const fancyDarkTheme: TerminalTheme;
|
|
259
367
|
|
|
260
|
-
|
|
368
|
+
/** The subset of a KeyboardEvent the copy decision reads. */
|
|
369
|
+
interface CopyKeyEvent {
|
|
370
|
+
type: string;
|
|
371
|
+
key: string;
|
|
372
|
+
ctrlKey: boolean;
|
|
373
|
+
shiftKey: boolean;
|
|
374
|
+
metaKey: boolean;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Whether a key event should copy the current selection to the clipboard.
|
|
378
|
+
*
|
|
379
|
+
* - **Ctrl+Shift+C** (Windows / Linux terminal convention) — always.
|
|
380
|
+
* - **Cmd / Meta+C** (macOS) — only when there is a selection.
|
|
381
|
+
* - **Plain Ctrl+C is never a copy** — it must stay SIGINT.
|
|
382
|
+
*
|
|
383
|
+
* Only fires on `keydown`. When this returns true the caller copies the
|
|
384
|
+
* selection and consumes the event (returns `false` from xterm's
|
|
385
|
+
* `attachCustomKeyEventHandler` so no control byte is sent).
|
|
386
|
+
*/
|
|
387
|
+
declare function shouldCopyEvent(e: CopyKeyEvent, hasSelection: boolean): boolean;
|
|
388
|
+
|
|
389
|
+
interface MenuPoint {
|
|
390
|
+
x: number;
|
|
391
|
+
y: number;
|
|
392
|
+
}
|
|
393
|
+
interface MenuSize {
|
|
394
|
+
width: number;
|
|
395
|
+
height: number;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Clamp a menu's top-left so the whole menu stays inside the viewport (minus a
|
|
399
|
+
* margin). Pushes left/up when it would overflow the right/bottom edge, and never
|
|
400
|
+
* goes past the top-left margin. Pure.
|
|
401
|
+
*/
|
|
402
|
+
declare function clampMenuPosition(at: MenuPoint, menu: MenuSize, viewport: MenuSize, margin?: number): MenuPoint;
|
|
403
|
+
|
|
404
|
+
export { BUILTIN_SHELLS, type ClipboardPayload, type CopyKeyEvent, type CursorStyle, type ShellProfile, ShellSwitcher, type ShellSwitcherProps, Terminal, TerminalContextMenu, type TerminalContextMenuConfig, type TerminalContextMenuContext, type TerminalContextMenuItem, type TerminalHandle, type TerminalMenuActions, type TerminalOptions, type TerminalProps, type TerminalSessionApi, type TerminalSessionTransport, type TerminalTheme, type UseTerminalSessionOptions, clampMenuPosition, defaultMenuItems, fancyDarkTheme, isImageFile, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { forwardRef, useRef, useState, useCallback, useImperativeHandle, useEffect, useId } from 'react';
|
|
1
|
+
import { forwardRef, useRef, useState, useCallback, useImperativeHandle, useEffect, useLayoutEffect, useId } from 'react';
|
|
2
2
|
import { Terminal as Terminal$1 } from '@xterm/xterm';
|
|
3
3
|
import { FitAddon } from '@xterm/addon-fit';
|
|
4
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
6
|
// src/components/Terminal.tsx
|
|
7
7
|
|
|
@@ -50,6 +50,77 @@ function useTerminalFit(ref, fit, enabled = true) {
|
|
|
50
50
|
}, [ref, enabled]);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// src/clipboard.ts
|
|
54
|
+
function isImageFile(f) {
|
|
55
|
+
return typeof f.type === "string" && f.type.startsWith("image/");
|
|
56
|
+
}
|
|
57
|
+
function readDataTransfer(dt) {
|
|
58
|
+
const files = [];
|
|
59
|
+
let text = "";
|
|
60
|
+
if (dt) {
|
|
61
|
+
try {
|
|
62
|
+
text = dt.getData("text/plain") ?? "";
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
const items = dt.items;
|
|
66
|
+
if (items) {
|
|
67
|
+
for (let i = 0; i < items.length; i++) {
|
|
68
|
+
const it = items[i];
|
|
69
|
+
if (it && it.kind === "file") {
|
|
70
|
+
const f = it.getAsFile();
|
|
71
|
+
if (f) files.push(f);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (files.length === 0 && dt.files) {
|
|
76
|
+
for (let i = 0; i < dt.files.length; i++) files.push(dt.files[i]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { text, files, images: files.filter(isImageFile) };
|
|
80
|
+
}
|
|
81
|
+
async function writeClipboardText(text) {
|
|
82
|
+
try {
|
|
83
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
84
|
+
if (nav?.clipboard?.writeText) {
|
|
85
|
+
await nav.clipboard.writeText(text);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
if (typeof document === "undefined") return false;
|
|
92
|
+
const ta = document.createElement("textarea");
|
|
93
|
+
ta.value = text;
|
|
94
|
+
ta.style.position = "fixed";
|
|
95
|
+
ta.style.left = "-9999px";
|
|
96
|
+
ta.style.opacity = "0";
|
|
97
|
+
document.body.appendChild(ta);
|
|
98
|
+
ta.select();
|
|
99
|
+
const ok = document.execCommand("copy");
|
|
100
|
+
document.body.removeChild(ta);
|
|
101
|
+
return ok;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function readClipboardText() {
|
|
107
|
+
try {
|
|
108
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
109
|
+
if (nav?.clipboard?.readText) return await nav.clipboard.readText();
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/copy-keybinding.ts
|
|
116
|
+
function shouldCopyEvent(e, hasSelection) {
|
|
117
|
+
if (e.type !== "keydown") return false;
|
|
118
|
+
if (e.key !== "c" && e.key !== "C") return false;
|
|
119
|
+
if (e.ctrlKey && e.shiftKey && !e.metaKey) return true;
|
|
120
|
+
if (e.metaKey && !e.ctrlKey && hasSelection) return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
53
124
|
// src/hooks/use-terminal.ts
|
|
54
125
|
var DEFAULT_FONT = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
|
55
126
|
function readBuffer(term) {
|
|
@@ -92,6 +163,17 @@ function useTerminal(containerRef, options = {}) {
|
|
|
92
163
|
focus: () => xtermRef.current?.focus(),
|
|
93
164
|
getBuffer: () => readBuffer(xtermRef.current),
|
|
94
165
|
getSelection: () => xtermRef.current?.getSelection() ?? "",
|
|
166
|
+
copySelection: async () => {
|
|
167
|
+
const sel = xtermRef.current?.getSelection() ?? "";
|
|
168
|
+
if (!sel) return false;
|
|
169
|
+
return writeClipboardText(sel);
|
|
170
|
+
},
|
|
171
|
+
paste: async (text) => {
|
|
172
|
+
const data = text ?? await readClipboardText();
|
|
173
|
+
if (data) xtermRef.current?.paste(data);
|
|
174
|
+
},
|
|
175
|
+
selectAll: () => xtermRef.current?.selectAll(),
|
|
176
|
+
clearSelection: () => xtermRef.current?.clearSelection(),
|
|
95
177
|
// Shell selection is owned by the <Terminal> component layer (it needs the
|
|
96
178
|
// `shells` list + onShellChange). The headless engine is shell-agnostic, so
|
|
97
179
|
// these are no-ops here and get overridden when <Terminal> composes them.
|
|
@@ -129,9 +211,29 @@ function useTerminal(containerRef, options = {}) {
|
|
|
129
211
|
const resizeSub = term.onResize(
|
|
130
212
|
({ cols, rows }) => optsRef.current.onResize?.({ cols, rows })
|
|
131
213
|
);
|
|
214
|
+
term.attachCustomKeyEventHandler((e) => {
|
|
215
|
+
if (optsRef.current.clipboard === false) return true;
|
|
216
|
+
if (shouldCopyEvent(e, term.hasSelection())) {
|
|
217
|
+
void writeClipboardText(term.getSelection());
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
const onPasteEvent = (e) => {
|
|
223
|
+
const o2 = optsRef.current;
|
|
224
|
+
if (o2.clipboard === false) return;
|
|
225
|
+
if (o2.readOnly) {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const payload = readDataTransfer(e.clipboardData);
|
|
230
|
+
if (o2.onPaste?.(payload) === false) e.preventDefault();
|
|
231
|
+
};
|
|
232
|
+
el.addEventListener("paste", onPasteEvent, true);
|
|
132
233
|
if (o.initialOutput) term.write(o.initialOutput);
|
|
133
234
|
if (o.fit ?? true) handle.fit();
|
|
134
235
|
return () => {
|
|
236
|
+
el.removeEventListener("paste", onPasteEvent, true);
|
|
135
237
|
dataSub.dispose();
|
|
136
238
|
resizeSub.dispose();
|
|
137
239
|
term.dispose();
|
|
@@ -362,6 +464,122 @@ function ShellSwitcher({
|
|
|
362
464
|
);
|
|
363
465
|
}
|
|
364
466
|
|
|
467
|
+
// src/menu-position.ts
|
|
468
|
+
function clampMenuPosition(at, menu, viewport, margin = 8) {
|
|
469
|
+
const maxX = Math.max(margin, viewport.width - menu.width - margin);
|
|
470
|
+
const maxY = Math.max(margin, viewport.height - menu.height - margin);
|
|
471
|
+
return {
|
|
472
|
+
x: Math.min(Math.max(margin, at.x), maxX),
|
|
473
|
+
y: Math.min(Math.max(margin, at.y), maxY)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function TerminalContextMenu({
|
|
477
|
+
at,
|
|
478
|
+
items,
|
|
479
|
+
ctx,
|
|
480
|
+
onClose
|
|
481
|
+
}) {
|
|
482
|
+
const ref = useRef(null);
|
|
483
|
+
const [pos, setPos] = useState(at);
|
|
484
|
+
useLayoutEffect(() => {
|
|
485
|
+
const el = ref.current;
|
|
486
|
+
if (!el || typeof window === "undefined") return;
|
|
487
|
+
const r = el.getBoundingClientRect();
|
|
488
|
+
setPos(
|
|
489
|
+
clampMenuPosition(
|
|
490
|
+
at,
|
|
491
|
+
{ width: r.width, height: r.height },
|
|
492
|
+
{ width: window.innerWidth, height: window.innerHeight }
|
|
493
|
+
)
|
|
494
|
+
);
|
|
495
|
+
}, [at]);
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
const onDown = (e) => {
|
|
498
|
+
if (ref.current && !ref.current.contains(e.target)) onClose();
|
|
499
|
+
};
|
|
500
|
+
const onKey = (e) => {
|
|
501
|
+
if (e.key === "Escape") onClose();
|
|
502
|
+
};
|
|
503
|
+
document.addEventListener("mousedown", onDown, true);
|
|
504
|
+
document.addEventListener("keydown", onKey, true);
|
|
505
|
+
return () => {
|
|
506
|
+
document.removeEventListener("mousedown", onDown, true);
|
|
507
|
+
document.removeEventListener("keydown", onKey, true);
|
|
508
|
+
};
|
|
509
|
+
}, [onClose]);
|
|
510
|
+
return /* @__PURE__ */ jsx(
|
|
511
|
+
"div",
|
|
512
|
+
{
|
|
513
|
+
ref,
|
|
514
|
+
"data-fancy-terminal-menu": "",
|
|
515
|
+
role: "menu",
|
|
516
|
+
style: {
|
|
517
|
+
position: "fixed",
|
|
518
|
+
left: pos.x,
|
|
519
|
+
top: pos.y,
|
|
520
|
+
zIndex: 2147483e3,
|
|
521
|
+
minWidth: 176,
|
|
522
|
+
padding: 4,
|
|
523
|
+
background: "#18181b",
|
|
524
|
+
border: "1px solid #3f3f46",
|
|
525
|
+
borderRadius: 8,
|
|
526
|
+
boxShadow: "0 10px 32px -8px rgba(0,0,0,.6)",
|
|
527
|
+
color: "#e4e4e7",
|
|
528
|
+
font: '13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
529
|
+
userSelect: "none"
|
|
530
|
+
},
|
|
531
|
+
children: items.map(
|
|
532
|
+
(it) => it.separator ? /* @__PURE__ */ jsx(
|
|
533
|
+
"div",
|
|
534
|
+
{
|
|
535
|
+
role: "separator",
|
|
536
|
+
style: { height: 1, margin: "4px 6px", background: "#3f3f46" }
|
|
537
|
+
},
|
|
538
|
+
it.id
|
|
539
|
+
) : /* @__PURE__ */ jsxs(
|
|
540
|
+
"button",
|
|
541
|
+
{
|
|
542
|
+
type: "button",
|
|
543
|
+
role: "menuitem",
|
|
544
|
+
"data-menu-item": it.id,
|
|
545
|
+
disabled: it.disabled,
|
|
546
|
+
onClick: () => {
|
|
547
|
+
if (it.disabled) return;
|
|
548
|
+
it.onSelect?.(ctx);
|
|
549
|
+
onClose();
|
|
550
|
+
},
|
|
551
|
+
onMouseEnter: (e) => {
|
|
552
|
+
if (!it.disabled) e.currentTarget.style.background = "#27272a";
|
|
553
|
+
},
|
|
554
|
+
onMouseLeave: (e) => {
|
|
555
|
+
e.currentTarget.style.background = "transparent";
|
|
556
|
+
},
|
|
557
|
+
style: {
|
|
558
|
+
display: "flex",
|
|
559
|
+
alignItems: "center",
|
|
560
|
+
gap: 9,
|
|
561
|
+
width: "100%",
|
|
562
|
+
textAlign: "left",
|
|
563
|
+
padding: "6px 9px",
|
|
564
|
+
border: "none",
|
|
565
|
+
borderRadius: 5,
|
|
566
|
+
background: "transparent",
|
|
567
|
+
color: it.disabled ? "#52525b" : "inherit",
|
|
568
|
+
cursor: it.disabled ? "default" : "pointer",
|
|
569
|
+
font: "inherit"
|
|
570
|
+
},
|
|
571
|
+
children: [
|
|
572
|
+
/* @__PURE__ */ jsx("span", { style: { width: 16, textAlign: "center", opacity: 0.8 }, children: it.icon ?? "" }),
|
|
573
|
+
/* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: it.label })
|
|
574
|
+
]
|
|
575
|
+
},
|
|
576
|
+
it.id
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
365
583
|
// src/output-diff.ts
|
|
366
584
|
function diffOutput(written, next) {
|
|
367
585
|
if (next === written) return null;
|
|
@@ -378,6 +596,28 @@ function decideShellSelect(shells, current, id) {
|
|
|
378
596
|
function decideShellSwitch(current, next) {
|
|
379
597
|
return { changed: current !== next };
|
|
380
598
|
}
|
|
599
|
+
|
|
600
|
+
// src/context-menu.ts
|
|
601
|
+
function defaultMenuItems(ctx, actions) {
|
|
602
|
+
const items = [
|
|
603
|
+
{ id: "copy", label: "Copy", icon: "\u29C9", disabled: !ctx.hasSelection, onSelect: actions.copy }
|
|
604
|
+
];
|
|
605
|
+
if (!ctx.readOnly) {
|
|
606
|
+
items.push({ id: "paste", label: "Paste", icon: "\u2398", onSelect: actions.paste });
|
|
607
|
+
}
|
|
608
|
+
items.push({ id: "select-all", label: "Select all", onSelect: actions.selectAll });
|
|
609
|
+
if (!ctx.readOnly) {
|
|
610
|
+
items.push({ id: "sep-default", separator: true });
|
|
611
|
+
items.push({ id: "clear", label: "Clear", onSelect: actions.clear });
|
|
612
|
+
}
|
|
613
|
+
return items;
|
|
614
|
+
}
|
|
615
|
+
function resolveMenuItems(config, ctx, actions) {
|
|
616
|
+
if (config === false) return null;
|
|
617
|
+
const defaults = defaultMenuItems(ctx, actions);
|
|
618
|
+
const items = config === void 0 || config === true ? defaults : Array.isArray(config) ? config : config(ctx, defaults);
|
|
619
|
+
return items.length > 0 ? items : null;
|
|
620
|
+
}
|
|
381
621
|
var Terminal = forwardRef(function Terminal2({
|
|
382
622
|
output,
|
|
383
623
|
theme,
|
|
@@ -397,6 +637,9 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
397
637
|
activeShell,
|
|
398
638
|
onShellChange,
|
|
399
639
|
showShellBar = false,
|
|
640
|
+
clipboard,
|
|
641
|
+
onPaste,
|
|
642
|
+
contextMenu,
|
|
400
643
|
className,
|
|
401
644
|
style,
|
|
402
645
|
...rest
|
|
@@ -429,7 +672,9 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
429
672
|
// diffing effect below owns the buffer, so don't double-write.
|
|
430
673
|
initialOutput: output === void 0 ? initialOutput : void 0,
|
|
431
674
|
onData,
|
|
432
|
-
onResize
|
|
675
|
+
onResize,
|
|
676
|
+
clipboard,
|
|
677
|
+
onPaste
|
|
433
678
|
});
|
|
434
679
|
useImperativeHandle(
|
|
435
680
|
ref,
|
|
@@ -449,18 +694,46 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
449
694
|
handle.write(change.write);
|
|
450
695
|
written.current = output;
|
|
451
696
|
}, [output, handle]);
|
|
697
|
+
const [menu, setMenu] = useState(null);
|
|
698
|
+
const openMenu = useCallback(
|
|
699
|
+
(e) => {
|
|
700
|
+
if (contextMenu === false) return;
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
const selection = handle.getSelection();
|
|
703
|
+
const ctx = {
|
|
704
|
+
hasSelection: selection.length > 0,
|
|
705
|
+
selection,
|
|
706
|
+
readOnly: !!readOnly
|
|
707
|
+
};
|
|
708
|
+
const items = resolveMenuItems(contextMenu, ctx, {
|
|
709
|
+
copy: () => void handle.copySelection(),
|
|
710
|
+
paste: () => void handle.paste(),
|
|
711
|
+
selectAll: () => handle.selectAll(),
|
|
712
|
+
clear: () => handle.clear()
|
|
713
|
+
});
|
|
714
|
+
if (items) setMenu({ at: { x: e.clientX, y: e.clientY }, items, ctx });
|
|
715
|
+
},
|
|
716
|
+
[contextMenu, handle, readOnly]
|
|
717
|
+
);
|
|
718
|
+
const menuNode = menu ? /* @__PURE__ */ jsx(TerminalContextMenu, { at: menu.at, items: menu.items, ctx: menu.ctx, onClose: () => setMenu(null) }) : null;
|
|
452
719
|
const surface = /* @__PURE__ */ jsx(
|
|
453
720
|
"div",
|
|
454
721
|
{
|
|
455
722
|
ref: containerRef,
|
|
456
723
|
"data-fancy-terminal": "",
|
|
724
|
+
onContextMenu: openMenu,
|
|
457
725
|
"data-readonly": readOnly ? "" : void 0,
|
|
458
726
|
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
459
727
|
style: showShellBar && shells ? { width: "100%", flex: 1, minHeight: 0 } : { width: "100%", height: "100%", ...style },
|
|
460
728
|
...showShellBar && shells ? {} : rest
|
|
461
729
|
}
|
|
462
730
|
);
|
|
463
|
-
if (!showShellBar || !shells)
|
|
731
|
+
if (!showShellBar || !shells) {
|
|
732
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
733
|
+
surface,
|
|
734
|
+
menuNode
|
|
735
|
+
] });
|
|
736
|
+
}
|
|
464
737
|
return /* @__PURE__ */ jsxs(
|
|
465
738
|
"div",
|
|
466
739
|
{
|
|
@@ -491,7 +764,8 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
491
764
|
)
|
|
492
765
|
}
|
|
493
766
|
),
|
|
494
|
-
surface
|
|
767
|
+
surface,
|
|
768
|
+
menuNode
|
|
495
769
|
]
|
|
496
770
|
}
|
|
497
771
|
);
|
|
@@ -540,6 +814,6 @@ function useTerminalSession(options) {
|
|
|
540
814
|
return { output, sendData, append, clear, shell, switchShell };
|
|
541
815
|
}
|
|
542
816
|
|
|
543
|
-
export { BUILTIN_SHELLS, ShellSwitcher, Terminal, fancyDarkTheme, resolveShell, useTerminal, useTerminalFit, useTerminalSession };
|
|
817
|
+
export { BUILTIN_SHELLS, ShellSwitcher, Terminal, TerminalContextMenu, clampMenuPosition, defaultMenuItems, fancyDarkTheme, isImageFile, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
|
|
544
818
|
//# sourceMappingURL=index.js.map
|
|
545
819
|
//# sourceMappingURL=index.js.map
|