silvery 0.19.2 → 0.21.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 (218) hide show
  1. package/README.md +9 -4
  2. package/dist/Text-Lq0dmj8-.mjs +239 -0
  3. package/dist/Text-Lq0dmj8-.mjs.map +1 -0
  4. package/dist/UPNG-Bo33r8rA.mjs +3 -0
  5. package/dist/UPNG-DosRPdF4.mjs +5075 -0
  6. package/dist/UPNG-DosRPdF4.mjs.map +1 -0
  7. package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs +6 -0
  8. package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs.map +1 -0
  9. package/dist/{animation-Cn64yepo.mjs → animation-ZMN2_XKv.mjs} +2 -2
  10. package/dist/animation-ZMN2_XKv.mjs.map +1 -0
  11. package/dist/{ansi-Cc33mW54.d.mts → ansi-2Xn0yatP.d.mts} +1 -1
  12. package/dist/{ansi-Cc33mW54.d.mts.map → ansi-2Xn0yatP.d.mts.map} +1 -1
  13. package/dist/{ansi-CLOitHKx.mjs → ansi-D1KQMAbf.mjs} +1 -1
  14. package/dist/{ansi-CLOitHKx.mjs.map → ansi-D1KQMAbf.mjs.map} +1 -1
  15. package/dist/ansi-yC4RyBNY.mjs +22441 -0
  16. package/dist/ansi-yC4RyBNY.mjs.map +1 -0
  17. package/dist/apng-CR08rIaH.mjs +58 -0
  18. package/dist/apng-CR08rIaH.mjs.map +1 -0
  19. package/dist/apng-DaHfVaVI.mjs +3 -0
  20. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  21. package/dist/assets/skia.darwin-arm64-DQs5sT6N.node +0 -0
  22. package/dist/backend-B-WYLUib.mjs +13396 -0
  23. package/dist/backend-B-WYLUib.mjs.map +1 -0
  24. package/dist/backends-CUtan80W.mjs +3 -0
  25. package/dist/backends-DIVYzKqd.mjs +1083 -0
  26. package/dist/backends-DIVYzKqd.mjs.map +1 -0
  27. package/dist/bound-term-0sPrrzH1.d.mts +4640 -0
  28. package/dist/bound-term-0sPrrzH1.d.mts.map +1 -0
  29. package/dist/canvas-1v7dPT-_.mjs +3 -0
  30. package/dist/canvas-CSuPOMNt.mjs +1442 -0
  31. package/dist/canvas-CSuPOMNt.mjs.map +1 -0
  32. package/dist/{chunk-Vs_PY4HZ.mjs → chunk-BSw8zbkd.mjs} +1 -1
  33. package/dist/cli-dvo0r2fs.mjs +4 -0
  34. package/dist/compare-CQodSH4G.mjs +376 -0
  35. package/dist/compare-CQodSH4G.mjs.map +1 -0
  36. package/dist/compare-DHlcxEYA.mjs +3 -0
  37. package/dist/context-BU5LkkIy.mjs.map +1 -1
  38. package/dist/devtools-CJdt5H0X.mjs +2 -0
  39. package/dist/{devtools-DxkSLXDA.mjs → devtools-DcQjgyjL.mjs} +5 -4
  40. package/dist/{devtools-DxkSLXDA.mjs.map → devtools-DcQjgyjL.mjs.map} +1 -1
  41. package/dist/easing-BI-ASGMO.d.mts +24 -0
  42. package/dist/easing-BI-ASGMO.d.mts.map +1 -0
  43. package/dist/{eta-Bb3RH3wh.mjs → eta-CJlGH06n.mjs} +1 -1
  44. package/dist/{eta-Bb3RH3wh.mjs.map → eta-CJlGH06n.mjs.map} +1 -1
  45. package/dist/flexily-zero-adapter-C3Vj0fPt.mjs +306 -0
  46. package/dist/flexily-zero-adapter-C3Vj0fPt.mjs.map +1 -0
  47. package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-C4lW_Ov5.mjs} +1 -1
  48. package/dist/fonts-BFmhXDv7.mjs +88 -0
  49. package/dist/fonts-BFmhXDv7.mjs.map +1 -0
  50. package/dist/gif-C_AjaT9d.mjs +188 -0
  51. package/dist/gif-C_AjaT9d.mjs.map +1 -0
  52. package/dist/gif-DaC4XrxA.mjs +3 -0
  53. package/dist/gifenc-BOUT-KFB.mjs +730 -0
  54. package/dist/gifenc-BOUT-KFB.mjs.map +1 -0
  55. package/dist/image-C2Birh2x.mjs +1252 -0
  56. package/dist/image-C2Birh2x.mjs.map +1 -0
  57. package/dist/index-BUMxS65f.d.mts +453 -0
  58. package/dist/index-BUMxS65f.d.mts.map +1 -0
  59. package/dist/{index-D3saHouR.d.mts → index-CSQf13CI.d.mts} +1057 -1133
  60. package/dist/index-CSQf13CI.d.mts.map +1 -0
  61. package/dist/{index-BXslOebb.d.mts → index-Cl9KKjQ_.d.mts} +4919 -3921
  62. package/dist/index-Cl9KKjQ_.d.mts.map +1 -0
  63. package/dist/index-XbNrPhWl.d.mts +336 -0
  64. package/dist/index-XbNrPhWl.d.mts.map +1 -0
  65. package/dist/index.d.mts +8 -5
  66. package/dist/index.d.mts.map +1 -1
  67. package/dist/index.mjs +14 -12
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/key-mapping-CS-YD_cD.mjs +132 -0
  70. package/dist/key-mapping-CS-YD_cD.mjs.map +1 -0
  71. package/dist/key-mapping-Yn-Jgrij.mjs +3 -0
  72. package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-C07LEXWT.mjs} +1 -1
  73. package/dist/layout-engine-C2px0RJE.mjs +67 -0
  74. package/dist/layout-engine-C2px0RJE.mjs.map +1 -0
  75. package/dist/layout-signals-Cnw6xk8Q.mjs +988 -0
  76. package/dist/layout-signals-Cnw6xk8Q.mjs.map +1 -0
  77. package/dist/mouse-events-Dki3ISIp.mjs +1044 -0
  78. package/dist/mouse-events-Dki3ISIp.mjs.map +1 -0
  79. package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CIRjrzma.mjs} +3 -3
  80. package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CIRjrzma.mjs.map} +1 -1
  81. package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-DHZ2xUT2.d.mts} +2 -2
  82. package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-DHZ2xUT2.d.mts.map} +1 -1
  83. package/dist/{node-BeWlnCPY.mjs → node-CjM5Rt-M.mjs} +4 -4
  84. package/dist/node-CjM5Rt-M.mjs.map +1 -0
  85. package/dist/playwright-D5YiZcNS.mjs +76397 -0
  86. package/dist/playwright-D5YiZcNS.mjs.map +1 -0
  87. package/dist/png-codec-Dp84742B.mjs +36 -0
  88. package/dist/png-codec-Dp84742B.mjs.map +1 -0
  89. package/dist/png-codec-QwOtJ8Zs.mjs +3 -0
  90. package/dist/progress-DB_Xo071.mjs +675 -0
  91. package/dist/progress-DB_Xo071.mjs.map +1 -0
  92. package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-oJwq22CR.mjs} +4 -4
  93. package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-oJwq22CR.mjs.map} +1 -1
  94. package/dist/rasterizer-BRXrDdWx.mjs +3 -0
  95. package/dist/rasterizer-CpEhJvdR.mjs +296 -0
  96. package/dist/rasterizer-CpEhJvdR.mjs.map +1 -0
  97. package/dist/reconciler-DldIJB93.mjs +2083 -0
  98. package/dist/reconciler-DldIJB93.mjs.map +1 -0
  99. package/dist/{render-string-CDCeYkS3.mjs → render-string-BcoCpjCB.mjs} +1 -1
  100. package/dist/{render-string-Darrg7ku.mjs → render-string-DkQacASz.mjs} +2707 -549
  101. package/dist/render-string-DkQacASz.mjs.map +1 -0
  102. package/dist/resvg-js-DkOndZI3.mjs +203 -0
  103. package/dist/resvg-js-DkOndZI3.mjs.map +1 -0
  104. package/dist/runtime.d.mts +3 -2
  105. package/dist/runtime.mjs +3 -3
  106. package/dist/schemes-JjNp4aSl.mjs +2611 -0
  107. package/dist/schemes-JjNp4aSl.mjs.map +1 -0
  108. package/dist/{spinner-CGo34vyR.d.mts → spinner-CZINHpkV.d.mts} +2 -2
  109. package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CZINHpkV.d.mts.map} +1 -1
  110. package/dist/{spinner-CeOmcuw_.mjs → spinner-D9lrHr8s.mjs} +7 -7
  111. package/dist/spinner-D9lrHr8s.mjs.map +1 -0
  112. package/dist/src-5w9QR6_8.mjs +1071 -0
  113. package/dist/src-5w9QR6_8.mjs.map +1 -0
  114. package/dist/src-BNTToU7l.mjs +4387 -0
  115. package/dist/src-BNTToU7l.mjs.map +1 -0
  116. package/dist/{src-CF-6UN01.mjs → src-BR4xNwdG.mjs} +10436 -2622
  117. package/dist/src-BR4xNwdG.mjs.map +1 -0
  118. package/dist/{types-Bk2yw9Qj.mjs → src-DKp-_OFG.mjs} +34 -94
  119. package/dist/src-DKp-_OFG.mjs.map +1 -0
  120. package/dist/src-bt8wSrfJ.mjs +258 -0
  121. package/dist/src-bt8wSrfJ.mjs.map +1 -0
  122. package/dist/src-e33Y6kNJ.mjs +3 -0
  123. package/dist/src-iDwu25UD.mjs +1814 -0
  124. package/dist/src-iDwu25UD.mjs.map +1 -0
  125. package/dist/steps-Bp2uNqnn.d.mts +202 -0
  126. package/dist/steps-Bp2uNqnn.d.mts.map +1 -0
  127. package/dist/svg-15lZZzxq.mjs +486 -0
  128. package/dist/svg-15lZZzxq.mjs.map +1 -0
  129. package/dist/svg-Cz0UXcDj.mjs +255 -0
  130. package/dist/svg-Cz0UXcDj.mjs.map +1 -0
  131. package/dist/svg-DY72a4HK.mjs +3 -0
  132. package/dist/svg-g1D6ErwR.d.mts +82 -0
  133. package/dist/svg-g1D6ErwR.d.mts.map +1 -0
  134. package/dist/term.d.mts +3 -0
  135. package/dist/term.mjs +9 -0
  136. package/dist/term.mjs.map +1 -0
  137. package/dist/theme.d.mts +95 -2
  138. package/dist/theme.d.mts.map +1 -0
  139. package/dist/theme.mjs +9 -3
  140. package/dist/theme.mjs.map +1 -0
  141. package/dist/{types-BH_v3iMT.d.mts → types-kt_fKR37.d.mts} +2 -15
  142. package/dist/types-kt_fKR37.d.mts.map +1 -0
  143. package/dist/ui/animation.d.mts +2 -1
  144. package/dist/ui/animation.mjs +1 -1
  145. package/dist/ui/ansi.d.mts +1 -1
  146. package/dist/ui/ansi.mjs +1 -1
  147. package/dist/ui/cli.d.mts +3 -3
  148. package/dist/ui/cli.mjs +5 -5
  149. package/dist/ui/display.d.mts +1 -1
  150. package/dist/ui/image.d.mts +2 -2
  151. package/dist/ui/image.mjs +2 -2
  152. package/dist/ui/input.d.mts +1 -1
  153. package/dist/ui/input.mjs +4 -2
  154. package/dist/ui/input.mjs.map +1 -1
  155. package/dist/ui/progress.d.mts +5 -249
  156. package/dist/ui/progress.mjs +5 -858
  157. package/dist/ui/react.d.mts +1 -1
  158. package/dist/ui/react.mjs +2 -2
  159. package/dist/ui/recording-chrome-react.d.mts +21 -0
  160. package/dist/ui/recording-chrome-react.d.mts.map +1 -0
  161. package/dist/ui/recording-chrome-react.mjs +105 -0
  162. package/dist/ui/recording-chrome-react.mjs.map +1 -0
  163. package/dist/ui/recording-chrome.d.mts +2 -0
  164. package/dist/ui/recording-chrome.mjs +2 -0
  165. package/dist/ui/utils.mjs +1 -1
  166. package/dist/ui/wrappers.d.mts +3 -3
  167. package/dist/ui/wrappers.mjs +2 -2
  168. package/dist/ui.d.mts +7 -6
  169. package/dist/ui.mjs +8 -7
  170. package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DRDDVwjh.d.mts} +5 -25
  171. package/dist/useLatest-DRDDVwjh.d.mts.map +1 -0
  172. package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-YeohVLeo.d.mts} +4 -55
  173. package/dist/with-text-input-YeohVLeo.d.mts.map +1 -0
  174. package/dist/wrapper-C70ATkVv.mjs +3527 -0
  175. package/dist/wrapper-C70ATkVv.mjs.map +1 -0
  176. package/dist/{wrappers-UTADQkSY.mjs → wrappers-BCUYITrY.mjs} +5 -157
  177. package/dist/wrappers-BCUYITrY.mjs.map +1 -0
  178. package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-BnZX1PAY.mjs} +28 -2
  179. package/dist/yoga-adapter-BnZX1PAY.mjs.map +1 -0
  180. package/dist/yoga-adapter-DxgsQ_gg.mjs +2 -0
  181. package/dist/zipBundle-3nqeDRtm.mjs +3 -0
  182. package/dist/zipBundle-VNAYFmqJ.mjs +2003 -0
  183. package/dist/zipBundle-VNAYFmqJ.mjs.map +1 -0
  184. package/package.json +20 -9
  185. package/dist/animation-Cn64yepo.mjs.map +0 -1
  186. package/dist/cli-BKp0YtBD.mjs +0 -4
  187. package/dist/devtools-9QY4teqI.mjs +0 -2
  188. package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
  189. package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
  190. package/dist/image-CTII5QWI.mjs +0 -477
  191. package/dist/image-CTII5QWI.mjs.map +0 -1
  192. package/dist/index-BXslOebb.d.mts.map +0 -1
  193. package/dist/index-BnA7mNpo.d.mts +0 -175
  194. package/dist/index-BnA7mNpo.d.mts.map +0 -1
  195. package/dist/index-D3saHouR.d.mts.map +0 -1
  196. package/dist/layout-engine-ClUgv6jB.mjs +0 -50
  197. package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
  198. package/dist/node-BeWlnCPY.mjs.map +0 -1
  199. package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
  200. package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
  201. package/dist/render-string-Darrg7ku.mjs.map +0 -1
  202. package/dist/spinner-CeOmcuw_.mjs.map +0 -1
  203. package/dist/src-B5GjfG7g.mjs +0 -4305
  204. package/dist/src-B5GjfG7g.mjs.map +0 -1
  205. package/dist/src-CChwjk0Z.mjs +0 -738
  206. package/dist/src-CChwjk0Z.mjs.map +0 -1
  207. package/dist/src-CF-6UN01.mjs.map +0 -1
  208. package/dist/src-NCKb8kE5.mjs +0 -2660
  209. package/dist/src-NCKb8kE5.mjs.map +0 -1
  210. package/dist/types-BH_v3iMT.d.mts.map +0 -1
  211. package/dist/types-Bk2yw9Qj.mjs.map +0 -1
  212. package/dist/ui/progress.d.mts.map +0 -1
  213. package/dist/ui/progress.mjs.map +0 -1
  214. package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
  215. package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
  216. package/dist/wrappers-UTADQkSY.mjs.map +0 -1
  217. package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
  218. package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
