@nuvin/ink 7.0.2-alpha → 7.1.1-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 +26 -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 +26 -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 +116 -0
  38. package/build/text-selection-controller.js +340 -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 +47 -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
package/readme.md CHANGED
@@ -2527,6 +2527,53 @@ 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
+ - `registerViewportProvider(fn)` — register a function returning `{top, left, width, height, scrollY}` for the selection container (in output grid cells, `scrollY` in rendered rows). Providers stack: the most recently registered one is active (e.g. a modal's selection area over the main list's); the returned unregister function restores the previous provider. Registering or unregistering clears any live selection (its coordinate space changed). The provider returning `null` means "bounds not measurable this frame" and the frame is skipped. With no provider registered, the whole output grid is the viewport.
2560
+ - `setViewportProvider(fn | null)` — single-slot convenience over `registerViewportProvider`: replaces the provider set by the previous call (`null` unregisters it). Prefer `registerViewportProvider` where selection areas can overlap (overlays, modals).
2561
+
2562
+ #### `<Text selectable>`
2563
+
2564
+ Type: `boolean`\
2565
+ Default: `true`
2566
+
2567
+ 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).
2568
+
2569
+ #### Known limits
2570
+
2571
+ - Mouse-to-grid mapping (in `useMouseTextSelection`) assumes the Ink output starts at terminal cell (1,1) — i.e. fullscreen/alternate-screen apps.
2572
+ - The selection container must not be nested inside another scroll container (`getBounds()` ignores ancestor scroll offsets).
2573
+ - A Text node's own padding cells count as selectable.
2574
+ - Horizontal scrolling containers are not supported.
2575
+ - Clipboard integration is app-owned: read `useTextSelection().text` and apply your own copy policy.
2576
+
2530
2577
  ## API
2531
2578
 
2532
2579
  #### render(tree, options?)
