tinky 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +47 -0
  2. package/README.ja-JP.md +350 -0
  3. package/README.md +350 -0
  4. package/README.zh-CN.md +350 -0
  5. package/lib/colorize.d.ts +16 -0
  6. package/lib/colorize.js +55 -0
  7. package/lib/components/AccessibilityContext.d.ts +9 -0
  8. package/lib/components/AccessibilityContext.js +10 -0
  9. package/lib/components/App.d.ts +169 -0
  10. package/lib/components/App.js +314 -0
  11. package/lib/components/AppContext.d.ts +14 -0
  12. package/lib/components/AppContext.js +11 -0
  13. package/lib/components/BackgroundContext.d.ts +10 -0
  14. package/lib/components/BackgroundContext.js +5 -0
  15. package/lib/components/Box.d.ts +153 -0
  16. package/lib/components/Box.js +57 -0
  17. package/lib/components/ErrorOverview.d.ts +9 -0
  18. package/lib/components/ErrorOverview.js +49 -0
  19. package/lib/components/FocusContext.d.ts +51 -0
  20. package/lib/components/FocusContext.js +35 -0
  21. package/lib/components/Newline.d.ts +29 -0
  22. package/lib/components/Newline.js +21 -0
  23. package/lib/components/Spacer.d.ts +19 -0
  24. package/lib/components/Spacer.js +23 -0
  25. package/lib/components/Static.d.ts +48 -0
  26. package/lib/components/Static.js +46 -0
  27. package/lib/components/StderrContext.d.ts +21 -0
  28. package/lib/components/StderrContext.js +12 -0
  29. package/lib/components/StdinContext.d.ts +36 -0
  30. package/lib/components/StdinContext.js +16 -0
  31. package/lib/components/StdoutContext.d.ts +22 -0
  32. package/lib/components/StdoutContext.js +13 -0
  33. package/lib/components/Text.d.ts +84 -0
  34. package/lib/components/Text.js +74 -0
  35. package/lib/components/Transform.d.ts +41 -0
  36. package/lib/components/Transform.js +32 -0
  37. package/lib/devtools-window-polyfill.d.ts +1 -0
  38. package/lib/devtools-window-polyfill.js +70 -0
  39. package/lib/devtools.d.ts +1 -0
  40. package/lib/devtools.js +8 -0
  41. package/lib/dom.d.ts +130 -0
  42. package/lib/dom.js +209 -0
  43. package/lib/get-max-width.d.ts +9 -0
  44. package/lib/get-max-width.js +14 -0
  45. package/lib/hooks/use-app.d.ts +26 -0
  46. package/lib/hooks/use-app.js +28 -0
  47. package/lib/hooks/use-focus-manager.d.ts +40 -0
  48. package/lib/hooks/use-focus-manager.js +18 -0
  49. package/lib/hooks/use-focus.d.ts +67 -0
  50. package/lib/hooks/use-focus.js +65 -0
  51. package/lib/hooks/use-input.d.ts +118 -0
  52. package/lib/hooks/use-input.js +101 -0
  53. package/lib/hooks/use-is-screen-reader-enabled.d.ts +7 -0
  54. package/lib/hooks/use-is-screen-reader-enabled.js +12 -0
  55. package/lib/hooks/use-stderr.d.ts +6 -0
  56. package/lib/hooks/use-stderr.js +8 -0
  57. package/lib/hooks/use-stdin.d.ts +6 -0
  58. package/lib/hooks/use-stdin.js +8 -0
  59. package/lib/hooks/use-stdout.d.ts +7 -0
  60. package/lib/hooks/use-stdout.js +9 -0
  61. package/lib/index.d.ts +22 -0
  62. package/lib/index.js +20 -0
  63. package/lib/instances.d.ts +10 -0
  64. package/lib/instances.js +9 -0
  65. package/lib/log-update.d.ts +32 -0
  66. package/lib/log-update.js +147 -0
  67. package/lib/measure-element.d.ts +19 -0
  68. package/lib/measure-element.js +13 -0
  69. package/lib/measure-text.d.ts +13 -0
  70. package/lib/measure-text.js +26 -0
  71. package/lib/output.d.ts +73 -0
  72. package/lib/output.js +190 -0
  73. package/lib/parse-keypress.d.ts +20 -0
  74. package/lib/parse-keypress.js +228 -0
  75. package/lib/reconciler.d.ts +12 -0
  76. package/lib/reconciler.js +282 -0
  77. package/lib/render-background.d.ts +11 -0
  78. package/lib/render-background.js +32 -0
  79. package/lib/render-border.d.ts +13 -0
  80. package/lib/render-border.js +82 -0
  81. package/lib/render-node-to-output.d.ts +43 -0
  82. package/lib/render-node-to-output.js +179 -0
  83. package/lib/render.d.ts +129 -0
  84. package/lib/render.js +84 -0
  85. package/lib/renderer.d.ts +27 -0
  86. package/lib/renderer.js +62 -0
  87. package/lib/signal-exit.d.ts +11 -0
  88. package/lib/signal-exit.js +24 -0
  89. package/lib/squash-text-nodes.d.ts +10 -0
  90. package/lib/squash-text-nodes.js +36 -0
  91. package/lib/styles.d.ts +402 -0
  92. package/lib/styles.js +633 -0
  93. package/lib/taffy-node.d.ts +39 -0
  94. package/lib/taffy-node.js +60 -0
  95. package/lib/tinky.d.ts +153 -0
  96. package/lib/tinky.js +396 -0
  97. package/lib/wrap-text.d.ts +11 -0
  98. package/lib/wrap-text.js +38 -0
  99. package/package.json +87 -0
