@code-yeongyu/senpi-tui 2026.11.12 → 2026.11.13
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/components/box.js +32 -7
- package/dist/components/markdown-source.d.ts +17 -0
- package/dist/components/markdown-source.js +262 -0
- package/dist/components/markdown.d.ts +7 -0
- package/dist/components/markdown.js +412 -48
- package/dist/components/scroll-view.js +4 -1
- package/dist/components/spacer.js +2 -0
- package/dist/components/text.js +5 -0
- package/dist/components/v-stack.js +11 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/selection-source.d.ts +43 -0
- package/dist/selection-source.js +200 -0
- package/dist/source-ranges.d.ts +25 -0
- package/dist/source-ranges.js +122 -0
- package/dist/tui-alt-screen.d.ts +1 -0
- package/dist/tui-alt-screen.js +20 -5
- package/dist/tui.js +12 -4
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +39 -6
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LAYOUT_NODE } from "../layout-node.js";
|
|
2
|
+
import { copyLineSources } from "../selection-source.js";
|
|
2
3
|
import { Container } from "../tui.js";
|
|
3
4
|
export class ScrollView extends Container {
|
|
4
5
|
constructor(component, options = {}) {
|
|
@@ -175,7 +176,9 @@ export class ScrollView extends Container {
|
|
|
175
176
|
render(width) {
|
|
176
177
|
const contentWidth = this.getContentWidth(width);
|
|
177
178
|
const lines = this.child.render(contentWidth);
|
|
178
|
-
return contentWidth === width
|
|
179
|
+
return contentWidth === width
|
|
180
|
+
? lines
|
|
181
|
+
: copyLineSources(lines, lines.map((line) => `${line} `));
|
|
179
182
|
}
|
|
180
183
|
[LAYOUT_NODE]() {
|
|
181
184
|
return { type: "scroll", component: this.child, state: this };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nextRenderRevision } from "../tui.js";
|
|
2
|
+
import { setLineSource } from "../selection-source.js";
|
|
2
3
|
/**
|
|
3
4
|
* Spacer component that renders empty lines
|
|
4
5
|
*/
|
|
@@ -25,6 +26,7 @@ export class Spacer {
|
|
|
25
26
|
const result = [];
|
|
26
27
|
for (let i = 0; i < this.lines; i++) {
|
|
27
28
|
result.push("");
|
|
29
|
+
setLineSource(result, i, null);
|
|
28
30
|
}
|
|
29
31
|
return result;
|
|
30
32
|
}
|
package/dist/components/text.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nextRenderRevision } from "../tui.js";
|
|
2
|
+
import { setLineSource } from "../selection-source.js";
|
|
2
3
|
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
3
4
|
/**
|
|
4
5
|
* Text component - displays multi-line text with word wrapping
|
|
@@ -90,6 +91,10 @@ export class Text {
|
|
|
90
91
|
emptyLines.push(line);
|
|
91
92
|
}
|
|
92
93
|
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
|
94
|
+
for (let row = 0; row < emptyLines.length; row++) {
|
|
95
|
+
setLineSource(result, row, null);
|
|
96
|
+
setLineSource(result, result.length - row - 1, null);
|
|
97
|
+
}
|
|
93
98
|
flattenLines(result);
|
|
94
99
|
// Update cache
|
|
95
100
|
this.cachedText = this.text;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { copyLineSources, setLineSource } from "../selection-source.js";
|
|
1
2
|
import { allocateStackSizes, Stack, visibleStackEntries } from "./stack.js";
|
|
2
3
|
export class VStack extends Stack {
|
|
3
4
|
constructor(children = [], options = {}) {
|
|
@@ -12,13 +13,20 @@ export class VStack extends Stack {
|
|
|
12
13
|
const lines = [];
|
|
13
14
|
for (let index = 0; index < entries.length; index++) {
|
|
14
15
|
if (index > 0) {
|
|
15
|
-
for (let gap = 0; gap < this.gap; gap++)
|
|
16
|
+
for (let gap = 0; gap < this.gap; gap++) {
|
|
16
17
|
lines.push("");
|
|
18
|
+
setLineSource(lines, lines.length - 1, null);
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
|
-
const
|
|
21
|
+
const original = rendered[index];
|
|
22
|
+
const childLines = copyLineSources(original, original.slice(0, sizes[index]));
|
|
23
|
+
const offset = lines.length;
|
|
19
24
|
lines.push(...childLines);
|
|
20
|
-
|
|
25
|
+
copyLineSources(childLines, lines, offset);
|
|
26
|
+
for (let padding = childLines.length; padding < sizes[index]; padding++) {
|
|
21
27
|
lines.push("");
|
|
28
|
+
setLineSource(lines, lines.length - 1, null);
|
|
29
|
+
}
|
|
22
30
|
}
|
|
23
31
|
return lines;
|
|
24
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export { parseTerminalColorSchemeReport, type RgbColor, type TerminalColorScheme
|
|
|
34
34
|
export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
|
|
35
35
|
export { allocateImageId, buildKittyPlaceholderRow, type CellDimensions, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, type ImageDimensions, type ImageProtocol, type ImageRenderOptions, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, type TerminalCapabilities, type TmuxPassthroughState, wrapTmuxPassthrough, } from "./terminal-image.ts";
|
|
36
36
|
export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.ts";
|
|
37
|
+
export { copyLineSources } from "./selection-source.ts";
|
|
37
38
|
export { encodeProgramStatus, type ProgramStatus, type ProgramStatusKind, type ProgramStatusState, } from "./program-status.ts";
|
|
38
39
|
export { type Component, CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, type Focusable, type FrameLineBytesTotals, frameLineBytesTotals, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, type OverlayAnchor, type OverlayBounds, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, renderAtFrameRow, resetMainScreenHistoryLines, type SizeValue, TUI, type TuiInputListener, type TuiInputListenerResult, type TuiMode, type TuiMouseButton, type TuiMouseDispatchResult, type TuiMouseEvent, type TuiMouseEventResult, type TuiMouseEventType, type TuiStopOptions, type ViewportTUI, } from "./tui.ts";
|
|
39
40
|
export { TuiAltScreen, type TuiAltScreenOptions } from "./tui-alt-screen.ts";
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export function calculateImageRows(imageDimensions, targetWidthCells, cellDimens
|
|
|
49
49
|
}
|
|
50
50
|
export { allocateImageId, buildKittyPlaceholderRow, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, wrapTmuxPassthrough, } from "./terminal-image.js";
|
|
51
51
|
export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.js";
|
|
52
|
+
export { copyLineSources } from "./selection-source.js";
|
|
52
53
|
export { encodeProgramStatus, } from "./program-status.js";
|
|
53
54
|
export { CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, frameLineBytesTotals, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, renderAtFrameRow, resetMainScreenHistoryLines, TUI, } from "./tui.js";
|
|
54
55
|
export { TuiAltScreen } from "./tui-alt-screen.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type SourceRange, type SourceRanges } from "./source-ranges.ts";
|
|
2
|
+
export type { SourceRange } from "./source-ranges.ts";
|
|
3
|
+
export interface SourceDocument {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly enclosures: Array<SourceRange & {
|
|
6
|
+
readonly content: SourceRange;
|
|
7
|
+
}>;
|
|
8
|
+
readonly resolveLines?: () => readonly (SourceLine | null | undefined)[] | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface SourceLine {
|
|
11
|
+
readonly document: SourceDocument;
|
|
12
|
+
readonly ranges: SourceRanges;
|
|
13
|
+
readonly sourceRow?: number;
|
|
14
|
+
readonly columnOffset?: number;
|
|
15
|
+
readonly unavailable?: boolean;
|
|
16
|
+
readonly renderedFallback?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function setLineSources(lines: readonly string[], mapping: readonly (SourceLine | null | undefined)[]): void;
|
|
19
|
+
export declare function setLineSource(lines: readonly string[], row: number, source: SourceLine | null): void;
|
|
20
|
+
export declare function getLineSources(lines: readonly string[]): readonly (SourceLine | null | undefined)[] | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Preserve provenance when a component copies, pads, or decorates an existing rendered array.
|
|
23
|
+
* columnOffset is the number of inserted ASCII padding spaces, not the width of arbitrary text.
|
|
24
|
+
* Populate unpublished arrays only; retained frames must keep their original provenance.
|
|
25
|
+
*/
|
|
26
|
+
export declare function copyLineSources(from: readonly string[], to: string[], rowOffset?: number, columnOffset?: number): string[];
|
|
27
|
+
type SourceSelection = {
|
|
28
|
+
readonly text: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly unavailable: true;
|
|
31
|
+
} | {
|
|
32
|
+
readonly renderedFallback: true;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the exact source interval of each selected Markdown document. Enclosures are included
|
|
36
|
+
* only when their complete visible contents were selected; an interior selection remains interior.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveSourceSelection(lines: readonly string[], rows: readonly {
|
|
39
|
+
readonly row: number;
|
|
40
|
+
readonly start: number;
|
|
41
|
+
readonly end: number;
|
|
42
|
+
}[]): SourceSelection | undefined;
|
|
43
|
+
//# sourceMappingURL=selection-source.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { getGraphemeSegmenter, stripTerminalSequences, visibleWidth } from "./utils.js";
|
|
2
|
+
import { concatRanges, emptyRanges, repeatedRange, sliceRanges, } from "./source-ranges.js";
|
|
3
|
+
// Undefined rows retain rendered plain text; null rows are known layout padding.
|
|
4
|
+
const sources = new WeakMap();
|
|
5
|
+
export function setLineSources(lines, mapping) {
|
|
6
|
+
sources.set(lines, [...mapping]);
|
|
7
|
+
}
|
|
8
|
+
export function setLineSource(lines, row, source) {
|
|
9
|
+
const mapping = sources.get(lines) ?? [];
|
|
10
|
+
mapping[row] = source;
|
|
11
|
+
sources.set(lines, mapping);
|
|
12
|
+
}
|
|
13
|
+
export function getLineSources(lines) {
|
|
14
|
+
return sources.get(lines);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Preserve provenance when a component copies, pads, or decorates an existing rendered array.
|
|
18
|
+
* columnOffset is the number of inserted ASCII padding spaces, not the width of arbitrary text.
|
|
19
|
+
* Populate unpublished arrays only; retained frames must keep their original provenance.
|
|
20
|
+
*/
|
|
21
|
+
export function copyLineSources(from, to, rowOffset = 0, columnOffset = 0) {
|
|
22
|
+
const mapping = sources.get(from);
|
|
23
|
+
if (mapping) {
|
|
24
|
+
const result = sources.get(to) ?? [];
|
|
25
|
+
for (let row = 0; row < mapping.length && row + rowOffset < to.length; row++) {
|
|
26
|
+
const line = mapping[row];
|
|
27
|
+
result[row + rowOffset] =
|
|
28
|
+
line && columnOffset
|
|
29
|
+
? line.sourceRow === undefined
|
|
30
|
+
? { ...line, ranges: concatRanges([repeatedRange(undefined, columnOffset), line.ranges]) }
|
|
31
|
+
: { ...line, columnOffset: (line.columnOffset ?? 0) + columnOffset }
|
|
32
|
+
: line;
|
|
33
|
+
}
|
|
34
|
+
sources.set(to, result);
|
|
35
|
+
}
|
|
36
|
+
return to;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the exact source interval of each selected Markdown document. Enclosures are included
|
|
40
|
+
* only when their complete visible contents were selected; an interior selection remains interior.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveSourceSelection(lines, rows) {
|
|
43
|
+
const mapping = sources.get(lines);
|
|
44
|
+
if (!mapping)
|
|
45
|
+
return undefined;
|
|
46
|
+
const resolvedDocuments = new Map();
|
|
47
|
+
for (const selection of rows) {
|
|
48
|
+
const document = mapping[selection.row]?.document;
|
|
49
|
+
if (!document?.resolveLines || resolvedDocuments.has(document))
|
|
50
|
+
continue;
|
|
51
|
+
const resolved = document.resolveLines();
|
|
52
|
+
resolvedDocuments.set(document, {
|
|
53
|
+
lines: resolved,
|
|
54
|
+
document: { ...(resolved?.find((line) => line)?.document ?? document) },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const resolveLine = (line) => {
|
|
58
|
+
if (!line)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (line.sourceRow === undefined)
|
|
61
|
+
return line;
|
|
62
|
+
const resolved = resolvedDocuments.get(line.document);
|
|
63
|
+
if (!resolved)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (!resolved.lines)
|
|
66
|
+
return { document: resolved.document, ranges: emptyRanges, unavailable: true };
|
|
67
|
+
const original = resolved.lines[line.sourceRow];
|
|
68
|
+
if (!original)
|
|
69
|
+
return undefined;
|
|
70
|
+
return {
|
|
71
|
+
...original,
|
|
72
|
+
document: resolved.document,
|
|
73
|
+
ranges: line.columnOffset
|
|
74
|
+
? concatRanges([repeatedRange(undefined, line.columnOffset), original.ranges])
|
|
75
|
+
: original.ranges,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const documentLines = new Map();
|
|
79
|
+
for (const source of mapping) {
|
|
80
|
+
const line = resolveLine(source);
|
|
81
|
+
if (!line)
|
|
82
|
+
continue;
|
|
83
|
+
const group = documentLines.get(line.document);
|
|
84
|
+
if (group)
|
|
85
|
+
group.push(line);
|
|
86
|
+
else
|
|
87
|
+
documentLines.set(line.document, [line]);
|
|
88
|
+
}
|
|
89
|
+
const pieces = [];
|
|
90
|
+
let active;
|
|
91
|
+
let usedSource = false;
|
|
92
|
+
let unavailable = false;
|
|
93
|
+
let renderedFallback = false;
|
|
94
|
+
const flush = () => {
|
|
95
|
+
if (!active)
|
|
96
|
+
return;
|
|
97
|
+
let { start, end } = active;
|
|
98
|
+
const covered = [];
|
|
99
|
+
for (const range of active.selected.sort((left, right) => left.start - right.start)) {
|
|
100
|
+
const previous = covered.at(-1);
|
|
101
|
+
if (previous && previous.end >= range.start)
|
|
102
|
+
previous.end = Math.max(previous.end, range.end);
|
|
103
|
+
else
|
|
104
|
+
covered.push({ ...range });
|
|
105
|
+
}
|
|
106
|
+
// A wrapped table can visit source cells out of order. Never fill an unselected hole.
|
|
107
|
+
for (const line of documentLines.get(active.document) ?? []) {
|
|
108
|
+
for (const span of line.ranges.spans) {
|
|
109
|
+
const range = span.source;
|
|
110
|
+
if (!range)
|
|
111
|
+
continue;
|
|
112
|
+
const left = span.linear ? Math.max(start, range.start) : range.start;
|
|
113
|
+
const right = span.linear ? Math.min(end, range.end) : range.end;
|
|
114
|
+
if (left < start || right > end || left >= right)
|
|
115
|
+
continue;
|
|
116
|
+
let low = 0;
|
|
117
|
+
let high = covered.length;
|
|
118
|
+
while (low < high) {
|
|
119
|
+
const middle = (low + high) >>> 1;
|
|
120
|
+
if (covered[middle].start <= left)
|
|
121
|
+
low = middle + 1;
|
|
122
|
+
else
|
|
123
|
+
high = middle;
|
|
124
|
+
}
|
|
125
|
+
if (low === 0 || covered[low - 1].end < right)
|
|
126
|
+
unavailable = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Inner enclosures can make an outer enclosure complete.
|
|
130
|
+
for (const enclosure of active.document.enclosures) {
|
|
131
|
+
if (start <= enclosure.content.start && end >= enclosure.content.end) {
|
|
132
|
+
start = Math.min(start, enclosure.start);
|
|
133
|
+
end = Math.max(end, enclosure.end);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
pieces.push(active.document.text.slice(start, end));
|
|
137
|
+
active = undefined;
|
|
138
|
+
};
|
|
139
|
+
for (const selection of rows) {
|
|
140
|
+
const source = mapping[selection.row];
|
|
141
|
+
if (source === null)
|
|
142
|
+
continue;
|
|
143
|
+
const line = resolveLine(source);
|
|
144
|
+
const text = stripTerminalSequences(lines[selection.row] ?? "");
|
|
145
|
+
if (!line) {
|
|
146
|
+
// Source-owned blank rows are represented by the source interval.
|
|
147
|
+
// Unmapped plain-text rows must retain their hard blank lines.
|
|
148
|
+
if (source && !text.trim())
|
|
149
|
+
continue;
|
|
150
|
+
flush();
|
|
151
|
+
let column = 0;
|
|
152
|
+
let selected = "";
|
|
153
|
+
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
|
154
|
+
const width = visibleWidth(segment);
|
|
155
|
+
if (column < selection.end && column + width > selection.start)
|
|
156
|
+
selected += segment;
|
|
157
|
+
column += width;
|
|
158
|
+
}
|
|
159
|
+
pieces.push(selected.trimEnd());
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
usedSource = true;
|
|
163
|
+
if (active && active.document !== line.document)
|
|
164
|
+
flush();
|
|
165
|
+
let column = 0;
|
|
166
|
+
let unitStart = text.length;
|
|
167
|
+
let unitEnd = 0;
|
|
168
|
+
for (const { segment, index } of getGraphemeSegmenter().segment(text)) {
|
|
169
|
+
const width = visibleWidth(segment);
|
|
170
|
+
if (column < selection.end && column + width > selection.start) {
|
|
171
|
+
if (line.unavailable && segment.trim())
|
|
172
|
+
return { unavailable: true };
|
|
173
|
+
if (line.renderedFallback && segment.trim())
|
|
174
|
+
renderedFallback = true;
|
|
175
|
+
unitStart = Math.min(unitStart, index);
|
|
176
|
+
unitEnd = index + segment.length;
|
|
177
|
+
}
|
|
178
|
+
column += width;
|
|
179
|
+
}
|
|
180
|
+
for (const span of sliceRanges(line.ranges, unitStart, unitEnd).spans) {
|
|
181
|
+
const range = span.source;
|
|
182
|
+
if (range === null)
|
|
183
|
+
return { unavailable: true };
|
|
184
|
+
if (!active)
|
|
185
|
+
active = { document: line.document, ...range, selected: [] };
|
|
186
|
+
else {
|
|
187
|
+
active.start = Math.min(active.start, range.start);
|
|
188
|
+
active.end = Math.max(active.end, range.end);
|
|
189
|
+
}
|
|
190
|
+
active.selected.push(range);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
flush();
|
|
194
|
+
if (unavailable)
|
|
195
|
+
return { unavailable: true };
|
|
196
|
+
if (renderedFallback)
|
|
197
|
+
return { renderedFallback: true };
|
|
198
|
+
return usedSource ? { text: pieces.join("\n") } : undefined;
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=selection-source.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface SourceRange {
|
|
2
|
+
readonly start: number;
|
|
3
|
+
readonly end: number;
|
|
4
|
+
}
|
|
5
|
+
interface SourceSpan {
|
|
6
|
+
readonly offset: number;
|
|
7
|
+
readonly length: number;
|
|
8
|
+
/** null marks visible output whose original source cannot be recovered. */
|
|
9
|
+
readonly source: SourceRange | null;
|
|
10
|
+
/** Linear spans map UTF-16 units one-to-one; other spans repeat one source interval. */
|
|
11
|
+
readonly linear: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Display-only padding occupies length but has no span. */
|
|
14
|
+
export interface SourceRanges {
|
|
15
|
+
readonly length: number;
|
|
16
|
+
readonly spans: readonly SourceSpan[];
|
|
17
|
+
}
|
|
18
|
+
export declare const emptyRanges: SourceRanges;
|
|
19
|
+
export declare function linearRanges(start: number, length: number): SourceRanges;
|
|
20
|
+
export declare function repeatedRange(source: SourceRange | null | undefined, length: number): SourceRanges;
|
|
21
|
+
export declare function rangeAt(ranges: SourceRanges, offset: number): SourceRange | null | undefined;
|
|
22
|
+
export declare function sliceRanges(ranges: SourceRanges, from: number, to?: number): SourceRanges;
|
|
23
|
+
export declare function concatRanges(parts: readonly SourceRanges[]): SourceRanges;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=source-ranges.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const emptyRanges = { length: 0, spans: [] };
|
|
2
|
+
/** Most token slices are contiguous; retain two numbers instead of a span object graph. */
|
|
3
|
+
class LinearRanges {
|
|
4
|
+
constructor(start, length) {
|
|
5
|
+
this.start = start;
|
|
6
|
+
this.length = length;
|
|
7
|
+
}
|
|
8
|
+
get spans() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
offset: 0,
|
|
12
|
+
length: this.length,
|
|
13
|
+
source: { start: this.start, end: this.start + this.length },
|
|
14
|
+
linear: true,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function linearRanges(start, length) {
|
|
20
|
+
return length ? new LinearRanges(start, length) : emptyRanges;
|
|
21
|
+
}
|
|
22
|
+
export function repeatedRange(source, length) {
|
|
23
|
+
return length
|
|
24
|
+
? { length, spans: source === undefined ? [] : [{ offset: 0, length, source, linear: false }] }
|
|
25
|
+
: emptyRanges;
|
|
26
|
+
}
|
|
27
|
+
function firstSpan(ranges, offset) {
|
|
28
|
+
let low = 0;
|
|
29
|
+
let high = ranges.spans.length;
|
|
30
|
+
while (low < high) {
|
|
31
|
+
const middle = (low + high) >>> 1;
|
|
32
|
+
const span = ranges.spans[middle];
|
|
33
|
+
if (span.offset + span.length <= offset)
|
|
34
|
+
low = middle + 1;
|
|
35
|
+
else
|
|
36
|
+
high = middle;
|
|
37
|
+
}
|
|
38
|
+
return low;
|
|
39
|
+
}
|
|
40
|
+
export function rangeAt(ranges, offset) {
|
|
41
|
+
if (ranges instanceof LinearRanges) {
|
|
42
|
+
if (offset < 0 || offset >= ranges.length)
|
|
43
|
+
return undefined;
|
|
44
|
+
const start = ranges.start + offset;
|
|
45
|
+
return { start, end: start + 1 };
|
|
46
|
+
}
|
|
47
|
+
const span = ranges.spans[firstSpan(ranges, offset)];
|
|
48
|
+
if (!span || offset < span.offset)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (!span.linear || !span.source)
|
|
51
|
+
return span.source;
|
|
52
|
+
const start = span.source.start + offset - span.offset;
|
|
53
|
+
return { start, end: start + 1 };
|
|
54
|
+
}
|
|
55
|
+
export function sliceRanges(ranges, from, to = ranges.length) {
|
|
56
|
+
const start = Math.max(0, Math.min(ranges.length, from < 0 ? ranges.length + from : from));
|
|
57
|
+
const end = Math.max(start, Math.min(ranges.length, to < 0 ? ranges.length + to : to));
|
|
58
|
+
if (start === 0 && end === ranges.length)
|
|
59
|
+
return ranges;
|
|
60
|
+
if (ranges instanceof LinearRanges)
|
|
61
|
+
return linearRanges(ranges.start + start, end - start);
|
|
62
|
+
const spans = [];
|
|
63
|
+
for (let index = firstSpan(ranges, start); index < ranges.spans.length; index++) {
|
|
64
|
+
const span = ranges.spans[index];
|
|
65
|
+
if (span.offset >= end)
|
|
66
|
+
break;
|
|
67
|
+
const left = Math.max(start, span.offset);
|
|
68
|
+
const right = Math.min(end, span.offset + span.length);
|
|
69
|
+
spans.push({
|
|
70
|
+
offset: left - start,
|
|
71
|
+
length: right - left,
|
|
72
|
+
source: span.linear && span.source
|
|
73
|
+
? {
|
|
74
|
+
start: span.source.start + left - span.offset,
|
|
75
|
+
end: span.source.start + right - span.offset,
|
|
76
|
+
}
|
|
77
|
+
: span.source,
|
|
78
|
+
linear: span.linear,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return compactRanges(end - start, spans);
|
|
82
|
+
}
|
|
83
|
+
export function concatRanges(parts) {
|
|
84
|
+
if (parts.length === 1)
|
|
85
|
+
return parts[0];
|
|
86
|
+
const spans = [];
|
|
87
|
+
let length = 0;
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
for (const span of part.spans) {
|
|
90
|
+
const offset = length + span.offset;
|
|
91
|
+
const previous = spans.at(-1);
|
|
92
|
+
if (previous &&
|
|
93
|
+
previous.offset + previous.length === offset &&
|
|
94
|
+
previous.linear === span.linear &&
|
|
95
|
+
(previous.linear
|
|
96
|
+
? previous.source && span.source && previous.source.end === span.source.start
|
|
97
|
+
: previous.source?.start === span.source?.start && previous.source?.end === span.source?.end)) {
|
|
98
|
+
spans[spans.length - 1] = {
|
|
99
|
+
...previous,
|
|
100
|
+
length: previous.length + span.length,
|
|
101
|
+
source: previous.linear && previous.source && span.source
|
|
102
|
+
? { start: previous.source.start, end: span.source.end }
|
|
103
|
+
: previous.source,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
spans.push(length ? { ...span, offset } : span);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
length += part.length;
|
|
111
|
+
}
|
|
112
|
+
return compactRanges(length, spans);
|
|
113
|
+
}
|
|
114
|
+
function compactRanges(length, spans) {
|
|
115
|
+
if (!length)
|
|
116
|
+
return emptyRanges;
|
|
117
|
+
const span = spans[0];
|
|
118
|
+
return spans.length === 1 && span.offset === 0 && span.length === length && span.linear && span.source
|
|
119
|
+
? linearRanges(span.source.start, length)
|
|
120
|
+
: { length, spans };
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=source-ranges.js.map
|
package/dist/tui-alt-screen.d.ts
CHANGED
|
@@ -151,6 +151,7 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
151
151
|
private handleSelectionMouseEvent;
|
|
152
152
|
private getSelectionBounds;
|
|
153
153
|
private getSelectionColumns;
|
|
154
|
+
private selectionSourceUnavailable;
|
|
154
155
|
private getActiveSelectionText;
|
|
155
156
|
private copySelectionToClipboard;
|
|
156
157
|
private copyTextToClipboard;
|
package/dist/tui-alt-screen.js
CHANGED
|
@@ -7,6 +7,7 @@ import { isKeyRelease } from "./keys.js";
|
|
|
7
7
|
import { getLayoutBoxesAt, getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
|
|
8
8
|
import { getLayoutNode } from "./layout-node.js";
|
|
9
9
|
import { decodeMouseButton, isMouseSequence, parseSgrMouseEvent, parseWheelEvent } from "./mouse-input.js";
|
|
10
|
+
import { resolveSourceSelection } from "./selection-source.js";
|
|
10
11
|
import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, isImageLine, setCapabilities, } from "./terminal-image.js";
|
|
11
12
|
import { Container, CURSOR_MARKER, compositeTuiLine, dispatchMouseEvent, retargetMouseEvent, TuiBase, VIEWPORT_TUI, } from "./tui.js";
|
|
12
13
|
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, } from "./utils.js";
|
|
@@ -54,6 +55,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
54
55
|
this.selectionPressActive = false;
|
|
55
56
|
this.selectionDragged = false;
|
|
56
57
|
this.mousePressMoved = false;
|
|
58
|
+
this.selectionSourceUnavailable = false;
|
|
57
59
|
this.implicitDocument = {
|
|
58
60
|
render: (width) => super.render(width),
|
|
59
61
|
handleMouse: (event) => super.handleMouse(event),
|
|
@@ -93,14 +95,11 @@ export class TuiAltScreen extends TuiBase {
|
|
|
93
95
|
}
|
|
94
96
|
/** Whether the fullscreen viewport has a non-empty active text selection. */
|
|
95
97
|
hasActiveSelection() {
|
|
96
|
-
return this.getActiveSelectionText() !== undefined;
|
|
98
|
+
return this.getActiveSelectionText() !== undefined || this.selectionSourceUnavailable;
|
|
97
99
|
}
|
|
98
100
|
/** Copy the active fullscreen text selection, if any, using the configured selection clipboard path. */
|
|
99
101
|
async copyActiveSelectionToClipboard() {
|
|
100
|
-
|
|
101
|
-
if (!text)
|
|
102
|
-
return false;
|
|
103
|
-
return this.copyTextToClipboard(text);
|
|
102
|
+
return this.copySelectionToClipboard();
|
|
104
103
|
}
|
|
105
104
|
/** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
|
|
106
105
|
getScreenLines() {
|
|
@@ -1126,6 +1125,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1126
1125
|
return { start: Math.max(minColumn, start), end: Math.min(maxColumn, end) };
|
|
1127
1126
|
}
|
|
1128
1127
|
getActiveSelectionText() {
|
|
1128
|
+
this.selectionSourceUnavailable = false;
|
|
1129
1129
|
const selection = this.getSelectionBounds();
|
|
1130
1130
|
if (!selection)
|
|
1131
1131
|
return undefined;
|
|
@@ -1138,6 +1138,17 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1138
1138
|
return undefined;
|
|
1139
1139
|
sourceLines = box.scrollContentLines;
|
|
1140
1140
|
}
|
|
1141
|
+
const rows = [];
|
|
1142
|
+
for (let row = selection.start.row; row <= selection.end.row; row++) {
|
|
1143
|
+
rows.push({ row, ...this.getSelectionColumns(sourceLines[row] ?? "", row, selection) });
|
|
1144
|
+
}
|
|
1145
|
+
const original = resolveSourceSelection(sourceLines, rows);
|
|
1146
|
+
if (original && "unavailable" in original) {
|
|
1147
|
+
this.selectionSourceUnavailable = true;
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
if (original && "text" in original)
|
|
1151
|
+
return original.text || undefined;
|
|
1141
1152
|
const lines = [];
|
|
1142
1153
|
for (let row = selection.start.row; row <= selection.end.row; row++) {
|
|
1143
1154
|
const line = sourceLines[row] ?? "";
|
|
@@ -1149,6 +1160,10 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1149
1160
|
}
|
|
1150
1161
|
async copySelectionToClipboard() {
|
|
1151
1162
|
const text = this.getActiveSelectionText();
|
|
1163
|
+
if (this.selectionSourceUnavailable) {
|
|
1164
|
+
this.flash("Original source unavailable for this selection", COPY_ERROR_FLASH_DURATION_MS);
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1152
1167
|
if (!text)
|
|
1153
1168
|
return false;
|
|
1154
1169
|
return this.copyTextToClipboard(text);
|
package/dist/tui.js
CHANGED
|
@@ -9,6 +9,7 @@ import { performance } from "node:perf_hooks";
|
|
|
9
9
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
10
10
|
import { buildKittyGraphicsQuery, KITTY_GRAPHICS_PROBE_TIMEOUT_MS, parseKittyGraphicsProbeResponse, } from "./kitty-graphics-probe.js";
|
|
11
11
|
import { isMultiplexerSession, useLegacyMuxRender, viewportRenderEnabled } from "./mux.js";
|
|
12
|
+
import { copyLineSources } from "./selection-source.js";
|
|
12
13
|
import { parseOscColorResponse, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
13
14
|
import { allocateImageId, deleteKittyImage, getCapabilities, isImageLine, resetCapabilitiesCache, setCellDimensions, setProbedImageProtocol, } from "./terminal-image.js";
|
|
14
15
|
import { consumeTmuxFocusEvent, DISABLE_FOCUS_REPORTING, ENABLE_FOCUS_REPORTING } from "./tmux-focus.js";
|
|
@@ -574,13 +575,20 @@ const JOIN_BATCH = 1024;
|
|
|
574
575
|
* which keeps a frame over a long transcript from paying a per-line iterator and push.
|
|
575
576
|
*/
|
|
576
577
|
export function joinLineArrays(chunks) {
|
|
577
|
-
if (chunks.length <= JOIN_BATCH)
|
|
578
|
-
|
|
578
|
+
if (chunks.length <= JOIN_BATCH) {
|
|
579
|
+
const result = [].concat(...chunks);
|
|
580
|
+
let offset = 0;
|
|
581
|
+
for (const chunk of chunks) {
|
|
582
|
+
copyLineSources(chunk, result, offset);
|
|
583
|
+
offset += chunk.length;
|
|
584
|
+
}
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
579
587
|
const batches = [];
|
|
580
588
|
for (let start = 0; start < chunks.length; start += JOIN_BATCH) {
|
|
581
|
-
batches.push(
|
|
589
|
+
batches.push(joinLineArrays(chunks.slice(start, start + JOIN_BATCH)));
|
|
582
590
|
}
|
|
583
|
-
return
|
|
591
|
+
return joinLineArrays(batches);
|
|
584
592
|
}
|
|
585
593
|
/**
|
|
586
594
|
* TUI - Main class for managing terminal UI with differential rendering
|
package/dist/utils.d.ts
CHANGED
|
@@ -60,7 +60,8 @@ export declare function getActiveBackgroundAnsi(text: string): string;
|
|
|
60
60
|
* reads it whole, so V8 flattens it in place; the strings' values do not change.
|
|
61
61
|
*/
|
|
62
62
|
export declare function flattenLines(lines: readonly string[]): void;
|
|
63
|
-
|
|
63
|
+
/** onLine receives each output line's consumed UTF-16 interval in the ANSI-stripped input. */
|
|
64
|
+
export declare function wrapTextWithAnsi(text: string, width: number, onLine?: (start: number, end: number) => void): string[];
|
|
64
65
|
export declare const PUNCTUATION_REGEX: RegExp;
|
|
65
66
|
/**
|
|
66
67
|
* Check if a character is whitespace.
|