@@ -1,27 +0,0 @@
1
- export type SelectionScope = {
2
- surfaceId: string;
3
- itemKey: string;
4
- itemIndex: number;
5
- itemOffsetY: number;
6
- originX: number;
7
- originY: number;
8
- };
9
- export type RegisteredSelectionLine = {
10
- key: string;
11
- order: number;
12
- text: string;
13
- viewportX: number;
14
- viewportY: number;
15
- itemKey: string;
16
- };
17
- export type LogicalSelectionPoint = {
18
- lineKey: string;
19
- column: number;
20
- };
21
- export declare const recordSelectionFrame: (lines: readonly string[], scrollOffsetY: number, viewportTopY?: number) => void;
22
- export declare const getSelectionFrameCache: () => ReadonlyMap<number, string>;
23
- export declare const clearSelectionFrameCache: () => void;
24
- export declare const registerSelectionLines: (lines: readonly string[], scope: SelectionScope) => void;
25
- export declare const getRegisteredSelectionLines: () => ReadonlyMap<string, RegisteredSelectionLine>;
26
- export declare const clearRegisteredSelectionLines: () => void;
27
- export declare const getSelectionPointAtCell: (x: number, y: number) => LogicalSelectionPoint | null;
@@ -1,73 +0,0 @@
1
- import stringWidth from 'string-width';
2
- const MAX_CACHED_ROWS = 2000;
3
- const MAX_REGISTERED_LINES = 5000;
4
- let selectionFrameCache = new Map();
5
- let registeredSelectionLines = new Map();
6
- export const recordSelectionFrame = (lines, scrollOffsetY, viewportTopY = 0) => {
7
- for (let viewportY = viewportTopY; viewportY < lines.length; viewportY++) {
8
- const contentY = scrollOffsetY + viewportY - viewportTopY;
9
- const line = lines[viewportY] ?? '';
10
- const existing = selectionFrameCache.get(contentY);
11
- if (line.length === 0 && existing && existing.length > 0) {
12
- continue;
13
- }
14
- selectionFrameCache.set(contentY, line);
15
- }
16
- while (selectionFrameCache.size > MAX_CACHED_ROWS) {
17
- const oldestKey = selectionFrameCache.keys().next().value;
18
- if (oldestKey === undefined)
19
- break;
20
- selectionFrameCache.delete(oldestKey);
21
- }
22
- };
23
- export const getSelectionFrameCache = () => selectionFrameCache;
24
- export const clearSelectionFrameCache = () => {
25
- selectionFrameCache = new Map();
26
- };
27
- const widthToColumn = (text, targetWidth) => {
28
- let width = 0;
29
- let column = 0;
30
- for (const character of text) {
31
- const nextWidth = width + Math.max(1, stringWidth(character));
32
- if (targetWidth < nextWidth)
33
- break;
34
- width = nextWidth;
35
- column++;
36
- }
37
- return column;
38
- };
39
- export const registerSelectionLines = (lines, scope) => {
40
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
41
- const text = lines[lineIndex] ?? '';
42
- const key = `${scope.surfaceId}:${scope.itemKey}:${lineIndex}`;
43
- const viewportY = scope.originY + scope.itemOffsetY + lineIndex;
44
- registeredSelectionLines.set(key, {
45
- key,
46
- order: viewportY * 100_000 + scope.originX,
47
- text,
48
- viewportX: scope.originX,
49
- viewportY,
50
- itemKey: scope.itemKey,
51
- });
52
- }
53
- while (registeredSelectionLines.size > MAX_REGISTERED_LINES) {
54
- const oldestKey = registeredSelectionLines.keys().next().value;
55
- if (oldestKey === undefined)
56
- break;
57
- registeredSelectionLines.delete(oldestKey);
58
- }
59
- };
60
- export const getRegisteredSelectionLines = () => registeredSelectionLines;
61
- export const clearRegisteredSelectionLines = () => {
62
- registeredSelectionLines = new Map();
63
- };
64
- export const getSelectionPointAtCell = (x, y) => {
65
- const line = [...registeredSelectionLines.values()]
66
- .filter(candidate => candidate.viewportY === y)
67
- .sort((left, right) => right.order - left.order || right.itemKey.length - left.itemKey.length)[0];
68
- if (!line)
69
- return null;
70
- const column = widthToColumn(line.text, Math.max(0, x - line.viewportX));
71
- return { lineKey: line.key, column };
72
- };
73
- //# sourceMappingURL=current-frame.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"current-frame.js","sourceRoot":"","sources":["../src/current-frame.ts"],"names":[],"mappings":"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"}
@@ -1,3 +0,0 @@
1
- import { type SelectionRange } from '../selection.js';
2
- declare const useSelectionOverlay: (range: SelectionRange | null) => void;
3
- export default useSelectionOverlay;
@@ -1,14 +0,0 @@
1
- import { useLayoutEffect } from 'react';
2
- import { setSelectionRange } from '../selection.js';
3
- const useSelectionOverlay = (range) => {
4
- useLayoutEffect(() => {
5
- setSelectionRange(range);
6
- });
7
- useLayoutEffect(() => {
8
- return () => {
9
- setSelectionRange(null);
10
- };
11
- }, []);
12
- };
13
- export default useSelectionOverlay;
14
- //# sourceMappingURL=use-selection-overlay.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-selection-overlay.js","sourceRoot":"","sources":["../../src/hooks/use-selection-overlay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,eAAe,EAAC,MAAM,OAAO,CAAC;AACtC,OAAO,EAAC,iBAAiB,EAAsB,MAAM,iBAAiB,CAAC;AAEvE,MAAM,mBAAmB,GAAG,CAAC,KAA4B,EAAQ,EAAE;IAClE,eAAe,CAAC,GAAG,EAAE;QACpB,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,eAAe,CAAC,GAAG,EAAE;QACpB,OAAO,GAAG,EAAE;YACX,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC;AACR,CAAC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -1,28 +0,0 @@
1
- export type SelectionPoint = {
2
- x: number;
3
- y: number;
4
- };
5
- export type SelectionStyle = {
6
- backgroundColor?: string;
7
- color?: string;
8
- };
9
- export type SelectionScope = {
10
- surfaceId: string;
11
- itemKey: string;
12
- itemIndex: number;
13
- itemOffsetY: number;
14
- originX: number;
15
- originY: number;
16
- };
17
- export type SelectionRange = {
18
- anchor: SelectionPoint;
19
- focus: SelectionPoint;
20
- scrollOffsetY: number;
21
- viewportTopY?: number;
22
- viewportLeftX?: number;
23
- scope?: SelectionScope;
24
- style: SelectionStyle;
25
- };
26
- export declare const setSelectionRange: (range: SelectionRange | null) => void;
27
- export declare const getSelectionRange: () => SelectionRange | null;
28
- export declare const clearSelectionRange: () => void;
@@ -1,9 +0,0 @@
1
- let currentSelectionRange = null;
2
- export const setSelectionRange = (range) => {
3
- currentSelectionRange = range;
4
- };
5
- export const getSelectionRange = () => currentSelectionRange;
6
- export const clearSelectionRange = () => {
7
- currentSelectionRange = null;
8
- };
9
- //# sourceMappingURL=selection.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"selection.js","sourceRoot":"","sources":["../src/selection.ts"],"names":[],"mappings":"AA6BA,IAAI,qBAAqB,GAA0B,IAAI,CAAC;AAExD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAA4B,EAAQ,EAAE;IACvE,qBAAqB,GAAG,KAAK,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAA0B,EAAE,CAAC,qBAAqB,CAAC;AAEpF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAS,EAAE;IAC7C,qBAAqB,GAAG,IAAI,CAAC;AAC9B,CAAC,CAAC"}