@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11

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 (223) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +98 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  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-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -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-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3211 -0
  27. package/dist/jsx-dev-runtime.d.ts +2 -0
  28. package/dist/jsx-dev-runtime.js +227 -0
  29. package/dist/jsx-runtime-XtwIVFsg.js +255 -0
  30. package/dist/jsx-runtime.d.ts +2 -0
  31. package/dist/jsx-runtime.js +8 -0
  32. package/dist/osc-BFKKSqpg.js +71 -0
  33. package/dist/osc-Cn0fw77g.d.ts +23 -0
  34. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  35. package/dist/query-BNc2B8GD.d.ts +152 -0
  36. package/dist/react-CTZ_8dwh.js +1197 -0
  37. package/dist/react.d.ts +2 -0
  38. package/dist/react.js +49 -0
  39. package/dist/router.d.ts +392 -0
  40. package/dist/router.js +711 -0
  41. package/dist/sample-Cqw1bjUL.js +445 -0
  42. package/dist/screen-CgC2WlVM.d.ts +49 -0
  43. package/dist/screen-CiPytswf.js +342 -0
  44. package/dist/screen.d.ts +5 -0
  45. package/dist/screen.js +5 -0
  46. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  47. package/dist/serialize-BTkAZgw1.js +79 -0
  48. package/dist/session-DDQ5V300.js +723 -0
  49. package/dist/sgr-BhwaWAJB.js +246 -0
  50. package/dist/store-C1P5fOUi.d.ts +72 -0
  51. package/dist/string-width-CijQwpIk.js +69 -0
  52. package/dist/strip-BvU4toXG.js +6 -0
  53. package/dist/terminal.d.ts +121 -0
  54. package/dist/terminal.js +2 -0
  55. package/dist/tokenize-AjqbvtiT.js +1242 -0
  56. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  57. package/dist/truncate-D31fhU6i.js +562 -0
  58. package/dist/use-focus-B5npZrJU.js +18736 -0
  59. package/dist/yoga-5jKhYCJC.js +3465 -0
  60. package/dist/yoga.d.ts +2 -0
  61. package/dist/yoga.js +2 -0
  62. package/package.json +89 -29
  63. package/src/ansi/chalk.ts +138 -0
  64. package/src/ansi/cursor.ts +46 -0
  65. package/src/ansi/east-asian-width.ts +259 -0
  66. package/src/ansi/escapes.ts +120 -0
  67. package/src/ansi/graphemes.ts +8 -0
  68. package/src/ansi/hyperlink.ts +44 -0
  69. package/src/ansi/index.ts +27 -0
  70. package/src/ansi/osc.ts +77 -0
  71. package/src/ansi/sgr.ts +234 -0
  72. package/src/ansi/slice.ts +43 -0
  73. package/src/ansi/string-width.ts +121 -0
  74. package/src/ansi/strip.ts +13 -0
  75. package/src/ansi/tokenize.ts +380 -0
  76. package/src/ansi/truncate.ts +196 -0
  77. package/src/ansi/wrap.ts +765 -0
  78. package/src/ansi-tokenizer.ts +510 -0
  79. package/src/capabilities/color-policy.ts +34 -0
  80. package/src/capabilities/detect.ts +608 -0
  81. package/src/capabilities/index.ts +37 -0
  82. package/src/capabilities/query.ts +679 -0
  83. package/src/capabilities/store.ts +394 -0
  84. package/src/code-excerpt.ts +39 -0
  85. package/src/color/index.ts +3 -0
  86. package/src/color/paint.ts +169 -0
  87. package/src/color/palette.ts +48 -0
  88. package/src/color/sample.ts +323 -0
  89. package/src/color.ts +1 -0
  90. package/src/components/AccessibilityContext.ts +5 -0
  91. package/src/components/AnimationContext.ts +24 -0
  92. package/src/components/AnsiText.tsx +42 -0
  93. package/src/components/App.tsx +878 -0
  94. package/src/components/AppContext.ts +111 -0
  95. package/src/components/BackgroundContext.ts +7 -0
  96. package/src/components/Box.tsx +100 -0
  97. package/src/components/CursorContext.ts +19 -0
  98. package/src/components/ErrorBoundary.tsx +39 -0
  99. package/src/components/ErrorOverview.tsx +135 -0
  100. package/src/components/FocusContext.ts +30 -0
  101. package/src/components/Hyperlink.tsx +56 -0
  102. package/src/components/Newline.tsx +16 -0
  103. package/src/components/Spacer.tsx +11 -0
  104. package/src/components/Static.tsx +60 -0
  105. package/src/components/StderrContext.ts +24 -0
  106. package/src/components/StdinContext.ts +48 -0
  107. package/src/components/StdoutContext.ts +26 -0
  108. package/src/components/TerminalOscContext.ts +25 -0
  109. package/src/components/Text.tsx +122 -0
  110. package/src/components/Transform.tsx +38 -0
  111. package/src/components/VirtualList.tsx +128 -0
  112. package/src/cursor-position.ts +103 -0
  113. package/src/devtools.ts +103 -0
  114. package/src/dom.ts +301 -0
  115. package/src/env.ts +12 -0
  116. package/src/get-max-width.ts +11 -0
  117. package/src/global.d.ts +38 -0
  118. package/src/glyphs.ts +99 -0
  119. package/src/hooks/use-animation.ts +142 -0
  120. package/src/hooks/use-app.ts +8 -0
  121. package/src/hooks/use-box-metrics.ts +134 -0
  122. package/src/hooks/use-capabilities.ts +73 -0
  123. package/src/hooks/use-cursor.ts +33 -0
  124. package/src/hooks/use-focus-manager.ts +62 -0
  125. package/src/hooks/use-focus.ts +82 -0
  126. package/src/hooks/use-input.ts +267 -0
  127. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  128. package/src/hooks/use-paste.ts +78 -0
  129. package/src/hooks/use-stderr.ts +8 -0
  130. package/src/hooks/use-stdin.ts +10 -0
  131. package/src/hooks/use-stdout.ts +8 -0
  132. package/src/hooks/use-terminal-osc.ts +59 -0
  133. package/src/hooks/use-virtual-scroll.ts +84 -0
  134. package/src/hooks/use-window-size.ts +37 -0
  135. package/src/index.ts +96 -0
  136. package/src/ink.tsx +1452 -0
  137. package/src/input-parser.ts +303 -0
  138. package/src/instances.ts +9 -0
  139. package/src/jsx-dev-runtime.ts +3 -0
  140. package/src/jsx-runtime.ts +6 -0
  141. package/src/kitty-keyboard.ts +185 -0
  142. package/src/measure-element.ts +62 -0
  143. package/src/measure-text.ts +31 -0
  144. package/src/paint-tree.ts +220 -0
  145. package/src/parse-keypress.ts +515 -0
  146. package/src/parse-stack-line.ts +138 -0
  147. package/src/patch-console.ts +106 -0
  148. package/src/quick-lru.ts +85 -0
  149. package/src/react.ts +53 -0
  150. package/src/reconciler.ts +476 -0
  151. package/src/render-background.ts +59 -0
  152. package/src/render-border.ts +167 -0
  153. package/src/render-frame.ts +83 -0
  154. package/src/render-to-string.ts +146 -0
  155. package/src/render.ts +284 -0
  156. package/src/router/components.tsx +343 -0
  157. package/src/router/context.ts +41 -0
  158. package/src/router/history.ts +194 -0
  159. package/src/router/hooks.tsx +391 -0
  160. package/src/router/index.ts +34 -0
  161. package/src/router/matcher.ts +571 -0
  162. package/src/sanitize-ansi.ts +33 -0
  163. package/src/screen/ansi.ts +184 -0
  164. package/src/screen/canvas.ts +160 -0
  165. package/src/screen/cell.ts +138 -0
  166. package/src/screen/color-profile.ts +47 -0
  167. package/src/screen/geometry.ts +9 -0
  168. package/src/screen/index.ts +6 -0
  169. package/src/screen/screen.ts +305 -0
  170. package/src/screen/serialize.ts +129 -0
  171. package/src/screen.ts +1 -0
  172. package/src/semantic-text-style.ts +118 -0
  173. package/src/signal-exit.ts +106 -0
  174. package/src/squash-text-nodes.ts +37 -0
  175. package/src/stream.ts +28 -0
  176. package/src/structured-text.ts +325 -0
  177. package/src/styles.ts +753 -0
  178. package/src/terminal/index.ts +2 -0
  179. package/src/terminal/inline-presenter.ts +120 -0
  180. package/src/terminal/input.ts +92 -0
  181. package/src/terminal/render-scheduler.ts +37 -0
  182. package/src/terminal/screen-presenter.ts +242 -0
  183. package/src/terminal/session.ts +408 -0
  184. package/src/terminal-size.ts +58 -0
  185. package/src/terminal.ts +1 -0
  186. package/src/testing/browser.ts +588 -0
  187. package/src/testing/emulators.ts +205 -0
  188. package/src/testing/explorer-app/index.html +12 -0
  189. package/src/testing/explorer-app/main.ts +381 -0
  190. package/src/testing/explorer-app/style.css +194 -0
  191. package/src/testing/explorer-app/tsconfig.json +15 -0
  192. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  193. package/src/testing/index.ts +26 -0
  194. package/src/testing/keys.ts +56 -0
  195. package/src/testing/live.ts +85 -0
  196. package/src/testing/matchers.ts +70 -0
  197. package/src/testing/public.ts +94 -0
  198. package/src/testing/terminal.ts +361 -0
  199. package/src/testing/vitest.ts +157 -0
  200. package/src/throttle.ts +73 -0
  201. package/src/transform-adapter.ts +14 -0
  202. package/src/types.ts +10 -0
  203. package/src/virtual-scroll.ts +133 -0
  204. package/src/wrap-text.ts +54 -0
  205. package/src/yoga/config.ts +57 -0
  206. package/src/yoga/core/absoluteLayout.ts +626 -0
  207. package/src/yoga/core/baseline.ts +66 -0
  208. package/src/yoga/core/cache.ts +136 -0
  209. package/src/yoga/core/calculateLayout.ts +2926 -0
  210. package/src/yoga/core/config.ts +104 -0
  211. package/src/yoga/core/flexLine.ts +177 -0
  212. package/src/yoga/core/helpers.ts +293 -0
  213. package/src/yoga/core/layoutResults.ts +167 -0
  214. package/src/yoga/core/node.ts +611 -0
  215. package/src/yoga/core/numeric.ts +44 -0
  216. package/src/yoga/core/pixelGrid.ts +151 -0
  217. package/src/yoga/core/style.ts +887 -0
  218. package/src/yoga/core/types.ts +224 -0
  219. package/src/yoga/generated/YGEnums.ts +263 -0
  220. package/src/yoga/index.ts +19 -0
  221. package/src/yoga/node.ts +1140 -0
  222. package/dist/index.d.mts +0 -2379
  223. package/dist/index.mjs +0 -10072
