@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5

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 (207) hide show
  1. package/README.md +23 -13
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-D5HUf3Fj.d.ts +452 -0
  4. package/dist/ansi.d.ts +137 -143
  5. package/dist/ansi.js +87 -2
  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/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
  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-Bmc2tRPk.d.ts +21 -0
  24. package/dist/index.d.ts +235 -754
  25. package/dist/index.js +1362 -3001
  26. package/dist/osc-CCH7xDoS.js +71 -0
  27. package/dist/osc-Cn0fw77g.d.ts +23 -0
  28. package/dist/paint-C19minOS.d.ts +81 -0
  29. package/dist/query-vaIeGOkH.d.ts +152 -0
  30. package/dist/router.d.ts +392 -0
  31. package/dist/router.js +709 -0
  32. package/dist/sample-Cqw1bjUL.js +445 -0
  33. package/dist/screen-BOh__-65.js +324 -0
  34. package/dist/screen-BReKIheE.d.ts +40 -0
  35. package/dist/screen.d.ts +5 -0
  36. package/dist/screen.js +5 -0
  37. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  38. package/dist/serialize-BTkAZgw1.js +79 -0
  39. package/dist/session-BJmzX2NJ.js +664 -0
  40. package/dist/sgr-BhwaWAJB.js +246 -0
  41. package/dist/store-CgrG9K4y.d.ts +72 -0
  42. package/dist/string-width-CijQwpIk.js +69 -0
  43. package/dist/strip-BvU4toXG.js +6 -0
  44. package/dist/terminal.d.ts +118 -0
  45. package/dist/terminal.js +2 -0
  46. package/dist/tokenize-AjqbvtiT.js +1242 -0
  47. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  48. package/dist/truncate-D31fhU6i.js +562 -0
  49. package/dist/use-focus-C2sciZOo.js +1335 -0
  50. package/package.json +43 -17
  51. package/src/ansi/chalk.ts +12 -53
  52. package/src/ansi/cursor.ts +2 -4
  53. package/src/ansi/east-asian-width.ts +44 -0
  54. package/src/ansi/escapes.ts +18 -26
  55. package/src/ansi/graphemes.ts +8 -0
  56. package/src/ansi/hyperlink.ts +44 -0
  57. package/src/ansi/index.ts +14 -14
  58. package/src/ansi/osc.ts +77 -0
  59. package/src/ansi/sgr.ts +2 -5
  60. package/src/ansi/slice.ts +1 -1
  61. package/src/ansi/string-width.ts +91 -206
  62. package/src/ansi/strip.ts +13 -33
  63. package/src/ansi/tokenize.ts +96 -169
  64. package/src/ansi/truncate.ts +5 -3
  65. package/src/ansi/wrap.ts +13 -14
  66. package/src/ansi-tokenizer.ts +1 -1
  67. package/src/capabilities/color-policy.ts +34 -0
  68. package/src/capabilities/detect.ts +594 -0
  69. package/src/capabilities/index.ts +37 -0
  70. package/src/capabilities/query.ts +657 -0
  71. package/src/capabilities/store.ts +379 -0
  72. package/src/color/index.ts +3 -0
  73. package/src/color/paint.ts +169 -0
  74. package/src/color/palette.ts +48 -0
  75. package/src/color/sample.ts +323 -0
  76. package/src/color.ts +1 -0
  77. package/src/components/AnsiText.tsx +42 -0
  78. package/src/components/App.tsx +109 -22
  79. package/src/components/BackgroundContext.ts +2 -3
  80. package/src/components/Box.tsx +43 -60
  81. package/src/components/CursorContext.ts +1 -1
  82. package/src/components/ErrorBoundary.tsx +1 -1
  83. package/src/components/ErrorOverview.tsx +8 -7
  84. package/src/components/Hyperlink.tsx +56 -0
  85. package/src/components/Spacer.tsx +1 -1
  86. package/src/components/Static.tsx +1 -1
  87. package/src/components/StderrContext.ts +0 -2
  88. package/src/components/StdinContext.ts +0 -1
  89. package/src/components/StdoutContext.ts +1 -3
  90. package/src/components/TerminalOscContext.ts +25 -0
  91. package/src/components/Text.tsx +23 -46
  92. package/src/components/Transform.tsx +2 -2
  93. package/src/cursor-position.ts +1 -1
  94. package/src/devtools.ts +84 -24
  95. package/src/dom.ts +11 -8
  96. package/src/env.ts +12 -0
  97. package/src/get-max-width.ts +1 -1
  98. package/src/global.d.ts +6 -4
  99. package/src/{boxes.ts → glyphs.ts} +17 -18
  100. package/src/hooks/use-animation.ts +1 -1
  101. package/src/hooks/use-app.ts +1 -1
  102. package/src/hooks/use-box-metrics.ts +1 -1
  103. package/src/hooks/use-capabilities.ts +73 -0
  104. package/src/hooks/use-cursor.ts +3 -3
  105. package/src/hooks/use-focus-manager.ts +1 -1
  106. package/src/hooks/use-focus.ts +5 -6
  107. package/src/hooks/use-input.ts +4 -4
  108. package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
  109. package/src/hooks/use-paste.ts +2 -2
  110. package/src/hooks/use-stderr.ts +1 -1
  111. package/src/hooks/use-stdin.ts +1 -1
  112. package/src/hooks/use-stdout.ts +1 -1
  113. package/src/hooks/use-terminal-osc.ts +59 -0
  114. package/src/hooks/use-window-size.ts +2 -2
  115. package/src/index.ts +88 -44
  116. package/src/ink.tsx +620 -745
  117. package/src/input-parser.ts +1 -1
  118. package/src/instances.ts +1 -1
  119. package/src/kitty-keyboard.ts +128 -0
  120. package/src/measure-element.ts +1 -1
  121. package/src/measure-text.ts +2 -2
  122. package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
  123. package/src/parse-keypress.ts +4 -5
  124. package/src/parse-stack-line.ts +0 -1
  125. package/src/patch-console.ts +44 -0
  126. package/src/reconciler.ts +36 -62
  127. package/src/render-background.ts +37 -16
  128. package/src/render-border.ts +97 -64
  129. package/src/render-frame.ts +83 -0
  130. package/src/render-to-string.ts +24 -9
  131. package/src/render.ts +24 -16
  132. package/src/router/components.tsx +343 -0
  133. package/src/router/context.ts +41 -0
  134. package/src/router/history.ts +194 -0
  135. package/src/router/hooks.tsx +391 -0
  136. package/src/router/index.ts +34 -0
  137. package/src/router/matcher.ts +571 -0
  138. package/src/sanitize-ansi.ts +1 -1
  139. package/src/screen/ansi.ts +184 -0
  140. package/src/screen/canvas.ts +137 -0
  141. package/src/screen/cell.ts +138 -0
  142. package/src/screen/color-profile.ts +47 -0
  143. package/src/screen/geometry.ts +9 -0
  144. package/src/screen/index.ts +6 -0
  145. package/src/screen/screen.ts +272 -0
  146. package/src/screen/serialize.ts +129 -0
  147. package/src/screen.ts +1 -0
  148. package/src/semantic-text-style.ts +118 -0
  149. package/src/signal-exit.ts +4 -5
  150. package/src/squash-text-nodes.ts +4 -7
  151. package/src/stream.ts +2 -4
  152. package/src/structured-text.ts +321 -0
  153. package/src/styles.ts +16 -17
  154. package/src/terminal/index.ts +2 -0
  155. package/src/terminal/inline-presenter.ts +120 -0
  156. package/src/terminal/input.ts +86 -0
  157. package/src/terminal/render-scheduler.ts +37 -0
  158. package/src/terminal/screen-presenter.ts +188 -0
  159. package/src/terminal/session.ts +407 -0
  160. package/src/terminal-size.ts +9 -8
  161. package/src/terminal.ts +1 -0
  162. package/src/testing/browser.ts +588 -0
  163. package/src/testing/emulators.ts +205 -0
  164. package/src/testing/explorer-app/index.html +12 -0
  165. package/src/testing/explorer-app/main.ts +381 -0
  166. package/src/testing/explorer-app/style.css +194 -0
  167. package/src/testing/explorer-app/tsconfig.json +15 -0
  168. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  169. package/src/testing/index.ts +26 -0
  170. package/src/testing/keys.ts +56 -0
  171. package/src/testing/live.ts +85 -0
  172. package/src/testing/matchers.ts +70 -0
  173. package/src/testing/public.ts +94 -0
  174. package/src/testing/terminal.ts +349 -0
  175. package/src/testing/vitest.ts +157 -0
  176. package/src/transform-adapter.ts +14 -0
  177. package/src/types.ts +1 -6
  178. package/src/utils.ts +2 -2
  179. package/src/wrap-text.ts +4 -4
  180. package/src/yoga/config.ts +2 -2
  181. package/src/yoga/core/absoluteLayout.ts +15 -15
  182. package/src/yoga/core/baseline.ts +3 -3
  183. package/src/yoga/core/cache.ts +4 -4
  184. package/src/yoga/core/calculateLayout.ts +33 -27
  185. package/src/yoga/core/config.ts +1 -1
  186. package/src/yoga/core/flexLine.ts +3 -3
  187. package/src/yoga/core/helpers.ts +3 -3
  188. package/src/yoga/core/layoutResults.ts +4 -4
  189. package/src/yoga/core/node.ts +16 -16
  190. package/src/yoga/core/pixelGrid.ts +4 -4
  191. package/src/yoga/core/style.ts +11 -11
  192. package/src/yoga/core/types.ts +2 -2
  193. package/src/yoga/index.ts +6 -6
  194. package/src/yoga/node.ts +8 -8
  195. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  196. package/dist/truncate-CBiyyZzw.js +0 -2156
  197. package/src/ansi/supports-color.ts +0 -213
  198. package/src/ansi/widest-line.ts +0 -12
  199. package/src/auto-bind.ts +0 -41
  200. package/src/colorize.ts +0 -60
  201. package/src/devtools-window-polyfill.ts +0 -73
  202. package/src/indent-string.ts +0 -16
  203. package/src/is-in-ci.ts +0 -7
  204. package/src/log-update.ts +0 -370
  205. package/src/output.ts +0 -308
  206. package/src/renderer.ts +0 -73
  207. package/src/write-synchronized.ts +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alchemy.run/sigil",
