@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6

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 (138) hide show
  1. package/README.md +21 -9
  2. package/THIRD_PARTY_NOTICES.md +39 -1
  3. package/dist/Text-BobFKi74.d.ts +452 -0
  4. package/dist/ansi.d.ts +122 -131
  5. package/dist/ansi.js +86 -6
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/env-YVw64yZS.js +9 -0
  20. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  21. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  22. package/dist/index-DZ88EXJv.d.ts +21 -0
  23. package/dist/index.d.ts +143 -498
  24. package/dist/index.js +1026 -2244
  25. package/dist/osc-CCH7xDoS.js +71 -0
  26. package/dist/osc-Cn0fw77g.d.ts +23 -0
  27. package/dist/paint-C19minOS.d.ts +81 -0
  28. package/dist/query-vaIeGOkH.d.ts +152 -0
  29. package/dist/router.d.ts +392 -0
  30. package/dist/router.js +709 -0
  31. package/dist/sample-Cqw1bjUL.js +445 -0
  32. package/dist/screen-BOSLQ8fF.d.ts +49 -0
  33. package/dist/screen-CiPytswf.js +342 -0
  34. package/dist/screen.d.ts +5 -0
  35. package/dist/screen.js +5 -0
  36. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  37. package/dist/serialize-BTkAZgw1.js +79 -0
  38. package/dist/session-aZr9O8h3.js +665 -0
  39. package/dist/sgr-BhwaWAJB.js +246 -0
  40. package/dist/store-CgrG9K4y.d.ts +72 -0
  41. package/dist/string-width-CijQwpIk.js +69 -0
  42. package/dist/strip-BvU4toXG.js +6 -0
  43. package/dist/terminal.d.ts +118 -0
  44. package/dist/terminal.js +2 -0
  45. package/dist/tokenize-AjqbvtiT.js +1242 -0
  46. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  47. package/dist/truncate-D31fhU6i.js +562 -0
  48. package/dist/use-focus-BzqAJi0n.js +1337 -0
  49. package/package.json +41 -9
  50. package/src/ansi/chalk.ts +5 -3
  51. package/src/ansi/escapes.ts +14 -0
  52. package/src/ansi/graphemes.ts +8 -0
  53. package/src/ansi/hyperlink.ts +44 -0
  54. package/src/ansi/index.ts +3 -1
  55. package/src/ansi/osc.ts +77 -0
  56. package/src/ansi/tokenize.ts +3 -4
  57. package/src/capabilities/color-policy.ts +34 -0
  58. package/src/capabilities/detect.ts +594 -0
  59. package/src/capabilities/index.ts +37 -0
  60. package/src/capabilities/query.ts +657 -0
  61. package/src/capabilities/store.ts +379 -0
  62. package/src/color/index.ts +3 -0
  63. package/src/color/paint.ts +169 -0
  64. package/src/color/palette.ts +48 -0
  65. package/src/color/sample.ts +323 -0
  66. package/src/color.ts +1 -0
  67. package/src/components/AnsiText.tsx +42 -0
  68. package/src/components/App.tsx +98 -10
  69. package/src/components/BackgroundContext.ts +2 -3
  70. package/src/components/Box.tsx +0 -8
  71. package/src/components/CursorContext.ts +1 -1
  72. package/src/components/Hyperlink.tsx +56 -0
  73. package/src/components/TerminalOscContext.ts +25 -0
  74. package/src/components/Text.tsx +22 -45
  75. package/src/components/Transform.tsx +1 -1
  76. package/src/dom.ts +11 -2
  77. package/src/global.d.ts +3 -0
  78. package/src/hooks/use-capabilities.ts +73 -0
  79. package/src/hooks/use-cursor.ts +2 -2
  80. package/src/hooks/use-terminal-osc.ts +59 -0
  81. package/src/index.ts +45 -1
  82. package/src/ink.tsx +213 -153
  83. package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
  84. package/src/reconciler.ts +24 -3
  85. package/src/render-background.ts +36 -15
  86. package/src/render-border.ts +94 -61
  87. package/src/render-frame.ts +83 -0
  88. package/src/render-to-string.ts +21 -6
  89. package/src/render.ts +15 -6
  90. package/src/router/components.tsx +343 -0
  91. package/src/router/context.ts +41 -0
  92. package/src/router/history.ts +194 -0
  93. package/src/router/hooks.tsx +391 -0
  94. package/src/router/index.ts +34 -0
  95. package/src/router/matcher.ts +571 -0
  96. package/src/screen/ansi.ts +184 -0
  97. package/src/screen/canvas.ts +160 -0
  98. package/src/screen/cell.ts +138 -0
  99. package/src/screen/color-profile.ts +47 -0
  100. package/src/screen/geometry.ts +9 -0
  101. package/src/screen/index.ts +6 -0
  102. package/src/screen/screen.ts +305 -0
  103. package/src/screen/serialize.ts +129 -0
  104. package/src/screen.ts +1 -0
  105. package/src/semantic-text-style.ts +118 -0
  106. package/src/squash-text-nodes.ts +2 -5
  107. package/src/structured-text.ts +325 -0
  108. package/src/styles.ts +19 -14
  109. package/src/terminal/index.ts +2 -0
  110. package/src/terminal/inline-presenter.ts +120 -0
  111. package/src/terminal/input.ts +86 -0
  112. package/src/terminal/render-scheduler.ts +37 -0
  113. package/src/terminal/screen-presenter.ts +188 -0
  114. package/src/terminal/session.ts +407 -0
  115. package/src/terminal.ts +1 -0
  116. package/src/testing/browser.ts +588 -0
  117. package/src/testing/emulators.ts +205 -0
  118. package/src/testing/explorer-app/index.html +12 -0
  119. package/src/testing/explorer-app/main.ts +381 -0
  120. package/src/testing/explorer-app/style.css +194 -0
  121. package/src/testing/explorer-app/tsconfig.json +15 -0
  122. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  123. package/src/testing/index.ts +26 -0
  124. package/src/testing/keys.ts +56 -0
  125. package/src/testing/live.ts +85 -0
  126. package/src/testing/matchers.ts +70 -0
  127. package/src/testing/public.ts +94 -0
  128. package/src/testing/terminal.ts +349 -0
  129. package/src/testing/vitest.ts +157 -0
  130. package/src/transform-adapter.ts +14 -0
  131. package/src/wrap-text.ts +4 -0
  132. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  133. package/dist/truncate-Cr6xVFMa.js +0 -2330
  134. package/src/ansi/supports-color.ts +0 -207
  135. package/src/colorize.ts +0 -60
  136. package/src/log-update.ts +0 -370
  137. package/src/output.ts +0 -308
  138. package/src/renderer.ts +0 -73
