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.
@@ -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/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element;
7
+ export declare function ErrorOverview({ error }: ErrorProps): import("react").JSX.Element;
8
8
  export {};
@@ -26,4 +26,4 @@ export interface NewlineProps {
26
26
  * );
27
27
  * ```
28
28
  */
29
- export declare function Newline({ count }: NewlineProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function Newline({ count }: NewlineProps): import("react").JSX.Element;
@@ -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/jsx-runtime").JSX.Element;
82
+ export declare function Separator({ char, direction, color, dimColor, backgroundColor, bold, italic, underline, strikethrough, inverse, }: SeparatorProps): import("react").JSX.Element;
@@ -16,4 +16,4 @@
16
16
  * );
17
17
  * ```
18
18
  */
19
- export declare function Spacer(): import("react/jsx-runtime").JSX.Element;
19
+ export declare function Spacer(): import("react").JSX.Element;
@@ -45,4 +45,4 @@ export interface StaticProps<T> {
45
45
  * );
46
46
  * ```
47
47
  */
48
- export declare function Static<T>(props: StaticProps<T>): import("react/jsx-runtime").JSX.Element;
48
+ export declare function Static<T>(props: StaticProps<T>): import("react").JSX.Element;
@@ -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/jsx-runtime").JSX.Element | null;
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/jsx-runtime").JSX.Element | null;
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;
@@ -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
  */
@@ -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
  *
@@ -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
+ };
@@ -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"] ||
@@ -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.
@@ -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
  };
@@ -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
- if (this.options.stdout.rows &&
246
- this.lastOutputHeight >= this.options.stdout.rows) {
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
- if (this.options.stdout.rows &&
329
- this.lastOutputHeight >= this.options.stdout.rows) {
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinky",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "React for CLIs, re-imagined with the Taffy engine",
5
5
  "keywords": [
6
6
  "react",