tinky 1.8.0 → 1.9.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/lib/components/App.d.ts +1 -1
- package/lib/components/ErrorOverview.d.ts +1 -1
- package/lib/components/Newline.d.ts +1 -1
- package/lib/components/Raw.d.ts +44 -0
- package/lib/components/Raw.js +45 -0
- package/lib/components/Separator.d.ts +1 -1
- package/lib/components/Spacer.d.ts +1 -1
- package/lib/components/Static.d.ts +1 -1
- package/lib/components/Text.d.ts +1 -1
- package/lib/components/Transform.d.ts +1 -1
- package/lib/core/cell-output.d.ts +4 -1
- package/lib/core/cell-output.js +10 -0
- package/lib/core/cell-renderer.d.ts +3 -0
- package/lib/core/cell-renderer.js +3 -1
- package/lib/core/dom.d.ts +1 -1
- package/lib/core/output.d.ts +16 -0
- package/lib/core/output.js +10 -0
- package/lib/core/raw-suffix.d.ts +14 -0
- package/lib/core/raw-suffix.js +28 -0
- package/lib/core/reconciler.js +3 -0
- package/lib/core/render-node-to-output.js +7 -0
- package/lib/core/renderer.d.ts +3 -0
- package/lib/core/renderer.js +3 -0
- package/lib/core/tinky.d.ts +10 -0
- package/lib/core/tinky.js +68 -6
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +1 -1
package/lib/components/App.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export declare class App extends PureComponent<AppProps, State> {
|
|
|
97
97
|
rawModeEnabledCount: number;
|
|
98
98
|
internal_eventEmitter: EventEmitter;
|
|
99
99
|
isRawModeSupported(): boolean;
|
|
100
|
-
render(): import("react
|
|
100
|
+
render(): import("react").JSX.Element;
|
|
101
101
|
componentDidMount(): void;
|
|
102
102
|
componentWillUnmount(): void;
|
|
103
103
|
componentDidCatch(error: Error): void;
|
|
@@ -4,5 +4,5 @@ interface ErrorProps {
|
|
|
4
4
|
/**
|
|
5
5
|
* Renders an overview of an error, including the message and stack trace.
|
|
6
6
|
*/
|
|
7
|
-
export declare function ErrorOverview({ error }: ErrorProps): import("react
|
|
7
|
+
export declare function ErrorOverview({ error }: ErrorProps): import("react").JSX.Element;
|
|
8
8
|
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for the Raw component.
|
|
3
|
+
*/
|
|
4
|
+
export interface RawProps {
|
|
5
|
+
/**
|
|
6
|
+
* Raw content to output unchanged at the component's layout position.
|
|
7
|
+
* This bypasses normal text rendering: no escaping, wrapping, or
|
|
8
|
+
* modification is applied.
|
|
9
|
+
*/
|
|
10
|
+
readonly content: string;
|
|
11
|
+
/**
|
|
12
|
+
* Raw content to write to stdout when the component unmounts.
|
|
13
|
+
* Used as a cleanup sequence (e.g., to clear a terminal image).
|
|
14
|
+
*/
|
|
15
|
+
readonly cleanup?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Outputs raw terminal escape sequences at a specific layout position.
|
|
19
|
+
*
|
|
20
|
+
* The content bypasses tinky's normal text rendering pipeline and is written
|
|
21
|
+
* directly to stdout after the main frame is rendered. This is useful for
|
|
22
|
+
* terminal image protocols (Kitty, iTerm2, Sixel), custom animations, or
|
|
23
|
+
* any content that must not be modified.
|
|
24
|
+
*
|
|
25
|
+
* `Raw` has zero width and height in layout. To reserve space for the raw
|
|
26
|
+
* content (e.g., for an image), wrap it in a `Box` with the desired
|
|
27
|
+
* dimensions:
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <Box width={40} height={10}>
|
|
32
|
+
* <Raw content={kittyImageSequence} />
|
|
33
|
+
* </Box>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Raw
|
|
39
|
+
* content={imageSequence}
|
|
40
|
+
* cleanup={'\x1b_Ga=d\x1b\\'}
|
|
41
|
+
* />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function Raw({ content, cleanup }: RawProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useStdout } from "../hooks/use-stdout.js";
|
|
4
|
+
const rawStyle = { width: 0, height: 0 };
|
|
5
|
+
/**
|
|
6
|
+
* Outputs raw terminal escape sequences at a specific layout position.
|
|
7
|
+
*
|
|
8
|
+
* The content bypasses tinky's normal text rendering pipeline and is written
|
|
9
|
+
* directly to stdout after the main frame is rendered. This is useful for
|
|
10
|
+
* terminal image protocols (Kitty, iTerm2, Sixel), custom animations, or
|
|
11
|
+
* any content that must not be modified.
|
|
12
|
+
*
|
|
13
|
+
* `Raw` has zero width and height in layout. To reserve space for the raw
|
|
14
|
+
* content (e.g., for an image), wrap it in a `Box` with the desired
|
|
15
|
+
* dimensions:
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <Box width={40} height={10}>
|
|
20
|
+
* <Raw content={kittyImageSequence} />
|
|
21
|
+
* </Box>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Raw
|
|
27
|
+
* content={imageSequence}
|
|
28
|
+
* cleanup={'\x1b_Ga=d\x1b\\'}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function Raw({ content, cleanup }) {
|
|
33
|
+
const { stdout } = useStdout();
|
|
34
|
+
const cleanupRef = useRef(cleanup);
|
|
35
|
+
cleanupRef.current = cleanup;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
return () => {
|
|
38
|
+
const output = cleanupRef.current;
|
|
39
|
+
if (output) {
|
|
40
|
+
stdout.write(output);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}, [stdout]);
|
|
44
|
+
return _jsx("tinky-raw", { style: rawStyle, internal_rawContent: content });
|
|
45
|
+
}
|
|
@@ -79,4 +79,4 @@ export interface SeparatorProps extends TextStyles {
|
|
|
79
79
|
* />
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
-
export declare function Separator({ char, direction, color, dimColor, backgroundColor, bold, italic, underline, strikethrough, inverse, }: SeparatorProps): import("react
|
|
82
|
+
export declare function Separator({ char, direction, color, dimColor, backgroundColor, bold, italic, underline, strikethrough, inverse, }: SeparatorProps): import("react").JSX.Element;
|
package/lib/components/Text.d.ts
CHANGED
|
@@ -47,4 +47,4 @@ export interface TextProps extends TextStyles {
|
|
|
47
47
|
* );
|
|
48
48
|
* ```
|
|
49
49
|
*/
|
|
50
|
-
export declare function Text({ color, backgroundColor, dimColor, bold, italic, underline, strikethrough, inverse, wrap, children, "aria-label": ariaLabel, "aria-hidden": ariaHidden, }: TextProps): import("react
|
|
50
|
+
export declare function Text({ color, backgroundColor, dimColor, bold, italic, underline, strikethrough, inverse, wrap, children, "aria-label": ariaLabel, "aria-hidden": ariaHidden, }: TextProps): import("react").JSX.Element | null;
|
|
@@ -38,4 +38,4 @@ export interface TransformProps {
|
|
|
38
38
|
* // Output: HELLO WORLD
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
|
-
export declare function Transform({ children, transform, accessibilityLabel, }: TransformProps): import("react
|
|
41
|
+
export declare function Transform({ children, transform, accessibilityLabel, }: TransformProps): import("react").JSX.Element | null;
|
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
* Output implementation that writes directly into CellBuffer.
|
|
3
3
|
*/
|
|
4
4
|
import { type CellBuffer } from "./cell-buffer.js";
|
|
5
|
-
import { type Clip, type OutputLike, type OutputWriteOptions } from "./output.js";
|
|
5
|
+
import { type Clip, type OutputLike, type OutputWriteOptions, type RawEntry } from "./output.js";
|
|
6
6
|
import { type AnsiCode } from "../types/ansi.js";
|
|
7
7
|
export declare class CellOutput implements OutputLike {
|
|
8
8
|
private readonly clips;
|
|
9
9
|
private readonly styleIdByRef;
|
|
10
10
|
private lastStyleRef;
|
|
11
11
|
private lastStyleId;
|
|
12
|
+
private readonly rawEntries;
|
|
12
13
|
readonly buffer: CellBuffer;
|
|
13
14
|
constructor(buffer: CellBuffer);
|
|
14
15
|
clip(clip: Clip): void;
|
|
15
16
|
unclip(): void;
|
|
17
|
+
writeRaw(x: number, y: number, content: string): void;
|
|
18
|
+
getRawEntries(): RawEntry[];
|
|
16
19
|
fill(x: number, y: number, length: number, char: string, charWidth: number, styles: AnsiCode[]): void;
|
|
17
20
|
private resolveStyleId;
|
|
18
21
|
private writePlainLine;
|
package/lib/core/cell-output.js
CHANGED
|
@@ -37,6 +37,7 @@ export class CellOutput {
|
|
|
37
37
|
styleIdByRef = new WeakMap();
|
|
38
38
|
lastStyleRef;
|
|
39
39
|
lastStyleId = 0;
|
|
40
|
+
rawEntries = [];
|
|
40
41
|
buffer;
|
|
41
42
|
constructor(buffer) {
|
|
42
43
|
this.buffer = buffer;
|
|
@@ -47,6 +48,15 @@ export class CellOutput {
|
|
|
47
48
|
unclip() {
|
|
48
49
|
this.clips.pop();
|
|
49
50
|
}
|
|
51
|
+
writeRaw(x, y, content) {
|
|
52
|
+
if (!content) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.rawEntries.push({ x, y, content });
|
|
56
|
+
}
|
|
57
|
+
getRawEntries() {
|
|
58
|
+
return this.rawEntries;
|
|
59
|
+
}
|
|
50
60
|
fill(x, y, length, char, charWidth, styles) {
|
|
51
61
|
if (length <= 0) {
|
|
52
62
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RawEntry } from "./output.js";
|
|
1
2
|
import { type DOMElement } from "./dom.js";
|
|
2
3
|
import { type CellBuffer } from "./cell-buffer.js";
|
|
3
4
|
interface CellRendererResult {
|
|
@@ -9,6 +10,8 @@ interface CellRendererResult {
|
|
|
9
10
|
staticOutput: string;
|
|
10
11
|
/** Screen-reader text when screen-reader mode is enabled. */
|
|
11
12
|
screenReaderOutput?: string;
|
|
13
|
+
/** Raw entries to write after the main output. */
|
|
14
|
+
rawEntries: RawEntry[];
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
17
|
* Renderer that writes interactive output into a CellBuffer.
|
|
@@ -16,7 +16,7 @@ export const cellRenderer = (node, buffer, isScreenReaderEnabled) => {
|
|
|
16
16
|
if (!node.taffyNode) {
|
|
17
17
|
buffer.resize(0, 0);
|
|
18
18
|
buffer.clear();
|
|
19
|
-
return { buffer, outputHeight: 0, staticOutput: "" };
|
|
19
|
+
return { buffer, outputHeight: 0, staticOutput: "", rawEntries: [] };
|
|
20
20
|
}
|
|
21
21
|
if (isScreenReaderEnabled) {
|
|
22
22
|
const output = renderNodeToScreenReaderOutput(node, {
|
|
@@ -34,6 +34,7 @@ export const cellRenderer = (node, buffer, isScreenReaderEnabled) => {
|
|
|
34
34
|
outputHeight,
|
|
35
35
|
staticOutput: staticOutput ? `${staticOutput}\n` : "",
|
|
36
36
|
screenReaderOutput: output,
|
|
37
|
+
rawEntries: [],
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
const layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
@@ -56,5 +57,6 @@ export const cellRenderer = (node, buffer, isScreenReaderEnabled) => {
|
|
|
56
57
|
buffer,
|
|
57
58
|
outputHeight: buffer.height,
|
|
58
59
|
staticOutput: staticOutput ? `${staticOutput.get().output}\n` : "",
|
|
60
|
+
rawEntries: output.getRawEntries(),
|
|
59
61
|
};
|
|
60
62
|
};
|
package/lib/core/dom.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type TextName = "#text";
|
|
|
22
22
|
/**
|
|
23
23
|
* Type representing element names in the Tinky DOM.
|
|
24
24
|
*/
|
|
25
|
-
export type ElementNames = "tinky-root" | "tinky-box" | "tinky-text" | "tinky-virtual-text" | "tinky-separator";
|
|
25
|
+
export type ElementNames = "tinky-root" | "tinky-box" | "tinky-text" | "tinky-virtual-text" | "tinky-separator" | "tinky-raw";
|
|
26
26
|
/**
|
|
27
27
|
* Union type of all possible node names.
|
|
28
28
|
*/
|
package/lib/core/output.d.ts
CHANGED
|
@@ -21,6 +21,17 @@ export interface Clip {
|
|
|
21
21
|
/** Bottom boundary (undefined for no limit). */
|
|
22
22
|
y2: number | undefined;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Represents a raw output entry that bypasses normal text rendering.
|
|
26
|
+
*/
|
|
27
|
+
export interface RawEntry {
|
|
28
|
+
/** X coordinate (column) of the raw content. */
|
|
29
|
+
x: number;
|
|
30
|
+
/** Y coordinate (row) of the raw content. */
|
|
31
|
+
y: number;
|
|
32
|
+
/** Raw content to output unchanged. */
|
|
33
|
+
content: string;
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
36
|
* Shared write/clip/unclip surface used by render traversal.
|
|
26
37
|
*/
|
|
@@ -29,6 +40,8 @@ export interface OutputLike {
|
|
|
29
40
|
clip(clip: Clip): void;
|
|
30
41
|
unclip(): void;
|
|
31
42
|
fill(x: number, y: number, length: number, char: string, charWidth: number, styles: AnsiCode[]): void;
|
|
43
|
+
writeRaw(x: number, y: number, content: string): void;
|
|
44
|
+
getRawEntries(): RawEntry[];
|
|
32
45
|
}
|
|
33
46
|
/**
|
|
34
47
|
* "Virtual" output class
|
|
@@ -42,6 +55,7 @@ export declare class Output implements OutputLike {
|
|
|
42
55
|
width: number;
|
|
43
56
|
height: number;
|
|
44
57
|
private readonly operations;
|
|
58
|
+
private readonly rawEntries;
|
|
45
59
|
/**
|
|
46
60
|
* Creates a new Output instance.
|
|
47
61
|
*
|
|
@@ -68,6 +82,8 @@ export declare class Output implements OutputLike {
|
|
|
68
82
|
*/
|
|
69
83
|
unclip(): void;
|
|
70
84
|
fill(x: number, y: number, length: number, char: string, _charWidth: number, styles: AnsiCode[]): void;
|
|
85
|
+
writeRaw(x: number, y: number, content: string): void;
|
|
86
|
+
getRawEntries(): RawEntry[];
|
|
71
87
|
/**
|
|
72
88
|
* Generates the final output string and its height.
|
|
73
89
|
*
|
package/lib/core/output.js
CHANGED
|
@@ -14,6 +14,7 @@ export class Output {
|
|
|
14
14
|
width;
|
|
15
15
|
height;
|
|
16
16
|
operations = [];
|
|
17
|
+
rawEntries = [];
|
|
17
18
|
/**
|
|
18
19
|
* Creates a new Output instance.
|
|
19
20
|
*
|
|
@@ -75,6 +76,15 @@ export class Output {
|
|
|
75
76
|
const text = prefix + char.repeat(length) + suffix;
|
|
76
77
|
this.write(x, y, text, { transformers: [] });
|
|
77
78
|
}
|
|
79
|
+
writeRaw(x, y, content) {
|
|
80
|
+
if (!content) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.rawEntries.push({ x, y, content });
|
|
84
|
+
}
|
|
85
|
+
getRawEntries() {
|
|
86
|
+
return this.rawEntries;
|
|
87
|
+
}
|
|
78
88
|
/**
|
|
79
89
|
* Generates the final output string and its height.
|
|
80
90
|
*
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RawEntry } from "./output.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a raw suffix string from collected raw entries.
|
|
4
|
+
*
|
|
5
|
+
* After the main frame is written to stdout, the raw suffix moves the cursor
|
|
6
|
+
* to each raw entry's layout position and outputs its content unchanged.
|
|
7
|
+
* After all entries, the cursor is moved back to the end of the frame.
|
|
8
|
+
*
|
|
9
|
+
* @param entries - Raw entries collected during render traversal.
|
|
10
|
+
* @param frameHeight - Height of the rendered frame in rows.
|
|
11
|
+
* @param rowOffset - Rows already written above the interactive frame.
|
|
12
|
+
* @returns The raw suffix string, or empty string if no entries.
|
|
13
|
+
*/
|
|
14
|
+
export declare const buildRawSuffix: (entries: readonly RawEntry[], frameHeight: number, rowOffset?: number) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ansiEscapes from "../utils/ansi-escapes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a raw suffix string from collected raw entries.
|
|
4
|
+
*
|
|
5
|
+
* After the main frame is written to stdout, the raw suffix moves the cursor
|
|
6
|
+
* to each raw entry's layout position and outputs its content unchanged.
|
|
7
|
+
* After all entries, the cursor is moved back to the end of the frame.
|
|
8
|
+
*
|
|
9
|
+
* @param entries - Raw entries collected during render traversal.
|
|
10
|
+
* @param frameHeight - Height of the rendered frame in rows.
|
|
11
|
+
* @param rowOffset - Rows already written above the interactive frame.
|
|
12
|
+
* @returns The raw suffix string, or empty string if no entries.
|
|
13
|
+
*/
|
|
14
|
+
export const buildRawSuffix = (entries, frameHeight, rowOffset = 0) => {
|
|
15
|
+
if (entries.length === 0) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
const normalizedRowOffset = Math.max(0, Math.floor(rowOffset));
|
|
19
|
+
const parts = [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const x = Math.max(0, Math.floor(entry.x));
|
|
22
|
+
const y = normalizedRowOffset + Math.max(0, Math.floor(entry.y));
|
|
23
|
+
parts.push(ansiEscapes.cursorTo(x, y), entry.content);
|
|
24
|
+
}
|
|
25
|
+
const frameEndY = normalizedRowOffset + Math.max(0, Math.floor(frameHeight) - 1);
|
|
26
|
+
parts.push(ansiEscapes.cursorTo(0, frameEndY));
|
|
27
|
+
return parts.join("");
|
|
28
|
+
};
|
package/lib/core/reconciler.js
CHANGED
|
@@ -112,6 +112,9 @@ export const reconciler = createReconciler({
|
|
|
112
112
|
if (hostContext.isInsideText && originalType === "tinky-box") {
|
|
113
113
|
throw new Error(`<Box> can’t be nested inside <Text> component`);
|
|
114
114
|
}
|
|
115
|
+
if (hostContext.isInsideText && originalType === "tinky-raw") {
|
|
116
|
+
throw new Error(`<Raw> can’t be nested inside <Text> component`);
|
|
117
|
+
}
|
|
115
118
|
const type = originalType === "tinky-text" && hostContext.isInsideText
|
|
116
119
|
? "tinky-virtual-text"
|
|
117
120
|
: originalType;
|
|
@@ -143,6 +143,13 @@ export const renderNodeToOutput = (node, output, options) => {
|
|
|
143
143
|
}
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
|
+
if (node.nodeName === "tinky-raw") {
|
|
147
|
+
const content = node.attributes["internal_rawContent"];
|
|
148
|
+
if (content) {
|
|
149
|
+
output.writeRaw(x, y, content);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
146
153
|
if (node.nodeName === "tinky-separator") {
|
|
147
154
|
const separatorChar = node.attributes["internal_separatorChar"] || "─";
|
|
148
155
|
const direction = node.attributes["internal_separatorDirection"] ||
|
package/lib/core/renderer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DOMElement } from "./dom.js";
|
|
2
|
+
import { type RawEntry } from "./output.js";
|
|
2
3
|
/**
|
|
3
4
|
* Interface representing the result of a render operation.
|
|
4
5
|
*/
|
|
@@ -15,6 +16,8 @@ interface Result {
|
|
|
15
16
|
* The static output generated during rendering (e.g., from <Static>).
|
|
16
17
|
*/
|
|
17
18
|
staticOutput: string;
|
|
19
|
+
/** Raw entries to write after the main output. */
|
|
20
|
+
rawEntries: RawEntry[];
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* Renders the DOM tree to a string output.
|
package/lib/core/renderer.js
CHANGED
|
@@ -25,6 +25,7 @@ export const renderer = (node, isScreenReaderEnabled) => {
|
|
|
25
25
|
output,
|
|
26
26
|
outputHeight,
|
|
27
27
|
staticOutput: staticOutput ? `${staticOutput}\n` : "",
|
|
28
|
+
rawEntries: [],
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
const layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
@@ -55,11 +56,13 @@ export const renderer = (node, isScreenReaderEnabled) => {
|
|
|
55
56
|
// Newline at the end is needed, because static output doesn't have one,
|
|
56
57
|
// so interactive output will override last line of static output
|
|
57
58
|
staticOutput: staticOutput ? `${staticOutput.buffer.toString()}\n` : "",
|
|
59
|
+
rawEntries: output.getRawEntries(),
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
return {
|
|
61
63
|
output: "",
|
|
62
64
|
outputHeight: 0,
|
|
63
65
|
staticOutput: "",
|
|
66
|
+
rawEntries: [],
|
|
64
67
|
};
|
|
65
68
|
};
|
package/lib/core/tinky.d.ts
CHANGED
|
@@ -101,6 +101,12 @@ export declare class Tinky {
|
|
|
101
101
|
private readonly rootNode;
|
|
102
102
|
/** Full static output for debug mode. */
|
|
103
103
|
private fullStaticOutput;
|
|
104
|
+
/** Raw entries from the last rendered frame. */
|
|
105
|
+
private lastRawEntries;
|
|
106
|
+
/** Height of the last frame used to place raw output. */
|
|
107
|
+
private lastRawFrameHeight;
|
|
108
|
+
/** Raw suffix from the last rendered frame. */
|
|
109
|
+
private lastRawSuffix;
|
|
104
110
|
/** Promise that resolves when the app exits. */
|
|
105
111
|
private exitPromise?;
|
|
106
112
|
/** Function to restore console after patching. */
|
|
@@ -160,6 +166,10 @@ export declare class Tinky {
|
|
|
160
166
|
* the next render cycle.
|
|
161
167
|
*/
|
|
162
168
|
private swapRunBuffers;
|
|
169
|
+
private updateRawState;
|
|
170
|
+
private clearRawState;
|
|
171
|
+
private buildRawSuffix;
|
|
172
|
+
private writeRawSuffix;
|
|
163
173
|
private redrawRunBuffer;
|
|
164
174
|
/**
|
|
165
175
|
* Renders the given React node.
|
package/lib/core/tinky.js
CHANGED
|
@@ -11,6 +11,7 @@ import wrapAnsi from "wrap-ansi";
|
|
|
11
11
|
import { reconciler } from "./reconciler.js";
|
|
12
12
|
import { renderer } from "./renderer.js";
|
|
13
13
|
import { cellRenderer } from "./cell-renderer.js";
|
|
14
|
+
import { buildRawSuffix } from "./raw-suffix.js";
|
|
14
15
|
import * as dom from "./dom.js";
|
|
15
16
|
import { logUpdate } from "./log-update.js";
|
|
16
17
|
import { cellLogUpdateRun, } from "./cell-log-update-run.js";
|
|
@@ -23,6 +24,15 @@ import { notifySizeObservers } from "./size-observer.js";
|
|
|
23
24
|
const noop = () => {
|
|
24
25
|
// no-op
|
|
25
26
|
};
|
|
27
|
+
const countOutputRows = (output) => {
|
|
28
|
+
let rows = 0;
|
|
29
|
+
for (const character of output) {
|
|
30
|
+
if (character === "\n") {
|
|
31
|
+
rows++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
};
|
|
26
36
|
/**
|
|
27
37
|
* Tinky core class responsible for managing the React tree rendering,
|
|
28
38
|
* lifecycle, and terminal output.
|
|
@@ -50,6 +60,12 @@ export class Tinky {
|
|
|
50
60
|
rootNode;
|
|
51
61
|
/** Full static output for debug mode. */
|
|
52
62
|
fullStaticOutput;
|
|
63
|
+
/** Raw entries from the last rendered frame. */
|
|
64
|
+
lastRawEntries;
|
|
65
|
+
/** Height of the last frame used to place raw output. */
|
|
66
|
+
lastRawFrameHeight;
|
|
67
|
+
/** Raw suffix from the last rendered frame. */
|
|
68
|
+
lastRawSuffix;
|
|
53
69
|
/** Promise that resolves when the app exits. */
|
|
54
70
|
exitPromise;
|
|
55
71
|
/** Function to restore console after patching. */
|
|
@@ -132,6 +148,9 @@ export class Tinky {
|
|
|
132
148
|
// that it's rerendered every time, not just new static parts, like in
|
|
133
149
|
// non-debug mode
|
|
134
150
|
this.fullStaticOutput = "";
|
|
151
|
+
this.lastRawEntries = [];
|
|
152
|
+
this.lastRawFrameHeight = 0;
|
|
153
|
+
this.lastRawSuffix = "";
|
|
135
154
|
this.container = reconciler.createContainer(this.rootNode, LegacyRoot, null, false, null, "id", noop, noop, noop, noop, null);
|
|
136
155
|
// Unmount when process exits
|
|
137
156
|
this.unsubscribeExit = onExit(() => {
|
|
@@ -236,20 +255,26 @@ export class Tinky {
|
|
|
236
255
|
}
|
|
237
256
|
if (this.usesRunIncrementalRendering && this.backBuffer) {
|
|
238
257
|
const startTime = performance.now();
|
|
239
|
-
const { buffer, outputHeight, staticOutput } = cellRenderer(this.rootNode, this.backBuffer, false);
|
|
258
|
+
const { buffer, outputHeight, staticOutput, rawEntries } = cellRenderer(this.rootNode, this.backBuffer, false);
|
|
240
259
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
241
260
|
const hasStaticOutput = staticOutput && staticOutput !== "\n";
|
|
242
261
|
if (hasStaticOutput) {
|
|
243
262
|
this.fullStaticOutput += staticOutput;
|
|
244
263
|
}
|
|
245
|
-
|
|
246
|
-
|
|
264
|
+
const rawSuffix = this.updateRawState(rawEntries, outputHeight);
|
|
265
|
+
// Raw content uses absolute CUP positioning, so redraw from a known
|
|
266
|
+
// static prefix before writing the raw suffix.
|
|
267
|
+
const needsClearTerminal = rawSuffix.length > 0 ||
|
|
268
|
+
Boolean(this.options.stdout.rows &&
|
|
269
|
+
this.lastOutputHeight >= this.options.stdout.rows);
|
|
270
|
+
if (needsClearTerminal) {
|
|
247
271
|
const output = buffer.toString();
|
|
248
272
|
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
249
273
|
this.lastOutput = output;
|
|
250
274
|
this.lastOutputHeight = outputHeight;
|
|
251
275
|
this.cellLog?.sync(buffer);
|
|
252
276
|
this.swapRunBuffers();
|
|
277
|
+
this.writeRawSuffix();
|
|
253
278
|
return;
|
|
254
279
|
}
|
|
255
280
|
if (hasStaticOutput) {
|
|
@@ -269,7 +294,7 @@ export class Tinky {
|
|
|
269
294
|
return;
|
|
270
295
|
}
|
|
271
296
|
const startTime = performance.now();
|
|
272
|
-
const { output, outputHeight, staticOutput } = renderer(this.rootNode, this.isScreenReaderEnabled);
|
|
297
|
+
const { output, outputHeight, staticOutput, rawEntries } = renderer(this.rootNode, this.isScreenReaderEnabled);
|
|
273
298
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
274
299
|
// If <Static> output isn't empty, it means new children have been added to
|
|
275
300
|
// it
|
|
@@ -278,10 +303,13 @@ export class Tinky {
|
|
|
278
303
|
if (hasStaticOutput) {
|
|
279
304
|
this.fullStaticOutput += staticOutput;
|
|
280
305
|
}
|
|
306
|
+
this.updateRawState(rawEntries, outputHeight);
|
|
281
307
|
this.options.stdout.write(this.fullStaticOutput + output);
|
|
308
|
+
this.writeRawSuffix();
|
|
282
309
|
return;
|
|
283
310
|
}
|
|
284
311
|
if (this.isCI) {
|
|
312
|
+
this.clearRawState();
|
|
285
313
|
if (hasStaticOutput) {
|
|
286
314
|
this.options.stdout.write(staticOutput);
|
|
287
315
|
}
|
|
@@ -290,6 +318,7 @@ export class Tinky {
|
|
|
290
318
|
return;
|
|
291
319
|
}
|
|
292
320
|
if (this.isScreenReaderEnabled) {
|
|
321
|
+
this.clearRawState();
|
|
293
322
|
if (hasStaticOutput) {
|
|
294
323
|
// We need to erase the main output before writing new static output
|
|
295
324
|
const erase = this.lastOutputHeight > 0
|
|
@@ -325,12 +354,18 @@ export class Tinky {
|
|
|
325
354
|
if (hasStaticOutput) {
|
|
326
355
|
this.fullStaticOutput += staticOutput;
|
|
327
356
|
}
|
|
328
|
-
|
|
329
|
-
|
|
357
|
+
const rawSuffix = this.updateRawState(rawEntries, outputHeight);
|
|
358
|
+
// Raw content uses absolute CUP positioning, so redraw from a known static
|
|
359
|
+
// prefix before writing the raw suffix.
|
|
360
|
+
const needsClearTerminal = rawSuffix.length > 0 ||
|
|
361
|
+
Boolean(this.options.stdout.rows &&
|
|
362
|
+
this.lastOutputHeight >= this.options.stdout.rows);
|
|
363
|
+
if (needsClearTerminal) {
|
|
330
364
|
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
331
365
|
this.lastOutput = output;
|
|
332
366
|
this.lastOutputHeight = outputHeight;
|
|
333
367
|
this.log.sync(output);
|
|
368
|
+
this.writeRawSuffix();
|
|
334
369
|
return;
|
|
335
370
|
}
|
|
336
371
|
// To ensure static output is cleanly rendered before main output, clear
|
|
@@ -357,6 +392,28 @@ export class Tinky {
|
|
|
357
392
|
this.frontBuffer = this.backBuffer;
|
|
358
393
|
this.backBuffer = previous;
|
|
359
394
|
}
|
|
395
|
+
updateRawState(rawEntries, outputHeight) {
|
|
396
|
+
this.lastRawEntries = rawEntries;
|
|
397
|
+
this.lastRawFrameHeight = outputHeight;
|
|
398
|
+
this.lastRawSuffix = this.buildRawSuffix();
|
|
399
|
+
return this.lastRawSuffix;
|
|
400
|
+
}
|
|
401
|
+
clearRawState() {
|
|
402
|
+
this.lastRawEntries = [];
|
|
403
|
+
this.lastRawFrameHeight = 0;
|
|
404
|
+
this.lastRawSuffix = "";
|
|
405
|
+
}
|
|
406
|
+
buildRawSuffix(extraRowOffset = 0) {
|
|
407
|
+
return buildRawSuffix(this.lastRawEntries, this.lastRawFrameHeight, countOutputRows(this.fullStaticOutput) + extraRowOffset);
|
|
408
|
+
}
|
|
409
|
+
writeRawSuffix(prefix = "") {
|
|
410
|
+
const rawSuffix = prefix
|
|
411
|
+
? this.buildRawSuffix(countOutputRows(prefix))
|
|
412
|
+
: this.lastRawSuffix;
|
|
413
|
+
if (rawSuffix) {
|
|
414
|
+
this.options.stdout.write(rawSuffix);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
360
417
|
redrawRunBuffer() {
|
|
361
418
|
if (!this.frontBuffer || !this.cellLog) {
|
|
362
419
|
return;
|
|
@@ -364,6 +421,7 @@ export class Tinky {
|
|
|
364
421
|
this.cellLog(this.frontBuffer, this.frontBuffer, { forceFull: true });
|
|
365
422
|
this.lastOutput = this.frontBuffer.toString();
|
|
366
423
|
this.lastOutputHeight = this.frontBuffer.height;
|
|
424
|
+
this.writeRawSuffix();
|
|
367
425
|
}
|
|
368
426
|
/**
|
|
369
427
|
* Renders the given React node.
|
|
@@ -386,6 +444,7 @@ export class Tinky {
|
|
|
386
444
|
}
|
|
387
445
|
if (this.options.debug) {
|
|
388
446
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
447
|
+
this.writeRawSuffix(data);
|
|
389
448
|
return;
|
|
390
449
|
}
|
|
391
450
|
if (this.isCI) {
|
|
@@ -401,6 +460,7 @@ export class Tinky {
|
|
|
401
460
|
this.log.clear();
|
|
402
461
|
this.options.stdout.write(data);
|
|
403
462
|
this.log(this.lastOutput);
|
|
463
|
+
this.writeRawSuffix(data);
|
|
404
464
|
}
|
|
405
465
|
/**
|
|
406
466
|
* Writes data to stderr.
|
|
@@ -414,6 +474,7 @@ export class Tinky {
|
|
|
414
474
|
if (this.options.debug) {
|
|
415
475
|
this.options.stderr.write(data);
|
|
416
476
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
477
|
+
this.writeRawSuffix(data);
|
|
417
478
|
return;
|
|
418
479
|
}
|
|
419
480
|
if (this.isCI) {
|
|
@@ -429,6 +490,7 @@ export class Tinky {
|
|
|
429
490
|
this.log.clear();
|
|
430
491
|
this.options.stderr.write(data);
|
|
431
492
|
this.log(this.lastOutput);
|
|
493
|
+
this.writeRawSuffix(data);
|
|
432
494
|
}
|
|
433
495
|
/**
|
|
434
496
|
* Unmounts the Tinky app.
|
package/lib/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { Transform, type TransformProps } from "./components/Transform.js";
|
|
|
12
12
|
export { Newline, type NewlineProps } from "./components/Newline.js";
|
|
13
13
|
export { Separator, type SeparatorProps } from "./components/Separator.js";
|
|
14
14
|
export { Spacer } from "./components/Spacer.js";
|
|
15
|
+
export { Raw, type RawProps } from "./components/Raw.js";
|
|
15
16
|
export { useInput, type InputHandler, type InputOptions, type Key, } from "./hooks/use-input.js";
|
|
16
17
|
export { useApp } from "./hooks/use-app.js";
|
|
17
18
|
export { useStdin } from "./hooks/use-stdin.js";
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { Transform } from "./components/Transform.js";
|
|
|
10
10
|
export { Newline } from "./components/Newline.js";
|
|
11
11
|
export { Separator } from "./components/Separator.js";
|
|
12
12
|
export { Spacer } from "./components/Spacer.js";
|
|
13
|
+
export { Raw } from "./components/Raw.js";
|
|
13
14
|
export { useInput, } from "./hooks/use-input.js";
|
|
14
15
|
export { useApp } from "./hooks/use-app.js";
|
|
15
16
|
export { useStdin } from "./hooks/use-stdin.js";
|