tinky 1.7.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/Text.js +12 -3
- 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 +4 -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 +6 -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/size-observer.d.ts +60 -0
- package/lib/core/size-observer.js +133 -0
- package/lib/core/tinky.d.ts +10 -0
- package/lib/core/tinky.js +71 -6
- package/lib/hooks/use-size-observer.d.ts +44 -0
- package/lib/hooks/use-size-observer.js +70 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -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;
|
package/lib/components/Text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useContext } from "react";
|
|
2
|
+
import { useContext, useCallback } from "react";
|
|
3
3
|
import { applyTextStyles, } from "../utils/apply-text-styles.js";
|
|
4
4
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
5
5
|
import { backgroundContext } from "../contexts/BackgroundContext.js";
|
|
@@ -41,7 +41,7 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
|
|
|
41
41
|
if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
-
const transform = (children) => {
|
|
44
|
+
const transform = useCallback((children) => {
|
|
45
45
|
return applyTextStyles(children, {
|
|
46
46
|
color,
|
|
47
47
|
backgroundColor: effectiveBackgroundColor,
|
|
@@ -52,7 +52,16 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
|
|
|
52
52
|
strikethrough,
|
|
53
53
|
inverse,
|
|
54
54
|
});
|
|
55
|
-
}
|
|
55
|
+
}, [
|
|
56
|
+
color,
|
|
57
|
+
effectiveBackgroundColor,
|
|
58
|
+
dimColor,
|
|
59
|
+
bold,
|
|
60
|
+
italic,
|
|
61
|
+
underline,
|
|
62
|
+
strikethrough,
|
|
63
|
+
inverse,
|
|
64
|
+
]);
|
|
56
65
|
if (isScreenReaderEnabled && ariaHidden) {
|
|
57
66
|
return null;
|
|
58
67
|
}
|
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Styles } from "./styles.js";
|
|
2
2
|
import { type OutputTransformer } from "./render-node-to-output.js";
|
|
3
3
|
import { TaffyNode } from "./taffy-node.js";
|
|
4
|
+
import { type SizeObserver } from "./size-observer.js";
|
|
4
5
|
/**
|
|
5
6
|
* Interface representing a node in the Tinky tree.
|
|
6
7
|
*/
|
|
@@ -21,7 +22,7 @@ export type TextName = "#text";
|
|
|
21
22
|
/**
|
|
22
23
|
* Type representing element names in the Tinky DOM.
|
|
23
24
|
*/
|
|
24
|
-
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";
|
|
25
26
|
/**
|
|
26
27
|
* Union type of all possible node names.
|
|
27
28
|
*/
|
|
@@ -74,6 +75,8 @@ export type DOMElement = {
|
|
|
74
75
|
onRender?: () => void;
|
|
75
76
|
/** Callback to trigger an immediate render. */
|
|
76
77
|
onImmediateRender?: () => void;
|
|
78
|
+
/** Set of resize observers attached to this element. */
|
|
79
|
+
resizeObservers?: Set<SizeObserver>;
|
|
77
80
|
} & TinkyNode;
|
|
78
81
|
/**
|
|
79
82
|
* Interface representing a text node in Tinky.
|
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
|
@@ -5,6 +5,7 @@ import { createContext } from "react";
|
|
|
5
5
|
import { createTextNode, appendChildNode, insertBeforeNode, removeChildNode, setStyle, setTextNodeValue, createNode, setAttribute, } from "./dom.js";
|
|
6
6
|
import { applyStyles } from "./styles.js";
|
|
7
7
|
import { process } from "../utils/node-adapter.js";
|
|
8
|
+
import { removeSizeObserversInSubtree } from "./size-observer.js";
|
|
8
9
|
// We need to conditionally perform devtools connection to avoid
|
|
9
10
|
// accidentally breaking other third-party code.
|
|
10
11
|
if (process?.env?.["DEV"] === "true") {
|
|
@@ -111,6 +112,9 @@ export const reconciler = createReconciler({
|
|
|
111
112
|
if (hostContext.isInsideText && originalType === "tinky-box") {
|
|
112
113
|
throw new Error(`<Box> can’t be nested inside <Text> component`);
|
|
113
114
|
}
|
|
115
|
+
if (hostContext.isInsideText && originalType === "tinky-raw") {
|
|
116
|
+
throw new Error(`<Raw> can’t be nested inside <Text> component`);
|
|
117
|
+
}
|
|
114
118
|
const type = originalType === "tinky-text" && hostContext.isInsideText
|
|
115
119
|
? "tinky-virtual-text"
|
|
116
120
|
: originalType;
|
|
@@ -204,6 +208,7 @@ export const reconciler = createReconciler({
|
|
|
204
208
|
insertInContainerBefore: insertBeforeNode,
|
|
205
209
|
removeChildFromContainer(node, removeNode) {
|
|
206
210
|
removeChildNode(node, removeNode);
|
|
211
|
+
removeSizeObserversInSubtree(removeNode);
|
|
207
212
|
cleanupTaffyNode(removeNode.taffyNode);
|
|
208
213
|
},
|
|
209
214
|
commitUpdate(node, _type, oldProps, newProps) {
|
|
@@ -241,6 +246,7 @@ export const reconciler = createReconciler({
|
|
|
241
246
|
},
|
|
242
247
|
removeChild(node, removeNode) {
|
|
243
248
|
removeChildNode(node, removeNode);
|
|
249
|
+
removeSizeObserversInSubtree(removeNode);
|
|
244
250
|
cleanupTaffyNode(removeNode.taffyNode);
|
|
245
251
|
},
|
|
246
252
|
setCurrentUpdatePriority(newPriority) {
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type DOMElement, type DOMNode } from "./dom.js";
|
|
2
|
+
import { type Dimension } from "../utils/dimension.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interface for elements returned by the SizeObserver callback.
|
|
5
|
+
*/
|
|
6
|
+
export interface SizeObserverEntry {
|
|
7
|
+
target: DOMElement;
|
|
8
|
+
dimension: Dimension;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Callback type for resize observers.
|
|
12
|
+
*/
|
|
13
|
+
export type SizeObserverCallback = (entries: SizeObserverEntry[], observer: SizeObserver) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Global registry mapping DOMElements to their registered SizeObservers.
|
|
16
|
+
*/
|
|
17
|
+
export declare const resizeObserverRegistry: Map<DOMElement, Set<SizeObserver>>;
|
|
18
|
+
/**
|
|
19
|
+
* Implements a SizeObserver API similar to the HTML specification.
|
|
20
|
+
* It allows observing changes to the computed dimensions of Tinky DOM elements.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SizeObserver {
|
|
23
|
+
private callback;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new SizeObserver.
|
|
26
|
+
*
|
|
27
|
+
* @param callback - The function to call when observed elements resize.
|
|
28
|
+
*/
|
|
29
|
+
constructor(callback: SizeObserverCallback);
|
|
30
|
+
/**
|
|
31
|
+
* Starts observing the specified element.
|
|
32
|
+
*
|
|
33
|
+
* @param target - The element to observe.
|
|
34
|
+
*/
|
|
35
|
+
observe(target: DOMElement): void;
|
|
36
|
+
/**
|
|
37
|
+
* Stops observing the specified element.
|
|
38
|
+
*
|
|
39
|
+
* @param target - The element to stop observing.
|
|
40
|
+
*/
|
|
41
|
+
unobserve(target: DOMElement): void;
|
|
42
|
+
/**
|
|
43
|
+
* Stops observing all elements.
|
|
44
|
+
*/
|
|
45
|
+
disconnect(): void;
|
|
46
|
+
/** @internal */
|
|
47
|
+
_invokeCallback(entries: SizeObserverEntry[]): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Removes all resize observers from a node and its descendants.
|
|
51
|
+
* Called during mutation cleanup to prevent stale observer entries.
|
|
52
|
+
*
|
|
53
|
+
* @param node - Root of the removed subtree.
|
|
54
|
+
*/
|
|
55
|
+
export declare const removeSizeObserversInSubtree: (node: DOMNode) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Notifies all registered resize observers of their current dimensions.
|
|
58
|
+
* Called by the Tinky instance after each layout computation pass.
|
|
59
|
+
*/
|
|
60
|
+
export declare const notifySizeObservers: () => void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global registry mapping DOMElements to their registered SizeObservers.
|
|
3
|
+
*/
|
|
4
|
+
export const resizeObserverRegistry = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Implements a SizeObserver API similar to the HTML specification.
|
|
7
|
+
* It allows observing changes to the computed dimensions of Tinky DOM elements.
|
|
8
|
+
*/
|
|
9
|
+
export class SizeObserver {
|
|
10
|
+
callback;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new SizeObserver.
|
|
13
|
+
*
|
|
14
|
+
* @param callback - The function to call when observed elements resize.
|
|
15
|
+
*/
|
|
16
|
+
constructor(callback) {
|
|
17
|
+
this.callback = callback;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Starts observing the specified element.
|
|
21
|
+
*
|
|
22
|
+
* @param target - The element to observe.
|
|
23
|
+
*/
|
|
24
|
+
observe(target) {
|
|
25
|
+
if (!target.resizeObservers) {
|
|
26
|
+
target.resizeObservers = new Set();
|
|
27
|
+
}
|
|
28
|
+
target.resizeObservers.add(this);
|
|
29
|
+
let observers = resizeObserverRegistry.get(target);
|
|
30
|
+
if (!observers) {
|
|
31
|
+
observers = new Set();
|
|
32
|
+
resizeObserverRegistry.set(target, observers);
|
|
33
|
+
}
|
|
34
|
+
observers.add(this);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stops observing the specified element.
|
|
38
|
+
*
|
|
39
|
+
* @param target - The element to stop observing.
|
|
40
|
+
*/
|
|
41
|
+
unobserve(target) {
|
|
42
|
+
target.resizeObservers?.delete(this);
|
|
43
|
+
if (target.resizeObservers?.size === 0) {
|
|
44
|
+
target.resizeObservers = undefined;
|
|
45
|
+
}
|
|
46
|
+
const observers = resizeObserverRegistry.get(target);
|
|
47
|
+
if (observers) {
|
|
48
|
+
observers.delete(this);
|
|
49
|
+
if (observers.size === 0) {
|
|
50
|
+
resizeObserverRegistry.delete(target);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stops observing all elements.
|
|
56
|
+
*/
|
|
57
|
+
disconnect() {
|
|
58
|
+
for (const [target, observers] of resizeObserverRegistry.entries()) {
|
|
59
|
+
if (observers.has(this)) {
|
|
60
|
+
this.unobserve(target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** @internal */
|
|
65
|
+
_invokeCallback(entries) {
|
|
66
|
+
this.callback(entries, this);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Removes all resize observers from a node and its descendants.
|
|
71
|
+
* Called during mutation cleanup to prevent stale observer entries.
|
|
72
|
+
*
|
|
73
|
+
* @param node - Root of the removed subtree.
|
|
74
|
+
*/
|
|
75
|
+
export const removeSizeObserversInSubtree = (node) => {
|
|
76
|
+
if (node.nodeName === "#text") {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (node.resizeObservers) {
|
|
80
|
+
// Collect all observers to avoid disconnecting while iterating
|
|
81
|
+
const observers = Array.from(node.resizeObservers);
|
|
82
|
+
for (const observer of observers) {
|
|
83
|
+
observer.unobserve(node);
|
|
84
|
+
}
|
|
85
|
+
node.resizeObservers = undefined;
|
|
86
|
+
}
|
|
87
|
+
// Not strictly necessary here since unobserve handles it,
|
|
88
|
+
// but good for completeness in case it fell through
|
|
89
|
+
resizeObserverRegistry.delete(node);
|
|
90
|
+
for (const childNode of node.childNodes) {
|
|
91
|
+
removeSizeObserversInSubtree(childNode);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Notifies all registered resize observers of their current dimensions.
|
|
96
|
+
* Called by the Tinky instance after each layout computation pass.
|
|
97
|
+
*/
|
|
98
|
+
export const notifySizeObservers = () => {
|
|
99
|
+
const entriesByObserver = new Map();
|
|
100
|
+
for (const [node, observers] of resizeObserverRegistry.entries()) {
|
|
101
|
+
if (!node.taffyNode) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let layout;
|
|
105
|
+
try {
|
|
106
|
+
layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Node was removed from the tree before passive effect cleanup ran.
|
|
110
|
+
const nodeObservers = Array.from(observers);
|
|
111
|
+
for (const observer of nodeObservers) {
|
|
112
|
+
observer.unobserve(node);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const dimension = {
|
|
117
|
+
width: layout?.width ?? 0,
|
|
118
|
+
height: layout?.height ?? 0,
|
|
119
|
+
};
|
|
120
|
+
const entry = { target: node, dimension };
|
|
121
|
+
for (const observer of observers) {
|
|
122
|
+
let entries = entriesByObserver.get(observer);
|
|
123
|
+
if (!entries) {
|
|
124
|
+
entries = [];
|
|
125
|
+
entriesByObserver.set(observer, entries);
|
|
126
|
+
}
|
|
127
|
+
entries.push(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [observer, entries] of entriesByObserver.entries()) {
|
|
131
|
+
observer._invokeCallback(entries);
|
|
132
|
+
}
|
|
133
|
+
};
|
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";
|
|
@@ -19,9 +20,19 @@ import { App } from "../components/App.js";
|
|
|
19
20
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
20
21
|
import { CellBuffer, StyleRegistry } from "./cell-buffer.js";
|
|
21
22
|
import { normalizeIncrementalRendering, } from "./incremental-rendering.js";
|
|
23
|
+
import { notifySizeObservers } from "./size-observer.js";
|
|
22
24
|
const noop = () => {
|
|
23
25
|
// no-op
|
|
24
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
|
+
};
|
|
25
36
|
/**
|
|
26
37
|
* Tinky core class responsible for managing the React tree rendering,
|
|
27
38
|
* lifecycle, and terminal output.
|
|
@@ -49,6 +60,12 @@ export class Tinky {
|
|
|
49
60
|
rootNode;
|
|
50
61
|
/** Full static output for debug mode. */
|
|
51
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;
|
|
52
69
|
/** Promise that resolves when the app exits. */
|
|
53
70
|
exitPromise;
|
|
54
71
|
/** Function to restore console after patching. */
|
|
@@ -131,6 +148,9 @@ export class Tinky {
|
|
|
131
148
|
// that it's rerendered every time, not just new static parts, like in
|
|
132
149
|
// non-debug mode
|
|
133
150
|
this.fullStaticOutput = "";
|
|
151
|
+
this.lastRawEntries = [];
|
|
152
|
+
this.lastRawFrameHeight = 0;
|
|
153
|
+
this.lastRawSuffix = "";
|
|
134
154
|
this.container = reconciler.createContainer(this.rootNode, LegacyRoot, null, false, null, "id", noop, noop, noop, noop, null);
|
|
135
155
|
// Unmount when process exits
|
|
136
156
|
this.unsubscribeExit = onExit(() => {
|
|
@@ -222,6 +242,8 @@ export class Tinky {
|
|
|
222
242
|
}
|
|
223
243
|
return context.measureFunc(availableSpace.width);
|
|
224
244
|
});
|
|
245
|
+
// Notify all registered resize observers of their current dimensions.
|
|
246
|
+
notifySizeObservers();
|
|
225
247
|
};
|
|
226
248
|
/**
|
|
227
249
|
* Performs the render operation.
|
|
@@ -233,20 +255,26 @@ export class Tinky {
|
|
|
233
255
|
}
|
|
234
256
|
if (this.usesRunIncrementalRendering && this.backBuffer) {
|
|
235
257
|
const startTime = performance.now();
|
|
236
|
-
const { buffer, outputHeight, staticOutput } = cellRenderer(this.rootNode, this.backBuffer, false);
|
|
258
|
+
const { buffer, outputHeight, staticOutput, rawEntries } = cellRenderer(this.rootNode, this.backBuffer, false);
|
|
237
259
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
238
260
|
const hasStaticOutput = staticOutput && staticOutput !== "\n";
|
|
239
261
|
if (hasStaticOutput) {
|
|
240
262
|
this.fullStaticOutput += staticOutput;
|
|
241
263
|
}
|
|
242
|
-
|
|
243
|
-
|
|
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) {
|
|
244
271
|
const output = buffer.toString();
|
|
245
272
|
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
246
273
|
this.lastOutput = output;
|
|
247
274
|
this.lastOutputHeight = outputHeight;
|
|
248
275
|
this.cellLog?.sync(buffer);
|
|
249
276
|
this.swapRunBuffers();
|
|
277
|
+
this.writeRawSuffix();
|
|
250
278
|
return;
|
|
251
279
|
}
|
|
252
280
|
if (hasStaticOutput) {
|
|
@@ -266,7 +294,7 @@ export class Tinky {
|
|
|
266
294
|
return;
|
|
267
295
|
}
|
|
268
296
|
const startTime = performance.now();
|
|
269
|
-
const { output, outputHeight, staticOutput } = renderer(this.rootNode, this.isScreenReaderEnabled);
|
|
297
|
+
const { output, outputHeight, staticOutput, rawEntries } = renderer(this.rootNode, this.isScreenReaderEnabled);
|
|
270
298
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
271
299
|
// If <Static> output isn't empty, it means new children have been added to
|
|
272
300
|
// it
|
|
@@ -275,10 +303,13 @@ export class Tinky {
|
|
|
275
303
|
if (hasStaticOutput) {
|
|
276
304
|
this.fullStaticOutput += staticOutput;
|
|
277
305
|
}
|
|
306
|
+
this.updateRawState(rawEntries, outputHeight);
|
|
278
307
|
this.options.stdout.write(this.fullStaticOutput + output);
|
|
308
|
+
this.writeRawSuffix();
|
|
279
309
|
return;
|
|
280
310
|
}
|
|
281
311
|
if (this.isCI) {
|
|
312
|
+
this.clearRawState();
|
|
282
313
|
if (hasStaticOutput) {
|
|
283
314
|
this.options.stdout.write(staticOutput);
|
|
284
315
|
}
|
|
@@ -287,6 +318,7 @@ export class Tinky {
|
|
|
287
318
|
return;
|
|
288
319
|
}
|
|
289
320
|
if (this.isScreenReaderEnabled) {
|
|
321
|
+
this.clearRawState();
|
|
290
322
|
if (hasStaticOutput) {
|
|
291
323
|
// We need to erase the main output before writing new static output
|
|
292
324
|
const erase = this.lastOutputHeight > 0
|
|
@@ -322,12 +354,18 @@ export class Tinky {
|
|
|
322
354
|
if (hasStaticOutput) {
|
|
323
355
|
this.fullStaticOutput += staticOutput;
|
|
324
356
|
}
|
|
325
|
-
|
|
326
|
-
|
|
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) {
|
|
327
364
|
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
328
365
|
this.lastOutput = output;
|
|
329
366
|
this.lastOutputHeight = outputHeight;
|
|
330
367
|
this.log.sync(output);
|
|
368
|
+
this.writeRawSuffix();
|
|
331
369
|
return;
|
|
332
370
|
}
|
|
333
371
|
// To ensure static output is cleanly rendered before main output, clear
|
|
@@ -354,6 +392,28 @@ export class Tinky {
|
|
|
354
392
|
this.frontBuffer = this.backBuffer;
|
|
355
393
|
this.backBuffer = previous;
|
|
356
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
|
+
}
|
|
357
417
|
redrawRunBuffer() {
|
|
358
418
|
if (!this.frontBuffer || !this.cellLog) {
|
|
359
419
|
return;
|
|
@@ -361,6 +421,7 @@ export class Tinky {
|
|
|
361
421
|
this.cellLog(this.frontBuffer, this.frontBuffer, { forceFull: true });
|
|
362
422
|
this.lastOutput = this.frontBuffer.toString();
|
|
363
423
|
this.lastOutputHeight = this.frontBuffer.height;
|
|
424
|
+
this.writeRawSuffix();
|
|
364
425
|
}
|
|
365
426
|
/**
|
|
366
427
|
* Renders the given React node.
|
|
@@ -383,6 +444,7 @@ export class Tinky {
|
|
|
383
444
|
}
|
|
384
445
|
if (this.options.debug) {
|
|
385
446
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
447
|
+
this.writeRawSuffix(data);
|
|
386
448
|
return;
|
|
387
449
|
}
|
|
388
450
|
if (this.isCI) {
|
|
@@ -398,6 +460,7 @@ export class Tinky {
|
|
|
398
460
|
this.log.clear();
|
|
399
461
|
this.options.stdout.write(data);
|
|
400
462
|
this.log(this.lastOutput);
|
|
463
|
+
this.writeRawSuffix(data);
|
|
401
464
|
}
|
|
402
465
|
/**
|
|
403
466
|
* Writes data to stderr.
|
|
@@ -411,6 +474,7 @@ export class Tinky {
|
|
|
411
474
|
if (this.options.debug) {
|
|
412
475
|
this.options.stderr.write(data);
|
|
413
476
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
477
|
+
this.writeRawSuffix(data);
|
|
414
478
|
return;
|
|
415
479
|
}
|
|
416
480
|
if (this.isCI) {
|
|
@@ -426,6 +490,7 @@ export class Tinky {
|
|
|
426
490
|
this.log.clear();
|
|
427
491
|
this.options.stderr.write(data);
|
|
428
492
|
this.log(this.lastOutput);
|
|
493
|
+
this.writeRawSuffix(data);
|
|
429
494
|
}
|
|
430
495
|
/**
|
|
431
496
|
* Unmounts the Tinky app.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DOMElement } from "../core/dom.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for the `useSizeObserver` hook.
|
|
4
|
+
*/
|
|
5
|
+
export interface SizeObserverOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Enable or disable the size observer.
|
|
8
|
+
*
|
|
9
|
+
* @defaultValue true
|
|
10
|
+
*/
|
|
11
|
+
isActive?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Hook that observes the computed layout dimensions of a `<Box>` element
|
|
15
|
+
* and triggers a re-render whenever its size changes.
|
|
16
|
+
*
|
|
17
|
+
* The observer is registered in a global resize observer registry that is
|
|
18
|
+
* invoked by the Tinky core after each layout computation pass. This means
|
|
19
|
+
* size changes are detected regardless of which component caused the
|
|
20
|
+
* re-render — including sibling updates and terminal resize events.
|
|
21
|
+
*
|
|
22
|
+
* Multiple observers can be attached to the same element.
|
|
23
|
+
*
|
|
24
|
+
* This is similar to HTML's `SizeObserver` API.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { Box, Text, useSizeObserver } from 'tinky';
|
|
29
|
+
*
|
|
30
|
+
* function MyComponent() {
|
|
31
|
+
* const [ref, width, height] = useSizeObserver();
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <Box ref={ref} width="50%" height={10}>
|
|
35
|
+
* <Text>Size: {width}x{height}</Text>
|
|
36
|
+
* </Box>
|
|
37
|
+
* );
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @param options - Configuration options.
|
|
42
|
+
* @returns A tuple containing `[refCallback, width, height]`. Attach `refCallback` to a `<Box>` element.
|
|
43
|
+
*/
|
|
44
|
+
export declare const useSizeObserver: (options?: SizeObserverOptions) => [(node: DOMElement | null) => void, number, number];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { measureElement } from "../utils/measure-element.js";
|
|
3
|
+
import { SizeObserver } from "../core/size-observer.js";
|
|
4
|
+
import { reconciler } from "../core/reconciler.js";
|
|
5
|
+
/**
|
|
6
|
+
* Hook that observes the computed layout dimensions of a `<Box>` element
|
|
7
|
+
* and triggers a re-render whenever its size changes.
|
|
8
|
+
*
|
|
9
|
+
* The observer is registered in a global resize observer registry that is
|
|
10
|
+
* invoked by the Tinky core after each layout computation pass. This means
|
|
11
|
+
* size changes are detected regardless of which component caused the
|
|
12
|
+
* re-render — including sibling updates and terminal resize events.
|
|
13
|
+
*
|
|
14
|
+
* Multiple observers can be attached to the same element.
|
|
15
|
+
*
|
|
16
|
+
* This is similar to HTML's `SizeObserver` API.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { Box, Text, useSizeObserver } from 'tinky';
|
|
21
|
+
*
|
|
22
|
+
* function MyComponent() {
|
|
23
|
+
* const [ref, width, height] = useSizeObserver();
|
|
24
|
+
*
|
|
25
|
+
* return (
|
|
26
|
+
* <Box ref={ref} width="50%" height={10}>
|
|
27
|
+
* <Text>Size: {width}x{height}</Text>
|
|
28
|
+
* </Box>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @param options - Configuration options.
|
|
34
|
+
* @returns A tuple containing `[refCallback, width, height]`. Attach `refCallback` to a `<Box>` element.
|
|
35
|
+
*/
|
|
36
|
+
export const useSizeObserver = (options = {}) => {
|
|
37
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
38
|
+
const observerRef = useRef(null);
|
|
39
|
+
const prevSize = useRef({ width: 0, height: 0 });
|
|
40
|
+
const { isActive = true } = options;
|
|
41
|
+
const callbackRef = useCallback((node) => {
|
|
42
|
+
// Cleanup previous observer
|
|
43
|
+
if (observerRef.current) {
|
|
44
|
+
observerRef.current.disconnect();
|
|
45
|
+
observerRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
// If active and node is attached, start observing
|
|
48
|
+
if (isActive && node) {
|
|
49
|
+
const callback = (entries) => {
|
|
50
|
+
if (entries.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
const { dimension } = entries[0];
|
|
53
|
+
if (dimension.width !== prevSize.current.width ||
|
|
54
|
+
dimension.height !== prevSize.current.height) {
|
|
55
|
+
prevSize.current = { ...dimension };
|
|
56
|
+
reconciler.batchedUpdates(() => {
|
|
57
|
+
setSize({ ...dimension });
|
|
58
|
+
}, null);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const observer = new SizeObserver(callback);
|
|
62
|
+
// Read initial dimensions
|
|
63
|
+
const initial = measureElement(node);
|
|
64
|
+
callback([{ target: node, dimension: initial }]);
|
|
65
|
+
observer.observe(node);
|
|
66
|
+
observerRef.current = observer;
|
|
67
|
+
}
|
|
68
|
+
}, [isActive]);
|
|
69
|
+
return [callbackRef, size.width, size.height];
|
|
70
|
+
};
|
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";
|
|
@@ -21,6 +22,7 @@ export { useFocus, type FocusOptions, type FocusState, } from "./hooks/use-focus
|
|
|
21
22
|
export { useFocusManager, type FocusManager, } from "./hooks/use-focus-manager.js";
|
|
22
23
|
export { useIsScreenReaderEnabled } from "./hooks/use-is-screen-reader-enabled.js";
|
|
23
24
|
export { measureElement } from "./utils/measure-element.js";
|
|
25
|
+
export { useSizeObserver, type SizeObserverOptions, } from "./hooks/use-size-observer.js";
|
|
24
26
|
export { type Dimension } from "./utils/dimension.js";
|
|
25
27
|
export { type ForegroundColorName } from "./utils/colorize.js";
|
|
26
28
|
export { applyTextStyles, type TextStyles } from "./utils/apply-text-styles.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";
|
|
@@ -19,6 +20,7 @@ export { useFocus, } from "./hooks/use-focus.js";
|
|
|
19
20
|
export { useFocusManager, } from "./hooks/use-focus-manager.js";
|
|
20
21
|
export { useIsScreenReaderEnabled } from "./hooks/use-is-screen-reader-enabled.js";
|
|
21
22
|
export { measureElement } from "./utils/measure-element.js";
|
|
23
|
+
export { useSizeObserver, } from "./hooks/use-size-observer.js";
|
|
22
24
|
export { applyTextStyles } from "./utils/apply-text-styles.js";
|
|
23
25
|
export { boxStyles } from "./core/box-styles.js";
|
|
24
26
|
export { EventEmitter } from "./utils/event-emitter.js";
|