@@ -0,0 +1,408 @@
1
+ import { ansiEscapes, type CursorShape } from "#/ansi/escapes.ts";
2
+ import {
3
+ notify,
4
+ setClipboard,
5
+ setPointerShape,
6
+ setTerminalProgress,
7
+ setWindowTitle,
8
+ setWorkingDirectory,
9
+ tmuxPassthrough,
10
+ type ClipboardSelection,
11
+ type TerminalProgressState,
12
+ } from "#/ansi/osc.ts";
13
+ import { colorState, type ColorPolicy } from "#/capabilities/color-policy.ts";
14
+ import { getCapabilities, type CapabilitiesStore } from "#/capabilities/store.ts";
15
+ import type { CursorPosition } from "#/cursor-position.ts";
16
+ import type { ColorProfile } from "#/screen/color-profile.ts";
17
+ import type { Screen } from "#/screen/screen.ts";
18
+ import { serializeScreen } from "#/screen/serialize.ts";
19
+ import type { OutputStream } from "#/stream.ts";
20
+ import { TerminalInput } from "#/terminal/input.ts";
21
+ import { ScreenPresenter } from "#/terminal/screen-presenter.ts";
22
+
23
+ export type TerminalSessionOptions = {
24
+ readonly stdin: NodeJS.ReadableStream;
25
+ readonly stdout: OutputStream;
26
+ readonly stderr: OutputStream;
27
+ readonly colorPolicy?: ColorPolicy;
28
+ readonly onCapabilitiesChange?: () => void;
29
+ };
30
+
31
+ export type TerminalMode = {
32
+ readonly id: string;
33
+ readonly enable: string;
34
+ readonly disable: string;
35
+ };
36
+
37
+ export type TerminalCursor = {
38
+ readonly position?: CursorPosition;
39
+ readonly visible: boolean;
40
+ readonly shape: CursorShape;
41
+ readonly blinking: boolean;
42
+ readonly color?: string;
43
+ };
44
+
45
+ type ActiveMode = TerminalMode & { count: number };
46
+ const imperativeProgressOwner = Symbol("imperative-progress");
47
+ const imperativeTitleOwner = Symbol("imperative-title");
48
+
49
+ /** Instance-scoped terminal state and lifecycle ownership. */
50
+ export class TerminalSession {
51
+ readonly stdin: NodeJS.ReadableStream;
52
+ readonly stdout: OutputStream;
53
+ readonly stderr: OutputStream;
54
+ readonly capabilities: CapabilitiesStore;
55
+ readonly input: TerminalInput;
56
+ readonly #presenter: ScreenPresenter;
57
+
58
+ cursor: TerminalCursor = {
59
+ visible: true,
60
+ shape: "block",
61
+ blinking: true,
62
+ };
63
+ suspended = false;
64
+ alternateScreen = false;
65
+ inlineScreen = true;
66
+
67
+ #policy: ColorPolicy;
68
+ #profile: ColorProfile;
69
+ #modes = new Map<string, ActiveMode>();
70
+ #pendingWrites = new Set<Promise<void>>();
71
+ #unsubscribe: () => void;
72
+ #cleaned = false;
73
+ #progressActive = false;
74
+ #progressSequence: string | undefined;
75
+ #progressHeartbeat: ReturnType<typeof setInterval> | undefined;
76
+ #progressPublishers = new Map<symbol, { state: TerminalProgressState; value?: number }>();
77
+ #titlePublishers = new Map<symbol, string>();
78
+ #titleActive = false;
79
+
80
+ constructor(options: TerminalSessionOptions) {
81
+ this.stdin = options.stdin;
82
+ this.stdout = options.stdout;
83
+ this.stderr = options.stderr;
84
+ this.capabilities = getCapabilities(this.stdin, this.stdout);
85
+ this.input = new TerminalInput(this.capabilities);
86
+ this.#presenter = new ScreenPresenter((data) => this.write(data), this.stdout);
87
+ this.#policy = options.colorPolicy ?? "auto";
88
+ this.#profile = colorState(this.capabilities.current, this.#policy).effective;
89
+ this.#unsubscribe = this.capabilities.subscribe(
90
+ (capabilities) => {
91
+ const profile = colorState(capabilities, this.#policy).effective;
92
+ if (profile !== this.#profile) {
93
+ this.#profile = profile;
94
+ this.#presenter.reset();
95
+ }
96
+ options.onCapabilitiesChange?.();
97
+ },
98
+ { resizes: false },
99
+ );
100
+ }
101
+
102
+ get colorProfile(): ColorProfile {
103
+ return this.#profile;
104
+ }
105
+
106
+ /** Encodes a structured frame at the terminal boundary. */
107
+ encode(screen: Screen): string {
108
+ return serializeScreen(screen, {
109
+ colorProfile: this.colorProfile,
110
+ styles: this.colorProfile !== "none",
111
+ });
112
+ }
113
+
114
+ present(
115
+ screen: Screen,
116
+ options: {
117
+ readonly fullscreen?: boolean;
118
+ readonly forceRewrite?: boolean;
119
+ } = {},
120
+ ): boolean {
121
+ return this.#presenter.present(screen, {
122
+ colorProfile: this.colorProfile,
123
+ cursor: this.cursor.position,
124
+ ...options,
125
+ });
126
+ }
127
+
128
+ willPresent(
129
+ screen: Screen,
130
+ options: {
131
+ readonly fullscreen?: boolean;
132
+ readonly forceRewrite?: boolean;
133
+ } = {},
134
+ ): boolean {
135
+ return this.#presenter.willPresent(
136
+ screen,
137
+ this.cursor.position,
138
+ options.fullscreen,
139
+ options.forceRewrite,
140
+ );
141
+ }
142
+
143
+ /** Erases the presented frame; pass the current `columns` after a resize so the erase covers rows the emulator rewrapped. */
144
+ clearFrame(options: { readonly columns?: number } = {}): void {
145
+ this.#presenter.clear(options);
146
+ }
147
+
148
+ resetFrame(): void {
149
+ this.#presenter.reset();
150
+ }
151
+
152
+ finishFrame(): void {
153
+ this.#presenter.done();
154
+ }
155
+
156
+ setColorPolicy(policy: ColorPolicy): void {
157
+ this.#policy = policy;
158
+ const profile = colorState(this.capabilities.current, policy).effective;
159
+ if (profile !== this.#profile) {
160
+ this.#profile = profile;
161
+ this.#presenter.reset();
162
+ }
163
+ }
164
+
165
+ /** Writes through the session and tracks callback-based backpressure completion. */
166
+ write(data: string): boolean {
167
+ let settle!: () => void;
168
+ const completion = new Promise<void>((resolve) => {
169
+ settle = resolve;
170
+ });
171
+ this.#pendingWrites.add(completion);
172
+ try {
173
+ return this.stdout.write(data, () => {
174
+ this.#pendingWrites.delete(completion);
175
+ settle();
176
+ });
177
+ } catch (error) {
178
+ this.#pendingWrites.delete(completion);
179
+ settle();
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ async flush(): Promise<void> {
185
+ await Promise.all(this.#pendingWrites);
186
+ }
187
+
188
+ /** Copy text through OSC 52, including tmux passthrough when required. */
189
+ copyToClipboard(text: string, selection: ClipboardSelection = "clipboard"): boolean {
190
+ return this.write(this.#wrapOsc(setClipboard(text, selection)));
191
+ }
192
+
193
+ setTitle(title: string): boolean {
194
+ return this.publishTitle(imperativeTitleOwner, title);
195
+ }
196
+
197
+ publishTitle(owner: symbol, title?: string): boolean {
198
+ this.#titlePublishers.delete(owner);
199
+ if (title !== undefined) this.#titlePublishers.set(owner, title);
200
+ const current = [...this.#titlePublishers.values()].at(-1);
201
+ if (current !== undefined) {
202
+ this.#titleActive = true;
203
+ return this.write(this.#wrapOsc(setWindowTitle(current)));
204
+ }
205
+ if (!this.#titleActive) return true;
206
+ this.#titleActive = false;
207
+ return this.write(this.#wrapOsc(setWindowTitle("")));
208
+ }
209
+
210
+ setWorkingDirectory(directory: URL | string): boolean {
211
+ return this.write(this.#wrapOsc(setWorkingDirectory(directory)));
212
+ }
213
+
214
+ notify(title: string): boolean {
215
+ return this.write(this.#wrapOsc(notify(title)));
216
+ }
217
+
218
+ setPointerShape(shape: string): boolean {
219
+ return this.write(this.#wrapOsc(setPointerShape(shape)));
220
+ }
221
+
222
+ publishProgress(owner: symbol, state: TerminalProgressState, value?: number): boolean {
223
+ this.#progressPublishers.delete(owner);
224
+ if (state !== "inactive") this.#progressPublishers.set(owner, { state, value });
225
+ return this.#applyPublishedProgress();
226
+ }
227
+
228
+ #applyPublishedProgress(): boolean {
229
+ const current = [...this.#progressPublishers.values()].at(-1);
230
+ return current === undefined
231
+ ? this.#writeProgress("inactive")
232
+ : this.#writeProgress(current.state, current.value);
233
+ }
234
+
235
+ /**
236
+ * Update terminal-native progress. Active progress is reset during cleanup
237
+ * so a completed or failed process cannot leave a stale terminal indicator.
238
+ */
239
+ setProgress(state: TerminalProgressState, value?: number): boolean {
240
+ this.#progressPublishers.delete(imperativeProgressOwner);
241
+ if (state !== "inactive") {
242
+ this.#progressPublishers.set(imperativeProgressOwner, { state, value });
243
+ }
244
+ return this.#applyPublishedProgress();
245
+ }
246
+
247
+ #writeProgress(state: TerminalProgressState, value?: number): boolean {
248
+ this.#progressActive = state !== "inactive";
249
+ const sequence = this.#wrapOsc(setTerminalProgress(state, value));
250
+ if (!this.#progressActive) {
251
+ this.#stopProgressHeartbeat();
252
+ } else {
253
+ this.#progressSequence = sequence;
254
+ if (this.#progressHeartbeat === undefined) {
255
+ this.#progressHeartbeat = setInterval(() => {
256
+ if (this.#progressSequence !== undefined) this.write(this.#progressSequence);
257
+ }, 1000);
258
+ this.#progressHeartbeat.unref?.();
259
+ }
260
+ }
261
+ return this.write(sequence);
262
+ }
263
+
264
+ #stopProgressHeartbeat(): void {
265
+ if (this.#progressHeartbeat !== undefined) {
266
+ clearInterval(this.#progressHeartbeat);
267
+ this.#progressHeartbeat = undefined;
268
+ }
269
+ this.#progressSequence = undefined;
270
+ }
271
+
272
+ enableMode(mode: TerminalMode): void;
273
+ enableMode(enable: string, disable: string): void;
274
+ enableMode(modeOrEnable: TerminalMode | string, disable?: string): void {
275
+ const mode =
276
+ typeof modeOrEnable === "string"
277
+ ? {
278
+ id: disable ?? modeOrEnable,
279
+ enable: modeOrEnable,
280
+ disable: disable ?? "",
281
+ }
282
+ : modeOrEnable;
283
+ const active = this.#modes.get(mode.id);
284
+ if (active) {
285
+ active.count++;
286
+ return;
287
+ }
288
+ this.write(mode.enable);
289
+ this.#modes.set(mode.id, { ...mode, count: 1 });
290
+ }
291
+
292
+ disableMode(idOrDisable: string): void {
293
+ const entry =
294
+ this.#modes.get(idOrDisable) ??
295
+ [...this.#modes.values()].find((mode) => mode.disable === idOrDisable);
296
+ if (!entry) return;
297
+ if (--entry.count > 0) return;
298
+ this.#modes.delete(entry.id);
299
+ this.write(entry.disable);
300
+ }
301
+
302
+ setCursor(position: CursorPosition | undefined): void {
303
+ this.cursor = { ...this.cursor, position, visible: position !== undefined };
304
+ }
305
+
306
+ setCursorAppearance(options: {
307
+ readonly visible?: boolean;
308
+ readonly shape?: CursorShape;
309
+ readonly blinking?: boolean;
310
+ readonly color?: string | null;
311
+ }): void {
312
+ const next = {
313
+ ...this.cursor,
314
+ ...(options.visible === undefined ? {} : { visible: options.visible }),
315
+ ...(options.shape === undefined ? {} : { shape: options.shape }),
316
+ ...(options.blinking === undefined ? {} : { blinking: options.blinking }),
317
+ ...(options.color === undefined ? {} : { color: options.color ?? undefined }),
318
+ };
319
+ let output = "";
320
+ if (next.shape !== this.cursor.shape || next.blinking !== this.cursor.blinking)
321
+ output += ansiEscapes.cursorShape(next.shape, next.blinking);
322
+ if (options.color !== undefined)
323
+ output +=
324
+ options.color === null
325
+ ? ansiEscapes.resetCursorColor
326
+ : ansiEscapes.cursorColor(options.color);
327
+ if (next.visible !== this.cursor.visible)
328
+ output += next.visible ? ansiEscapes.cursorShow : ansiEscapes.cursorHide;
329
+ this.cursor = next;
330
+ if (output) this.write(output);
331
+ }
332
+
333
+ setAlternateScreen(enabled: boolean, options: { hideCursor?: boolean } = {}): void {
334
+ if (enabled === this.alternateScreen) return;
335
+ this.alternateScreen = enabled;
336
+ this.inlineScreen = !enabled;
337
+ if (enabled) {
338
+ this.enableMode({
339
+ id: "alternate-screen",
340
+ enable: ansiEscapes.enterAlternativeScreen,
341
+ disable: ansiEscapes.exitAlternativeScreen,
342
+ });
343
+ if (options.hideCursor) this.setCursorAppearance({ visible: false });
344
+ } else {
345
+ this.disableMode("alternate-screen");
346
+ }
347
+ this.#presenter.reset();
348
+ }
349
+
350
+ beginSuspension(): void {
351
+ if (this.suspended) throw new Error("The terminal session is already suspended");
352
+ this.suspended = true;
353
+ }
354
+
355
+ resume(): void {
356
+ if (!this.suspended) return;
357
+ this.suspended = false;
358
+ this.#presenter.reset();
359
+ }
360
+
361
+ cleanup(): void {
362
+ if (this.#cleaned) return;
363
+ this.#cleaned = true;
364
+ this.#unsubscribe();
365
+ this.#stopProgressHeartbeat();
366
+ this.#progressPublishers.clear();
367
+ if (this.#progressActive) {
368
+ try {
369
+ this.stdout.write(this.#wrapOsc(setTerminalProgress("inactive")));
370
+ } catch {}
371
+ this.#progressActive = false;
372
+ }
373
+ this.#titlePublishers.clear();
374
+ if (this.#titleActive) {
375
+ try {
376
+ this.stdout.write(this.#wrapOsc(setWindowTitle("")));
377
+ } catch {}
378
+ this.#titleActive = false;
379
+ }
380
+ for (const mode of [...this.#modes.values()].reverse()) {
381
+ try {
382
+ this.stdout.write(mode.disable);
383
+ } catch {}
384
+ }
385
+ if (this.cursor.color !== undefined) {
386
+ try {
387
+ this.stdout.write(ansiEscapes.resetCursorColor);
388
+ } catch {}
389
+ }
390
+ if (!this.cursor.visible) {
391
+ try {
392
+ this.stdout.write(ansiEscapes.cursorShow);
393
+ } catch {}
394
+ }
395
+ this.#modes.clear();
396
+ this.alternateScreen = false;
397
+ this.inlineScreen = true;
398
+ this.suspended = false;
399
+ this.cursor = { visible: true, shape: "block", blinking: true };
400
+ this.#presenter.done();
401
+ }
402
+
403
+ #wrapOsc(sequence: string): string {
404
+ return this.capabilities.current.terminal.multiplexer === "tmux"
405
+ ? tmuxPassthrough(sequence)
406
+ : sequence;
407
+ }
408
+ }
@@ -0,0 +1,58 @@
1
+ // Derived from `terminal-size` (MIT, Sindre Sorhus), reduced for Ink's usage as a
2
+ // fallback when the stdout stream reports no dimensions. Unlike the original,
3
+ // this never shells out to `tput`/`resize` — it checks the process streams,
4
+ // the COLUMNS/LINES environment variables and /dev/tty, then falls back to
5
+ // the standard 80×24.
6
+ import { openSync, constants } from "node:fs";
7
+ import { WriteStream } from "node:tty";
8
+
9
+ import { isMacos } from "#/env.ts";
10
+
11
+ type TerminalSize = {
12
+ columns: number;
13
+ rows: number;
14
+ };
15
+
16
+ const create = (columns: number | string, rows: number | string): TerminalSize => ({
17
+ columns: Number.parseInt(String(columns), 10),
18
+ rows: Number.parseInt(String(rows), 10),
19
+ });
20
+
21
+ const devTty = (): TerminalSize | undefined => {
22
+ try {
23
+ // O_EVTONLY is macOS-only and missing from the Node type definitions.
24
+ const { O_EVTONLY: evtOnly } = constants as { O_EVTONLY?: number };
25
+ const flags =
26
+ isMacos && evtOnly !== undefined
27
+ ? // eslint-disable-next-line no-bitwise
28
+ evtOnly | constants.O_NONBLOCK
29
+ : constants.O_NONBLOCK;
30
+
31
+ const { columns, rows } = new WriteStream(openSync("/dev/tty", flags));
32
+
33
+ if (columns && rows) {
34
+ return { columns, rows };
35
+ }
36
+ } catch {}
37
+
38
+ return;
39
+ };
40
+
41
+ export const terminalSize = (): TerminalSize => {
42
+ const { env, stdout, stderr } = process;
43
+
44
+ if (stdout?.columns && stdout?.rows) {
45
+ return create(stdout.columns, stdout.rows);
46
+ }
47
+
48
+ if (stderr?.columns && stderr?.rows) {
49
+ return create(stderr.columns, stderr.rows);
50
+ }
51
+
52
+ // These values are static, so not the first choice.
53
+ if (env["COLUMNS"] && env["LINES"]) {
54
+ return create(env["COLUMNS"], env["LINES"]);
55
+ }
56
+
57
+ return devTty() ?? { columns: 80, rows: 24 };
58
+ };
@@ -0,0 +1 @@
1
+ export * from "#/terminal/index.ts";