@nuvin/ink 7.0.2-alpha → 7.1.0-alpha

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.
Files changed (53) hide show
  1. package/build/components/App.d.ts +3 -1
  2. package/build/components/App.js +4 -2
  3. package/build/components/App.js.map +1 -1
  4. package/build/components/Text.d.ts +9 -1
  5. package/build/components/Text.js +2 -2
  6. package/build/components/Text.js.map +1 -1
  7. package/build/components/TextSelectionContext.d.ts +6 -0
  8. package/build/components/TextSelectionContext.js +7 -0
  9. package/build/components/TextSelectionContext.js.map +1 -0
  10. package/build/dom.d.ts +1 -0
  11. package/build/dom.js.map +1 -1
  12. package/build/hooks/use-text-selection-actions.d.ts +10 -0
  13. package/build/hooks/use-text-selection-actions.js +25 -0
  14. package/build/hooks/use-text-selection-actions.js.map +1 -0
  15. package/build/hooks/use-text-selection.d.ts +9 -0
  16. package/build/hooks/use-text-selection.js +25 -0
  17. package/build/hooks/use-text-selection.js.map +1 -0
  18. package/build/index.d.ts +4 -0
  19. package/build/index.js +2 -0
  20. package/build/index.js.map +1 -1
  21. package/build/ink.d.ts +1 -0
  22. package/build/ink.js +12 -2
  23. package/build/ink.js.map +1 -1
  24. package/build/output.d.ts +9 -1
  25. package/build/output.js +31 -4
  26. package/build/output.js.map +1 -1
  27. package/build/paint-selection.d.ts +12 -2
  28. package/build/paint-selection.js +45 -98
  29. package/build/paint-selection.js.map +1 -1
  30. package/build/reconciler.js +8 -0
  31. package/build/reconciler.js.map +1 -1
  32. package/build/render-node-to-output.js +8 -2
  33. package/build/render-node-to-output.js.map +1 -1
  34. package/build/renderer.d.ts +2 -1
  35. package/build/renderer.js +13 -2
  36. package/build/renderer.js.map +1 -1
  37. package/build/text-selection-controller.d.ts +106 -0
  38. package/build/text-selection-controller.js +311 -0
  39. package/build/text-selection-controller.js.map +1 -0
  40. package/build/text-selection.d.ts +41 -0
  41. package/build/text-selection.js +101 -0
  42. package/build/text-selection.js.map +1 -0
  43. package/package.json +1 -1
  44. package/readme.md +46 -0
  45. package/build/current-frame.d.ts +0 -27
  46. package/build/current-frame.js +0 -73
  47. package/build/current-frame.js.map +0 -1
  48. package/build/hooks/use-selection-overlay.d.ts +0 -3
  49. package/build/hooks/use-selection-overlay.js +0 -14
  50. package/build/hooks/use-selection-overlay.js.map +0 -1
  51. package/build/selection.d.ts +0 -28
  52. package/build/selection.js +0 -9
  53. package/build/selection.js.map +0 -1