3
- "version": "0.0.0-alpha.3",
3
+ "version": "0.0.0-alpha.5",
4
4
  "description": "React for CLIs. A self-contained fork of Ink with an integrated TypeScript Yoga layout engine.",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,29 +33,48 @@
33
33
  ],
34
34
  "type": "module",
35
35
  "imports": {
36
- "#/": "./src"
36
+ "#/*": "./src/*"
37
37
  },
38
38
  "exports": {
39
39
  ".": {
40
- "typescript": "./src/index.ts",
41
- "bun": "./src/index.ts",
42
40
  "import": "./dist/index.js",
43
41
  "types": "./dist/index.d.ts",
44
42
  "default": "./dist/index.js"
45
43
  },
46
44
  "./ansi": {
47
- "typescript": "./src/ansi/index.ts",
48
- "bun": "./src/ansi/index.ts",
49
45
  "import": "./dist/ansi.js",
50
46
  "types": "./dist/ansi.d.ts",
51
47
  "default": "./dist/ansi.js"
52
48
  },
53
49
  "./yoga": {
54
- "typescript": "./src/yoga/index.ts",
55
- "bun": "./src/yoga/index.ts",
56
50
  "import": "./dist/yoga.js",
57
51
  "types": "./dist/yoga.d.ts",
58
52
  "default": "./dist/yoga.js"
53
+ },
54
+ "./router": {
55
+ "import": "./dist/router.js",
56
+ "types": "./dist/router.d.ts",
57
+ "default": "./dist/router.js"
58
+ },
59
+ "./capabilities": {
60
+ "import": "./dist/capabilities.js",
61
+ "types": "./dist/capabilities.d.ts",
62
+ "default": "./dist/capabilities.js"
63
+ },
64
+ "./color": {
65
+ "import": "./dist/color.js",
66
+ "types": "./dist/color.d.ts",
67
+ "default": "./dist/color.js"
68
+ },
69
+ "./screen": {
70
+ "import": "./dist/screen.js",
71
+ "types": "./dist/screen.d.ts",
72
+ "default": "./dist/screen.js"
73
+ },
74
+ "./terminal": {
75
+ "import": "./dist/terminal.js",
76
+ "types": "./dist/terminal.d.ts",
77
+ "default": "./dist/terminal.js"
59
78
  }
60
79
  },