@@ -0,0 +1,147 @@
1
+ import ansiEscapes from "ansi-escapes";
2
+ import cliCursor from "cli-cursor";
3
+ /**
4
+ * Creates a standard LogUpdate instance.
5
+ * Re-renders the entire previous output on each update.
6
+ *
7
+ * @param stream - The writable stream to log to.
8
+ * @param options - Configuration options.
9
+ * @param options.showCursor - Whether to show the cursor.
10
+ * @returns A LogUpdate instance.
11
+ */
12
+ const createStandard = (stream, { showCursor = false } = {}) => {
13
+ let previousLineCount = 0;
14
+ let previousOutput = "";
15
+ let hasHiddenCursor = false;
16
+ const render = (str) => {
17
+ if (!showCursor && !hasHiddenCursor) {
18
+ cliCursor.hide(stream);
19
+ hasHiddenCursor = true;
20
+ }
21
+ const output = str + "\n";
22
+ if (output === previousOutput) {
23
+ return;
24
+ }
25
+ previousOutput = output;
26
+ stream.write(ansiEscapes.eraseLines(previousLineCount) + output);
27
+ previousLineCount = output.split("\n").length;
28
+ };
29
+ render.clear = () => {
30
+ stream.write(ansiEscapes.eraseLines(previousLineCount));
31
+ previousOutput = "";
32
+ previousLineCount = 0;
33
+ };
34
+ render.done = () => {
35
+ previousOutput = "";
36
+ previousLineCount = 0;
37
+ if (!showCursor) {
38
+ cliCursor.show(stream);
39
+ hasHiddenCursor = false;
40
+ }
41
+ };
42
+ render.sync = (str) => {
43
+ const output = str + "\n";
44
+ previousOutput = output;
45
+ previousLineCount = output.split("\n").length;
46
+ };
47
+ return render;
48
+ };
49
+ /**
50
+ * Creates an incremental LogUpdate instance.
51
+ * Updates only changed lines to improve performance and reduce flickering.
52
+ *
53
+ * @param stream - The writable stream to log to.
54
+ * @param options - Configuration options.
55
+ * @param options.showCursor - Whether to show the cursor.
56
+ * @returns A LogUpdate instance.
57
+ */
58
+ const createIncremental = (stream, { showCursor = false } = {}) => {
59
+ let previousLines = [];
60
+ let previousOutput = "";
61
+ let hasHiddenCursor = false;
62
+ const render = (str) => {
63
+ if (!showCursor && !hasHiddenCursor) {
64
+ cliCursor.hide(stream);
65
+ hasHiddenCursor = true;
66
+ }
67
+ const output = str + "\n";
68
+ if (output === previousOutput) {
69
+ return;
70
+ }
71
+ const previousCount = previousLines.length;
72
+ const nextLines = output.split("\n");
73
+ const nextCount = nextLines.length;
74
+ const visibleCount = nextCount - 1;
75
+ if (output === "\n" || previousOutput.length === 0) {
76
+ stream.write(ansiEscapes.eraseLines(previousCount) + output);
77
+ previousOutput = output;
78
+ previousLines = nextLines;
79
+ return;
80
+ }
81
+ // We aggregate all chunks for incremental rendering into a buffer, and then
82
+ // write them to stdout at the end.
83
+ const buffer = [];
84
+ // Clear extra lines if the current content's line count is lower than the
85
+ // previous.
86
+ if (nextCount < previousCount) {
87
+ buffer.push(
88
+ // Erases the trailing lines and the final newline slot.
89
+ ansiEscapes.eraseLines(previousCount - nextCount + 1),
90
+ // Positions cursor to the top of the rendered output.
91
+ ansiEscapes.cursorUp(visibleCount));
92
+ }
93
+ else {
94
+ buffer.push(ansiEscapes.cursorUp(previousCount - 1));
95
+ }
96
+ for (let i = 0; i < visibleCount; i++) {
97
+ // We do not write lines if the contents are the same. This prevents
98
+ // flickering during renders.
99
+ if (nextLines[i] === previousLines[i]) {
100
+ buffer.push(ansiEscapes.cursorNextLine);
101
+ continue;
102
+ }
103
+ buffer.push(ansiEscapes.cursorTo(0) +
104
+ nextLines[i] +
105
+ ansiEscapes.eraseEndLine +
106
+ "\n");
107
+ }
108
+ stream.write(buffer.join(""));
109
+ previousOutput = output;
110
+ previousLines = nextLines;
111
+ };
112
+ render.clear = () => {
113
+ stream.write(ansiEscapes.eraseLines(previousLines.length));
114
+ previousOutput = "";
115
+ previousLines = [];
116
+ };
117
+ render.done = () => {
118
+ previousOutput = "";
119
+ previousLines = [];
120
+ if (!showCursor) {
121
+ cliCursor.show(stream);
122
+ hasHiddenCursor = false;
123
+ }
124
+ };
125
+ render.sync = (str) => {
126
+ const output = str + "\n";
127
+ previousOutput = output;
128
+ previousLines = output.split("\n");
129
+ };
130
+ return render;
131
+ };
132
+ /**
133
+ * Creates a LogUpdate instance.
134
+ *
135
+ * @param stream - The writable stream to log to.
136
+ * @param options - Configuration options.
137
+ * @param options.showCursor - Whether to show the cursor. Default is false.
138
+ * @param options.incremental - Whether to use incremental rendering.
139
+ * @returns A LogUpdate instance.
140
+ */
141
+ const create = (stream, { showCursor = false, incremental = false } = {}) => {
142
+ if (incremental) {
143
+ return createIncremental(stream, { showCursor });
144
+ }
145
+ return createStandard(stream, { showCursor });
146
+ };
147
+ export const logUpdate = { create };
@@ -0,0 +1,19 @@
1
+ import { type DOMElement } from "./dom.js";
2
+ interface Output {
3
+ /**
4
+ * Element width.
5
+ */
6
+ width: number;
7
+ /**
8
+ * Element height.
9
+ */
10
+ height: number;
11
+ }
12
+ /**
13
+ * Measure the dimensions of a particular `<Box>` element.
14
+ *
15
+ * @param node - The DOM element to measure.
16
+ * @returns The measured dimensions.
17
+ */
18
+ export declare const measureElement: (node: DOMElement) => Output;
19
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Measure the dimensions of a particular `<Box>` element.
3
+ *
4
+ * @param node - The DOM element to measure.
5
+ * @returns The measured dimensions.
6
+ */
7
+ export const measureElement = (node) => {
8
+ const layout = node.taffyNode?.tree.getLayout(node.taffyNode.id);
9
+ return {
10
+ width: layout?.width ?? 0,
11
+ height: layout?.height ?? 0,
12
+ };
13
+ };
@@ -0,0 +1,13 @@
1
+ interface Output {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /**
6
+ * Measures the width and height of a text string.
7
+ * Results are cached to improve performance.
8
+ *
9
+ * @param text - The text string to measure.
10
+ * @returns The width and height of the text.
11
+ */
12
+ export declare const measureText: (text: string) => Output;
13
+ export {};
@@ -0,0 +1,26 @@
1
+ import widestLine from "widest-line";
2
+ const cache = new Map();
3
+ /**
4
+ * Measures the width and height of a text string.
5
+ * Results are cached to improve performance.
6
+ *
7
+ * @param text - The text string to measure.
8
+ * @returns The width and height of the text.
9
+ */
10
+ export const measureText = (text) => {
11
+ if (text.length === 0) {
12
+ return {
13
+ width: 0,
14
+ height: 0,
15
+ };
16
+ }
17
+ const cachedDimensions = cache.get(text);
18
+ if (cachedDimensions) {
19
+ return cachedDimensions;
20
+ }
21
+ const width = widestLine(text);
22
+ const height = text.split("\n").length;
23
+ const dimensions = { width, height };
24
+ cache.set(text, dimensions);
25
+ return dimensions;
26
+ };
@@ -0,0 +1,73 @@
1
+ import { type OutputTransformer } from "./render-node-to-output.js";
2
+ /**
3
+ * Options for creating an Output instance.
4
+ */
5
+ interface Options {
6
+ /**
7
+ * Width of the output area.
8
+ */
9
+ width: number;
10
+ /**
11
+ * Height of the output area.
12
+ */
13
+ height: number;
14
+ }
15
+ /**
16
+ * Represents a clipping rectangle.
17
+ */
18
+ interface Clip {
19
+ x1: number | undefined;
20
+ x2: number | undefined;
21
+ y1: number | undefined;
22
+ y2: number | undefined;
23
+ }
24
+ /**
25
+ * "Virtual" output class
26
+ *
27
+ * Handles the positioning and saving of the output of each node in the tree.
28
+ * Also responsible for applying transformations to each character.
29
+ *
30
+ * Used to generate the final output of all nodes before writing to stdout.
31
+ */
32
+ export declare class Output {
33
+ width: number;
34
+ height: number;
35
+ private readonly operations;
36
+ /**
37
+ * Creates a new Output instance.
38
+ *
39
+ * @param options - Configuration options containing width and height.
40
+ */
41
+ constructor(options: Options);
42
+ /**
43
+ * Writes text to the output at a specified position.
44
+ *
45
+ * @param x - The x-coordinate (column).
46
+ * @param y - The y-coordinate (row).
47
+ * @param text - The text to write.
48
+ * @param options - Options containing transformers.
49
+ */
50
+ write(x: number, y: number, text: string, options: {
51
+ transformers: OutputTransformer[];
52
+ }): void;
53
+ /**
54
+ * Sets a clipping area for subsequent operations.
55
+ *
56
+ * @param clip - The clipping rectangle.
57
+ */
58
+ clip(clip: Clip): void;
59
+ /**
60
+ * Removes the last clipping area, restoring the previous one.
61
+ */
62
+ unclip(): void;
63
+ /**
64
+ * Generates the final output string and its height.
65
+ *
66
+ * @returns An object containing the generated output string and its height.
67
+ */
68
+ get(): {
69
+ output: string;
70
+ height: number;
71
+ };
72
+ }
73
+ export {};
package/lib/output.js ADDED
@@ -0,0 +1,190 @@
1
+ import sliceAnsi from "slice-ansi";
2
+ import stringWidth from "string-width";
3
+ import widestLine from "widest-line";
4
+ import { styledCharsFromTokens, styledCharsToString, tokenize, } from "@alcalzone/ansi-tokenize";
5
+ /**
6
+ * "Virtual" output class
7
+ *
8
+ * Handles the positioning and saving of the output of each node in the tree.
9
+ * Also responsible for applying transformations to each character.
10
+ *
11
+ * Used to generate the final output of all nodes before writing to stdout.
12
+ */
13
+ export class Output {
14
+ width;
15
+ height;
16
+ operations = [];
17
+ /**
18
+ * Creates a new Output instance.
19
+ *
20
+ * @param options - Configuration options containing width and height.
21
+ */
22
+ constructor(options) {
23
+ const { width, height } = options;
24
+ this.width = width;
25
+ this.height = height;
26
+ }
27
+ /**
28
+ * Writes text to the output at a specified position.
29
+ *
30
+ * @param x - The x-coordinate (column).
31
+ * @param y - The y-coordinate (row).
32
+ * @param text - The text to write.
33
+ * @param options - Options containing transformers.
34
+ */
35
+ write(x, y, text, options) {
36
+ const { transformers } = options;
37
+ if (!text) {
38
+ return;
39
+ }
40
+ this.operations.push({
41
+ type: "write",
42
+ x,
43
+ y,
44
+ text,
45
+ transformers,
46
+ });
47
+ }
48
+ /**
49
+ * Sets a clipping area for subsequent operations.
50
+ *
51
+ * @param clip - The clipping rectangle.
52
+ */
53
+ clip(clip) {
54
+ this.operations.push({
55
+ type: "clip",
56
+ clip,
57
+ });
58
+ }
59
+ /**
60
+ * Removes the last clipping area, restoring the previous one.
61
+ */
62
+ unclip() {
63
+ this.operations.push({
64
+ type: "unclip",
65
+ });
66
+ }
67
+ /**
68
+ * Generates the final output string and its height.
69
+ *
70
+ * @returns An object containing the generated output string and its height.
71
+ */
72
+ get() {
73
+ // Initialize output array with a specific set of rows, so that
74
+ // margin/padding at the bottom is preserved
75
+ const output = [];
76
+ for (let y = 0; y < this.height; y++) {
77
+ const row = [];
78
+ for (let x = 0; x < this.width; x++) {
79
+ row.push({
80
+ type: "char",
81
+ value: " ",
82
+ fullWidth: false,
83
+ styles: [],
84
+ });
85
+ }
86
+ output.push(row);
87
+ }
88
+ const clips = [];
89
+ for (const operation of this.operations) {
90
+ if (operation.type === "clip") {
91
+ clips.push(operation.clip);
92
+ }
93
+ if (operation.type === "unclip") {
94
+ clips.pop();
95
+ }
96
+ if (operation.type === "write") {
97
+ const { text, transformers } = operation;
98
+ let { x, y } = operation;
99
+ let lines = text.split("\n");
100
+ const clip = clips.at(-1);
101
+ if (clip) {
102
+ const clipHorizontally = typeof clip.x1 === "number" && typeof clip.x2 === "number";
103
+ const clipVertically = typeof clip.y1 === "number" && typeof clip.y2 === "number";
104
+ const clipX1 = clip.x1 ?? 0;
105
+ const clipX2 = clip.x2 ?? 0;
106
+ const clipY1 = clip.y1 ?? 0;
107
+ const clipY2 = clip.y2 ?? 0;
108
+ // If text is positioned outside of clipping area altogether,
109
+ // skip to the next operation to avoid unnecessary calculations
110
+ if (clipHorizontally) {
111
+ const width = widestLine(text);
112
+ if (x + width < clipX1 || x > clipX2) {
113
+ continue;
114
+ }
115
+ }
116
+ if (clipVertically) {
117
+ const height = lines.length;
118
+ if (y + height < clipY1 || y > clipY2) {
119
+ continue;
120
+ }
121
+ }
122
+ if (clipHorizontally) {
123
+ lines = lines.map((line) => {
124
+ const from = x < clipX1 ? clipX1 - x : 0;
125
+ const width = stringWidth(line);
126
+ const to = x + width > clipX2 ? clipX2 - x : width;
127
+ return sliceAnsi(line, from, to);
128
+ });
129
+ if (x < clipX1) {
130
+ x = clipX1;
131
+ }
132
+ }
133
+ if (clipVertically) {
134
+ const from = y < clipY1 ? clipY1 - y : 0;
135
+ const height = lines.length;
136
+ const to = y + height > clipY2 ? clipY2 - y : height;
137
+ lines = lines.slice(from, to);
138
+ if (y < clipY1) {
139
+ y = clipY1;
140
+ }
141
+ }
142
+ }
143
+ let offsetY = 0;
144
+ for (const [index, line] of lines.entries()) {
145
+ const currentLine = output[y + offsetY];
146
+ // Line can be missing if `text` is taller than height of
147
+ // pre-initialized `this.output`
148
+ if (!currentLine) {
149
+ continue;
150
+ }
151
+ let transformedLine = line;
152
+ for (const transformer of transformers) {
153
+ transformedLine = transformer(transformedLine, index);
154
+ }
155
+ const characters = styledCharsFromTokens(tokenize(transformedLine));
156
+ let offsetX = x;
157
+ for (const character of characters) {
158
+ currentLine[offsetX] = character;
159
+ // Determine printed width using string-width to align with measurement
160
+ const characterWidth = Math.max(1, stringWidth(character.value));
161
+ // For multi-column characters, clear following cells to avoid stray
162
+ // spaces/artifacts
163
+ if (characterWidth > 1) {
164
+ for (let index = 1; index < characterWidth; index++) {
165
+ currentLine[offsetX + index] = {
166
+ type: "char",
167
+ value: "",
168
+ fullWidth: false,
169
+ styles: character.styles,
170
+ };
171
+ }
172
+ }
173
+ offsetX += characterWidth;
174
+ }
175
+ offsetY++;
176
+ }
177
+ }
178
+ }
179
+ const generatedOutput = output
180
+ .map((line) => {
181
+ const lineWithoutEmptyItems = line.filter((item) => item !== undefined);
182
+ return styledCharsToString(lineWithoutEmptyItems).trimEnd();
183
+ })
184
+ .join("\n");
185
+ return {
186
+ output: generatedOutput,
187
+ height: output.length,
188
+ };
189
+ }
190
+ }
@@ -0,0 +1,20 @@
1
+ import { Buffer } from "node:buffer";
2
+ export declare const nonAlphanumericKeys: string[];
3
+ interface ParsedKey {
4
+ name: string;
5
+ ctrl: boolean;
6
+ meta: boolean;
7
+ shift: boolean;
8
+ option: boolean;
9
+ sequence: string;
10
+ raw: string | undefined;
11
+ code?: string;
12
+ }
13
+ /**
14
+ * Parses a keypress string or buffer into a structured key object.
15
+ *
16
+ * @param s - The string or buffer to parse.
17
+ * @returns The parsed key object.
18
+ */
19
+ export declare const parseKeypress: (s?: Buffer | string) => ParsedKey;
20
+ export {};