@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/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 { ITheme, Terminal as Terminal$1 } from '@xterm/xterm';
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
- * 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.
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
- clipboard?: boolean;
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 { ITheme, Terminal as Terminal$1 } from '@xterm/xterm';
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
- * 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.
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
- clipboard?: boolean;
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-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;
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 writeClipboardText(sel);
275
+ return providerWrite(resolveClipboard(optsRef.current.clipboard).provider, sel);
170
276
  },
171
277
  paste: async (text) => {
172
- const data = text ?? await readClipboardText();
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
- if (optsRef.current.clipboard === false) return true;
216
- if (shouldCopyEvent(e, term.hasSelection())) {
217
- void writeClipboardText(term.getSelection());
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
- ...handle,
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
- export { BUILTIN_SHELLS, ShellSwitcher, Terminal, TerminalContextMenu, clampMenuPosition, defaultMenuItems, fancyDarkTheme, isImageFile, resolveMenuItems, resolveShell, shouldCopyEvent, useTerminal, useTerminalFit, useTerminalSession };
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