61
80
  "publishConfig": {
@@ -66,26 +85,33 @@
66
85
  "scheduler": "^0.27.0"
67
86
  },
68
87
  "devDependencies": {
88
+ "@slopus/ghostty-wasm": "^1.3.6",
69
89
  "@types/node": "^25.5.2",
70
90
  "@types/react": "^19.2.18",
71
91
  "@types/react-reconciler": "^0.33.0",
72
92
  "@types/scheduler": "^0.26.0",
73
- "@types/ws": "^8.18.1",
93
+ "@vitest/ui": "^4.1.11",
94
+ "@xterm/addon-clipboard": "^0.2.0",
95
+ "@xterm/addon-fit": "^0.11.0",
96
+ "@xterm/addon-image": "^0.9.0",
97
+ "@xterm/addon-unicode11": "^0.9.0",
98
+ "@xterm/addon-web-links": "^0.12.0",
99
+ "@xterm/addon-webgl": "^0.19.0",
100
+ "@xterm/headless": "^6.0.0",
101
+ "@xterm/xterm": "^6.0.0",
74
102
  "react": "^19.2.8",
75
103
  "react-devtools-core": "^7.0.1",
76
- "react-router": "^8.3.0",
77
104
  "tsx": "^4.21.0",
78
105
  "typescript": "^7.0.2",
106
+ "vite": "npm:@voidzero-dev/vite-plus-core@^0.2.9",
79
107
  "vite-plus": "^0.2.9",
80
108
  "vitest": "4.1.11",
81
- "ws": "^8.20.0",
82
109
  "zigpty": "^0.2.1"
83
110
  },