@@ -1,207 +0,0 @@
1
- // Terminal color support detection.
2
- // Ported from `supports-color` (MIT, Sindre Sorhus & Josh Junon).
3
- import { isatty } from "node:tty";
4
-
5
- import { isWindows } from "#/env.ts";
6
-
7
- export type ColorSupportLevel = 0 | 1 | 2 | 3;
8
-
9
- export type ColorSupport = {
10
- readonly level: ColorSupportLevel;
11
- readonly hasBasic: boolean;
12
- readonly has256: boolean;
13
- readonly has16m: boolean;
14
- };
15
-
16
- export type ColorInfo = ColorSupport | false;
17
-
18
- function hasFlag(flag: string, argv: readonly string[] = process.argv): boolean {
19
- const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
20
- const position = argv.indexOf(prefix + flag);
21
- const terminatorPosition = argv.indexOf("--");
22
- return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
23
- }
24
-
25
- const { env } = process;
26
-
27
- let flagForceColor: number | undefined;
28
- if (
29
- hasFlag("no-color") ||
30
- hasFlag("no-colors") ||
31
- hasFlag("color=false") ||
32
- hasFlag("color=never")
33
- ) {
34
- flagForceColor = 0;
35
- } else if (
36
- hasFlag("color") ||
37
- hasFlag("colors") ||
38
- hasFlag("color=true") ||
39
- hasFlag("color=always")
40
- ) {
41
- flagForceColor = 1;
42
- }
43
-
44
- function envForceColor(): number | undefined {
45
- if (!("FORCE_COLOR" in env)) {
46
- return;
47
- }
48
-
49
- if (env["FORCE_COLOR"] === "true") {
50
- return 1;
51
- }
52
-
53
- if (env["FORCE_COLOR"] === "false") {
54
- return 0;
55
- }
56
-
57
- return env["FORCE_COLOR"]!.length === 0
58
- ? 1
59
- : Math.min(Number.parseInt(env["FORCE_COLOR"]!, 10), 3);
60
- }
61
-
62
- function translateLevel(level: number): ColorInfo {
63
- if (level === 0) {
64
- return false;
65
- }
66
-
67
- return {
68
- level: level as ColorSupportLevel,
69
- hasBasic: true,
70
- has256: level >= 2,
71
- has16m: level >= 3,
72
- };
73
- }
74
-
75
- type Options = {
76
- readonly streamIsTTY?: boolean;
77
- readonly sniffFlags?: boolean;
78
- };
79
-
80
- // eslint-disable-next-line complexity
81
- function supportsColorLevel(
82
- haveStream: boolean,
83
- { streamIsTTY, sniffFlags = true }: Options = {},
84
- ): number {
85
- const noFlagForceColor = envForceColor();
86
- if (noFlagForceColor !== undefined) {
87
- flagForceColor = noFlagForceColor;
88
- }
89
-
90
- const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
91
-
92
- if (forceColor === 0) {
93
- return 0;
94
- }
95
-
96
- if (sniffFlags) {
97
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
98
- return 3;
99
- }
100
-
101
- if (hasFlag("color=256")) {
102
- return 2;
103
- }
104
- }
105
-
106
- // Check for Azure DevOps pipelines. Has to be above the `!streamIsTTY` check.
107
- if ("TF_BUILD" in env && "AGENT_NAME" in env) {
108
- return 1;
109
- }
110
-
111
- if (haveStream && !streamIsTTY && forceColor === undefined) {
112
- return 0;
113
- }
114
-
115
- const min = forceColor ?? 0;
116
-
117
- if (env["TERM"] === "dumb") {
118
- return min;
119
- }
120
-
121
- if (isWindows) {
122
- // Node 22 requires Windows 10 1809+ (build 17763), which supports TrueColor.
123
- return 3;
124
- }
125
-
126
- if ("CI" in env) {
127
- if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
128
- return 3;
129
- }
130
-
131
- if (
132
- ["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) ||
133
- env["CI_NAME"] === "codeship"
134
- ) {
135
- return 1;
136
- }
137
-
138
- return min;
139
- }
140
-
141
- if ("TEAMCITY_VERSION" in env) {
142
- return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env["TEAMCITY_VERSION"]!) ? 1 : 0;
143
- }
144
-
145
- if (env["COLORTERM"] === "truecolor") {
146
- return 3;
147
- }
148
-
149
- if (env["TERM"] === "xterm-kitty") {
150
- return 3;
151
- }
152
-
153
- if (env["TERM"] === "xterm-ghostty") {
154
- return 3;
155
- }
156
-
157
- if (env["TERM"] === "wezterm") {
158
- return 3;
159
- }
160
-
161
- if ("TERM_PROGRAM" in env) {
162
- const version = Number.parseInt((env["TERM_PROGRAM_VERSION"] ?? "").split(".")[0]!, 10);
163
-
164
- switch (env["TERM_PROGRAM"]) {
165
- case "iTerm.app": {
166
- return version >= 3 ? 3 : 2;
167
- }
168
-
169
- case "Apple_Terminal": {
170
- return 2;
171
- }
172
-
173
- default:
174
- }
175
- }
176
-
177
- if (/-256(color)?$/i.test(env["TERM"] ?? "")) {
178
- return 2;
179
- }
180
-
181
- if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env["TERM"] ?? "")) {
182
- return 1;
183
- }
184
-
185
- if ("COLORTERM" in env) {
186
- return 1;
187
- }
188
-
189
- return min;
190
- }
191
-
192
- export function createSupportsColor(
193
- stream: { isTTY?: boolean } | undefined,
194
- options: Options = {},
195
- ): ColorInfo {
196
- const level = supportsColorLevel(Boolean(stream), {
197
- streamIsTTY: Boolean(stream?.isTTY),
198
- ...options,
199
- });
200
-
201
- return translateLevel(level);
202
- }
203
-
204
- export const supportsColor = {
205
- stdout: createSupportsColor({ isTTY: isatty(1) }),
206
- stderr: createSupportsColor({ isTTY: isatty(2) }),
207
- };
package/src/colorize.ts DELETED
@@ -1,60 +0,0 @@
1
- import { chalk, type ForegroundColorName, type BackgroundColorName } from "#/ansi/chalk.ts";
2
-
3
- type ColorType = "foreground" | "background";
4
-
5
- const rgbRegex = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/;
6
- const ansiRegex = /^ansi256\(\s?(\d+)\s?\)$/;
7
-
8
- const isNamedColor = (color: string): color is ForegroundColorName => {
9
- return color in chalk;
10
- };
11
-
12
- export const colorize = (str: string, color: string | undefined, type: ColorType): string => {
13
- if (!color) {
14
- return str;
15
- }
16
-
17
- if (isNamedColor(color)) {
18
- if (type === "foreground") {
19
- return chalk[color](str);
20
- }
21
-
22
- const methodName = `bg${color[0]!.toUpperCase() + color.slice(1)}` as BackgroundColorName;
23
-
24
- return chalk[methodName](str);
25
- }
26
-
27
- if (color.startsWith("#")) {
28
- return type === "foreground" ? chalk.hex(color)(str) : chalk.bgHex(color)(str);
29
- }
30
-
31
- if (color.startsWith("ansi256")) {
32
- const matches = ansiRegex.exec(color);
33
-
34
- if (!matches) {
35
- return str;
36
- }
37
-
38
- const value = Number(matches[1]);
39
-
40
- return type === "foreground" ? chalk.ansi256(value)(str) : chalk.bgAnsi256(value)(str);
41
- }
42
-
43
- if (color.startsWith("rgb")) {
44
- const matches = rgbRegex.exec(color);
45
-
46
- if (!matches) {
47
- return str;
48
- }
49
-
50
- const firstValue = Number(matches[1]);
51
- const secondValue = Number(matches[2]);
52
- const thirdValue = Number(matches[3]);
53
-
54
- return type === "foreground"
55
- ? chalk.rgb(firstValue, secondValue, thirdValue)(str)
56
- : chalk.bgRgb(firstValue, secondValue, thirdValue)(str);
57
- }
58
-
59
- return str;
60
- };
package/src/log-update.ts DELETED
@@ -1,370 +0,0 @@
1
- import { cliCursor } from "#/ansi/cursor.ts";
2
- import { ansiEscapes } from "#/ansi/escapes.ts";
3
- import {
4
- type CursorPosition,
5
- cursorPositionChanged,
6
- buildCursorSuffix,
7
- buildCursorOnlySequence,
8
- buildReturnToBottomPrefix,
9
- hideCursorEscape,
10
- } from "#/cursor-position.ts";
11
-
12
- export type { CursorPosition } from "#/cursor-position.ts";
13
-
14
- export type LogUpdate = {
15
- clear: () => void;
16
- done: () => void;
17
- reset: () => void;
18
- sync: (str: string) => void;
19
- setCursorPosition: (position: CursorPosition | undefined) => void;
20
- isCursorDirty: () => boolean;
21
- willRender: (str: string) => boolean;
22
- (str: string): boolean;
23
- };
24
-
25
- // Count visible lines in a string, ignoring the trailing empty element
26
- // that `split('\n')` produces when the string ends with '\n'.
27
- const visibleLineCount = (lines: string[], str: string): number =>
28
- str.endsWith("\n") ? lines.length - 1 : lines.length;
29
-
30
- const createStandard = (stream: NodeJS.WritableStream, { showCursor = false } = {}): LogUpdate => {
31
- let previousLineCount = 0;
32
- let previousOutput = "";
33
- let hasHiddenCursor = false;
34
- let cursorPosition: CursorPosition | undefined;
35
- let cursorDirty = false;
36
- let previousCursorPosition: CursorPosition | undefined;
37
- let cursorWasShown = false;
38
-
39
- const getActiveCursor = () => (cursorDirty ? cursorPosition : undefined);
40
- const hasChanges = (str: string, activeCursor: CursorPosition | undefined): boolean => {
41
- const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
42
- return str !== previousOutput || cursorChanged;
43
- };
44
-
45
- const render = (str: string) => {
46
- if (!showCursor && !hasHiddenCursor) {
47
- cliCursor.hide(stream);
48
- hasHiddenCursor = true;
49
- }
50
-
51
- // Only use cursor if setCursorPosition was called since last render.
52
- // This ensures stale positions don't persist after component unmount.
53
- const activeCursor = getActiveCursor();
54
- cursorDirty = false;
55
- const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
56
-
57
- if (!hasChanges(str, activeCursor)) {
58
- return false;
59
- }
60
-
61
- const lines = str.split("\n");
62
- const cursorSuffix = buildCursorSuffix(lines.length - 1, activeCursor);
63
-
64
- if (str === previousOutput && cursorChanged) {
65
- stream.write(
66
- buildCursorOnlySequence({
67
- cursorWasShown,
68
- previousLineCount,
69
- previousCursorPosition,
70
- cursorPosition: activeCursor,
71
- }),
72
- );
73
- } else {
74
- previousOutput = str;
75
- const returnPrefix = buildReturnToBottomPrefix(
76
- cursorWasShown,
77
- previousLineCount,
78
- previousCursorPosition,
79
- );
80
- stream.write(returnPrefix + ansiEscapes.eraseLines(previousLineCount) + str + cursorSuffix);
81
- previousLineCount = lines.length;
82
- }
83
-
84
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
85
- cursorWasShown = activeCursor !== undefined;
86
- return true;
87
- };
88
-
89
- render.clear = () => {
90
- const prefix = buildReturnToBottomPrefix(
91
- cursorWasShown,
92
- previousLineCount,
93
- previousCursorPosition,
94
- );
95
- stream.write(prefix + ansiEscapes.eraseLines(previousLineCount));
96
- previousOutput = "";
97
- previousLineCount = 0;
98
- previousCursorPosition = undefined;
99
- cursorWasShown = false;
100
- };
101
-
102
- render.done = () => {
103
- previousOutput = "";
104
- previousLineCount = 0;
105
- previousCursorPosition = undefined;
106
- cursorWasShown = false;
107
-
108
- if (!showCursor) {
109
- cliCursor.show(stream);
110
- hasHiddenCursor = false;
111
- }
112
- };
113
-
114
- render.reset = () => {
115
- previousOutput = "";
116
- previousLineCount = 0;
117
- previousCursorPosition = undefined;
118
- cursorWasShown = false;
119
- };
120
-
121
- render.sync = (str: string) => {
122
- const activeCursor = cursorDirty ? cursorPosition : undefined;
123
- cursorDirty = false;
124
-
125
- const lines = str.split("\n");
126
- previousOutput = str;
127
- previousLineCount = lines.length;
128
-
129
- if (!activeCursor && cursorWasShown) {
130
- stream.write(hideCursorEscape);
131
- }
132
-
133
- if (activeCursor) {
134
- stream.write(buildCursorSuffix(lines.length - 1, activeCursor));
135
- }
136
-
137
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
138
- cursorWasShown = activeCursor !== undefined;
139
- };
140
-
141
- render.setCursorPosition = (position: CursorPosition | undefined) => {
142
- cursorPosition = position;
143
- cursorDirty = true;
144
- };
145
-
146
- render.isCursorDirty = () => cursorDirty;
147
- render.willRender = (str: string) => hasChanges(str, getActiveCursor());
148
-
149
- return render;
150
- };
151
-
152
- const createIncremental = (
153
- stream: NodeJS.WritableStream,
154
- { showCursor = false } = {},
155
- ): LogUpdate => {
156
- let previousLines: string[] = [];
157
- let previousOutput = "";
158
- let hasHiddenCursor = false;
159
- let cursorPosition: CursorPosition | undefined;
160
- let cursorDirty = false;
161
- let previousCursorPosition: CursorPosition | undefined;
162
- let cursorWasShown = false;
163
-
164
- const getActiveCursor = () => (cursorDirty ? cursorPosition : undefined);
165
- const hasChanges = (str: string, activeCursor: CursorPosition | undefined): boolean => {
166
- const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
167
- return str !== previousOutput || cursorChanged;
168
- };
169
-
170
- const render = (str: string) => {
171
- if (!showCursor && !hasHiddenCursor) {
172
- cliCursor.hide(stream);
173
- hasHiddenCursor = true;
174
- }
175
-
176
- // Only use cursor if setCursorPosition was called since last render.
177
- // This ensures stale positions don't persist after component unmount.
178
- const activeCursor = getActiveCursor();
179
- cursorDirty = false;
180
- const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
181
-
182
- if (!hasChanges(str, activeCursor)) {
183
- return false;
184
- }
185
-
186
- const nextLines = str.split("\n");
187
- const visibleCount = visibleLineCount(nextLines, str);
188
- const previousVisible = visibleLineCount(previousLines, previousOutput);
189
-
190
- if (str === previousOutput && cursorChanged) {
191
- stream.write(
192
- buildCursorOnlySequence({
193
- cursorWasShown,
194
- previousLineCount: previousLines.length,
195
- previousCursorPosition,
196
- cursorPosition: activeCursor,
197
- }),
198
- );
199
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
200
- cursorWasShown = activeCursor !== undefined;
201
- return true;
202
- }
203
-
204
- const returnPrefix = buildReturnToBottomPrefix(
205
- cursorWasShown,
206
- previousLines.length,
207
- previousCursorPosition,
208
- );
209
-
210
- if (str === "\n" || previousOutput.length === 0) {
211
- const cursorSuffix = buildCursorSuffix(nextLines.length - 1, activeCursor);
212
- stream.write(
213
- returnPrefix + ansiEscapes.eraseLines(previousLines.length) + str + cursorSuffix,
214
- );
215
- cursorWasShown = activeCursor !== undefined;
216
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
217
- previousOutput = str;
218
- previousLines = nextLines;
219
- return true;
220
- }
221
-
222
- // A frame that grew may need rows past the bottom margin. The
223
- // incremental walk moves over unchanged lines with cursorNextLine,
224
- // which clamps at the bottom margin instead of scrolling (only a
225
- // line feed scrolls), so a grown frame that reaches the bottom
226
- // desynchronizes the tracked cursor position from the terminal.
227
- // Rewrite the whole frame instead — every line is emitted with a
228
- // newline, which scrolls correctly at the bottom.
229
- if (visibleCount > previousVisible) {
230
- const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);
231
- stream.write(
232
- returnPrefix + ansiEscapes.eraseLines(previousLines.length) + str + cursorSuffix,
233
- );
234
- cursorWasShown = activeCursor !== undefined;
235
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
236
- previousOutput = str;
237
- previousLines = nextLines;
238
- return true;
239
- }
240
-
241
- const hasTrailingNewline = str.endsWith("\n");
242
-
243
- // We aggregate all chunks for incremental rendering into a buffer, and then write them to stdout at the end.
244
- const buffer: string[] = [];
245
-
246
- buffer.push(returnPrefix);
247
-
248
- // Clear extra lines if the current content's line count is lower than the previous.
249
- if (visibleCount < previousVisible) {
250
- const previousHadTrailingNewline = previousOutput.endsWith("\n");
251
- const extraSlot = previousHadTrailingNewline ? 1 : 0;
252
- buffer.push(
253
- ansiEscapes.eraseLines(previousVisible - visibleCount + extraSlot),
254
- ansiEscapes.cursorUp(visibleCount),
255
- );
256
- } else {
257
- buffer.push(ansiEscapes.cursorUp(previousLines.length - 1));
258
- }
259
-
260
- for (let i = 0; i < visibleCount; i++) {
261
- const isLastLine = i === visibleCount - 1;
262
-
263
- // We do not write lines if the contents are the same. This prevents flickering during renders.
264
- if (nextLines[i] === previousLines[i]) {
265
- // Don't move past the last line when there's no trailing newline,
266
- // otherwise the cursor overshoots the rendered block.
267
- if (!isLastLine || hasTrailingNewline) {
268
- buffer.push(ansiEscapes.cursorNextLine);
269
- }
270
-
271
- continue;
272
- }
273
-
274
- buffer.push(
275
- ansiEscapes.cursorTo(0) +
276
- nextLines[i] +
277
- ansiEscapes.eraseEndLine +
278
- // Don't append newline after the last line when the input
279
- // has no trailing newline (fullscreen mode).
280
- (isLastLine && !hasTrailingNewline ? "" : "\n"),
281
- );
282
- }
283
-
284
- const cursorSuffix = buildCursorSuffix(nextLines.length - 1, activeCursor);
285
- buffer.push(cursorSuffix);
286
-
287
- stream.write(buffer.join(""));
288
-
289
- cursorWasShown = activeCursor !== undefined;
290
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
291
- previousOutput = str;
292
- previousLines = nextLines;
293
- return true;
294
- };
295
-
296
- render.clear = () => {
297
- const prefix = buildReturnToBottomPrefix(
298
- cursorWasShown,
299
- previousLines.length,
300
- previousCursorPosition,
301
- );
302
- stream.write(prefix + ansiEscapes.eraseLines(previousLines.length));
303
- previousOutput = "";
304
- previousLines = [];
305
- previousCursorPosition = undefined;
306
- cursorWasShown = false;
307
- };
308
-
309
- render.done = () => {
310
- previousOutput = "";
311
- previousLines = [];
312
- previousCursorPosition = undefined;
313
- cursorWasShown = false;
314
-
315
- if (!showCursor) {
316
- cliCursor.show(stream);
317
- hasHiddenCursor = false;
318
- }
319
- };
320
-
321
- render.reset = () => {
322
- previousOutput = "";
323
- previousLines = [];
324
- previousCursorPosition = undefined;
325
- cursorWasShown = false;
326
- };
327
-
328
- render.sync = (str: string) => {
329
- const activeCursor = cursorDirty ? cursorPosition : undefined;
330
- cursorDirty = false;
331
-
332
- const lines = str.split("\n");
333
- previousOutput = str;
334
- previousLines = lines;
335
-
336
- if (!activeCursor && cursorWasShown) {
337
- stream.write(hideCursorEscape);
338
- }
339
-
340
- if (activeCursor) {
341
- stream.write(buildCursorSuffix(lines.length - 1, activeCursor));
342
- }
343
-
344
- previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
345
- cursorWasShown = activeCursor !== undefined;
346
- };
347
-
348
- render.setCursorPosition = (position: CursorPosition | undefined) => {
349
- cursorPosition = position;
350
- cursorDirty = true;
351
- };
352
-
353
- render.isCursorDirty = () => cursorDirty;
354
- render.willRender = (str: string) => hasChanges(str, getActiveCursor());
355
-
356
- return render;
357
- };
358
-
359
- const create = (
360
- stream: NodeJS.WritableStream,
361
- { showCursor = false, incremental = false } = {},
362
- ): LogUpdate => {
363
- if (incremental) {
364
- return createIncremental(stream, { showCursor });
365
- }
366
-
367
- return createStandard(stream, { showCursor });
368
- };
369
-
370
- export const logUpdate = { create };