@@ -0,0 +1,106 @@
1
+ import { type TextSelectionPoint, type TextSelectionSpan } from './text-selection.js';
2
+ /**
3
+ Live viewport of the selection container, in Ink output grid cells.
4
+ */
5
+ export type SelectionViewport = {
6
+ top: number;
7
+ left: number;
8
+ width: number;
9
+ height: number;
10
+ /** Vertical scroll offset in rendered rows: content row shown at `top`. */
11
+ scrollY: number;
12
+ };
13
+ export type TextSelectionSnapshot = {
14
+ /** Content coordinates (see TextSelectionPoint). */
15
+ anchor: TextSelectionPoint | null;
16
+ focus: TextSelectionPoint | null;
17
+ isDragging: boolean;
18
+ text: string;
19
+ isEmpty: boolean;
20
+ };
21
+ export type TextSelectionActions = {
22
+ /**
23
+ Points passed to start/update are 0-based SCREEN cells on the Ink output
24
+ grid; the controller converts to content space via the viewport provider.
25
+ */
26
+ start: (screenPoint: {
27
+ x: number;
28
+ y: number;
29
+ }) => void;
30
+ update: (screenPoint: {
31
+ x: number;
32
+ y: number;
33
+ }) => void;
34
+ finish: () => void;
35
+ clear: () => void;
36
+ /**
37
+ Registered by the mouse bridge. Returning null means "no scroll container":
38
+ the whole output grid is the viewport with scrollY = 0.
39
+ */
40
+ setViewportProvider: (provider: (() => SelectionViewport | null) | null) => void;
41
+ };
42
+ /**
43
+ Selection actions plus an imperative snapshot read. Unlike the snapshot from
44
+ `useTextSelection()`, holding this handle does NOT subscribe the component to
45
+ selection changes — use it where selection state is only needed at event time
46
+ (mouse bridges, key handlers) so drags never re-render the host component.
47
+ */
48
+ export type TextSelectionHandle = TextSelectionActions & {
49
+ getSnapshot: () => TextSelectionSnapshot;
50
+ };
51
+ /**
52
+ Framework-agnostic selection state owned by an Ink instance. Selection points
53
+ live in content space (container-relative rows that survive scrolling); the
54
+ viewport provider converts screen ↔ content at event time and paint time.
55
+ */
56
+ export declare class TextSelectionController {
57
+ /** Wired by Ink to schedule a repaint when the highlight must change. */
58
+ onInvalidate?: () => void;
59
+ private anchor;
60
+ private focus;
61
+ private isDragging;
62
+ private text;
63
+ private readonly rowCache;
64
+ private viewportProvider;
65
+ private lastViewport;
66
+ private readonly listeners;
67
+ private snapshot;
68
+ private notifyScheduled;
69
+ subscribe: (listener: () => void) => (() => void);
70
+ getSnapshot: () => TextSelectionSnapshot;
71
+ setViewportProvider: (provider: (() => SelectionViewport | null) | null) => void;
72
+ start: (screenPoint: {
73
+ x: number;
74
+ y: number;
75
+ }) => void;
76
+ update: (screenPoint: {
77
+ x: number;
78
+ y: number;
79
+ }) => void;
80
+ finish: () => void;
81
+ clear: () => void;
82
+ /**
83
+ Called from the renderer's paint hook on every render of the main output.
84
+ Caches visible rows under content-row keys. Never invalidates synchronously
85
+ — it runs during a render pass and must not schedule another render.
86
+
87
+ While dragging, cache entries update freely (auto-scroll depends on it).
88
+ After `finish()`, the cache is frozen: existing entries are never
89
+ overwritten, so extracted text is immutable. If a visible row inside the
90
+ selected range no longer matches its frozen entry (content reflowed
91
+ underneath the selection), the selection auto-clears.
92
+ */
93
+ captureRows: (plainRows: readonly string[], maskRows?: ReadonlyArray<readonly boolean[]>) => void;
94
+ /**
95
+ Convert the selection's content spans to screen spans using the current
96
+ viewport. Rows outside the viewport or the grid are dropped (they are
97
+ still part of the extracted text).
98
+ */
99
+ getPaintSpans: (outputWidth: number, outputHeight: number) => TextSelectionSpan[];
100
+ private resolveViewport;
101
+ private toContentPoint;
102
+ /** Returns true when the extracted text changed. */
103
+ private recomputeText;
104
+ private emit;
105
+ private scheduleNotify;
106
+ }
@@ -0,0 +1,311 @@
1
+ /* eslint-disable @typescript-eslint/no-restricted-types --
2
+ The public selection API uses null (matching React ref idioms and the
3
+ mouse bridge in ink-input); keep it consistent throughout this file. */
4
+ import stringWidth from 'string-width';
5
+ import { extractTextFromRows, getSelectionSpans, } from './text-selection.js';
6
+ const fullGridViewport = {
7
+ top: 0,
8
+ left: 0,
9
+ width: Number.POSITIVE_INFINITY,
10
+ height: Number.POSITIVE_INFINITY,
11
+ scrollY: 0,
12
+ };
13
+ /**
14
+ Framework-agnostic selection state owned by an Ink instance. Selection points
15
+ live in content space (container-relative rows that survive scrolling); the
16
+ viewport provider converts screen ↔ content at event time and paint time.
17
+ */
18
+ export class TextSelectionController {
19
+ /** Wired by Ink to schedule a repaint when the highlight must change. */
20
+ onInvalidate;
21
+ anchor = null;
22
+ focus = null;
23
+ isDragging = false;
24
+ text = '';
25
+ rowCache = new Map();
26
+ viewportProvider = null;
27
+ // Last viewport a registered provider returned. Reused for one-frame gaps
28
+ // where the provider returns null (e.g. bounds momentarily 0 mid-re-render),
29
+ // so the highlight doesn't flash to full-grid coordinates and the frozen
30
+ // row cache isn't compared against a differently-sliced viewport.
31
+ lastViewport = null;
32
+ listeners = new Set();
33
+ snapshot;
34
+ notifyScheduled = false;
35
+ subscribe = (listener) => {
36
+ this.listeners.add(listener);
37
+ return () => {
38
+ this.listeners.delete(listener);
39
+ };
40
+ };
41
+ getSnapshot = () => {
42
+ this.snapshot ??= {
43
+ anchor: this.anchor,
44
+ focus: this.focus,
45
+ isDragging: this.isDragging,
46
+ text: this.text,
47
+ isEmpty: this.text.length === 0,
48
+ };
49
+ return this.snapshot;
50
+ };
51
+ setViewportProvider = (provider) => {
52
+ this.viewportProvider = provider;
53
+ };
54
+ start = (screenPoint) => {
55
+ const point = this.toContentPoint(screenPoint);
56
+ this.anchor = point;
57
+ this.focus = point;
58
+ this.isDragging = true;
59
+ this.recomputeText();
60
+ this.emit();
61
+ this.onInvalidate?.();
62
+ };
63
+ update = (screenPoint) => {
64
+ if (!this.anchor) {
65
+ return;
66
+ }
67
+ const point = this.toContentPoint(screenPoint);
68
+ const focusChanged = this.focus?.x !== point.x || this.focus.y !== point.y;
69
+ this.focus = point;
70
+ const textChanged = this.recomputeText();
71
+ if (focusChanged || textChanged) {
72
+ this.emit();
73
+ this.onInvalidate?.();
74
+ }
75
+ };
76
+ finish = () => {
77
+ if (!this.isDragging) {
78
+ return;
79
+ }
80
+ this.isDragging = false;
81
+ this.emit();
82
+ };
83
+ clear = () => {
84
+ const hadSelection = this.anchor !== null || this.text.length > 0 || this.isDragging;
85
+ this.anchor = null;
86
+ this.focus = null;
87
+ this.isDragging = false;
88
+ this.text = '';
89
+ this.rowCache.clear();
90
+ if (hadSelection) {
91
+ this.emit();
92
+ this.onInvalidate?.();
93
+ }
94
+ };
95
+ /**
96
+ Called from the renderer's paint hook on every render of the main output.
97
+ Caches visible rows under content-row keys. Never invalidates synchronously
98
+ — it runs during a render pass and must not schedule another render.
99
+
100
+ While dragging, cache entries update freely (auto-scroll depends on it).
101
+ After `finish()`, the cache is frozen: existing entries are never
102
+ overwritten, so extracted text is immutable. If a visible row inside the
103
+ selected range no longer matches its frozen entry (content reflowed
104
+ underneath the selection), the selection auto-clears.
105
+ */
106
+ captureRows = (plainRows, maskRows) => {
107
+ const resolved = this.resolveViewport();
108
+ // A registered provider that returns null means the container's bounds are
109
+ // not measurable this frame (transient during a re-render). Capturing or
110
+ // comparing against the full-grid fallback would corrupt the cache and
111
+ // spuriously auto-clear a finished selection. Skip the frame instead.
112
+ if (!resolved.available && this.anchor !== null) {
113
+ return;
114
+ }
115
+ const { viewport } = resolved;
116
+ const top = Math.max(0, viewport.top);
117
+ const left = Math.max(0, viewport.left);
118
+ const bottom = Math.min(plainRows.length, viewport.top + viewport.height);
119
+ const hasSelection = this.anchor !== null && this.focus !== null;
120
+ const frozen = hasSelection && !this.isDragging;
121
+ // Without a selection there is nothing to preserve: reset the cache to
122
+ // the currently visible rows so a subsequent start() has fresh data and
123
+ // stale rows from earlier scroll positions don't accumulate.
124
+ if (!hasSelection) {
125
+ this.rowCache.clear();
126
+ }
127
+ let mismatch = false;
128
+ const range = hasSelection
129
+ ? (() => {
130
+ const [a, b] = [this.anchor, this.focus];
131
+ return a.y <= b.y ? { min: a.y, max: b.y } : { min: b.y, max: a.y };
132
+ })()
133
+ : undefined;
134
+ for (let y = top; y < bottom; y++) {
135
+ const contentY = viewport.scrollY + (y - viewport.top);
136
+ const text = sliceRow(plainRows[y] ?? '', left, viewport.width);
137
+ const mask = maskRows
138
+ ? sliceMask(maskRows[y], left, viewport.width)
139
+ : undefined;
140
+ const row = { text, mask };
141
+ const existing = this.rowCache.get(contentY);
142
+ if (frozen && existing) {
143
+ if (range &&
144
+ contentY >= range.min &&
145
+ contentY <= range.max &&
146
+ existing.text !== text) {
147
+ mismatch = true;
148
+ break;
149
+ }
150
+ continue;
151
+ }
152
+ this.rowCache.set(contentY, row);
153
+ }
154
+ if (mismatch) {
155
+ // Content reflowed underneath a finished selection: clearing is the
156
+ // only honest option. Defer the notification outside the render pass.
157
+ this.anchor = null;
158
+ this.focus = null;
159
+ this.isDragging = false;
160
+ this.text = '';
161
+ this.rowCache.clear();
162
+ this.snapshot = undefined;
163
+ this.scheduleNotify(true);
164
+ return;
165
+ }
166
+ if (hasSelection && !frozen && this.recomputeText()) {
167
+ this.scheduleNotify(false);
168
+ }
169
+ else if (hasSelection &&
170
+ frozen &&
171
+ this.text.length === 0 && // Rows may have arrived only after the gesture ended.
172
+ this.recomputeText()) {
173
+ this.scheduleNotify(false);
174
+ }
175
+ };
176
+ /**
177
+ Convert the selection's content spans to screen spans using the current
178
+ viewport. Rows outside the viewport or the grid are dropped (they are
179
+ still part of the extracted text).
180
+ */
181
+ getPaintSpans = (outputWidth, outputHeight) => {
182
+ if (!this.anchor || !this.focus) {
183
+ return [];
184
+ }
185
+ const { viewport } = this.resolveViewport();
186
+ const spans = getSelectionSpans(this.anchor, this.focus, y => this.rowCache.get(y));
187
+ const left = Math.max(0, viewport.left);
188
+ const right = Math.min(outputWidth, viewport.left + viewport.width);
189
+ const screenTop = Math.max(0, viewport.top);
190
+ const screenBottom = Math.min(outputHeight, viewport.top + viewport.height);
191
+ const result = [];
192
+ for (const span of spans) {
193
+ const screenY = viewport.top + (span.y - viewport.scrollY);
194
+ if (screenY < screenTop || screenY >= screenBottom) {
195
+ continue;
196
+ }
197
+ const x1 = Math.max(left, viewport.left + span.x1);
198
+ const x2 = Math.min(right, viewport.left + span.x2);
199
+ if (x2 <= x1) {
200
+ continue;
201
+ }
202
+ result.push({ y: screenY, x1, x2 });
203
+ }
204
+ return result;
205
+ };
206
+ resolveViewport() {
207
+ if (!this.viewportProvider) {
208
+ // No scroll container: the whole grid is the viewport.
209
+ return { viewport: fullGridViewport, available: true };
210
+ }
211
+ const current = this.viewportProvider();
212
+ if (current) {
213
+ this.lastViewport = current;
214
+ return { viewport: current, available: true };
215
+ }
216
+ // Provider registered but bounds unavailable this frame. Reuse the last
217
+ // known viewport so the highlight stays put; report unavailable so
218
+ // captureRows skips the frame.
219
+ return {
220
+ viewport: this.lastViewport ?? fullGridViewport,
221
+ available: false,
222
+ };
223
+ }
224
+ toContentPoint(screenPoint) {
225
+ const { viewport } = this.resolveViewport();
226
+ const maxX = viewport.width === Number.POSITIVE_INFINITY
227
+ ? Number.POSITIVE_INFINITY
228
+ : viewport.left + viewport.width - 1;
229
+ const maxY = viewport.height === Number.POSITIVE_INFINITY
230
+ ? Number.POSITIVE_INFINITY
231
+ : viewport.top + viewport.height - 1;
232
+ const x = Math.min(Math.max(screenPoint.x, viewport.left), maxX);
233
+ const y = Math.min(Math.max(screenPoint.y, viewport.top), maxY);
234
+ return {
235
+ x: x - viewport.left,
236
+ y: y - viewport.top + viewport.scrollY,
237
+ };
238
+ }
239
+ /** Returns true when the extracted text changed. */
240
+ recomputeText() {
241
+ const next = this.anchor && this.focus
242
+ ? extractTextFromRows(this.anchor, this.focus, y => this.rowCache.get(y))
243
+ : '';
244
+ if (next === this.text) {
245
+ return false;
246
+ }
247
+ this.text = next;
248
+ return true;
249
+ }
250
+ emit() {
251
+ this.snapshot = undefined;
252
+ for (const listener of this.listeners) {
253
+ listener();
254
+ }
255
+ }
256
+ scheduleNotify(invalidate) {
257
+ this.snapshot = undefined;
258
+ if (this.notifyScheduled) {
259
+ return;
260
+ }
261
+ this.notifyScheduled = true;
262
+ queueMicrotask(() => {
263
+ this.notifyScheduled = false;
264
+ for (const listener of this.listeners) {
265
+ listener();
266
+ }
267
+ if (invalidate) {
268
+ this.onInvalidate?.();
269
+ }
270
+ });
271
+ }
272
+ }
273
+ // Slice a row by display cells, not string indices, so wide characters keep
274
+ // the text aligned with the cell-indexed mask. A wide glyph straddling the
275
+ // viewport edge is replaced by spaces for the cells that remain visible.
276
+ const sliceRow = (row, left, width) => {
277
+ if (left === 0 && width === Number.POSITIVE_INFINITY) {
278
+ return row.trimEnd();
279
+ }
280
+ const right = width === Number.POSITIVE_INFINITY
281
+ ? Number.POSITIVE_INFINITY
282
+ : left + width;
283
+ let cell = 0;
284
+ let result = '';
285
+ for (const char of row) {
286
+ const charWidth = Math.max(1, stringWidth(char));
287
+ const start = cell;
288
+ const end = cell + charWidth;
289
+ cell = end;
290
+ if (end <= left) {
291
+ continue;
292
+ }
293
+ if (start >= right) {
294
+ break;
295
+ }
296
+ result +=
297
+ start >= left && end <= right
298
+ ? char
299
+ : ' '.repeat(Math.min(end, right) - Math.max(start, left));
300
+ }
301
+ return result.trimEnd();
302
+ };
303
+ const sliceMask = (mask, left, width) => {
304
+ if (!mask) {
305
+ return undefined;
306
+ }
307
+ return width === Number.POSITIVE_INFINITY
308
+ ? mask.slice(left)
309
+ : mask.slice(left, left + width);
310
+ };
311
+ //# sourceMappingURL=text-selection-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-selection-controller.js","sourceRoot":"","sources":["../src/text-selection-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ A point in container content space: `x` is cells from the container's content
3
+ left edge, `y` is the content row index (survives scrolling).
4
+ */
5
+ export type TextSelectionPoint = {
6
+ x: number;
7
+ y: number;
8
+ };
9
+ export type TextSelectionSpan = {
10
+ y: number;
11
+ x1: number;
12
+ x2: number;
13
+ };
14
+ /**
15
+ A single cached row of rendered content. `mask` flags which display cells are
16
+ selectable (written by `<Text>` and not opted out); a missing mask means every
17
+ cell is selectable.
18
+ */
19
+ export type SelectionRow = {
20
+ text: string;
21
+ mask?: readonly boolean[];
22
+ };
23
+ export type RowLookup = (y: number) => SelectionRow | undefined;
24
+ export declare function normalizeSelection(anchor: TextSelectionPoint, focus: TextSelectionPoint): {
25
+ start: TextSelectionPoint;
26
+ end: TextSelectionPoint;
27
+ };
28
+ /**
29
+ One past the last printable selectable cell of the row. Trailing whitespace
30
+ and trailing chrome cells are excluded.
31
+ */
32
+ export declare function getRowEnd(row: SelectionRow): number;
33
+ export declare function getSelectionSpans(anchor: TextSelectionPoint, focus: TextSelectionPoint, getRow: RowLookup): TextSelectionSpan[];
34
+ /**
35
+ Extract the selectable text of `row` between display cells `[from, to)`.
36
+ Non-selectable runs at the edges are dropped; interior non-selectable runs
37
+ collapse to a single space. Wide glyphs are included when the range overlaps
38
+ either of their cells.
39
+ */
40
+ export declare function sliceSelectableCells(row: SelectionRow, from: number, to: number): string;
41
+ export declare function extractTextFromRows(anchor: TextSelectionPoint, focus: TextSelectionPoint, getRow: RowLookup): string;
@@ -0,0 +1,101 @@
1
+ import stringWidth from 'string-width';
2
+ export function normalizeSelection(anchor, focus) {
3
+ if (anchor.y < focus.y || (anchor.y === focus.y && anchor.x <= focus.x)) {
4
+ return { start: anchor, end: focus };
5
+ }
6
+ return { start: focus, end: anchor };
7
+ }
8
+ const isCharSelectable = (mask, charStart, charEnd) => {
9
+ if (!mask) {
10
+ return true;
11
+ }
12
+ for (let cell = charStart; cell < charEnd; cell++) {
13
+ if (mask[cell] === false) {
14
+ return false;
15
+ }
16
+ }
17
+ return true;
18
+ };
19
+ /**
20
+ One past the last printable selectable cell of the row. Trailing whitespace
21
+ and trailing chrome cells are excluded.
22
+ */
23
+ export function getRowEnd(row) {
24
+ const { text, mask } = row;
25
+ let cell = 0;
26
+ let end = 0;
27
+ for (const char of text) {
28
+ const width = Math.max(1, stringWidth(char));
29
+ const charStart = cell;
30
+ const charEnd = cell + width;
31
+ cell = charEnd;
32
+ if (/^\s*$/.test(char)) {
33
+ continue;
34
+ }
35
+ if (isCharSelectable(mask, charStart, charEnd)) {
36
+ end = charEnd;
37
+ }
38
+ }
39
+ return end;
40
+ }
41
+ export function getSelectionSpans(anchor, focus, getRow) {
42
+ const { start, end } = normalizeSelection(anchor, focus);
43
+ if (start.x === end.x && start.y === end.y) {
44
+ return [];
45
+ }
46
+ const spans = [];
47
+ for (let { y } = start; y <= end.y; y++) {
48
+ const rowEnd = getRowEnd(getRow(y) ?? { text: '' });
49
+ let x1 = y === start.y ? start.x : 0;
50
+ let x2 = y === end.y ? end.x : rowEnd;
51
+ x1 = Math.max(0, Math.min(x1, rowEnd));
52
+ x2 = Math.max(0, Math.min(x2, rowEnd));
53
+ // Emit empty spans (x1 === x2) so blank lines survive extraction.
54
+ spans.push({ y, x1, x2 });
55
+ }
56
+ return spans;
57
+ }
58
+ /**
59
+ Extract the selectable text of `row` between display cells `[from, to)`.
60
+ Non-selectable runs at the edges are dropped; interior non-selectable runs
61
+ collapse to a single space. Wide glyphs are included when the range overlaps
62
+ either of their cells.
63
+ */
64
+ export function sliceSelectableCells(row, from, to) {
65
+ if (to <= from) {
66
+ return '';
67
+ }
68
+ const { text, mask } = row;
69
+ let cell = 0;
70
+ let result = '';
71
+ let pendingGap = false;
72
+ for (const char of text) {
73
+ const width = Math.max(1, stringWidth(char));
74
+ const charStart = cell;
75
+ const charEnd = cell + width;
76
+ cell = charEnd;
77
+ if (charEnd <= from) {
78
+ continue;
79
+ }
80
+ if (charStart >= to) {
81
+ break;
82
+ }
83
+ if (isCharSelectable(mask, charStart, charEnd)) {
84
+ if (pendingGap && result.length > 0) {
85
+ result += ' ';
86
+ }
87
+ pendingGap = false;
88
+ result += char;
89
+ }
90
+ else if (result.length > 0) {
91
+ pendingGap = true;
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ export function extractTextFromRows(anchor, focus, getRow) {
97
+ return getSelectionSpans(anchor, focus, getRow)
98
+ .map(span => sliceSelectableCells(getRow(span.y) ?? { text: '' }, span.x1, span.x2))
99
+ .join('\n');
100
+ }
101
+ //# sourceMappingURL=text-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-selection.js","sourceRoot":"","sources":["../src/text-selection.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuvin/ink",
3
- "version": "7.0.2-alpha",
3
+ "version": "7.1.0-alpha",
4
4
  "description": "React for CLI",
5
5
  "license": "MIT",
6
6
  "repository": "marschhuynh/ink",
package/readme.md CHANGED
@@ -2527,6 +2527,52 @@ Type: `() => void`
2527
2527
 
2528
2528
  Resets `frame`, `time`, and `delta` to `0` and restarts timing from the current moment. Useful for one-shot animations triggered by events.
2529
2529
 
2530
+ ### useTextSelection()
2531
+
2532
+ A React hook exposing the instance-owned text selection: the currently selected terminal text plus actions to drive a selection programmatically. Ink paints the highlight as a final overlay on the rendered output and recomputes `text` as rows render.
2533
+
2534
+ Selection is **Text-only**: only cells written by `<Text>` components are highlighted and extracted. Box borders, backgrounds, and padding/margin gaps are never part of a selection. Mark chrome text (icons, line-number gutters, diff markers) with `<Text selectable={false}>` to exclude it too — extraction drops chrome at the edges of a line and collapses interior chrome gaps to a single space.
2535
+
2536
+ ```jsx
2537
+ import {Text, useTextSelection} from 'ink';
2538
+
2539
+ const SelectionFooter = () => {
2540
+ const selection = useTextSelection();
2541
+
2542
+ return <Text dimColor>{selection.isEmpty ? 'no selection' : selection.text}</Text>;
2543
+ };
2544
+ ```
2545
+
2546
+ This hook is mouse-agnostic; mouse-driven selection (click-and-drag, edge auto-scroll) is provided by `useMouseTextSelection()` in `@nuvin/ink-input`, which drives these same actions.
2547
+
2548
+ When a component only needs to *drive* the selection (mouse bridges, key handlers that clear it), use `useTextSelectionActions()` instead: it returns the same actions plus an imperative `getSnapshot()` but never subscribes, so the host component does not re-render on selection changes. Reserve `useTextSelection()` for components that display selection state.
2549
+
2550
+ #### Return value
2551
+
2552
+ - `anchor` / `focus` — `{x, y} | null` in **content space**: `x` is cells from the selection container's left edge, `y` is the content row index (survives scrolling).
2553
+ - `isDragging` — `boolean`, true between `start()` and `finish()`.
2554
+ - `text` — `string`, the selected text. Blank lines inside the selection are preserved; trailing whitespace is not selectable.
2555
+ - `isEmpty` — `boolean`, true when no text is selected.
2556
+ - `start(screenPoint)` / `update(screenPoint)` — begin/extend a selection. Points are 0-based **screen cells** on the Ink output grid; the registered viewport provider converts them to content space.
2557
+ - `finish()` — end the drag; the selected text stays available (and is frozen — if content reflows underneath a finished selection, it auto-clears rather than silently changing).
2558
+ - `clear()` — clear the selection and its row cache.
2559
+ - `setViewportProvider(fn)` — register a function returning `{top, left, width, height, scrollY}` for the selection container (in output grid cells, `scrollY` in rendered rows). Return `null`, or pass `null`, to treat the whole output grid as the viewport.
2560
+
2561
+ #### `<Text selectable>`
2562
+
2563
+ Type: `boolean`\
2564
+ Default: `true`
2565
+
2566
+ When `false`, the text is never highlighted and never appears in extracted selection text. Note that nested `<Text>` squashes into its outermost `<Text>` ancestor's write, so `selectable` must be set on the outermost text element (use sibling `<Text>` elements in a `<Box>` to mix chrome and content on one line).
2567
+
2568
+ #### Known limits
2569
+
2570
+ - Mouse-to-grid mapping (in `useMouseTextSelection`) assumes the Ink output starts at terminal cell (1,1) — i.e. fullscreen/alternate-screen apps.
2571
+ - The selection container must not be nested inside another scroll container (`getBounds()` ignores ancestor scroll offsets).
2572
+ - A Text node's own padding cells count as selectable.
2573
+ - Horizontal scrolling containers are not supported.
2574
+ - Clipboard integration is app-owned: read `useTextSelection().text` and apply your own copy policy.
2575
+
2530
2576
  ## API
2531
2577
 
2532
2578
  #### render(tree, options?)