84
111
  "peerDependencies": {
85
112
  "@types/react": ">=19.2.0",
86
113
  "react": ">=19.2.0",
87
- "react-devtools-core": ">=6.1.2",
88
- "ws": ">=8.20.0"
114
+ "react-devtools-core": ">=6.1.2"
89
115
  },
90
116
  "peerDependenciesMeta": {
91
117
  "@types/react": {
@@ -93,9 +119,6 @@
93
119
  },
94
120
  "react-devtools-core": {
95
121
  "optional": true
96
- },
97
- "ws": {
98
- "optional": true
99
122
  }
100
123
  },
101
124
  "engines": {
@@ -105,12 +128,15 @@
105
128
  "dev": "vp pack -w",
106
129
  "build": "vp pack",
107
130
  "test": "vp test",
131
+ "e2e": "vp test test/e2e.test.ts",
132
+ "explorer": "node --import=tsx scripts/explorer.ts",
108
133
  "lint": "vp lint",
109
134
  "format": "vp fmt .",
110
135
  "check": "vp check",
111
- "typecheck": "tsc --noEmit",
136
+ "typecheck": "tsc --noEmit && tsc --noEmit -p test/contracts/tsconfig.json",
112
137
  "inspect": "vpx react-devtools",
113
138
  "build:reference": "tsx scripts/build-reference.ts",
139
+ "bench:terminal": "tsx benchmark/terminal-core.tsx --check",
114
140
  "fuzz": "tsx test/yoga/fuzz/typescript.ts",
115
141
  "fuzz:differential": "vpr build:reference && tsx test/yoga/fuzz/differential.ts",
116
142
  "fuzz:loop": "vpr build:reference && tsx test/yoga/fuzz/loop.ts"
package/src/ansi/chalk.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { isatty } from "node:tty";
2
+
1
3
  // Terminal string styling with a chalk-compatible API for the styles Ink uses.
2
4
  // Ported from `chalk` (MIT, Sindre Sorhus) without the chaining builder —
3
5
  // Ink only ever applies one style per call.
4
- import { ESC } from "./escapes.ts";
6
+ import { ESC } from "#/ansi/escapes.ts";
5
7
  import {
6
8
  background,
7
9
  backgroundColorNames,
@@ -15,18 +17,18 @@ import {
15
17
  type BackgroundColorName,
16
18
  type ForegroundColorName,
17
19
  type ModifierName,
18
- } from "./sgr.ts";
20
+ } from "#/ansi/sgr.ts";
19
21
  import {
20
- supportsColor as supportsColorDetection,
22
+ createSupportsColor,
21
23
  type ColorInfo,
22
24
  type ColorSupportLevel,
23
- } from "./supports-color.ts";
25
+ } from "#/capabilities/detect.ts";
24
26
 
25
27
  export type { BackgroundColorName, ForegroundColorName, ModifierName };
26
28
 
27
29
  export type StyleFunction = (text: string) => string;
28
30
 
29
- const { stdout: stdoutColor } = supportsColorDetection;
31
+ const stdoutColor = createSupportsColor({ isTTY: isatty(1) });
30
32
 
31
33
  // `level` → color model for hex/rgb downsampling
32
34
  const levelMapping = ["ansi", "ansi", "ansi256", "ansi16m"] as const;