@@ -0,0 +1,486 @@
1
+ import { n as __esmMin } from "./chunk-BSw8zbkd.mjs";
2
+ import { a as BUNDLED_SYMBOL_FAMILY, c as init_fonts, i as BUNDLED_PRIMARY_FAMILY, n as BUNDLED_FONTS, r as BUNDLED_NERD_FAMILY, s as bundledFontsDir, t as BUNDLED_EMOJI_FAMILY } from "./fonts-BFmhXDv7.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ //#region ../termless/src/render/emoji.ts
7
+ /**
8
+ * Color emoji rendering for the resvg path.
9
+ *
10
+ * `@resvg/resvg-js` (the SVG→PNG rasterizer behind {@link screenshotPng},
11
+ * `view/gif.ts`, `view/apng.ts`) inherits from upstream `resvg` and as of
12
+ * 2.6.2 supports NO color font format — not CBDT/sbix (Apple Color Emoji),
13
+ * not COLR/CPAL (Twemoji Mozilla), not OT-SVG (TwitterColorEmoji-SVGinOT,
14
+ * Noto Color Emoji). Upstream issue resvg#487 has been open since Nov 2021.
15
+ *
16
+ * The bundled `NotoEmoji-Regular.ttf` (monochrome) is what resvg can actually
17
+ * render — a thin outlined glyph that drops the user-visible color. To match
18
+ * what users see in their real terminal we side-step the font path entirely:
19
+ *
20
+ * - {@link toTwemojiKey} converts an emoji string (one or more codepoints)
21
+ * to the canonical Twemoji asset key (e.g. "📁" → "1f4c1", "👨‍💻" →
22
+ * "1f468-200d-1f4bb"). The Twemoji rule strips U+FE0F unless a U+200D
23
+ * ZWJ is also present in the sequence.
24
+ * - {@link loadTwemojiSvg} reads the SVG asset for a key from the optional
25
+ * `@twemoji/svg` peer dependency (~17 MB of 3700 SVGs, MIT for code,
26
+ * CC-BY for graphics). Returns a base64-encoded `data:image/svg+xml;base64`
27
+ * URI suitable for the `href` of an SVG `<image>` element — resvg renders
28
+ * nested SVG via `<image>` correctly even when it cannot render the same
29
+ * emoji from a font.
30
+ * - {@link isLikelyEmoji} is a permissive codepoint test; the caller must
31
+ * still try {@link loadTwemojiSvg} and fall back to font rendering if the
32
+ * asset is missing (asset absence is a soft signal, not a hard error).
33
+ *
34
+ * The canvas path in `@termless/ghostty` does not need any of this — Skia
35
+ * (`@napi-rs/canvas`) renders Apple Color Emoji natively on macOS.
36
+ */
37
+ /**
38
+ * Convert an emoji codepoint sequence to the Twemoji asset filename stem.
39
+ *
40
+ * Twemoji rule: lowercase hex codepoints joined by `-`, with U+FE0F (the
41
+ * emoji variation selector) stripped UNLESS the sequence contains a U+200D
42
+ * (zero-width joiner) — joined sequences keep their variation selectors
43
+ * because the joined glyph requires them.
44
+ *
45
+ * Examples:
46
+ * "📁" → "1f4c1"
47
+ * "❤️" → "2764" (FE0F stripped)
48
+ * "☎️" → "260e" (FE0F stripped)
49
+ * "👨‍💻" → "1f468-200d-1f4bb" (ZWJ present; FE0F kept if any)
50
+ */
51
+ function toTwemojiKey(text) {
52
+ const cps = [];
53
+ for (const ch of text) cps.push(ch.codePointAt(0));
54
+ return (cps.includes(8205) ? cps : cps.filter((cp) => cp !== 65039)).map((cp) => cp.toString(16)).join("-");
55
+ }
56
+ /**
57
+ * Permissive emoji-codepoint test. Returns true for codepoints that LIKELY
58
+ * have a Twemoji asset; the caller must still attempt to load the asset and
59
+ * fall back to font rendering on miss.
60
+ *
61
+ * Covers the major emoji ranges in BMP and supplementary planes:
62
+ * - U+1F300 .. U+1FBFF — Misc Symbols & Pictographs, Emoticons, Transport,
63
+ * Supp Symbols & Pictographs, Symbols & Pictographs
64
+ * Extended-A, plus the legacy/extended blocks.
65
+ * - U+2300 .. U+27BF — Misc Technical, Dingbats, Misc Symbols
66
+ * - U+2600 .. U+26FF — Misc Symbols (☎ ☕ ☀ ★ etc.)
67
+ * - U+2700 .. U+27BF — Dingbats (✂ ✈ ❤ etc.)
68
+ * - U+1F000 .. U+1F2FF — Mahjong, Domino, Playing cards, Enclosed Alphanum
69
+ *
70
+ * Single-codepoint test is enough: a multi-codepoint sequence (ZWJ joined,
71
+ * skin tone modifiers, flags) always has its first codepoint in one of the
72
+ * emoji-bearing ranges.
73
+ */
74
+ function isLikelyEmoji(text) {
75
+ const cp = text.codePointAt(0);
76
+ if (cp === void 0) return false;
77
+ if (cp >= 126976 && cp <= 130047) return true;
78
+ if (cp >= 8960 && cp <= 10175) return true;
79
+ return false;
80
+ }
81
+ /**
82
+ * Load the Twemoji SVG asset for a key as a `data:image/svg+xml;base64` URI,
83
+ * or return null if the asset is missing (peer dep not installed, or the
84
+ * codepoint is not in the Twemoji catalogue). The URI is suitable for the
85
+ * `href` attribute of an SVG `<image>` element and embeds the entire SVG
86
+ * inline so the parent SVG is self-contained.
87
+ *
88
+ * Resolution is via Node-style `require.resolve("@twemoji/svg/<key>.svg")`
89
+ * — works under both Bun and Node and respects the consumer's `node_modules`
90
+ * layout. Missing assets are cached as `null` so repeated misses are cheap.
91
+ */
92
+ function loadTwemojiSvg(key) {
93
+ const cached = dataUriCache.get(key);
94
+ if (cached !== void 0) return cached;
95
+ try {
96
+ const svg = readFileSync(require.resolve(`@twemoji/svg/${key}.svg`), "utf8");
97
+ const uri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
98
+ dataUriCache.set(key, uri);
99
+ return uri;
100
+ } catch {
101
+ dataUriCache.set(key, null);
102
+ return null;
103
+ }
104
+ }
105
+ var require, dataUriCache;
106
+ var init_emoji = __esmMin((() => {
107
+ require = createRequire(import.meta.url);
108
+ dataUriCache = /* @__PURE__ */ new Map();
109
+ }));
110
+ //#endregion
111
+ //#region ../termless/src/render/svg.ts
112
+ /**
113
+ * A `<defs><style>` block embedding the bundled fonts as base64 `@font-face`
114
+ * rules. Makes an SVG self-contained — it renders identically in any
115
+ * rasterizer or browser, with no host-font dependency. Cached (the fonts are
116
+ * ~1.8 MB; build the data URIs once per process).
117
+ */
118
+ function embeddedFontFaceDefs() {
119
+ if (cachedFontDefs !== null) return cachedFontDefs;
120
+ const dir = bundledFontsDir();
121
+ const faces = [];
122
+ for (const { file, family } of BUNDLED_FONTS) try {
123
+ const b64 = readFileSync(join(dir, file)).toString("base64");
124
+ faces.push(`@font-face{font-family:'${family}';src:url(data:font/ttf;base64,${b64});}`);
125
+ } catch {}
126
+ cachedFontDefs = faces.length > 0 ? `<defs><style>${faces.join("")}</style></defs>` : "";
127
+ return cachedFontDefs;
128
+ }
129
+ function rgbToHex(color) {
130
+ return `#${color.r.toString(16).padStart(2, "0")}${color.g.toString(16).padStart(2, "0")}${color.b.toString(16).padStart(2, "0")}`;
131
+ }
132
+ function rgbToString(color, fallback) {
133
+ return color ? rgbToHex(color) : fallback;
134
+ }
135
+ /**
136
+ * Format a coordinate for SVG output: drop floating-point noise so cell
137
+ * positions like `28.799999999999997` emit as `28.8`. Trailing zeros are
138
+ * trimmed so integer coordinates stay integers.
139
+ */
140
+ function coord(value) {
141
+ return String(Math.round(value * 1e3) / 1e3);
142
+ }
143
+ function escapeXml(text) {
144
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
145
+ }
146
+ function cellFgBg(cell, themeFg, themeBg) {
147
+ let fg = rgbToString(cell.fg, themeFg);
148
+ let bg = rgbToString(cell.bg, themeBg);
149
+ if (cell.inverse) [fg, bg] = [bg, fg];
150
+ return {
151
+ fg,
152
+ bg
153
+ };
154
+ }
155
+ function spansMatch(a, fg, bold, italic, dim, underline, strikethrough) {
156
+ return a.fill === fg && a.bold === bold && a.italic === italic && a.dim === dim && a.underline === underline && a.strikethrough === strikethrough;
157
+ }
158
+ function buildBgRects(lines, cellWidth, cellHeight, themeFg, themeBg) {
159
+ const rects = [];
160
+ for (let row = 0; row < lines.length; row++) {
161
+ const cells = lines[row];
162
+ let runStart = -1;
163
+ let runColor = "";
164
+ for (let col = 0; col < cells.length; col++) {
165
+ const cell = cells[col];
166
+ const { bg } = cellFgBg(cell, themeFg, themeBg);
167
+ if (bg !== themeBg) if (runStart >= 0 && runColor === bg) {} else {
168
+ if (runStart >= 0) rects.push({
169
+ x: runStart * cellWidth,
170
+ y: row * cellHeight,
171
+ width: (col - runStart) * cellWidth,
172
+ height: cellHeight,
173
+ fill: runColor
174
+ });
175
+ runStart = col;
176
+ runColor = bg;
177
+ }
178
+ else if (runStart >= 0) {
179
+ rects.push({
180
+ x: runStart * cellWidth,
181
+ y: row * cellHeight,
182
+ width: (col - runStart) * cellWidth,
183
+ height: cellHeight,
184
+ fill: runColor
185
+ });
186
+ runStart = -1;
187
+ runColor = "";
188
+ }
189
+ }
190
+ if (runStart >= 0) rects.push({
191
+ x: runStart * cellWidth,
192
+ y: row * cellHeight,
193
+ width: (cells.length - runStart) * cellWidth,
194
+ height: cellHeight,
195
+ fill: runColor
196
+ });
197
+ }
198
+ return rects;
199
+ }
200
+ function buildTextSpans(cells, themeFg, themeBg) {
201
+ const spans = [];
202
+ for (let col = 0; col < cells.length; col++) {
203
+ const cell = cells[col];
204
+ if (cell.char === "" && col > 0 && cells[col - 1]?.wide) continue;
205
+ const { fg } = cellFgBg(cell, themeFg, themeBg);
206
+ const char = cell.char || " ";
207
+ const underline = cell.underline !== false;
208
+ const cellsForChar = cell.wide ? 2 : 1;
209
+ const current = spans.length > 0 ? spans[spans.length - 1] : null;
210
+ if (current && !cell.wide && !current.isWide && spansMatch(current, fg, cell.bold, cell.italic, cell.dim, underline, cell.strikethrough)) {
211
+ current.text += char;
212
+ current.cellCount += cellsForChar;
213
+ } else spans.push({
214
+ text: char,
215
+ fill: fg,
216
+ bold: cell.bold,
217
+ italic: cell.italic,
218
+ dim: cell.dim,
219
+ underline,
220
+ strikethrough: cell.strikethrough,
221
+ startCol: col,
222
+ cellCount: cellsForChar,
223
+ isWide: cell.wide === true
224
+ });
225
+ }
226
+ return spans;
227
+ }
228
+ function resolveOptions(options) {
229
+ return {
230
+ fontFamily: options?.fontFamily ?? BUNDLED_FONT_FAMILY,
231
+ fontSize: options?.fontSize ?? DEFAULT_FONT_SIZE,
232
+ cellWidth: options?.cellWidth ?? DEFAULT_CELL_WIDTH,
233
+ cellHeight: options?.cellHeight ?? DEFAULT_CELL_HEIGHT,
234
+ themeFg: options?.theme?.foreground ?? DEFAULT_THEME.foreground,
235
+ themeBg: options?.theme?.background ?? DEFAULT_THEME.background,
236
+ themeCursor: options?.theme?.cursor ?? DEFAULT_THEME.cursor,
237
+ padding: options?.padding ?? 0,
238
+ borderRadius: options?.borderRadius ?? 0,
239
+ windowBar: options?.windowBar ?? "none",
240
+ windowBarSize: options?.windowBarSize ?? 40,
241
+ windowTitle: options?.windowTitle ?? null,
242
+ shadow: options?.shadow ?? 0,
243
+ margin: options?.margin ?? 0,
244
+ marginFill: options?.marginFill ?? null,
245
+ embedFonts: options?.embedFonts ?? false
246
+ };
247
+ }
248
+ function spanToTspan(span, cellWidth, themeFg) {
249
+ const charCount = [...span.text].length;
250
+ let xValue;
251
+ if (charCount > 1) {
252
+ const xs = [];
253
+ for (let i = 0; i < charCount; i++) xs.push(coord((span.startCol + i) * cellWidth));
254
+ xValue = xs.join(" ");
255
+ } else xValue = coord(span.startCol * cellWidth);
256
+ const attrs = [`x="${xValue}"`];
257
+ if (span.fill !== themeFg) attrs.push(`fill="${span.fill}"`);
258
+ if (span.bold) attrs.push(`font-weight="bold"`);
259
+ if (span.italic) attrs.push(`font-style="italic"`);
260
+ if (span.dim) attrs.push(`opacity="0.5"`);
261
+ const decorations = [];
262
+ if (span.underline) decorations.push("underline");
263
+ if (span.strikethrough) decorations.push("line-through");
264
+ if (decorations.length > 0) attrs.push(`text-decoration="${decorations.join(" ")}"`);
265
+ return `<tspan ${attrs.join(" ")}>${escapeXml(span.text)}</tspan>`;
266
+ }
267
+ /**
268
+ * Resolve a wide-character span to a bundled Twemoji color SVG. Returns the
269
+ * data URI when the codepoint sequence has a Twemoji asset and the optional
270
+ * `@twemoji/svg` peer dependency is installed; null otherwise (in which case
271
+ * the span falls back to font rendering via {@link spanToTspan}, which still
272
+ * picks up the bundled monochrome Noto Emoji face).
273
+ *
274
+ * The text-span builder isolates every wide character in its own single-char
275
+ * span (see {@link buildTextSpans}), so a multi-char emoji ZWJ sequence will
276
+ * still arrive here as one span — there is no per-glyph splitting needed.
277
+ */
278
+ function resolveEmojiSpan(span) {
279
+ if (!span.isWide) return null;
280
+ if (!isLikelyEmoji(span.text)) return null;
281
+ return loadTwemojiSvg(toTwemojiKey(span.text));
282
+ }
283
+ function renderTextRows(lines, opts) {
284
+ const parts = [];
285
+ const { cellHeight, cellWidth, fontSize, fontFamily, themeFg, themeBg } = opts;
286
+ for (let row = 0; row < lines.length; row++) {
287
+ const cells = lines[row];
288
+ if (!cells || cells.length === 0) continue;
289
+ const spans = buildTextSpans(cells, themeFg, themeBg);
290
+ if (spans.length === 0) continue;
291
+ const y = row * cellHeight + fontSize;
292
+ let textOpen = false;
293
+ for (const span of spans) {
294
+ const emojiUri = resolveEmojiSpan(span);
295
+ if (emojiUri !== null) {
296
+ if (textOpen) {
297
+ parts.push(`</text>`);
298
+ textOpen = false;
299
+ }
300
+ const x = coord(span.startCol * cellWidth);
301
+ const yTop = coord(row * cellHeight);
302
+ const w = coord(span.cellCount * cellWidth);
303
+ const h = coord(cellHeight);
304
+ parts.push(`<image x="${x}" y="${yTop}" width="${w}" height="${h}" href="${emojiUri}"/>`);
305
+ continue;
306
+ }
307
+ if (!textOpen) {
308
+ parts.push(`<text x="0" y="${y}" font-family="${escapeXml(fontFamily)}" font-size="${fontSize}" fill="${themeFg}">`);
309
+ textOpen = true;
310
+ }
311
+ parts.push(spanToTspan(span, cellWidth, themeFg));
312
+ }
313
+ if (textOpen) parts.push(`</text>`);
314
+ }
315
+ return parts;
316
+ }
317
+ function renderCursor(cursor, opts) {
318
+ if (!cursor.visible) return null;
319
+ const cx = cursor.x * opts.cellWidth;
320
+ const cy = cursor.y * opts.cellHeight;
321
+ switch (cursor.style ?? "block") {
322
+ case "block": return `<rect x="${cx}" y="${cy}" width="${opts.cellWidth}" height="${opts.cellHeight}" fill="${opts.themeCursor}" opacity="0.5"/>`;
323
+ case "underline": return `<rect x="${cx}" y="${cy + opts.cellHeight - 2}" width="${opts.cellWidth}" height="2" fill="${opts.themeCursor}"/>`;
324
+ case "beam": return `<rect x="${cx}" y="${cy}" width="2" height="${opts.cellHeight}" fill="${opts.themeCursor}"/>`;
325
+ }
326
+ }
327
+ /**
328
+ * Parse a `#rrggbb` hex string into an RGB triple. Returns null for any
329
+ * non-hex input (named colors, `transparent`, etc.) so callers can fall back.
330
+ */
331
+ function parseHex(color) {
332
+ const m = /^#([0-9a-fA-F]{6})$/.exec(color.trim());
333
+ if (!m) return null;
334
+ const n = parseInt(m[1], 16);
335
+ return {
336
+ r: n >> 16 & 255,
337
+ g: n >> 8 & 255,
338
+ b: n & 255
339
+ };
340
+ }
341
+ /** Relative luminance (0..1) of an RGB color, per the sRGB perceptual weights. */
342
+ function luminance(c) {
343
+ return (.2126 * c.r + .7152 * c.g + .0722 * c.b) / 255;
344
+ }
345
+ /**
346
+ * Derive a window-bar background that sits naturally against the terminal
347
+ * background: a touch lighter on dark themes, a touch darker on light themes.
348
+ * Falls back to the classic macOS-grey `#333333` when the theme bg is not a
349
+ * parseable hex color.
350
+ */
351
+ function deriveBarColor(themeBg) {
352
+ const rgb = parseHex(themeBg);
353
+ if (!rgb) return "#333333";
354
+ const shift = luminance(rgb) < .5 ? 28 : -28;
355
+ const clamp = (v) => Math.max(0, Math.min(255, v + shift));
356
+ return rgbToHex({
357
+ r: clamp(rgb.r),
358
+ g: clamp(rgb.g),
359
+ b: clamp(rgb.b)
360
+ });
361
+ }
362
+ /** Title text color that contrasts a given bar background. */
363
+ function titleColorFor(barColor) {
364
+ const rgb = parseHex(barColor);
365
+ if (!rgb) return "#cccccc";
366
+ return luminance(rgb) < .5 ? "#cccccc" : "#333333";
367
+ }
368
+ function renderWindowBar(barWidth, barHeight, style, borderRadius, themeBg, title, fontSize) {
369
+ if (style === "none") return [];
370
+ const titleFontSize = Math.max(13, Math.min(fontSize, Math.round(barHeight * .5)));
371
+ const parts = [];
372
+ const barColor = deriveBarColor(themeBg);
373
+ parts.push(`<rect width="${barWidth}" height="${barHeight}" rx="${borderRadius}" ry="${borderRadius}" fill="${barColor}"/>`);
374
+ if (borderRadius > 0) parts.push(`<rect y="${barHeight - borderRadius}" width="${barWidth}" height="${borderRadius}" fill="${barColor}"/>`);
375
+ const titleColor = titleColorFor(barColor);
376
+ if (style === "windows") {
377
+ const slot = 46;
378
+ const glyphSize = 10;
379
+ const cy = barHeight / 2;
380
+ const closeX = barWidth - slot / 2;
381
+ const maxX = barWidth - slot * 1.5;
382
+ const minX = barWidth - slot * 2.5;
383
+ const half = glyphSize / 2;
384
+ parts.push(`<line x1="${minX - half}" y1="${cy}" x2="${minX + half}" y2="${cy}" stroke="${titleColor}" stroke-width="1.4"/>`);
385
+ parts.push(`<rect x="${maxX - half}" y="${cy - half}" width="${glyphSize}" height="${glyphSize}" fill="none" stroke="${titleColor}" stroke-width="1.4"/>`);
386
+ parts.push(`<line x1="${closeX - half}" y1="${cy - half}" x2="${closeX + half}" y2="${cy + half}" stroke="#e81123" stroke-width="1.6"/>`);
387
+ parts.push(`<line x1="${closeX - half}" y1="${cy + half}" x2="${closeX + half}" y2="${cy - half}" stroke="#e81123" stroke-width="1.6"/>`);
388
+ if (title) parts.push(`<text x="14" y="${cy}" font-size="${titleFontSize}" font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif" fill="${titleColor}" dominant-baseline="central">${escapeXml(title)}</text>`);
389
+ return parts;
390
+ }
391
+ const dotRadius = 6;
392
+ const dotY = barHeight / 2;
393
+ const dotStartX = 20;
394
+ if (style === "rings") {
395
+ parts.push(`<circle cx="${dotStartX}" cy="${dotY}" r="${dotRadius}" fill="none" stroke="#ff5f57" stroke-width="1.5"/>`);
396
+ parts.push(`<circle cx="${dotStartX + 20}" cy="${dotY}" r="${dotRadius}" fill="none" stroke="#febc2e" stroke-width="1.5"/>`);
397
+ parts.push(`<circle cx="${dotStartX + 40}" cy="${dotY}" r="${dotRadius}" fill="none" stroke="#28c840" stroke-width="1.5"/>`);
398
+ } else {
399
+ parts.push(`<circle cx="${dotStartX}" cy="${dotY}" r="${dotRadius}" fill="#ff5f57"/>`);
400
+ parts.push(`<circle cx="${dotStartX + 20}" cy="${dotY}" r="${dotRadius}" fill="#febc2e"/>`);
401
+ parts.push(`<circle cx="${dotStartX + 40}" cy="${dotY}" r="${dotRadius}" fill="#28c840"/>`);
402
+ }
403
+ if (title) {
404
+ const titleStartX = dotStartX + 40 + dotRadius + 14;
405
+ parts.push(`<text x="${titleStartX}" y="${dotY}" font-size="${titleFontSize}" font-family="'Helvetica Neue', Arial, sans-serif" font-weight="bold" fill="${titleColor}" dominant-baseline="central">${escapeXml(title)}</text>`);
406
+ }
407
+ return parts;
408
+ }
409
+ function screenshotSvg(terminal, options) {
410
+ const opts = resolveOptions(options);
411
+ const { cellWidth, cellHeight, fontSize, themeFg, themeBg, padding, borderRadius, windowBar, windowBarSize, windowTitle, shadow, margin, marginFill } = opts;
412
+ const lines = terminal.getLines();
413
+ const rows = lines.length;
414
+ const contentWidth = (rows > 0 ? Math.max(...lines.map((l) => l.length)) : 0) * cellWidth;
415
+ const contentHeight = rows * cellHeight;
416
+ if (!(padding > 0 || borderRadius > 0 || windowBar !== "none" || margin > 0 || shadow > 0)) {
417
+ const totalWidth = contentWidth;
418
+ const totalHeight = contentHeight;
419
+ const parts = [];
420
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" viewBox="0 0 ${totalWidth} ${totalHeight}" preserveAspectRatio="xMidYMid meet">`);
421
+ if (opts.embedFonts) parts.push(embeddedFontFaceDefs());
422
+ parts.push(`<rect width="100%" height="100%" fill="${themeBg}"/>`);
423
+ for (const rect of buildBgRects(lines, cellWidth, cellHeight, themeFg, themeBg)) parts.push(`<rect x="${rect.x}" y="${rect.y}" width="${rect.width}" height="${rect.height}" fill="${rect.fill}"/>`);
424
+ parts.push(...renderTextRows(lines, opts));
425
+ const cursorSvg = renderCursor(terminal.getCursor(), opts);
426
+ if (cursorSvg) parts.push(cursorSvg);
427
+ parts.push(`</svg>`);
428
+ return parts.join("\n");
429
+ }
430
+ const barHeight = windowBar !== "none" ? windowBarSize : 0;
431
+ const innerWidth = contentWidth + padding * 2;
432
+ const innerHeight = contentHeight + padding * 2 + barHeight;
433
+ const totalWidth = innerWidth + margin * 2;
434
+ const totalHeight = innerHeight + margin * 2;
435
+ const parts = [];
436
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" viewBox="0 0 ${totalWidth} ${totalHeight}" preserveAspectRatio="xMidYMid meet">`);
437
+ if (opts.embedFonts) parts.push(embeddedFontFaceDefs());
438
+ const hasDefs = shadow > 0 || borderRadius > 0;
439
+ if (hasDefs) parts.push(`<defs>`);
440
+ if (shadow > 0) parts.push(`<filter id="window-shadow" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="0" dy="${coord(shadow * .35)}" stdDeviation="${coord(shadow)}" flood-color="#000000" flood-opacity="0.45"/></filter>`);
441
+ if (borderRadius > 0) parts.push(`<clipPath id="terminal-clip"><rect x="${margin}" y="${margin}" width="${innerWidth}" height="${innerHeight}" rx="${borderRadius}" ry="${borderRadius}"/></clipPath>`);
442
+ if (hasDefs) parts.push(`</defs>`);
443
+ if (margin > 0 && marginFill) parts.push(`<rect width="100%" height="100%" fill="${marginFill}"/>`);
444
+ if (shadow > 0) {
445
+ const shAttrs = borderRadius > 0 ? `x="${margin}" y="${margin}" width="${innerWidth}" height="${innerHeight}" rx="${borderRadius}" ry="${borderRadius}"` : `x="${margin}" y="${margin}" width="${innerWidth}" height="${innerHeight}"`;
446
+ parts.push(`<rect ${shAttrs} fill="${themeBg}" filter="url(#window-shadow)"/>`);
447
+ }
448
+ if (borderRadius > 0) parts.push(`<g clip-path="url(#terminal-clip)">`);
449
+ const bgAttrs = borderRadius > 0 ? `x="${margin}" y="${margin}" width="${innerWidth}" height="${innerHeight}" rx="${borderRadius}" ry="${borderRadius}"` : `x="${margin}" y="${margin}" width="${innerWidth}" height="${innerHeight}"`;
450
+ parts.push(`<rect ${bgAttrs} fill="${themeBg}"/>`);
451
+ if (windowBar !== "none") {
452
+ parts.push(`<g transform="translate(${margin}, ${margin})">`);
453
+ parts.push(...renderWindowBar(innerWidth, barHeight, windowBar, borderRadius, themeBg, windowTitle, fontSize));
454
+ parts.push(`</g>`);
455
+ }
456
+ const contentOffsetX = margin + padding;
457
+ const contentOffsetY = margin + padding + barHeight;
458
+ parts.push(`<g transform="translate(${contentOffsetX}, ${contentOffsetY})">`);
459
+ for (const rect of buildBgRects(lines, cellWidth, cellHeight, themeFg, themeBg)) parts.push(`<rect x="${rect.x}" y="${rect.y}" width="${rect.width}" height="${rect.height}" fill="${rect.fill}"/>`);
460
+ parts.push(...renderTextRows(lines, opts));
461
+ const cursorSvg = renderCursor(terminal.getCursor(), opts);
462
+ if (cursorSvg) parts.push(cursorSvg);
463
+ parts.push(`</g>`);
464
+ if (borderRadius > 0) parts.push(`</g>`);
465
+ parts.push(`</svg>`);
466
+ return parts.join("\n");
467
+ }
468
+ var BUNDLED_FONT_FAMILY, cachedFontDefs, DEFAULT_FONT_SIZE, DEFAULT_CELL_WIDTH, DEFAULT_CELL_HEIGHT, DEFAULT_THEME;
469
+ var init_svg = __esmMin((() => {
470
+ init_fonts();
471
+ init_emoji();
472
+ BUNDLED_FONT_FAMILY = `'${BUNDLED_PRIMARY_FAMILY}', '${BUNDLED_SYMBOL_FAMILY}', '${BUNDLED_NERD_FAMILY}', '${BUNDLED_EMOJI_FAMILY}', 'Menlo', 'Monaco', monospace`;
473
+ cachedFontDefs = null;
474
+ DEFAULT_FONT_SIZE = 16;
475
+ DEFAULT_CELL_WIDTH = 9.6;
476
+ DEFAULT_CELL_HEIGHT = 20;
477
+ DEFAULT_THEME = {
478
+ foreground: "#d4d4d4",
479
+ background: "#1e1e1e",
480
+ cursor: "#aeafad"
481
+ };
482
+ }));
483
+ //#endregion
484
+ export { screenshotSvg as a, rgbToString as i, init_svg as n, rgbToHex as r, embeddedFontFaceDefs as t };
485
+
486
+ //# sourceMappingURL=svg-15lZZzxq.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg-15lZZzxq.mjs","names":[],"sources":["../../termless/src/render/emoji.ts","../../termless/src/render/svg.ts"],"sourcesContent":["/**\n * Color emoji rendering for the resvg path.\n *\n * `@resvg/resvg-js` (the SVG→PNG rasterizer behind {@link screenshotPng},\n * `view/gif.ts`, `view/apng.ts`) inherits from upstream `resvg` and as of\n * 2.6.2 supports NO color font format — not CBDT/sbix (Apple Color Emoji),\n * not COLR/CPAL (Twemoji Mozilla), not OT-SVG (TwitterColorEmoji-SVGinOT,\n * Noto Color Emoji). Upstream issue resvg#487 has been open since Nov 2021.\n *\n * The bundled `NotoEmoji-Regular.ttf` (monochrome) is what resvg can actually\n * render — a thin outlined glyph that drops the user-visible color. To match\n * what users see in their real terminal we side-step the font path entirely:\n *\n * - {@link toTwemojiKey} converts an emoji string (one or more codepoints)\n * to the canonical Twemoji asset key (e.g. \"📁\" → \"1f4c1\", \"👨‍💻\" →\n * \"1f468-200d-1f4bb\"). The Twemoji rule strips U+FE0F unless a U+200D\n * ZWJ is also present in the sequence.\n * - {@link loadTwemojiSvg} reads the SVG asset for a key from the optional\n * `@twemoji/svg` peer dependency (~17 MB of 3700 SVGs, MIT for code,\n * CC-BY for graphics). Returns a base64-encoded `data:image/svg+xml;base64`\n * URI suitable for the `href` of an SVG `<image>` element — resvg renders\n * nested SVG via `<image>` correctly even when it cannot render the same\n * emoji from a font.\n * - {@link isLikelyEmoji} is a permissive codepoint test; the caller must\n * still try {@link loadTwemojiSvg} and fall back to font rendering if the\n * asset is missing (asset absence is a soft signal, not a hard error).\n *\n * The canvas path in `@termless/ghostty` does not need any of this — Skia\n * (`@napi-rs/canvas`) renders Apple Color Emoji natively on macOS.\n */\n\nimport { readFileSync } from \"node:fs\"\nimport { createRequire } from \"node:module\"\n\nconst require = createRequire(import.meta.url)\n\n/**\n * Convert an emoji codepoint sequence to the Twemoji asset filename stem.\n *\n * Twemoji rule: lowercase hex codepoints joined by `-`, with U+FE0F (the\n * emoji variation selector) stripped UNLESS the sequence contains a U+200D\n * (zero-width joiner) — joined sequences keep their variation selectors\n * because the joined glyph requires them.\n *\n * Examples:\n * \"📁\" → \"1f4c1\"\n * \"❤️\" → \"2764\" (FE0F stripped)\n * \"☎️\" → \"260e\" (FE0F stripped)\n * \"👨‍💻\" → \"1f468-200d-1f4bb\" (ZWJ present; FE0F kept if any)\n */\nexport function toTwemojiKey(text: string): string {\n const cps: number[] = []\n for (const ch of text) cps.push(ch.codePointAt(0)!)\n const hasZwj = cps.includes(0x200d)\n const filtered = hasZwj ? cps : cps.filter((cp) => cp !== 0xfe0f)\n return filtered.map((cp) => cp.toString(16)).join(\"-\")\n}\n\n/**\n * Permissive emoji-codepoint test. Returns true for codepoints that LIKELY\n * have a Twemoji asset; the caller must still attempt to load the asset and\n * fall back to font rendering on miss.\n *\n * Covers the major emoji ranges in BMP and supplementary planes:\n * - U+1F300 .. U+1FBFF — Misc Symbols & Pictographs, Emoticons, Transport,\n * Supp Symbols & Pictographs, Symbols & Pictographs\n * Extended-A, plus the legacy/extended blocks.\n * - U+2300 .. U+27BF — Misc Technical, Dingbats, Misc Symbols\n * - U+2600 .. U+26FF — Misc Symbols (☎ ☕ ☀ ★ etc.)\n * - U+2700 .. U+27BF — Dingbats (✂ ✈ ❤ etc.)\n * - U+1F000 .. U+1F2FF — Mahjong, Domino, Playing cards, Enclosed Alphanum\n *\n * Single-codepoint test is enough: a multi-codepoint sequence (ZWJ joined,\n * skin tone modifiers, flags) always has its first codepoint in one of the\n * emoji-bearing ranges.\n */\nexport function isLikelyEmoji(text: string): boolean {\n const cp = text.codePointAt(0)\n if (cp === undefined) return false\n // Supplementary plane emoji ranges\n if (cp >= 0x1f000 && cp <= 0x1fbff) return true\n // BMP symbol/dingbat/misc ranges\n if (cp >= 0x2300 && cp <= 0x27bf) return true\n // Regional indicators (flags) — U+1F1E6 .. U+1F1FF covered above\n return false\n}\n\n// In-process cache: key → base64 data URI (or null if the asset is missing).\nconst dataUriCache = new Map<string, string | null>()\n\n/**\n * Load the Twemoji SVG asset for a key as a `data:image/svg+xml;base64` URI,\n * or return null if the asset is missing (peer dep not installed, or the\n * codepoint is not in the Twemoji catalogue). The URI is suitable for the\n * `href` attribute of an SVG `<image>` element and embeds the entire SVG\n * inline so the parent SVG is self-contained.\n *\n * Resolution is via Node-style `require.resolve(\"@twemoji/svg/<key>.svg\")`\n * — works under both Bun and Node and respects the consumer's `node_modules`\n * layout. Missing assets are cached as `null` so repeated misses are cheap.\n */\nexport function loadTwemojiSvg(key: string): string | null {\n const cached = dataUriCache.get(key)\n if (cached !== undefined) return cached\n try {\n const path = require.resolve(`@twemoji/svg/${key}.svg`)\n const svg = readFileSync(path, \"utf8\")\n const b64 = Buffer.from(svg).toString(\"base64\")\n const uri = `data:image/svg+xml;base64,${b64}`\n dataUriCache.set(key, uri)\n return uri\n } catch {\n dataUriCache.set(key, null)\n return null\n }\n}\n","/**\n * SVG screenshot renderer for termless.\n *\n * Converts a terminal cell grid (TerminalReadable) into an SVG string.\n * Pure function with no side effects — suitable for snapshots, docs, and debugging.\n *\n * Supports VHS-style visual polish: padding, border radius, window bar\n * (macOS traffic light dots), margin, and margin fill color.\n */\n\nimport type {\n TerminalReadable,\n SvgScreenshotOptions,\n SvgTheme,\n Cell,\n RGB,\n CursorState,\n WindowBar,\n} from \"../terminal/types.ts\"\nimport { readFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport {\n BUNDLED_FONTS,\n bundledFontsDir,\n BUNDLED_PRIMARY_FAMILY,\n BUNDLED_SYMBOL_FAMILY,\n BUNDLED_NERD_FAMILY,\n BUNDLED_EMOJI_FAMILY,\n} from \"./fonts.ts\"\nimport { isLikelyEmoji, loadTwemojiSvg, toTwemojiKey } from \"./emoji.ts\"\n\n// ── Defaults ──\n\n/** font-family stack naming the bundled faces — used when `embedFonts` is set. */\nconst BUNDLED_FONT_FAMILY = `'${BUNDLED_PRIMARY_FAMILY}', '${BUNDLED_SYMBOL_FAMILY}', '${BUNDLED_NERD_FAMILY}', '${BUNDLED_EMOJI_FAMILY}', 'Menlo', 'Monaco', monospace`\n\nlet cachedFontDefs: string | null = null\n/**\n * A `<defs><style>` block embedding the bundled fonts as base64 `@font-face`\n * rules. Makes an SVG self-contained — it renders identically in any\n * rasterizer or browser, with no host-font dependency. Cached (the fonts are\n * ~1.8 MB; build the data URIs once per process).\n */\nexport function embeddedFontFaceDefs(): string {\n if (cachedFontDefs !== null) return cachedFontDefs\n const dir = bundledFontsDir()\n const faces: string[] = []\n for (const { file, family } of BUNDLED_FONTS) {\n try {\n const b64 = readFileSync(join(dir, file)).toString(\"base64\")\n faces.push(`@font-face{font-family:'${family}';src:url(data:font/ttf;base64,${b64});}`)\n } catch {\n // a missing bundled font is non-fatal — skip it\n }\n }\n cachedFontDefs = faces.length > 0 ? `<defs><style>${faces.join(\"\")}</style></defs>` : \"\"\n return cachedFontDefs\n}\nconst DEFAULT_FONT_SIZE = 16\nconst DEFAULT_CELL_WIDTH = 9.6\nconst DEFAULT_CELL_HEIGHT = 20\n\nconst DEFAULT_THEME: Required<Pick<SvgTheme, \"foreground\" | \"background\" | \"cursor\">> = {\n foreground: \"#d4d4d4\",\n background: \"#1e1e1e\",\n cursor: \"#aeafad\",\n}\n\n// ── Color helpers ──\n\nexport function rgbToHex(color: RGB): string {\n const r = color.r.toString(16).padStart(2, \"0\")\n const g = color.g.toString(16).padStart(2, \"0\")\n const b = color.b.toString(16).padStart(2, \"0\")\n return `#${r}${g}${b}`\n}\n\nexport function rgbToString(color: RGB | null, fallback: string): string {\n return color ? rgbToHex(color) : fallback\n}\n\n// ── Coordinate formatting ──\n\n/**\n * Format a coordinate for SVG output: drop floating-point noise so cell\n * positions like `28.799999999999997` emit as `28.8`. Trailing zeros are\n * trimmed so integer coordinates stay integers.\n */\nfunction coord(value: number): string {\n return String(Math.round(value * 1000) / 1000)\n}\n\n// ── XML escaping ──\n\nfunction escapeXml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\n// ── Span grouping for text rendering ──\n\ninterface TextSpan {\n text: string\n fill: string\n bold: boolean\n italic: boolean\n dim: boolean\n underline: boolean\n strikethrough: boolean\n startCol: number\n /**\n * Visual cell width this span occupies. NOT text.length — wide chars\n * (emoji, CJK) count 2 cells per char; narrow chars count 1. Used to\n * keep adjacent spans aligned on the cell grid and to advance startCol\n * for following spans.\n */\n cellCount: number\n /**\n * True if this span contains a single wide character (emoji / CJK).\n * Wide-char spans are NEVER merged with adjacent narrow-char spans, so\n * a multi-char span is always all-narrow — which lets spanToTspan emit\n * a clean per-character `x` list (one cellWidth step per glyph).\n */\n isWide: boolean\n}\n\nfunction cellFgBg(cell: Cell, themeFg: string, themeBg: string): { fg: string; bg: string } {\n let fg = rgbToString(cell.fg, themeFg)\n let bg = rgbToString(cell.bg, themeBg)\n if (cell.inverse) {\n ;[fg, bg] = [bg, fg]\n }\n return { fg, bg }\n}\n\nfunction spansMatch(\n a: TextSpan,\n fg: string,\n bold: boolean,\n italic: boolean,\n dim: boolean,\n underline: boolean,\n strikethrough: boolean,\n): boolean {\n return (\n a.fill === fg &&\n a.bold === bold &&\n a.italic === italic &&\n a.dim === dim &&\n a.underline === underline &&\n a.strikethrough === strikethrough\n )\n}\n\n// ── Background rect merging ──\n\ninterface BgRect {\n x: number\n y: number\n width: number\n height: number\n fill: string\n}\n\nfunction buildBgRects(\n lines: Cell[][],\n cellWidth: number,\n cellHeight: number,\n themeFg: string,\n themeBg: string,\n): BgRect[] {\n const rects: BgRect[] = []\n for (let row = 0; row < lines.length; row++) {\n const cells = lines[row]\n let runStart = -1\n let runColor = \"\"\n\n for (let col = 0; col < cells!.length; col++) {\n const cell = cells![col]!\n const { bg } = cellFgBg(cell, themeFg, themeBg)\n const hasCustomBg = bg !== themeBg\n\n if (hasCustomBg) {\n if (runStart >= 0 && runColor === bg) {\n // Continue the current run\n } else {\n // Flush previous run if any\n if (runStart >= 0) {\n rects.push({\n x: runStart * cellWidth,\n y: row * cellHeight,\n width: (col - runStart) * cellWidth,\n height: cellHeight,\n fill: runColor,\n })\n }\n runStart = col\n runColor = bg\n }\n } else {\n // No custom bg — flush any active run\n if (runStart >= 0) {\n rects.push({\n x: runStart * cellWidth,\n y: row * cellHeight,\n width: (col - runStart) * cellWidth,\n height: cellHeight,\n fill: runColor,\n })\n runStart = -1\n runColor = \"\"\n }\n }\n }\n\n // Flush trailing run\n if (runStart >= 0) {\n rects.push({\n x: runStart * cellWidth,\n y: row * cellHeight,\n width: (cells!.length - runStart) * cellWidth,\n height: cellHeight,\n fill: runColor,\n })\n }\n }\n return rects\n}\n\n// ── Text span building ──\n\nfunction buildTextSpans(cells: Cell[], themeFg: string, themeBg: string): TextSpan[] {\n const spans: TextSpan[] = []\n\n for (let col = 0; col < cells.length; col++) {\n const cell = cells[col]!\n\n // Skip continuation cell of a wide character (the second cell).\n // A wide char's first cell has wide=true and non-empty text.\n // The second cell typically has empty text — skip it.\n if (cell.char === \"\" && col > 0 && cells[col - 1]?.wide) {\n continue\n }\n\n const { fg } = cellFgBg(cell, themeFg, themeBg)\n const char = cell.char || \" \"\n const underline = cell.underline !== false\n // Visual cells consumed by this cell: 2 for a wide-char head, 1 otherwise.\n // The continuation cell (col N+1 for wide at N) is `continue`d at the top\n // of the loop and never reaches here.\n const cellsForChar = cell.wide ? 2 : 1\n\n const current = spans.length > 0 ? spans[spans.length - 1] : null\n // Never merge a wide cell (emoji, CJK) with neighbors, even with same\n // styling. textLength+spacingAndGlyphs on a mixed tspan tries to\n // distribute N \"characters\" across cellCount × cellWidth pixels\n // uniformly — which squishes the emoji into a narrow slot, or stretches\n // surrounding letters off the cell grid. Isolating the wide char in\n // its own tspan lets the browser render the emoji at its natural width\n // and the surrounding text at cell-aligned positions.\n const canMerge =\n current &&\n !cell.wide &&\n !current.isWide &&\n spansMatch(current, fg, cell.bold, cell.italic, cell.dim, underline, cell.strikethrough)\n if (canMerge) {\n current!.text += char\n current!.cellCount += cellsForChar\n } else {\n spans.push({\n text: char,\n fill: fg,\n bold: cell.bold,\n italic: cell.italic,\n dim: cell.dim,\n underline,\n strikethrough: cell.strikethrough,\n startCol: col,\n cellCount: cellsForChar,\n isWide: cell.wide === true,\n })\n }\n }\n\n return spans\n}\n\n// ── Resolved options ──\n\ninterface ResolvedOptions {\n fontFamily: string\n fontSize: number\n cellWidth: number\n cellHeight: number\n themeFg: string\n themeBg: string\n themeCursor: string\n padding: number\n borderRadius: number\n windowBar: WindowBar\n windowBarSize: number\n windowTitle: string | null\n shadow: number\n margin: number\n marginFill: string | null\n embedFonts: boolean\n}\n\nfunction resolveOptions(options?: SvgScreenshotOptions): ResolvedOptions {\n return {\n fontFamily: options?.fontFamily ?? BUNDLED_FONT_FAMILY,\n fontSize: options?.fontSize ?? DEFAULT_FONT_SIZE,\n cellWidth: options?.cellWidth ?? DEFAULT_CELL_WIDTH,\n cellHeight: options?.cellHeight ?? DEFAULT_CELL_HEIGHT,\n themeFg: options?.theme?.foreground ?? DEFAULT_THEME.foreground,\n themeBg: options?.theme?.background ?? DEFAULT_THEME.background,\n themeCursor: options?.theme?.cursor ?? DEFAULT_THEME.cursor,\n padding: options?.padding ?? 0,\n borderRadius: options?.borderRadius ?? 0,\n windowBar: options?.windowBar ?? \"none\",\n windowBarSize: options?.windowBarSize ?? 40,\n windowTitle: options?.windowTitle ?? null,\n shadow: options?.shadow ?? 0,\n margin: options?.margin ?? 0,\n marginFill: options?.marginFill ?? null,\n embedFonts: options?.embedFonts ?? false,\n }\n}\n\n// ── Span → tspan conversion ──\n\nfunction spanToTspan(span: TextSpan, cellWidth: number, themeFg: string): string {\n // Per-character cell-grid positioning. SVG `<tspan>` accepts `x` as a\n // *list* of coordinates — one per character — pinning each glyph to an\n // exact cell origin. This is renderer-agnostic: librsvg, @resvg/resvg-js,\n // and browsers all honour the per-glyph `x` list identically.\n //\n // The earlier approach used `textLength` + `lengthAdjust=\"spacingAndGlyphs\"`\n // to stretch a whole span to `cellCount × cellWidth`. librsvg/browsers\n // render that cleanly, but @resvg/resvg-js distributes glyphs differently\n // — heavy, uneven letter-spacing, worst on bold faces (it picks a bold\n // face with different metrics, then spreads it). Since the GIF/PNG path\n // rasterizes via @resvg/resvg-js, that produced visibly broken output.\n // An explicit `x` list sidesteps stretch entirely: the renderer just\n // places each glyph at its cell origin and draws it at natural advance.\n //\n // Wide chars (emoji/CJK) are always isolated in their own single-char\n // span (see buildTextSpans), so a multi-char span is always all-narrow:\n // char `i` sits at `(startCol + i) × cellWidth`.\n const charCount = [...span.text].length\n let xValue: string\n if (charCount > 1) {\n const xs: string[] = []\n for (let i = 0; i < charCount; i++) xs.push(coord((span.startCol + i) * cellWidth))\n xValue = xs.join(\" \")\n } else {\n xValue = coord(span.startCol * cellWidth)\n }\n const attrs: string[] = [`x=\"${xValue}\"`]\n if (span.fill !== themeFg) attrs.push(`fill=\"${span.fill}\"`)\n if (span.bold) attrs.push(`font-weight=\"bold\"`)\n if (span.italic) attrs.push(`font-style=\"italic\"`)\n if (span.dim) attrs.push(`opacity=\"0.5\"`)\n\n const decorations: string[] = []\n if (span.underline) decorations.push(\"underline\")\n if (span.strikethrough) decorations.push(\"line-through\")\n if (decorations.length > 0) attrs.push(`text-decoration=\"${decorations.join(\" \")}\"`)\n\n return `<tspan ${attrs.join(\" \")}>${escapeXml(span.text)}</tspan>`\n}\n\n// ── Text row rendering ──\n\n/**\n * Resolve a wide-character span to a bundled Twemoji color SVG. Returns the\n * data URI when the codepoint sequence has a Twemoji asset and the optional\n * `@twemoji/svg` peer dependency is installed; null otherwise (in which case\n * the span falls back to font rendering via {@link spanToTspan}, which still\n * picks up the bundled monochrome Noto Emoji face).\n *\n * The text-span builder isolates every wide character in its own single-char\n * span (see {@link buildTextSpans}), so a multi-char emoji ZWJ sequence will\n * still arrive here as one span — there is no per-glyph splitting needed.\n */\nfunction resolveEmojiSpan(span: TextSpan): string | null {\n if (!span.isWide) return null\n if (!isLikelyEmoji(span.text)) return null\n return loadTwemojiSvg(toTwemojiKey(span.text))\n}\n\nfunction renderTextRows(lines: Cell[][], opts: ResolvedOptions): string[] {\n const parts: string[] = []\n const { cellHeight, cellWidth, fontSize, fontFamily, themeFg, themeBg } = opts\n\n for (let row = 0; row < lines.length; row++) {\n const cells = lines[row]\n if (!cells || cells.length === 0) continue\n\n const spans = buildTextSpans(cells, themeFg, themeBg)\n if (spans.length === 0) continue\n\n // Baseline: top of cell + font size (approximate ascent for monospace)\n const y = row * cellHeight + fontSize\n // Partition spans: emoji spans (with a resolved Twemoji asset) become\n // sibling `<image>` elements; non-emoji spans collect into one `<text>`\n // per contiguous run with `<tspan>` children. A row may interleave\n // `<text>` and `<image>` siblings — both are positioned absolutely so\n // ordering matches the source row.\n let textOpen = false\n for (const span of spans) {\n const emojiUri = resolveEmojiSpan(span)\n if (emojiUri !== null) {\n if (textOpen) {\n parts.push(`</text>`)\n textOpen = false\n }\n // The emoji image spans 2 cells horizontally (wide char) and one\n // cell vertically. The Twemoji SVG viewBox is 36×36 — resvg honours\n // `<image>` width/height as the rendered box and scales the inner\n // SVG to fit. Positioning at the cell-grid origin keeps the emoji\n // aligned with surrounding monospace text.\n const x = coord(span.startCol * cellWidth)\n const yTop = coord(row * cellHeight)\n const w = coord(span.cellCount * cellWidth)\n const h = coord(cellHeight)\n parts.push(`<image x=\"${x}\" y=\"${yTop}\" width=\"${w}\" height=\"${h}\" href=\"${emojiUri}\"/>`)\n continue\n }\n if (!textOpen) {\n parts.push(\n `<text x=\"0\" y=\"${y}\" font-family=\"${escapeXml(fontFamily)}\" font-size=\"${fontSize}\" fill=\"${themeFg}\">`,\n )\n textOpen = true\n }\n parts.push(spanToTspan(span, cellWidth, themeFg))\n }\n if (textOpen) parts.push(`</text>`)\n }\n\n return parts\n}\n\n// ── Cursor rendering ──\n\nfunction renderCursor(cursor: CursorState, opts: ResolvedOptions): string | null {\n if (!cursor.visible) return null\n\n const cx = cursor.x * opts.cellWidth\n const cy = cursor.y * opts.cellHeight\n\n // Default to block if backend doesn't report cursor style\n const style = cursor.style ?? \"block\"\n\n switch (style) {\n case \"block\":\n return `<rect x=\"${cx}\" y=\"${cy}\" width=\"${opts.cellWidth}\" height=\"${opts.cellHeight}\" fill=\"${opts.themeCursor}\" opacity=\"0.5\"/>`\n case \"underline\":\n return `<rect x=\"${cx}\" y=\"${cy + opts.cellHeight - 2}\" width=\"${opts.cellWidth}\" height=\"2\" fill=\"${opts.themeCursor}\"/>`\n case \"beam\":\n return `<rect x=\"${cx}\" y=\"${cy}\" width=\"2\" height=\"${opts.cellHeight}\" fill=\"${opts.themeCursor}\"/>`\n }\n}\n\n// ── Window bar rendering ──\n\n/**\n * Parse a `#rrggbb` hex string into an RGB triple. Returns null for any\n * non-hex input (named colors, `transparent`, etc.) so callers can fall back.\n */\nfunction parseHex(color: string): RGB | null {\n const m = /^#([0-9a-fA-F]{6})$/.exec(color.trim())\n if (!m) return null\n const n = parseInt(m[1]!, 16)\n return { r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff }\n}\n\n/** Relative luminance (0..1) of an RGB color, per the sRGB perceptual weights. */\nfunction luminance(c: RGB): number {\n return (0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) / 255\n}\n\n/**\n * Derive a window-bar background that sits naturally against the terminal\n * background: a touch lighter on dark themes, a touch darker on light themes.\n * Falls back to the classic macOS-grey `#333333` when the theme bg is not a\n * parseable hex color.\n */\nfunction deriveBarColor(themeBg: string): string {\n const rgb = parseHex(themeBg)\n if (!rgb) return \"#333333\"\n const dark = luminance(rgb) < 0.5\n const shift = dark ? 28 : -28\n const clamp = (v: number) => Math.max(0, Math.min(255, v + shift))\n return rgbToHex({ r: clamp(rgb.r), g: clamp(rgb.g), b: clamp(rgb.b) })\n}\n\n/** Title text color that contrasts a given bar background. */\nfunction titleColorFor(barColor: string): string {\n const rgb = parseHex(barColor)\n if (!rgb) return \"#cccccc\"\n return luminance(rgb) < 0.5 ? \"#cccccc\" : \"#333333\"\n}\n\nfunction renderWindowBar(\n barWidth: number,\n barHeight: number,\n style: WindowBar,\n borderRadius: number,\n themeBg: string,\n title: string | null,\n fontSize: number,\n): string[] {\n if (style === \"none\") return []\n\n // Title text scales with the cell fontSize so the chrome bar's title reads\n // at a similar weight to the terminal content inside — capped at ~half the\n // bar height so it can't overflow vertically. The cap floor of 13 preserves\n // pre-19232 behavior on small bars (windowBarSize ≤ 28).\n const titleFontSize = Math.max(13, Math.min(fontSize, Math.round(barHeight * 0.5)))\n\n const parts: string[] = []\n const barColor = deriveBarColor(themeBg)\n\n // Window bar background — use only top border radius\n parts.push(\n `<rect width=\"${barWidth}\" height=\"${barHeight}\" rx=\"${borderRadius}\" ry=\"${borderRadius}\" fill=\"${barColor}\"/>`,\n )\n // Cover the bottom corners so only top has border radius\n if (borderRadius > 0) {\n parts.push(\n `<rect y=\"${barHeight - borderRadius}\" width=\"${barWidth}\" height=\"${borderRadius}\" fill=\"${barColor}\"/>`,\n )\n }\n\n const titleColor = titleColorFor(barColor)\n\n if (style === \"windows\") {\n // Flat Windows-style title bar — minimize / maximize / close glyphs at the\n // right edge. The glyphs are drawn as line art so they render identically\n // in every rasterizer (no glyph-font dependency).\n const slot = 46 // px per control slot\n const glyphSize = 10\n const cy = barHeight / 2\n const closeX = barWidth - slot / 2\n const maxX = barWidth - slot * 1.5\n const minX = barWidth - slot * 2.5\n const half = glyphSize / 2\n // Minimize — a horizontal bar.\n parts.push(\n `<line x1=\"${minX - half}\" y1=\"${cy}\" x2=\"${minX + half}\" y2=\"${cy}\" stroke=\"${titleColor}\" stroke-width=\"1.4\"/>`,\n )\n // Maximize — a hollow square.\n parts.push(\n `<rect x=\"${maxX - half}\" y=\"${cy - half}\" width=\"${glyphSize}\" height=\"${glyphSize}\" fill=\"none\" stroke=\"${titleColor}\" stroke-width=\"1.4\"/>`,\n )\n // Close — an X, drawn on a red hover-style ground for visual weight.\n parts.push(\n `<line x1=\"${closeX - half}\" y1=\"${cy - half}\" x2=\"${closeX + half}\" y2=\"${cy + half}\" stroke=\"#e81123\" stroke-width=\"1.6\"/>`,\n )\n parts.push(\n `<line x1=\"${closeX - half}\" y1=\"${cy + half}\" x2=\"${closeX + half}\" y2=\"${cy - half}\" stroke=\"#e81123\" stroke-width=\"1.6\"/>`,\n )\n // Title text — left-aligned, the Windows convention.\n if (title) {\n parts.push(\n `<text x=\"14\" y=\"${cy}\" font-size=\"${titleFontSize}\" font-family=\"'Segoe UI', 'Helvetica Neue', Arial, sans-serif\" fill=\"${titleColor}\" dominant-baseline=\"central\">${escapeXml(title)}</text>`,\n )\n }\n return parts\n }\n\n // macOS traffic-light dots\n const dotRadius = 6\n const dotY = barHeight / 2\n const dotStartX = 20\n\n if (style === \"rings\") {\n // Outlined circles (like inactive/unfocused window)\n parts.push(\n `<circle cx=\"${dotStartX}\" cy=\"${dotY}\" r=\"${dotRadius}\" fill=\"none\" stroke=\"#ff5f57\" stroke-width=\"1.5\"/>`,\n )\n parts.push(\n `<circle cx=\"${dotStartX + 20}\" cy=\"${dotY}\" r=\"${dotRadius}\" fill=\"none\" stroke=\"#febc2e\" stroke-width=\"1.5\"/>`,\n )\n parts.push(\n `<circle cx=\"${dotStartX + 40}\" cy=\"${dotY}\" r=\"${dotRadius}\" fill=\"none\" stroke=\"#28c840\" stroke-width=\"1.5\"/>`,\n )\n } else {\n // Filled circles (colorful — like active/focused window)\n parts.push(`<circle cx=\"${dotStartX}\" cy=\"${dotY}\" r=\"${dotRadius}\" fill=\"#ff5f57\"/>`)\n parts.push(`<circle cx=\"${dotStartX + 20}\" cy=\"${dotY}\" r=\"${dotRadius}\" fill=\"#febc2e\"/>`)\n parts.push(`<circle cx=\"${dotStartX + 40}\" cy=\"${dotY}\" r=\"${dotRadius}\" fill=\"#28c840\"/>`)\n }\n\n // macOS title — flush-left to the right of the traffic-light dots,\n // matching the live recording overlay's bold-flush-left layout.\n // Dots sit at x=20, 40, 60 (radius 6) → rightmost edge ≈ 66; pad to 80.\n if (title) {\n const titleStartX = dotStartX + 40 + dotRadius + 14 // 80\n parts.push(\n `<text x=\"${titleStartX}\" y=\"${dotY}\" font-size=\"${titleFontSize}\" font-family=\"'Helvetica Neue', Arial, sans-serif\" font-weight=\"bold\" fill=\"${titleColor}\" dominant-baseline=\"central\">${escapeXml(title)}</text>`,\n )\n }\n\n return parts\n}\n\n// ── Main renderer ──\n\nexport function screenshotSvg(terminal: TerminalReadable, options?: SvgScreenshotOptions): string {\n const opts = resolveOptions(options)\n const {\n cellWidth,\n cellHeight,\n fontSize,\n themeFg,\n themeBg,\n padding,\n borderRadius,\n windowBar,\n windowBarSize,\n windowTitle,\n shadow,\n margin,\n marginFill,\n } = opts\n\n const lines = terminal.getLines()\n const rows = lines.length\n const cols = rows > 0 ? Math.max(...lines.map((l) => l.length)) : 0\n\n // Terminal content dimensions\n const contentWidth = cols * cellWidth\n const contentHeight = rows * cellHeight\n\n // Detect whether any visual chrome is active\n const hasChrome = padding > 0 || borderRadius > 0 || windowBar !== \"none\" || margin > 0 || shadow > 0\n\n // Fast path: no chrome — produce the classic minimal SVG (backward-compatible)\n if (!hasChrome) {\n const totalWidth = contentWidth\n const totalHeight = contentHeight\n const parts: string[] = []\n\n // viewBox lets rasterizers (rsvg-convert, Chromium, qlmanage) scale the\n // SVG to a target raster size while preserving aspect. Without it, some\n // rasterizers fall back to fixed-size canvases (qlmanage always produces\n // 1680x1680 thumbnails) and the SVG content shrinks-to-fit, distorting\n // dHash/pixel comparisons that expect a known output geometry.\n // preserveAspectRatio=\"xMidYMid meet\" is the standard SVG default — we\n // emit it explicitly so consumers can override via the SVG element if\n // they want a different fit strategy.\n parts.push(\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${totalWidth}\" height=\"${totalHeight}\" viewBox=\"0 0 ${totalWidth} ${totalHeight}\" preserveAspectRatio=\"xMidYMid meet\">`,\n )\n if (opts.embedFonts) parts.push(embeddedFontFaceDefs())\n parts.push(`<rect width=\"100%\" height=\"100%\" fill=\"${themeBg}\"/>`)\n\n for (const rect of buildBgRects(lines, cellWidth, cellHeight, themeFg, themeBg)) {\n parts.push(\n `<rect x=\"${rect.x}\" y=\"${rect.y}\" width=\"${rect.width}\" height=\"${rect.height}\" fill=\"${rect.fill}\"/>`,\n )\n }\n\n parts.push(...renderTextRows(lines, opts))\n\n const cursorSvg = renderCursor(terminal.getCursor(), opts)\n if (cursorSvg) parts.push(cursorSvg)\n\n parts.push(`</svg>`)\n return parts.join(\"\\n\")\n }\n\n // Chrome path: padding, border radius, window bar, margin\n const barHeight = windowBar !== \"none\" ? windowBarSize : 0\n const innerWidth = contentWidth + padding * 2\n const innerHeight = contentHeight + padding * 2 + barHeight\n const totalWidth = innerWidth + margin * 2\n const totalHeight = innerHeight + margin * 2\n\n const parts: string[] = []\n\n // viewBox + preserveAspectRatio mirror the fast-path emission so chrome-mode\n // SVGs scale predictably under rasterizers that ignore intrinsic dimensions.\n parts.push(\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${totalWidth}\" height=\"${totalHeight}\" viewBox=\"0 0 ${totalWidth} ${totalHeight}\" preserveAspectRatio=\"xMidYMid meet\">`,\n )\n if (opts.embedFonts) parts.push(embeddedFontFaceDefs())\n\n // Defs: a soft drop-shadow filter + (optionally) the border-radius clip path.\n // The shadow is a gaussian-blurred dark copy of the alpha channel, offset\n // slightly downward — the standard \"floating window\" look that Freeze / VHS\n // produce. It is rendered renderer-agnostically (resvg + browsers both\n // honour feDropShadow), so it survives the GIF/PNG rasterization path.\n const hasDefs = shadow > 0 || borderRadius > 0\n if (hasDefs) parts.push(`<defs>`)\n if (shadow > 0) {\n parts.push(\n `<filter id=\"window-shadow\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">` +\n `<feDropShadow dx=\"0\" dy=\"${coord(shadow * 0.35)}\" stdDeviation=\"${coord(shadow)}\" flood-color=\"#000000\" flood-opacity=\"0.45\"/>` +\n `</filter>`,\n )\n }\n if (borderRadius > 0) {\n parts.push(\n `<clipPath id=\"terminal-clip\">` +\n `<rect x=\"${margin}\" y=\"${margin}\" width=\"${innerWidth}\" height=\"${innerHeight}\" rx=\"${borderRadius}\" ry=\"${borderRadius}\"/>` +\n `</clipPath>`,\n )\n }\n if (hasDefs) parts.push(`</defs>`)\n\n // Outer margin fill\n if (margin > 0 && marginFill) {\n parts.push(`<rect width=\"100%\" height=\"100%\" fill=\"${marginFill}\"/>`)\n }\n\n // Drop shadow — a rounded rect matching the window outline, blurred via the\n // filter above. Drawn before (under) the window itself.\n if (shadow > 0) {\n const shAttrs =\n borderRadius > 0\n ? `x=\"${margin}\" y=\"${margin}\" width=\"${innerWidth}\" height=\"${innerHeight}\" rx=\"${borderRadius}\" ry=\"${borderRadius}\"`\n : `x=\"${margin}\" y=\"${margin}\" width=\"${innerWidth}\" height=\"${innerHeight}\"`\n parts.push(`<rect ${shAttrs} fill=\"${themeBg}\" filter=\"url(#window-shadow)\"/>`)\n }\n\n if (borderRadius > 0) {\n parts.push(`<g clip-path=\"url(#terminal-clip)\">`)\n }\n\n // Terminal background rect\n const bgAttrs =\n borderRadius > 0\n ? `x=\"${margin}\" y=\"${margin}\" width=\"${innerWidth}\" height=\"${innerHeight}\" rx=\"${borderRadius}\" ry=\"${borderRadius}\"`\n : `x=\"${margin}\" y=\"${margin}\" width=\"${innerWidth}\" height=\"${innerHeight}\"`\n parts.push(`<rect ${bgAttrs} fill=\"${themeBg}\"/>`)\n\n // Window bar\n if (windowBar !== \"none\") {\n parts.push(`<g transform=\"translate(${margin}, ${margin})\">`)\n parts.push(...renderWindowBar(innerWidth, barHeight, windowBar, borderRadius, themeBg, windowTitle, fontSize))\n parts.push(`</g>`)\n }\n\n // Content group — offset by margin + padding + window bar\n const contentOffsetX = margin + padding\n const contentOffsetY = margin + padding + barHeight\n parts.push(`<g transform=\"translate(${contentOffsetX}, ${contentOffsetY})\">`)\n\n for (const rect of buildBgRects(lines, cellWidth, cellHeight, themeFg, themeBg)) {\n parts.push(`<rect x=\"${rect.x}\" y=\"${rect.y}\" width=\"${rect.width}\" height=\"${rect.height}\" fill=\"${rect.fill}\"/>`)\n }\n\n parts.push(...renderTextRows(lines, opts))\n\n const cursorSvg = renderCursor(terminal.getCursor(), opts)\n if (cursorSvg) parts.push(cursorSvg)\n\n parts.push(`</g>`)\n\n if (borderRadius > 0) {\n parts.push(`</g>`)\n }\n\n parts.push(`</svg>`)\n return parts.join(\"\\n\")\n}\n"],"mappings":"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"}