@particle-academy/fancy-term 0.3.0 → 0.4.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 +49 -2
- package/dist/index.cjs +204 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +142 -6
- package/dist/index.d.ts +142 -6
- package/dist/index.js +196 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, CSSProperties, RefObject } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Terminal as Terminal$1, IDisposable, ITheme } from '@xterm/xterm';
|
|
4
4
|
|
|
5
5
|
/** What a paste / drop carried: plain text plus any files (e.g. pasted images). */
|
|
6
6
|
interface ClipboardPayload {
|
|
@@ -15,6 +15,107 @@ interface ClipboardPayload {
|
|
|
15
15
|
declare function isImageFile(f: {
|
|
16
16
|
type?: string;
|
|
17
17
|
}): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A host-supplied clipboard. Every copy/paste path in fancy-term — the copy
|
|
20
|
+
* chord, the context menu, OSC 52, `handle.copySelection` / `handle.paste` —
|
|
21
|
+
* routes through this, so an Electron host (where `navigator.clipboard` silently
|
|
22
|
+
* no-ops in a sandboxed renderer) can bridge to its main-process clipboard over
|
|
23
|
+
* IPC. `writeText` may return anything (a `Promise<void>` from IPC is fine);
|
|
24
|
+
* `readText` returns the clipboard text.
|
|
25
|
+
*/
|
|
26
|
+
interface ClipboardProvider {
|
|
27
|
+
writeText: (text: string) => void | Promise<unknown>;
|
|
28
|
+
readText: () => string | Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
/** The default provider — `navigator.clipboard` with the legacy `execCommand` fallback. */
|
|
31
|
+
declare const navigatorClipboard: ClipboardProvider;
|
|
32
|
+
/** The `clipboard` prop: `false` disables clipboard wiring; a provider object injects a host clipboard; `true`/omitted uses {@link navigatorClipboard}. */
|
|
33
|
+
type ClipboardOption = boolean | ClipboardProvider;
|
|
34
|
+
/** Normalize the `clipboard` prop into `{ enabled, provider }`, read live per call. */
|
|
35
|
+
declare function resolveClipboard(clipboard: ClipboardOption | undefined): {
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
provider: ClipboardProvider;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The copy/paste convention the terminal follows:
|
|
42
|
+
* - `contextmenu` — copy/paste via the right-click menu only (plus the always-on
|
|
43
|
+
* Ctrl+Shift+C copy chord).
|
|
44
|
+
* - `linux` — highlight-to-copy (X11 primary-selection style) + middle-click
|
|
45
|
+
* paste. Right-click still opens the context menu.
|
|
46
|
+
* - `winmac` — Ctrl/Cmd+C copies the selection (falling back to SIGINT when
|
|
47
|
+
* nothing is selected), Ctrl/Cmd+V pastes.
|
|
48
|
+
*
|
|
49
|
+
* Omitting the prop keeps the historical default: the Ctrl+Shift+C chord and
|
|
50
|
+
* Cmd+C-with-selection, no auto-copy, no mouse paste.
|
|
51
|
+
*/
|
|
52
|
+
type CopyPasteMode = "contextmenu" | "linux" | "winmac";
|
|
53
|
+
/** The resolved, event-agnostic behavior flags a {@link CopyPasteMode} implies. */
|
|
54
|
+
interface CopyPasteBehavior {
|
|
55
|
+
/** Auto-copy the selection to the clipboard as soon as it changes (linux). */
|
|
56
|
+
selectToCopy: boolean;
|
|
57
|
+
/** Paste on middle-mouse-button (linux). */
|
|
58
|
+
middleClickPaste: boolean;
|
|
59
|
+
/** (Ctrl|Cmd)+C copies the selection when one exists (winmac). */
|
|
60
|
+
keyCopy: boolean;
|
|
61
|
+
/** (Ctrl|Cmd)+V — and Ctrl+Shift+V — paste (winmac). */
|
|
62
|
+
keyPaste: boolean;
|
|
63
|
+
}
|
|
64
|
+
declare function copyPasteBehavior(mode: CopyPasteMode | undefined): CopyPasteBehavior;
|
|
65
|
+
/** The subset of a KeyboardEvent the copy/paste decision reads. */
|
|
66
|
+
interface KeyEventLike {
|
|
67
|
+
type: string;
|
|
68
|
+
key: string;
|
|
69
|
+
ctrlKey: boolean;
|
|
70
|
+
shiftKey: boolean;
|
|
71
|
+
metaKey: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a key event to a clipboard action, given the current selection state
|
|
75
|
+
* and the active behavior. Returns `"copy"`, `"paste"`, or `null` (do nothing —
|
|
76
|
+
* let the byte reach the shell). Only fires on `keydown`.
|
|
77
|
+
*
|
|
78
|
+
* Copy is honored across every mode for the terminal-safe **Ctrl+Shift+C** chord
|
|
79
|
+
* and macOS **Cmd+C-with-selection**; `keyCopy` adds plain (Ctrl|Cmd)+C. Plain
|
|
80
|
+
* Ctrl+C with no selection is never a copy — it stays SIGINT.
|
|
81
|
+
*/
|
|
82
|
+
declare function resolveKeyAction(e: KeyEventLike, hasSelection: boolean, behavior: CopyPasteBehavior): "copy" | "paste" | null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* OSC 52 policy: `copy` allows programs to WRITE the clipboard (the common TUI
|
|
86
|
+
* case), `read` allows a program to READ it back (a `?` request), `both` allows
|
|
87
|
+
* either, `false` disables OSC 52 entirely. Read is the real exfiltration risk
|
|
88
|
+
* (arbitrary terminal output could siphon your clipboard), so it must be opted
|
|
89
|
+
* into explicitly.
|
|
90
|
+
*/
|
|
91
|
+
type Osc52Mode = "copy" | "read" | "both" | false;
|
|
92
|
+
/** A parsed OSC 52 request: which selection buffer, and write-a-payload vs read-request. */
|
|
93
|
+
interface Osc52Request {
|
|
94
|
+
/** The `Pc` field — clipboard selection(s), e.g. `"c"` (clipboard), `"p"` (primary). */
|
|
95
|
+
selection: string;
|
|
96
|
+
kind: "write" | "read";
|
|
97
|
+
/** Decoded text for a write; `""` for a read request. */
|
|
98
|
+
data: string;
|
|
99
|
+
}
|
|
100
|
+
/** UTF-8-safe base64 encode (browser `btoa` operates on binary strings). */
|
|
101
|
+
declare function encodeBase64(text: string): string;
|
|
102
|
+
/** UTF-8-safe base64 decode; returns `""` on malformed input. */
|
|
103
|
+
declare function decodeBase64(b64: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Parse an OSC 52 payload (the part after `ESC ] 52 ;`, i.e. `<Pc> ; <Pd>`).
|
|
106
|
+
* `Pd === "?"` is a read request; otherwise `Pd` is base64 to write. Returns
|
|
107
|
+
* `null` for a malformed payload (no `;`).
|
|
108
|
+
*/
|
|
109
|
+
declare function parseOsc52(payload: string): Osc52Request | null;
|
|
110
|
+
/** Build the OSC 52 response a `read` request expects: `ESC ] 52 ; <Pc> ; <base64> BEL`. */
|
|
111
|
+
declare function osc52Response(selection: string, text: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* Register an OSC 52 handler on an xterm instance, routing writes/reads through
|
|
114
|
+
* `provider` per `mode`. Returns the {@link IDisposable} (or `null` when
|
|
115
|
+
* `mode === false`). The handler always returns `true` (consumed) so a
|
|
116
|
+
* disallowed direction is swallowed rather than echoed as garbage.
|
|
117
|
+
*/
|
|
118
|
+
declare function registerOsc52(term: Terminal$1, provider: () => ClipboardProvider, mode: () => Osc52Mode): IDisposable | null;
|
|
18
119
|
|
|
19
120
|
/** Context handed to the menu at right-click time + to custom item builders. */
|
|
20
121
|
interface TerminalContextMenuContext {
|
|
@@ -96,6 +197,13 @@ interface ShellProfile {
|
|
|
96
197
|
interface TerminalHandle {
|
|
97
198
|
/** The underlying xterm.js instance — escape hatch for addons / advanced use. Null before mount. */
|
|
98
199
|
readonly xterm: Terminal$1 | null;
|
|
200
|
+
/**
|
|
201
|
+
* Resolves with the xterm instance once it's opened + measured. Because
|
|
202
|
+
* `xterm` is null until the container lays out, a consumer that must wire an
|
|
203
|
+
* addon can `await handle.ready` instead of polling. Re-armed if the terminal
|
|
204
|
+
* is torn down + recreated (e.g. a container change).
|
|
205
|
+
*/
|
|
206
|
+
readonly ready: Promise<Terminal$1>;
|
|
99
207
|
/** Write raw data (ANSI escape sequences honored) to the terminal. */
|
|
100
208
|
write: (data: string) => void;
|
|
101
209
|
/** Write data followed by CRLF. */
|
|
@@ -160,11 +268,39 @@ interface TerminalOptions {
|
|
|
160
268
|
rows: number;
|
|
161
269
|
}) => void;
|
|
162
270
|
/**
|
|
163
|
-
*
|
|
164
|
-
* interceptor (
|
|
165
|
-
*
|
|
271
|
+
* Clipboard wiring — gates the Ctrl+Shift+C / Cmd+C copy chord, the paste
|
|
272
|
+
* interceptor ({@link onPaste}), the context-menu copy/paste, and OSC 52.
|
|
273
|
+
*
|
|
274
|
+
* - `true` / omitted — enabled, backed by `navigator.clipboard`.
|
|
275
|
+
* - `false` — disabled (no copy chord / OSC 52; native text paste still works).
|
|
276
|
+
* - a `{ writeText, readText }` **provider** — every copy/paste path routes
|
|
277
|
+
* through it. Supply this in a sandboxed Electron renderer, where
|
|
278
|
+
* `navigator.clipboard` silently no-ops, to bridge to the main-process
|
|
279
|
+
* clipboard over IPC.
|
|
280
|
+
*/
|
|
281
|
+
clipboard?: ClipboardOption;
|
|
282
|
+
/**
|
|
283
|
+
* OSC 52 clipboard policy — lets terminal programs (Claude Code, tmux, vim)
|
|
284
|
+
* set/read the system clipboard via `ESC ] 52`. `"copy"` (default) allows
|
|
285
|
+
* writes only; `"read"` / `"both"` also answer read requests (an exfiltration
|
|
286
|
+
* risk — opt in deliberately); `false` disables it. Routed through the same
|
|
287
|
+
* clipboard provider as {@link clipboard}; a no-op when clipboard is `false`.
|
|
288
|
+
* Default `"copy"`.
|
|
289
|
+
*/
|
|
290
|
+
osc52?: Osc52Mode;
|
|
291
|
+
/**
|
|
292
|
+
* Copy/paste UX convention: `"contextmenu"` (menu + Ctrl+Shift+C),
|
|
293
|
+
* `"linux"` (highlight-to-copy + middle-click paste), or `"winmac"`
|
|
294
|
+
* (Ctrl/Cmd+C copies the selection, Ctrl/Cmd+V pastes). Omit for the historical
|
|
295
|
+
* default (Ctrl+Shift+C + Cmd+C-with-selection). Ctrl+Shift+C always copies.
|
|
296
|
+
*/
|
|
297
|
+
copyPaste?: CopyPasteMode;
|
|
298
|
+
/**
|
|
299
|
+
* Called once the xterm instance is opened + attached — the imperative twin of
|
|
300
|
+
* {@link TerminalHandle.ready}. Use it to load an addon without racing the
|
|
301
|
+
* container layout (where `handle.xterm` is still null).
|
|
166
302
|
*/
|
|
167
|
-
|
|
303
|
+
onReady?: (xterm: Terminal$1) => void;
|
|
168
304
|
/**
|
|
169
305
|
* Fired on every paste with the clipboard payload — `{ text, files, images }`.
|
|
170
306
|
* Plain text still pastes into the terminal natively; this is where a host
|
|
@@ -401,4 +537,4 @@ interface MenuSize {
|
|
|
401
537
|
*/
|
|
402
538
|
declare function clampMenuPosition(at: MenuPoint, menu: MenuSize, viewport: MenuSize, margin?: number): MenuPoint;
|
|
403
539
|
|
|
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 };
|
|
540
|
+
export { BUILTIN_SHELLS, type ClipboardOption, type ClipboardPayload, type ClipboardProvider, type CopyKeyEvent, type CopyPasteBehavior, type CopyPasteMode, type CursorStyle, type KeyEventLike, type Osc52Mode, type Osc52Request, 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, copyPasteBehavior, decodeBase64, defaultMenuItems, encodeBase64, fancyDarkTheme, isImageFile, navigatorClipboard, osc52Response, parseOsc52, registerOsc52, resolveClipboard, resolveKeyAction, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, CSSProperties, RefObject } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Terminal as Terminal$1, IDisposable, ITheme } from '@xterm/xterm';
|
|
4
4
|
|
|
5
5
|
/** What a paste / drop carried: plain text plus any files (e.g. pasted images). */
|
|
6
6
|
interface ClipboardPayload {
|
|
@@ -15,6 +15,107 @@ interface ClipboardPayload {
|
|
|
15
15
|
declare function isImageFile(f: {
|
|
16
16
|
type?: string;
|
|
17
17
|
}): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A host-supplied clipboard. Every copy/paste path in fancy-term — the copy
|
|
20
|
+
* chord, the context menu, OSC 52, `handle.copySelection` / `handle.paste` —
|
|
21
|
+
* routes through this, so an Electron host (where `navigator.clipboard` silently
|
|
22
|
+
* no-ops in a sandboxed renderer) can bridge to its main-process clipboard over
|
|
23
|
+
* IPC. `writeText` may return anything (a `Promise<void>` from IPC is fine);
|
|
24
|
+
* `readText` returns the clipboard text.
|
|
25
|
+
*/
|
|
26
|
+
interface ClipboardProvider {
|
|
27
|
+
writeText: (text: string) => void | Promise<unknown>;
|
|
28
|
+
readText: () => string | Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
/** The default provider — `navigator.clipboard` with the legacy `execCommand` fallback. */
|
|
31
|
+
declare const navigatorClipboard: ClipboardProvider;
|
|
32
|
+
/** The `clipboard` prop: `false` disables clipboard wiring; a provider object injects a host clipboard; `true`/omitted uses {@link navigatorClipboard}. */
|
|
33
|
+
type ClipboardOption = boolean | ClipboardProvider;
|
|
34
|
+
/** Normalize the `clipboard` prop into `{ enabled, provider }`, read live per call. */
|
|
35
|
+
declare function resolveClipboard(clipboard: ClipboardOption | undefined): {
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
provider: ClipboardProvider;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The copy/paste convention the terminal follows:
|
|
42
|
+
* - `contextmenu` — copy/paste via the right-click menu only (plus the always-on
|
|
43
|
+
* Ctrl+Shift+C copy chord).
|
|
44
|
+
* - `linux` — highlight-to-copy (X11 primary-selection style) + middle-click
|
|
45
|
+
* paste. Right-click still opens the context menu.
|
|
46
|
+
* - `winmac` — Ctrl/Cmd+C copies the selection (falling back to SIGINT when
|
|
47
|
+
* nothing is selected), Ctrl/Cmd+V pastes.
|
|
48
|
+
*
|
|
49
|
+
* Omitting the prop keeps the historical default: the Ctrl+Shift+C chord and
|
|
50
|
+
* Cmd+C-with-selection, no auto-copy, no mouse paste.
|
|
51
|
+
*/
|
|
52
|
+
type CopyPasteMode = "contextmenu" | "linux" | "winmac";
|
|
53
|
+
/** The resolved, event-agnostic behavior flags a {@link CopyPasteMode} implies. */
|
|
54
|
+
interface CopyPasteBehavior {
|
|
55
|
+
/** Auto-copy the selection to the clipboard as soon as it changes (linux). */
|
|
56
|
+
selectToCopy: boolean;
|
|
57
|
+
/** Paste on middle-mouse-button (linux). */
|
|
58
|
+
middleClickPaste: boolean;
|
|
59
|
+
/** (Ctrl|Cmd)+C copies the selection when one exists (winmac). */
|
|
60
|
+
keyCopy: boolean;
|
|
61
|
+
/** (Ctrl|Cmd)+V — and Ctrl+Shift+V — paste (winmac). */
|
|
62
|
+
keyPaste: boolean;
|
|
63
|
+
}
|
|
64
|
+
declare function copyPasteBehavior(mode: CopyPasteMode | undefined): CopyPasteBehavior;
|
|
65
|
+
/** The subset of a KeyboardEvent the copy/paste decision reads. */
|
|
66
|
+
interface KeyEventLike {
|
|
67
|
+
type: string;
|
|
68
|
+
key: string;
|
|
69
|
+
ctrlKey: boolean;
|
|
70
|
+
shiftKey: boolean;
|
|
71
|
+
metaKey: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a key event to a clipboard action, given the current selection state
|
|
75
|
+
* and the active behavior. Returns `"copy"`, `"paste"`, or `null` (do nothing —
|
|
76
|
+
* let the byte reach the shell). Only fires on `keydown`.
|
|
77
|
+
*
|
|
78
|
+
* Copy is honored across every mode for the terminal-safe **Ctrl+Shift+C** chord
|
|
79
|
+
* and macOS **Cmd+C-with-selection**; `keyCopy` adds plain (Ctrl|Cmd)+C. Plain
|
|
80
|
+
* Ctrl+C with no selection is never a copy — it stays SIGINT.
|
|
81
|
+
*/
|
|
82
|
+
declare function resolveKeyAction(e: KeyEventLike, hasSelection: boolean, behavior: CopyPasteBehavior): "copy" | "paste" | null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* OSC 52 policy: `copy` allows programs to WRITE the clipboard (the common TUI
|
|
86
|
+
* case), `read` allows a program to READ it back (a `?` request), `both` allows
|
|
87
|
+
* either, `false` disables OSC 52 entirely. Read is the real exfiltration risk
|
|
88
|
+
* (arbitrary terminal output could siphon your clipboard), so it must be opted
|
|
89
|
+
* into explicitly.
|
|
90
|
+
*/
|
|
91
|
+
type Osc52Mode = "copy" | "read" | "both" | false;
|
|
92
|
+
/** A parsed OSC 52 request: which selection buffer, and write-a-payload vs read-request. */
|
|
93
|
+
interface Osc52Request {
|
|
94
|
+
/** The `Pc` field — clipboard selection(s), e.g. `"c"` (clipboard), `"p"` (primary). */
|
|
95
|
+
selection: string;
|
|
96
|
+
kind: "write" | "read";
|
|
97
|
+
/** Decoded text for a write; `""` for a read request. */
|
|
98
|
+
data: string;
|
|
99
|
+
}
|
|
100
|
+
/** UTF-8-safe base64 encode (browser `btoa` operates on binary strings). */
|
|
101
|
+
declare function encodeBase64(text: string): string;
|
|
102
|
+
/** UTF-8-safe base64 decode; returns `""` on malformed input. */
|
|
103
|
+
declare function decodeBase64(b64: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Parse an OSC 52 payload (the part after `ESC ] 52 ;`, i.e. `<Pc> ; <Pd>`).
|
|
106
|
+
* `Pd === "?"` is a read request; otherwise `Pd` is base64 to write. Returns
|
|
107
|
+
* `null` for a malformed payload (no `;`).
|
|
108
|
+
*/
|
|
109
|
+
declare function parseOsc52(payload: string): Osc52Request | null;
|
|
110
|
+
/** Build the OSC 52 response a `read` request expects: `ESC ] 52 ; <Pc> ; <base64> BEL`. */
|
|
111
|
+
declare function osc52Response(selection: string, text: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* Register an OSC 52 handler on an xterm instance, routing writes/reads through
|
|
114
|
+
* `provider` per `mode`. Returns the {@link IDisposable} (or `null` when
|
|
115
|
+
* `mode === false`). The handler always returns `true` (consumed) so a
|
|
116
|
+
* disallowed direction is swallowed rather than echoed as garbage.
|
|
117
|
+
*/
|
|
118
|
+
declare function registerOsc52(term: Terminal$1, provider: () => ClipboardProvider, mode: () => Osc52Mode): IDisposable | null;
|
|
18
119
|
|
|
19
120
|
/** Context handed to the menu at right-click time + to custom item builders. */
|
|
20
121
|
interface TerminalContextMenuContext {
|
|
@@ -96,6 +197,13 @@ interface ShellProfile {
|
|
|
96
197
|
interface TerminalHandle {
|
|
97
198
|
/** The underlying xterm.js instance — escape hatch for addons / advanced use. Null before mount. */
|
|
98
199
|
readonly xterm: Terminal$1 | null;
|
|
200
|
+
/**
|
|
201
|
+
* Resolves with the xterm instance once it's opened + measured. Because
|
|
202
|
+
* `xterm` is null until the container lays out, a consumer that must wire an
|
|
203
|
+
* addon can `await handle.ready` instead of polling. Re-armed if the terminal
|
|
204
|
+
* is torn down + recreated (e.g. a container change).
|
|
205
|
+
*/
|
|
206
|
+
readonly ready: Promise<Terminal$1>;
|
|
99
207
|
/** Write raw data (ANSI escape sequences honored) to the terminal. */
|
|
100
208
|
write: (data: string) => void;
|
|
101
209
|
/** Write data followed by CRLF. */
|
|
@@ -160,11 +268,39 @@ interface TerminalOptions {
|
|
|
160
268
|
rows: number;
|
|
161
269
|
}) => void;
|
|
162
270
|
/**
|
|
163
|
-
*
|
|
164
|
-
* interceptor (
|
|
165
|
-
*
|
|
271
|
+
* Clipboard wiring — gates the Ctrl+Shift+C / Cmd+C copy chord, the paste
|
|
272
|
+
* interceptor ({@link onPaste}), the context-menu copy/paste, and OSC 52.
|
|
273
|
+
*
|
|
274
|
+
* - `true` / omitted — enabled, backed by `navigator.clipboard`.
|
|
275
|
+
* - `false` — disabled (no copy chord / OSC 52; native text paste still works).
|
|
276
|
+
* - a `{ writeText, readText }` **provider** — every copy/paste path routes
|
|
277
|
+
* through it. Supply this in a sandboxed Electron renderer, where
|
|
278
|
+
* `navigator.clipboard` silently no-ops, to bridge to the main-process
|
|
279
|
+
* clipboard over IPC.
|
|
280
|
+
*/
|
|
281
|
+
clipboard?: ClipboardOption;
|
|
282
|
+
/**
|
|
283
|
+
* OSC 52 clipboard policy — lets terminal programs (Claude Code, tmux, vim)
|
|
284
|
+
* set/read the system clipboard via `ESC ] 52`. `"copy"` (default) allows
|
|
285
|
+
* writes only; `"read"` / `"both"` also answer read requests (an exfiltration
|
|
286
|
+
* risk — opt in deliberately); `false` disables it. Routed through the same
|
|
287
|
+
* clipboard provider as {@link clipboard}; a no-op when clipboard is `false`.
|
|
288
|
+
* Default `"copy"`.
|
|
289
|
+
*/
|
|
290
|
+
osc52?: Osc52Mode;
|
|
291
|
+
/**
|
|
292
|
+
* Copy/paste UX convention: `"contextmenu"` (menu + Ctrl+Shift+C),
|
|
293
|
+
* `"linux"` (highlight-to-copy + middle-click paste), or `"winmac"`
|
|
294
|
+
* (Ctrl/Cmd+C copies the selection, Ctrl/Cmd+V pastes). Omit for the historical
|
|
295
|
+
* default (Ctrl+Shift+C + Cmd+C-with-selection). Ctrl+Shift+C always copies.
|
|
296
|
+
*/
|
|
297
|
+
copyPaste?: CopyPasteMode;
|
|
298
|
+
/**
|
|
299
|
+
* Called once the xterm instance is opened + attached — the imperative twin of
|
|
300
|
+
* {@link TerminalHandle.ready}. Use it to load an addon without racing the
|
|
301
|
+
* container layout (where `handle.xterm` is still null).
|
|
166
302
|
*/
|
|
167
|
-
|
|
303
|
+
onReady?: (xterm: Terminal$1) => void;
|
|
168
304
|
/**
|
|
169
305
|
* Fired on every paste with the clipboard payload — `{ text, files, images }`.
|
|
170
306
|
* Plain text still pastes into the terminal natively; this is where a host
|
|
@@ -401,4 +537,4 @@ interface MenuSize {
|
|
|
401
537
|
*/
|
|
402
538
|
declare function clampMenuPosition(at: MenuPoint, menu: MenuSize, viewport: MenuSize, margin?: number): MenuPoint;
|
|
403
539
|
|
|
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 };
|
|
540
|
+
export { BUILTIN_SHELLS, type ClipboardOption, type ClipboardPayload, type ClipboardProvider, type CopyKeyEvent, type CopyPasteBehavior, type CopyPasteMode, type CursorStyle, type KeyEventLike, type Osc52Mode, type Osc52Request, 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, copyPasteBehavior, decodeBase64, defaultMenuItems, encodeBase64, fancyDarkTheme, isImageFile, navigatorClipboard, osc52Response, parseOsc52, registerOsc52, resolveClipboard, resolveKeyAction, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
|
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(() => {
|
|
@@ -638,6 +787,9 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
638
787
|
onShellChange,
|
|
639
788
|
showShellBar = false,
|
|
640
789
|
clipboard,
|
|
790
|
+
osc52,
|
|
791
|
+
copyPaste,
|
|
792
|
+
onReady,
|
|
641
793
|
onPaste,
|
|
642
794
|
contextMenu,
|
|
643
795
|
className,
|
|
@@ -674,12 +826,32 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
674
826
|
onData,
|
|
675
827
|
onResize,
|
|
676
828
|
clipboard,
|
|
829
|
+
osc52,
|
|
830
|
+
copyPaste,
|
|
831
|
+
onReady,
|
|
677
832
|
onPaste
|
|
678
833
|
});
|
|
679
834
|
useImperativeHandle(
|
|
680
835
|
ref,
|
|
681
836
|
() => ({
|
|
682
|
-
|
|
837
|
+
get xterm() {
|
|
838
|
+
return handle.xterm;
|
|
839
|
+
},
|
|
840
|
+
get ready() {
|
|
841
|
+
return handle.ready;
|
|
842
|
+
},
|
|
843
|
+
write: handle.write,
|
|
844
|
+
writeln: handle.writeln,
|
|
845
|
+
clear: handle.clear,
|
|
846
|
+
reset: handle.reset,
|
|
847
|
+
fit: handle.fit,
|
|
848
|
+
focus: handle.focus,
|
|
849
|
+
getBuffer: handle.getBuffer,
|
|
850
|
+
getSelection: handle.getSelection,
|
|
851
|
+
copySelection: handle.copySelection,
|
|
852
|
+
paste: handle.paste,
|
|
853
|
+
selectAll: handle.selectAll,
|
|
854
|
+
clearSelection: handle.clearSelection,
|
|
683
855
|
setShell: selectShell,
|
|
684
856
|
getShell: () => shellStateRef.current.shellId
|
|
685
857
|
}),
|
|
@@ -814,6 +986,15 @@ function useTerminalSession(options) {
|
|
|
814
986
|
return { output, sendData, append, clear, shell, switchShell };
|
|
815
987
|
}
|
|
816
988
|
|
|
817
|
-
|
|
989
|
+
// src/copy-keybinding.ts
|
|
990
|
+
function shouldCopyEvent(e, hasSelection) {
|
|
991
|
+
if (e.type !== "keydown") return false;
|
|
992
|
+
if (e.key !== "c" && e.key !== "C") return false;
|
|
993
|
+
if (e.ctrlKey && e.shiftKey && !e.metaKey) return true;
|
|
994
|
+
if (e.metaKey && !e.ctrlKey && hasSelection) return true;
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export { BUILTIN_SHELLS, ShellSwitcher, Terminal, TerminalContextMenu, clampMenuPosition, copyPasteBehavior, decodeBase64, defaultMenuItems, encodeBase64, fancyDarkTheme, isImageFile, navigatorClipboard, osc52Response, parseOsc52, registerOsc52, resolveClipboard, resolveKeyAction, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
|
|
818
999
|
//# sourceMappingURL=index.js.map
|
|
819
1000
|
//# sourceMappingURL=index.js.map
|