@@ -38,50 +40,6 @@ const state = {
38
40
 
39
41
  // Replace every occurrence of `substring` with `substring + replacer`,
40
42
  // re-opening a style after a nested close code would have ended it.
41
- const stringReplaceAll = (string: string, substring: string, replacer: string): string => {
42
- let index = string.indexOf(substring);
43
- if (index === -1) {
44
- return string;
45
- }
46
-
47
- const substringLength = substring.length;
48
- let endIndex = 0;
49
- let returnValue = "";
50
- do {
51
- returnValue += string.slice(endIndex, index) + substring + replacer;
52
- endIndex = index + substringLength;
53
- index = string.indexOf(substring, endIndex);
54
- } while (index !== -1);
55
-
56
- returnValue += string.slice(endIndex);
57
- return returnValue;
58
- };
59
-
60
- // Close the style before every line break and reopen it after, to prevent
61
- // background bleed across lines (https://github.com/chalk/chalk/pull/92).
62
- const stringEncaseCRLFWithFirstIndex = (
63
- string: string,
64
- prefix: string,
65
- postfix: string,
66
- index: number,
67
- ): string => {
68
- let endIndex = 0;
69
- let returnValue = "";
70
- do {
71
- const gotCR = string[index - 1] === "\r";
72
- returnValue +=
73
- string.slice(endIndex, gotCR ? index - 1 : index) +
74
- prefix +
75
- (gotCR ? "\r\n" : "\n") +
76
- postfix;
77
- endIndex = index + 1;
78
- index = string.indexOf("\n", endIndex);
79
- } while (index !== -1);
80
-
81
- returnValue += string.slice(endIndex);
82
- return returnValue;
83
- };
84
-
85
43
  const applyStyle = (open: string, close: string, text: string): string => {
86
44
  if (state.level <= 0 || !text) {
87
45
  return text;
@@ -92,12 +50,13 @@ const applyStyle = (open: string, close: string, text: string): string => {
92
50
  if (string.includes(ESC)) {
93
51
  // Re-open the style wherever the text already contains its close code,
94
52
  // otherwise only the part before that code would stay styled.
95
- string = stringReplaceAll(string, close, open);
53
+ string = string.replaceAll(close, close + open);
96
54
  }
97
55
 
98
- const lfIndex = string.indexOf("\n");
99
- if (lfIndex !== -1) {
100
- string = stringEncaseCRLFWithFirstIndex(string, close, open, lfIndex);
56
+ if (string.includes("\n")) {
57
+ // Close the style before every line break and reopen it after, to prevent
58
+ // background bleed across lines (https://github.com/chalk/chalk/pull/92).
59
+ string = string.replaceAll(/\r?\n/g, `${close}$&${open}`);
101
60
  }
102
61
 
103
62
  return open + string + close;
@@ -1,10 +1,8 @@
1
+ import { cursorHide, cursorShow } from "#/ansi/escapes.ts";
1
2
  // Derived from `cli-cursor` + `restore-cursor` (MIT, Sindre Sorhus),
2
3
  // collapsed into one module. Restores the cursor on process exit once
3
4
  // `hide()` has been called, like `restore-cursor` did.
4
- import process from "node:process";
5
-
6
- import { signalExit } from "../signal-exit.ts";
7
- import { cursorHide, cursorShow } from "./escapes.ts";
5
+ import { signalExit } from "#/signal-exit.ts";
8
6
 
9
7
  type CursorStream = {
10
8
  isTTY?: boolean;
@@ -206,6 +206,50 @@ export function eastAsianWidth(
206
206
  return 1;
207
207
  }
208
208
 
209
+ /**
210
+ Grapheme-cluster width test shared by the tokenizer (slice/clip path): the
211
+ base code point's East Asian Width, plus emoji presentation rules.
212
+ */
213
+ export function isFullwidthGrapheme(grapheme: string, baseCodePoint: number): boolean {
214
+ if (isFullwidthCodePoint(baseCodePoint)) return true;
215
+ // Variation Selector 16 forces emoji presentation (2 columns wide)
216
+ if (grapheme.includes("\uFE0F")) return true;
217
+ // Regional indicator pairs form flag emoji (2 columns wide)
218
+ if (baseCodePoint >= 0x1f1e6 && baseCodePoint <= 0x1f1ff) return true;
219
+ return false;
220
+ }
221
+
222
+ /**
223
+ Wide code points not covered by string-width's CJKT script regex or emoji
224
+ regex — the fallback bucket of its block parser. A disjoint decomposition of
225
+ the `wide` table above, kept alongside it so the width data lives in one
226
+ module.
227
+ */
228
+ export const isWideNotCJKTNotEmoji = (x: number): boolean => {
229
+ return (
230
+ x === 0x231b ||
231
+ x === 0x2329 ||
232
+ (x >= 0x2ff0 && x <= 0x2fff) ||
233
+ (x >= 0x3001 && x <= 0x303e) ||
234
+ (x >= 0x3099 && x <= 0x30ff) ||
235
+ (x >= 0x3105 && x <= 0x312f) ||
236
+ (x >= 0x3131 && x <= 0x318e) ||
237
+ (x >= 0x3190 && x <= 0x31e3) ||
238
+ (x >= 0x31ef && x <= 0x321e) ||
239
+ (x >= 0x3220 && x <= 0x3247) ||
240
+ (x >= 0x3250 && x <= 0x4dbf) ||
241
+ (x >= 0xfe10 && x <= 0xfe19) ||
242
+ (x >= 0xfe30 && x <= 0xfe52) ||
243
+ (x >= 0xfe54 && x <= 0xfe66) ||
244
+ (x >= 0xfe68 && x <= 0xfe6b) ||
245
+ (x >= 0x1f200 && x <= 0x1f202) ||
246
+ (x >= 0x1f210 && x <= 0x1f23b) ||
247
+ (x >= 0x1f240 && x <= 0x1f248) ||
248
+ (x >= 0x20000 && x <= 0x2fffd) ||
249
+ (x >= 0x30000 && x <= 0x3fffd)
250
+ );
251
+ };
252
+
209
253
  export function isFullwidthCodePoint(codePoint: number): boolean {
210
254
  if (!Number.isInteger(codePoint)) {
211
255
  return false;
@@ -1,7 +1,4 @@
1
- import os from "node:os";
2
1
  // Derived from `ansi-escapes` (MIT, Sindre Sorhus), reduced to the escapes Ink uses.
3
- import process from "node:process";
4
-
5
2
  // Control characters and sequence introducers. This module is the single
6
3
  // home for escape sequences: everything else imports these instead of
7
4
  // spelling out `\u001B[...` inline.
@@ -51,35 +48,27 @@ export const eraseLines = (count: number): string => {
51
48
  return clear;
52
49
  };
53
50
 
54
- // Windows 10 builds before 10586 don't support the `3J` escape.
55
- const isOldWindows = (): boolean => {
56
- if (process.platform !== "win32") {
57
- return false;
58
- }
59
-
60
- const parts = os.release().split(".");
61
- const major = Number(parts[0]);
62
- const build = Number(parts[2] ?? 0);
63
-
64
- if (major < 10) {
65
- return true;
66
- }
67
-
68
- return major === 10 && build < 10_586;
69
- };
70
-
71
- export const clearTerminal = isOldWindows()
72
- ? `${eraseScreen}${CSI}0f`
73
- : // 1. Erases the screen (only done in case `2` is not supported)
74
- // 2. Erases the whole screen including scrollback buffer
75
- // 3. Moves cursor to the top-left position
76
- `${eraseScreen}${CSI}3J${CSI}H`;
51
+ // 1. Erases the screen (only done in case `2` is not supported)
52
+ // 2. Erases the whole screen including scrollback buffer
53
+ // 3. Moves cursor to the top-left position
54
+ export const clearTerminal = `${eraseScreen}${CSI}3J${CSI}H`;
77
55
 
78
56
  export const enterAlternativeScreen = CSI + "?1049h";
79
57
  export const exitAlternativeScreen = CSI + "?1049l";
80
58
 
81
59
  export const cursorShow = CSI + "?25h";
82
60
  export const cursorHide = CSI + "?25l";
61
+ export type CursorShape = "block" | "underline" | "bar";
62
+ export const cursorShape = (shape: CursorShape, blinking = true): string => {
63
+ const value = {
64
+ block: blinking ? 1 : 2,
65
+ underline: blinking ? 3 : 4,
66
+ bar: blinking ? 5 : 6,
67
+ }[shape];
68
+ return `${CSI}${value} q`;
69
+ };
70
+ export const cursorColor = (color: string): string => `${OSC}12;${color}${BEL}`;
71
+ export const resetCursorColor = `${OSC}112${BEL}`;
83
72
 
84
73
  export const enableBracketedPaste = CSI + "?2004h";
85
74
  export const disableBracketedPaste = CSI + "?2004l";
@@ -115,6 +104,9 @@ export const ansiEscapes = {
115
104
  exitAlternativeScreen,
116
105
  cursorShow,
117
106
  cursorHide,
107
+ cursorShape,
108
+ cursorColor,
109
+ resetCursorColor,
118
110
  enableBracketedPaste,
119
111
  disableBracketedPaste,
120
112
  pasteStart,
@@ -0,0 +1,8 @@
1
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
2
+
3
+ /** The canonical grapheme segmentation service for measurement and rasterization. */
4
+ export function* graphemes(text: string): Generator<string> {
5
+ for (const { segment } of segmenter.segment(text)) {
6
+ yield segment;
7
+ }
8
+ }
@@ -0,0 +1,44 @@
1
+ // Standalone OSC 8 hyperlink builder with fallback — the string counterpart
2
+ // to the <Hyperlink> component, for use outside the React renderer.
3
+ import { link } from "#/ansi/escapes.ts";
4
+ import { detectHyperlinkSupport } from "#/capabilities/detect.ts";
5
+
6
+ type HyperlinkOptions = {
7
+ /**
8
+ When the terminal does not support OSC 8 hyperlinks, append the URL in
9
+ parentheses after the text so it stays reachable. Set to `false` to
10
+ return the text alone.
11
+
12
+ @default true
13
+ */
14
+ fallback?: boolean;
15
+
16
+ /**
17
+ The stream the text will be written to, for support detection.
18
+
19
+ @default process.stdout
20
+ */
21
+ stream?: { isTTY?: boolean };
22
+ };
23
+
24
+ /**
25
+ Wraps text in a clickable OSC 8 hyperlink when the terminal supports it,
26
+ falling back to `text (url)`.
27
+
28
+ ```ts
29
+ import { hyperlink } from "@alchemy.run/sigil/ansi";
30
+
31
+ console.log(hyperlink("Documentation", "https://example.com"));
32
+ ```
33
+ */
34
+ export const hyperlink = (
35
+ text: string,
36
+ url: string,
37
+ { fallback = true, stream = process.stdout }: HyperlinkOptions = {},
38
+ ): string => {
39
+ if (detectHyperlinkSupport(stream)) {
40
+ return link(text, url);
41
+ }
42
+
43
+ return fallback ? `${text} (${url})` : text;
44
+ };
package/src/ansi/index.ts CHANGED
@@ -3,25 +3,25 @@
3
3
  // `@alchemy.run/sigil/ansi`.
4
4
 
5
5
  // Styling
6
- export { chalk } from "./chalk.ts";
7
- export { supportsColor } from "./supports-color.ts";
8
- export * from "./sgr.ts";
6
+ export { chalk } from "#/ansi/chalk.ts";
7
+ export { supportsColor } from "#/ansi/chalk.ts";
8
+ export { hyperlink } from "#/ansi/hyperlink.ts";
9
+ export * from "#/ansi/sgr.ts";
9
10
 
10
11
  // Escape sequences (raw building blocks + named sequences)
11
- export { ansiEscapes } from "./escapes.ts";
12
- export * from "./escapes.ts";
13
- export { cliCursor } from "./cursor.ts";
12
+ export * from "#/ansi/escapes.ts";
13
+ export * from "#/ansi/osc.ts";
14
+ export { cliCursor } from "#/ansi/cursor.ts";
14
15
 
15
16
  // Measurement
16
- export { stringWidth } from "./string-width.ts";
17
- export { widestLine } from "./widest-line.ts";
18
- export * from "./east-asian-width.ts";
17
+ export { stringWidth, widestLine } from "#/ansi/string-width.ts";
18
+ export * from "#/ansi/east-asian-width.ts";
19
19
 
20
20
  // String manipulation
21
- export { stripAnsi, ansiRegex } from "./strip.ts";
22
- export { sliceAnsi } from "./slice.ts";
23
- export { cliTruncate as truncateAnsi } from "./truncate.ts";
24
- export { wrapAnsi } from "./wrap.ts";
21
+ export { stripAnsi } from "#/ansi/strip.ts";
22
+ export { sliceAnsi } from "#/ansi/slice.ts";
23
+ export { cliTruncate as truncateAnsi } from "#/ansi/truncate.ts";
24
+ export { wrapAnsi } from "#/ansi/wrap.ts";
25
25
 
26
26
  // Tokenizer: style-aware parsing and minimal re-emission
27
- export * from "./tokenize.ts";
27
+ export * from "#/ansi/tokenize.ts";
@@ -0,0 +1,77 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ import { ESC, OSC, ST } from "#/ansi/escapes.ts";
4
+
5
+ export type ClipboardSelection = "clipboard" | "primary" | "selection";
6
+ export type TerminalProgressState = "inactive" | "normal" | "error" | "indeterminate" | "paused";
7
+
8
+ const clipboardCode: Record<ClipboardSelection, string> = {
9
+ clipboard: "c",
10
+ primary: "p",
11
+ selection: "s",
12
+ };
13
+
14
+ const progressCode: Record<TerminalProgressState, number> = {
15
+ inactive: 0,
16
+ normal: 1,
17
+ error: 2,
18
+ indeterminate: 3,
19
+ paused: 4,
20
+ };
21
+
22
+ const field = (value: string): string => value.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
23
+
24
+ const command = (code: number, payload: string): string => `${OSC}${code};${payload}${ST}`;
25
+
26
+ /** Change the terminal window title (OSC 2). */
27
+ export const setWindowTitle = (title: string): string => command(2, field(title));
28
+
29
+ /** Announce the current directory as a file URI (OSC 7). */
30
+ export const setWorkingDirectory = (directory: URL | string): string => {
31
+ const uri =
32
+ directory instanceof URL
33
+ ? directory
34
+ : directory.startsWith("file:")
35
+ ? new URL(directory)
36
+ : pathToFileURL(directory);
37
+ if (uri.protocol !== "file:") throw new Error("Terminal working directory must be a file URI");
38
+ return command(7, uri.href);
39
+ };
40
+
41
+ /** Show a desktop notification (OSC 9). */
42
+ export const notify = (title: string): string => {
43
+ const safe = field(title);
44
+ return command(9, /^\d+;/.test(safe) ? ` ${safe}` : safe);
45
+ };
46
+
47
+ /** Change the terminal pointer shape (OSC 22; terminal support varies). */
48
+ export const setPointerShape = (shape: string): string => command(22, field(shape));
49
+
50
+ /** Write UTF-8 text to a terminal clipboard using OSC 52. */
51
+ export const setClipboard = (text: string, selection: ClipboardSelection = "clipboard"): string =>
52
+ command(52, `${clipboardCode[selection]};${Buffer.from(text).toString("base64")}`);
53
+
54
+ /** Request clipboard contents. The response is another OSC 52 sequence. */
55
+ export const queryClipboard = (selection: ClipboardSelection = "clipboard"): string =>
56
+ command(52, `${clipboardCode[selection]};?`);
57
+
58
+ /** Clear a terminal clipboard. */
59
+ export const clearClipboard = (selection: ClipboardSelection = "clipboard"): string =>
60
+ command(52, `${clipboardCode[selection]};`);
61
+
62
+ /** Report terminal-native progress using OSC 9;4. */
63
+ export const setTerminalProgress = (state: TerminalProgressState, value?: number): string => {
64
+ const code = progressCode[state];
65
+ if (state === "inactive" || state === "indeterminate") {
66
+ return command(9, `4;${code}`);
67
+ }
68
+ if (value === undefined && (state === "error" || state === "paused")) {
69
+ return command(9, `4;${code}`);
70
+ }
71
+ const percentage = Math.max(0, Math.min(100, Math.round(value ?? 0)));
72
+ return command(9, `4;${code};${percentage}`);
73
+ };
74
+
75
+ /** Wrap a sequence for transport through tmux's DCS passthrough. */
76
+ export const tmuxPassthrough = (sequence: string): string =>
77
+ `${ESC}Ptmux;${sequence.replaceAll(ESC, `${ESC}${ESC}`)}${ST}`;
package/src/ansi/sgr.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // ANSI SGR style tables and color-space conversions.
2
2
  // Ported from `ansi-styles` (MIT, Sindre Sorhus).
3
- import { CSI } from "./escapes.ts";
3
+ import { CSI } from "#/ansi/escapes.ts";
4
4
 
5
5
  const ANSI_BACKGROUND_OFFSET = 10;
6
6
 
@@ -173,10 +173,7 @@ export const hexToRgb = (hex: string): [number, number, number] => {
173
173
  let [colorString] = matches;
174
174
 
175
175
  if (colorString.length === 3) {
176
- colorString = colorString
177
- .split("")
178
- .map((character) => character + character)
179
- .join("");
176
+ colorString = colorString.replaceAll(/./g, "$&$&");
180
177
  }
181
178
 
182
179
  const integer = Number.parseInt(colorString, 16);
package/src/ansi/slice.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  styledCharsToString,
9
9
  tokenize,
10
10
  type StyledChar,
11
- } from "./tokenize.ts";
11
+ } from "#/ansi/tokenize.ts";
12
12
 
13
13
  export function sliceAnsi(string: string, start: number, end?: number): string {
14
14
  const sliceEnd = end ?? Number.POSITIVE_INFINITY;