@xzzpig/pi-starline 0.2.0 → 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 +7 -6
- package/extensions/starline/index.ts +34 -33
- package/extensions/starline/mouse/api-consumer.ts +31 -0
- package/extensions/starline/mouse/capabilities.ts +69 -115
- package/extensions/starline/mouse/index.ts +467 -536
- package/extensions/starline/mouse/tool-box.ts +112 -83
- package/extensions/starline/prototype-patch-registry.ts +8 -5
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -44,16 +44,17 @@ pieces of one product, not a theme:
|
|
|
44
44
|
|
|
45
45
|
The second half of the product: the statusline tells you where you are, the
|
|
46
46
|
mouse lets you act on what you see. Starline drives Pi 0.84's own renderer
|
|
47
|
-
(`tuiMode: "fullscreen"`)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
(`tuiMode: "fullscreen"`) through the [`@xzzpig/pi-mouse-events`](https://www.npmjs.com/package/@xzzpig/pi-mouse-events)
|
|
48
|
+
extension, which owns the renderer prototype and dispatches mouse events to
|
|
49
|
+
components — the extension API stops at high-level hooks, and the selection
|
|
50
|
+
hint, click-to-expand and clean-copy all need to reach into the renderer
|
|
51
|
+
itself. Install it alongside Starline; without it Starline's mouse features
|
|
52
|
+
are simply off (everything else keeps working).
|
|
52
53
|
|
|
53
54
|
- **Select and copy anywhere.** Drag to select in the transcript or the input box; double-click a word (paths and kebab-case stay whole), triple-click a line. Releasing copies — or set Pi's `fullscreenCopyOnSelect: false` and the selection stays highlighted, waiting for the copy key ([Pi #7720](https://github.com/earendil-works/pi/issues/7720), shipped in Pi 0.84.4).
|
|
54
55
|
- **Clean copies.** Transcript selections drop Starline's rails, rule rows and pi-toolbox frame borders before they reach the clipboard; editor selections copy the draft's text, not the painted rows.
|
|
55
56
|
- **Selection hint.** A selection that is not auto-copying shows exactly how many characters the copy key (`ctrl+x` by default, whatever `app.message.copy` is bound to) will copy, and a selection inside the editor names your editor — `ctrl+g to edit in nvim`.
|
|
56
|
-
- **Click to expand one
|
|
57
|
+
- **Click to expand one component.** A plain click anywhere on a tool box — title, output, hint row, borders — toggles just that box (MCP boxes included); bash boxes, skill blocks, branch summaries, compaction summaries and custom entries count too. The direction comes from the hint row when one is on screen and from the component's own state otherwise, so a box with no hint left once expanded still closes on a click. Only a plain click toggles: dragging across a box still selects and copies, a double-click still word-selects, and a click on a link still opens it.
|
|
57
58
|
- **Click to move the caret** in the input box; **backspace/delete remove a selected range**; the **wheel scrolls the draft** over the input box, and a draft taller than the box keeps the `ctrl+g` external-editor hint on screen.
|
|
58
59
|
|
|
59
60
|
0.2.x's `fixedEditor` config block was renamed to `mouse` (migrated
|
|
@@ -4,7 +4,8 @@ import type {
|
|
|
4
4
|
KeybindingsManager,
|
|
5
5
|
Theme,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
7
|
+
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { MouseEventsApi } from "@xzzpig/pi-mouse-events/api";
|
|
8
9
|
import {
|
|
9
10
|
type ColorSourcesConfig,
|
|
10
11
|
type ContextStyle,
|
|
@@ -42,7 +43,8 @@ import { installFooter } from "./footer";
|
|
|
42
43
|
import { buildSessionDurationLabel, invalidateUsageTotalsCache } from "./format";
|
|
43
44
|
import { emptyGitStatus, readGitHost, readGitStatus } from "./git";
|
|
44
45
|
import { LiveContextController } from "./live-context";
|
|
45
|
-
import {
|
|
46
|
+
import { installMouseFeaturesOn } from "./mouse";
|
|
47
|
+
import { getMouseEventsApi } from "./mouse/api-consumer";
|
|
46
48
|
import { setActiveEditor } from "./mouse/editor-mouse";
|
|
47
49
|
import { readPackageVersionResult } from "./package-version";
|
|
48
50
|
import { installPasteCollapse } from "./paste-collapse";
|
|
@@ -99,6 +101,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
101
|
let lastProjectCwd: string | undefined;
|
|
100
102
|
let disposePasteCollapse: (() => void) | undefined;
|
|
101
103
|
let disposeMouse: (() => void) | undefined;
|
|
104
|
+
let lastTuiCtx: ExtensionContext | undefined;
|
|
102
105
|
|
|
103
106
|
const refresh = () => {
|
|
104
107
|
if (sessionLifecycle.isCurrent()) requestFooterRender?.();
|
|
@@ -236,44 +239,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
239
|
};
|
|
237
240
|
|
|
238
241
|
/**
|
|
239
|
-
*
|
|
242
|
+
* Register Starline's mouse features with the `pi-mouse-events` extension.
|
|
240
243
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* adapter and only swaps the behaviour behind it — but disposing first keeps
|
|
248
|
-
* exactly one live registration, so the disposer held here always removes
|
|
249
|
-
* everything this extension put on the prototype.
|
|
244
|
+
* That extension owns `TuiAltScreen.prototype` now — Starline wraps none of
|
|
245
|
+
* it — so this only hands it behaviour: handlers for the wheel, press, and
|
|
246
|
+
* copy slots, and the keyboard half of click-to-caret on
|
|
247
|
+
* `ctx.ui.onTerminalInput`. Without the extension installed there is no
|
|
248
|
+
* fallback and nothing is registered; the mouse features are simply off,
|
|
249
|
+
* with one console note per process so the silence is diagnosable.
|
|
250
250
|
*
|
|
251
251
|
* `mouse.enabled` is read here rather than captured, and every sub-option is
|
|
252
|
-
* read inside the
|
|
253
|
-
* restart.
|
|
252
|
+
* read inside the handlers themselves, so `/starline` toggles apply without
|
|
253
|
+
* a restart.
|
|
254
254
|
*/
|
|
255
|
-
const
|
|
255
|
+
const installMouseFeatures = (ctx: ExtensionContext) => {
|
|
256
256
|
uninstallMouse();
|
|
257
|
+
lastTuiCtx = ctx;
|
|
257
258
|
if (!getCurrentConfig().mouse.enabled) return;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// `TuiAltScreen.prototype`.
|
|
262
|
-
if (!TuiAltScreen) {
|
|
263
|
-
console.warn(
|
|
264
|
-
"starline: mouse support needs @earendil-works/pi-tui >= 0.84.0 (" +
|
|
265
|
-
"TuiAltScreen is not exported by the installed pi-tui). Upgrade Pi, " +
|
|
266
|
-
"or wait for your distribution to bundle a newer one.",
|
|
267
|
-
);
|
|
259
|
+
const api = getMouseEventsApi();
|
|
260
|
+
if (!api) {
|
|
261
|
+
warnMouseApiMissing();
|
|
268
262
|
return;
|
|
269
263
|
}
|
|
270
|
-
|
|
271
|
-
// the live renderer it is running inside — to render. Handing it the
|
|
272
|
-
// extension's `refresh` instead tied the pending hint to the footer's
|
|
273
|
-
// existence, and the hint is in the editor.
|
|
274
|
-
disposeMouse = installMouse(TuiAltScreen.prototype, { getConfig: getCurrentConfig });
|
|
264
|
+
disposeMouse = installMouseFeaturesOn(api, ctx, { getConfig: getCurrentConfig });
|
|
275
265
|
};
|
|
276
266
|
|
|
267
|
+
let mouseApiWarned = false;
|
|
268
|
+
function warnMouseApiMissing() {
|
|
269
|
+
if (mouseApiWarned) return;
|
|
270
|
+
mouseApiWarned = true;
|
|
271
|
+
console.info(
|
|
272
|
+
"[starline] mouse features need the pi-mouse-events extension " +
|
|
273
|
+
"(`pi install npm:@xzzpig/pi-mouse-events`); they are off until it is installed.",
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
277
|
/**
|
|
278
278
|
* `editorCursor: "terminal"` hides the software cursor so the real one shows
|
|
279
279
|
* through, which needs the hardware cursor to actually be on. Pi re-applies
|
|
@@ -484,7 +484,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
484
484
|
syncFooterState(ctx);
|
|
485
485
|
stopProjectRefresh();
|
|
486
486
|
applyConfiguredUi(ctx);
|
|
487
|
-
|
|
487
|
+
installMouseFeatures(ctx);
|
|
488
488
|
refresh();
|
|
489
489
|
};
|
|
490
490
|
|
|
@@ -509,6 +509,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
509
509
|
stopProjectRefresh();
|
|
510
510
|
requestFooterRender = undefined;
|
|
511
511
|
getActiveExtensionStatuses = () => new Map();
|
|
512
|
+
lastTuiCtx = undefined;
|
|
512
513
|
if (isTuiContext(ctx)) {
|
|
513
514
|
ctx.ui.setFooter(undefined);
|
|
514
515
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
@@ -595,10 +596,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
595
596
|
setExtensionStatusColorMode(key: string, colorMode: ExtensionStatusColorMode) {
|
|
596
597
|
currentConfig = saveExtensionStatusColorMode(key, colorMode);
|
|
597
598
|
},
|
|
598
|
-
setMouseConfig(patch: Partial<MouseConfig>,
|
|
599
|
+
setMouseConfig(patch: Partial<MouseConfig>, ctx: ExtensionContext) {
|
|
599
600
|
currentConfig = saveMousePatch(patch);
|
|
600
601
|
if (patch.enabled === true) {
|
|
601
|
-
|
|
602
|
+
installMouseFeatures(lastTuiCtx ?? ctx);
|
|
602
603
|
} else if (patch.enabled === false) {
|
|
603
604
|
uninstallMouse();
|
|
604
605
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consumer-side accessor for the `pi-mouse-events` extension's API.
|
|
3
|
+
*
|
|
4
|
+
* All Starline needs from that package is its published runtime object, and
|
|
5
|
+
* the package publishes it under a process-global `Symbol.for` key precisely
|
|
6
|
+
* so consumers do not have to import it: Pi loads every extension through a
|
|
7
|
+
* jiti instance with `moduleCache: false`, so a static value import of
|
|
8
|
+
* `@xzzpig/pi-mouse-events/api` would both give Starline its own fresh copy
|
|
9
|
+
* of the module *and* make the whole extension fail to load when the package
|
|
10
|
+
* is not installed — the one situation this integration must survive
|
|
11
|
+
* gracefully. A type-only import carries the contract's types at development
|
|
12
|
+
* time; this duplicated key string is the only runtime surface, and
|
|
13
|
+
* `test/contract/mouse-api.test.ts` asserts it matches the real package's.
|
|
14
|
+
*
|
|
15
|
+
* `Symbol.for()` is process-global by spec, so the accessor observes exactly
|
|
16
|
+
* what the extension wrote, whatever module copy this file was loaded in.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { MouseEventsApi } from "@xzzpig/pi-mouse-events/api";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MUST match `MOUSE_EVENTS_API_KEY` in `@xzzpig/pi-mouse-events/api` — the
|
|
23
|
+
* contract test above is what keeps that true.
|
|
24
|
+
*/
|
|
25
|
+
const MOUSE_EVENTS_API_KEY = Symbol.for("pi-mouse-events.api.v1");
|
|
26
|
+
|
|
27
|
+
export function getMouseEventsApi(): MouseEventsApi | undefined {
|
|
28
|
+
return (globalThis as Record<symbol, unknown>)[MOUSE_EVENTS_API_KEY] as
|
|
29
|
+
| MouseEventsApi
|
|
30
|
+
| undefined;
|
|
31
|
+
}
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What of
|
|
2
|
+
* What of the live renderer Starline's mouse features can actually reach.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Starline no longer patches anything — the `pi-mouse-events` extension owns
|
|
5
|
+
* the prototype — so the probe is about *calling*, not replacing: every
|
|
6
|
+
* method a feature needs on the receiver must exist and be callable, probed
|
|
7
|
+
* structurally on the instance the extension hands over (`ctx.ui`, the Proxy
|
|
8
|
+
* that forwards to the live renderer). Signature drift is caught by the
|
|
9
|
+
* contract tests rather than guessed at here.
|
|
9
10
|
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* call needs (it also demands writable and configurable) costs nothing, because
|
|
16
|
-
* a class method declared the ordinary way is both.
|
|
17
|
-
*
|
|
18
|
-
* Since 0.84.4 the peer floor guarantees `copyActiveSelectionToClipboard`,
|
|
19
|
-
* `getCopyOnSelect` and `hasActiveSelection` exist; the probe still runs so a
|
|
20
|
-
* future Pi that moves them degrades feature-by-feature instead of breaking
|
|
21
|
-
* the whole install.
|
|
11
|
+
* The `pi-mouse-events` API gates everything before this table runs: without
|
|
12
|
+
* it no feature registers at all (see `installMouseFeaturesOn`). The one API
|
|
13
|
+
* capability that varies on its own is the copy slot — the extension installs
|
|
14
|
+
* it only when `copyActiveSelectionToClipboard` exists (pi-tui ≥ 0.84.3) —
|
|
15
|
+
* so it arrives as `copySlotAvailable` rather than being probed here.
|
|
22
16
|
*/
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| "routeWheel"
|
|
27
|
-
| "handleSelectionMouseEvent"
|
|
28
|
-
| "copyActiveSelectionToClipboard"
|
|
18
|
+
/** The receiver methods the features call, probed on the live renderer. */
|
|
19
|
+
export type MouseReceiverCapabilities =
|
|
29
20
|
| "getCopyOnSelect"
|
|
30
21
|
| "hasActiveSelection"
|
|
31
22
|
| "getSelectionBounds"
|
|
@@ -34,6 +25,8 @@ export type MouseCapability =
|
|
|
34
25
|
| "hasOverlay"
|
|
35
26
|
| "requestRender";
|
|
36
27
|
|
|
28
|
+
export type MouseCapability = MouseReceiverCapabilities | "copySlot";
|
|
29
|
+
|
|
37
30
|
export type MouseFeature =
|
|
38
31
|
| "selectionHint"
|
|
39
32
|
| "clickToExpandTools"
|
|
@@ -43,10 +36,6 @@ export type MouseFeature =
|
|
|
43
36
|
| "transcriptCleanCopy";
|
|
44
37
|
|
|
45
38
|
const CAPABILITIES: readonly MouseCapability[] = [
|
|
46
|
-
"handleViewportInput",
|
|
47
|
-
"routeWheel",
|
|
48
|
-
"handleSelectionMouseEvent",
|
|
49
|
-
"copyActiveSelectionToClipboard",
|
|
50
39
|
"getCopyOnSelect",
|
|
51
40
|
"hasActiveSelection",
|
|
52
41
|
"getSelectionBounds",
|
|
@@ -54,19 +43,21 @@ const CAPABILITIES: readonly MouseCapability[] = [
|
|
|
54
43
|
"flash",
|
|
55
44
|
"hasOverlay",
|
|
56
45
|
"requestRender",
|
|
46
|
+
"copySlot",
|
|
57
47
|
];
|
|
58
48
|
|
|
59
49
|
/**
|
|
60
|
-
* Every capability a feature needs before it may
|
|
50
|
+
* Every capability a feature needs before it may register.
|
|
61
51
|
*
|
|
62
52
|
* There is no `frameFreeSelection` entry, deliberately: the feature is cut
|
|
63
|
-
* (see
|
|
64
|
-
*
|
|
65
|
-
*
|
|
53
|
+
* (see the header of `mouse/index.ts` before the clean-copy successor
|
|
54
|
+
* replaced it). A feature listed here is one `installMouseFeaturesOn`
|
|
55
|
+
* registers, and a table that claims a feature nothing registers is worse
|
|
56
|
+
* than no table.
|
|
66
57
|
*/
|
|
67
58
|
const REQUIREMENTS: Record<MouseFeature, readonly MouseCapability[]> = {
|
|
68
|
-
// Derived from Pi
|
|
69
|
-
//
|
|
59
|
+
// Derived from Pi's own select-without-copy state: the hint shows only
|
|
60
|
+
// while the renderer is NOT auto-copying (`getCopyOnSelect` false) and
|
|
70
61
|
// something is actually selected (`hasActiveSelection`). The character
|
|
71
62
|
// count is computed from the live selection through `getSelectionBounds`
|
|
72
63
|
// and `getSelectionColumns`, matching the columns the copy itself would
|
|
@@ -79,74 +70,42 @@ const REQUIREMENTS: Record<MouseFeature, readonly MouseCapability[]> = {
|
|
|
79
70
|
"getSelectionBounds",
|
|
80
71
|
"getSelectionColumns",
|
|
81
72
|
],
|
|
82
|
-
// The press
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// a
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
clickToExpandTools: ["
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
// `
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// silently does not, is exactly the half-working install the rule exists to
|
|
117
|
-
// prevent.
|
|
118
|
-
editorClickToCaret: [
|
|
119
|
-
"handleSelectionMouseEvent",
|
|
120
|
-
"handleViewportInput",
|
|
121
|
-
"getSelectionBounds",
|
|
122
|
-
"hasOverlay",
|
|
123
|
-
"requestRender",
|
|
124
|
-
],
|
|
125
|
-
// It wraps `copyActiveSelectionToClipboard` — the method Pi 0.84.4's
|
|
126
|
-
// Ctrl+X handler reaches (`handleCopyCommand` with `preferSelection`), so
|
|
127
|
-
// the clean copy answers exactly the copies that key performs — and reads
|
|
128
|
-
// the selection through `getSelectionBounds` to find out whether it is the
|
|
129
|
-
// editor's. `hasOverlay` again: a selection dropped on a dialog must not be
|
|
130
|
-
// read as text from the draft behind it.
|
|
131
|
-
//
|
|
132
|
-
// The clipboard write goes through `terminal.write`, which is not listed
|
|
133
|
-
// because it is not on this prototype — `terminal` is a plain instance field
|
|
134
|
-
// holding somebody else's object, the same class of thing as `currentLayout`.
|
|
135
|
-
// It is checked at the call site instead, and a terminal that cannot be
|
|
136
|
-
// written to makes the copy fall through to Pi rather than disabling the
|
|
137
|
-
// feature at install time.
|
|
138
|
-
editorBufferCopy: ["copyActiveSelectionToClipboard", "getSelectionBounds", "hasOverlay", "flash"],
|
|
139
|
-
// Shares the `copyActiveSelectionToClipboard` patch with the two features
|
|
140
|
-
// above it. It reads the selection through `getSelectionBounds` to find the
|
|
141
|
-
// rows, through `getSelectionColumns` to slice the cleaned text along the
|
|
142
|
-
// same grapheme-aligned columns Pi would have used, asks `hasOverlay` before
|
|
143
|
-
// trusting those rows (an overlay is composited over a layout that still
|
|
144
|
-
// contains the transcript), and raises its own "Copied!" through `flash`
|
|
145
|
-
// when it answers the copy itself. The clipboard write goes through
|
|
146
|
-
// `terminal.write`, unlisted for the same reason as editorBufferCopy's — it
|
|
147
|
-
// is an instance field, checked at the call site instead.
|
|
73
|
+
// The press arrives through the `pi-mouse-events` handler slot, which
|
|
74
|
+
// always exists once the API does; what this feature needs beyond that is
|
|
75
|
+
// `hasOverlay` — the same question Pi's own press path asks before
|
|
76
|
+
// resolving a scroll view, without which a click on a dialog would toggle
|
|
77
|
+
// whatever tool box happens to sit behind it — and `requestRender`, because
|
|
78
|
+
// consuming the press means Pi never reaches its own repaint for that
|
|
79
|
+
// event.
|
|
80
|
+
clickToExpandTools: ["hasOverlay", "requestRender"],
|
|
81
|
+
// Same shape as click-to-expand: the notch arrives through the handler
|
|
82
|
+
// slot, `hasOverlay` keeps a notch aimed at a dialog from scrolling the
|
|
83
|
+
// draft hidden behind it, and consuming the notch means `requestRender` is
|
|
84
|
+
// the only thing that draws the scrolled box.
|
|
85
|
+
editorWheelScroll: ["hasOverlay", "requestRender"],
|
|
86
|
+
// The press half rides the same slot as click-to-expand; the keyboard half
|
|
87
|
+
// (backspace/delete over a selection) rides `ctx.ui.onTerminalInput`, which
|
|
88
|
+
// every interactive session exposes. Range delete reads the selection
|
|
89
|
+
// through `getSelectionBounds`, and because it *consumes* the key — so
|
|
90
|
+
// Pi's `handleInput` never reaches its own repaint — `requestRender` is
|
|
91
|
+
// the only thing that draws the shortened draft.
|
|
92
|
+
editorClickToCaret: ["getSelectionBounds", "hasOverlay", "requestRender"],
|
|
93
|
+
// Registers an `addCopyHandler`; needs the extension's copy slot to exist
|
|
94
|
+
// (`copyActiveSelectionToClipboard`, pi-tui ≥ 0.84.3), reads the selection
|
|
95
|
+
// through `getSelectionBounds` to find out whether it is the editor's, and
|
|
96
|
+
// raises its own "Copied!" through `flash`. `hasOverlay` again: a selection
|
|
97
|
+
// dropped on a dialog must not be read as text from the draft behind it.
|
|
98
|
+
// The clipboard write goes through `terminal.write`, unlisted because
|
|
99
|
+
// `terminal` is a plain instance field — checked at the call site, where a
|
|
100
|
+
// terminal that cannot be written to makes the copy fall through to Pi.
|
|
101
|
+
editorBufferCopy: ["copySlot", "getSelectionBounds", "hasOverlay", "flash"],
|
|
102
|
+
// Shares the copy slot with editorBufferCopy. It reads the selection
|
|
103
|
+
// through `getSelectionBounds` to find the rows, through
|
|
104
|
+
// `getSelectionColumns` to slice the cleaned text along the same
|
|
105
|
+
// grapheme-aligned columns Pi would have used, and asks `hasOverlay`
|
|
106
|
+
// before trusting those rows.
|
|
148
107
|
transcriptCleanCopy: [
|
|
149
|
-
"
|
|
108
|
+
"copySlot",
|
|
150
109
|
"getSelectionBounds",
|
|
151
110
|
"getSelectionColumns",
|
|
152
111
|
"hasOverlay",
|
|
@@ -154,41 +113,36 @@ const REQUIREMENTS: Record<MouseFeature, readonly MouseCapability[]> = {
|
|
|
154
113
|
],
|
|
155
114
|
};
|
|
156
115
|
|
|
157
|
-
function
|
|
116
|
+
function isCallable(receiver: object, name: string): boolean {
|
|
158
117
|
try {
|
|
159
|
-
|
|
160
|
-
while (current) {
|
|
161
|
-
const descriptor = Object.getOwnPropertyDescriptor(current, name);
|
|
162
|
-
if (descriptor) {
|
|
163
|
-
if (typeof descriptor.value !== "function") return false;
|
|
164
|
-
return descriptor.writable === true && descriptor.configurable === true;
|
|
165
|
-
}
|
|
166
|
-
current = Object.getPrototypeOf(current);
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
118
|
+
return typeof (receiver as Record<string, unknown>)[name] === "function";
|
|
169
119
|
} catch {
|
|
170
|
-
// A
|
|
120
|
+
// A receiver that throws on inspection is one we do not touch.
|
|
171
121
|
return false;
|
|
172
122
|
}
|
|
173
123
|
}
|
|
174
124
|
|
|
175
|
-
export function probeCapabilities(
|
|
125
|
+
export function probeCapabilities(receiver: object): ReadonlySet<MouseCapability> {
|
|
176
126
|
const found = new Set<MouseCapability>();
|
|
177
127
|
for (const capability of CAPABILITIES) {
|
|
178
|
-
if (
|
|
128
|
+
if (capability === "copySlot") continue;
|
|
129
|
+
if (isCallable(receiver, capability)) found.add(capability);
|
|
179
130
|
}
|
|
180
131
|
return found;
|
|
181
132
|
}
|
|
182
133
|
|
|
183
134
|
export function enabledFeatures(
|
|
184
135
|
available: ReadonlySet<MouseCapability>,
|
|
136
|
+
copySlotAvailable: boolean,
|
|
185
137
|
): ReadonlySet<MouseFeature> {
|
|
138
|
+
const withSlot = new Set(available);
|
|
139
|
+
if (copySlotAvailable) withSlot.add("copySlot");
|
|
186
140
|
const enabled = new Set<MouseFeature>();
|
|
187
141
|
for (const [feature, needed] of Object.entries(REQUIREMENTS) as [
|
|
188
142
|
MouseFeature,
|
|
189
143
|
readonly MouseCapability[],
|
|
190
144
|
][]) {
|
|
191
|
-
if (needed.every((capability) =>
|
|
145
|
+
if (needed.every((capability) => withSlot.has(capability))) enabled.add(feature);
|
|
192
146
|
}
|
|
193
147
|
return enabled;
|
|
194
148
|
}
|