@zaly/tui 0.0.4

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/assets/schemas/theme.schema.json +899 -0
  4. package/assets/themes/LICENSE-tokyonight.txt +201 -0
  5. package/assets/themes/NOTICES.md +69 -0
  6. package/assets/themes/ayu-dark.json +119 -0
  7. package/assets/themes/catppuccin-frappe.json +124 -0
  8. package/assets/themes/catppuccin-latte.json +124 -0
  9. package/assets/themes/catppuccin-macchiato.json +124 -0
  10. package/assets/themes/catppuccin-mocha.json +124 -0
  11. package/assets/themes/dracula-soft.json +119 -0
  12. package/assets/themes/dracula.json +119 -0
  13. package/assets/themes/everforest-dark.json +118 -0
  14. package/assets/themes/everforest-light.json +118 -0
  15. package/assets/themes/github-dark.json +123 -0
  16. package/assets/themes/github-light.json +123 -0
  17. package/assets/themes/gruvbox-dark-medium.json +112 -0
  18. package/assets/themes/kanagawa-dragon.json +115 -0
  19. package/assets/themes/kanagawa-wave.json +115 -0
  20. package/assets/themes/material-theme-palenight.json +114 -0
  21. package/assets/themes/min-dark.json +55 -0
  22. package/assets/themes/min-light.json +59 -0
  23. package/assets/themes/monokai.json +116 -0
  24. package/assets/themes/night-owl.json +116 -0
  25. package/assets/themes/nord.json +101 -0
  26. package/assets/themes/one-dark-pro.json +110 -0
  27. package/assets/themes/one-light.json +108 -0
  28. package/assets/themes/poimandres.json +123 -0
  29. package/assets/themes/rose-pine.json +98 -0
  30. package/assets/themes/solarized-dark.json +116 -0
  31. package/assets/themes/solarized-light.json +116 -0
  32. package/assets/themes/synthwave-84.json +100 -0
  33. package/assets/themes/tokyonight-day.json +157 -0
  34. package/assets/themes/tokyonight-moon.json +157 -0
  35. package/assets/themes/tokyonight-night.json +157 -0
  36. package/assets/themes/tokyonight-storm.json +157 -0
  37. package/assets/themes/vesper.json +103 -0
  38. package/assets/themes/vitesse-dark.json +126 -0
  39. package/assets/themes/vitesse-light.json +126 -0
  40. package/dist/ansi-Clq0ozyA.mjs +206 -0
  41. package/dist/ansi-Clq0ozyA.mjs.map +1 -0
  42. package/dist/ansi.d.mts +48 -0
  43. package/dist/ansi.d.mts.map +1 -0
  44. package/dist/ansi.mjs +2 -0
  45. package/dist/autocomplete-Dfw-yQDa.d.mts +100 -0
  46. package/dist/autocomplete-Dfw-yQDa.d.mts.map +1 -0
  47. package/dist/box-DyaByEiG.mjs +386 -0
  48. package/dist/box-DyaByEiG.mjs.map +1 -0
  49. package/dist/builder-D49ECBEf.mjs +385 -0
  50. package/dist/builder-D49ECBEf.mjs.map +1 -0
  51. package/dist/callbacks-CNjGNxVw.mjs +279 -0
  52. package/dist/callbacks-CNjGNxVw.mjs.map +1 -0
  53. package/dist/client-D6C_uczF.mjs +142 -0
  54. package/dist/client-D6C_uczF.mjs.map +1 -0
  55. package/dist/clipboard.d.mts +104 -0
  56. package/dist/clipboard.d.mts.map +1 -0
  57. package/dist/clipboard.mjs +404 -0
  58. package/dist/clipboard.mjs.map +1 -0
  59. package/dist/flex-Bguo-fUe.mjs +202 -0
  60. package/dist/flex-Bguo-fUe.mjs.map +1 -0
  61. package/dist/image-Cm5T-uw-.mjs +108 -0
  62. package/dist/image-Cm5T-uw-.mjs.map +1 -0
  63. package/dist/image.d.mts +123 -0
  64. package/dist/image.d.mts.map +1 -0
  65. package/dist/image.mjs +2 -0
  66. package/dist/index.d.mts +64 -0
  67. package/dist/index.d.mts.map +1 -0
  68. package/dist/index.mjs +222 -0
  69. package/dist/index.mjs.map +1 -0
  70. package/dist/input-B5Vy-iaa.d.mts +141 -0
  71. package/dist/input-B5Vy-iaa.d.mts.map +1 -0
  72. package/dist/inspect-D1d9t-ni.mjs +101 -0
  73. package/dist/inspect-D1d9t-ni.mjs.map +1 -0
  74. package/dist/inspect-D6FW_QUo.d.mts +67 -0
  75. package/dist/inspect-D6FW_QUo.d.mts.map +1 -0
  76. package/dist/kitty-JbCJC0iQ.mjs +673 -0
  77. package/dist/kitty-JbCJC0iQ.mjs.map +1 -0
  78. package/dist/markdown.d.mts +24 -0
  79. package/dist/markdown.d.mts.map +1 -0
  80. package/dist/markdown.mjs +2 -0
  81. package/dist/md.bun.d.mts +14 -0
  82. package/dist/md.bun.d.mts.map +1 -0
  83. package/dist/md.bun.mjs +28 -0
  84. package/dist/md.bun.mjs.map +1 -0
  85. package/dist/md.node.d.mts +14 -0
  86. package/dist/md.node.d.mts.map +1 -0
  87. package/dist/md.node.mjs +126 -0
  88. package/dist/md.node.mjs.map +1 -0
  89. package/dist/node-BZ7f6HzU.mjs +316 -0
  90. package/dist/node-BZ7f6HzU.mjs.map +1 -0
  91. package/dist/picker-D8OLb6kZ.d.mts +36 -0
  92. package/dist/picker-D8OLb6kZ.d.mts.map +1 -0
  93. package/dist/picker-ndVVt6qF.mjs +621 -0
  94. package/dist/picker-ndVVt6qF.mjs.map +1 -0
  95. package/dist/queries-DH3bj74K.d.mts +1963 -0
  96. package/dist/queries-DH3bj74K.d.mts.map +1 -0
  97. package/dist/reactive-f7NpAi69.mjs +623 -0
  98. package/dist/reactive-f7NpAi69.mjs.map +1 -0
  99. package/dist/registry-U654j61Y.mjs +356 -0
  100. package/dist/registry-U654j61Y.mjs.map +1 -0
  101. package/dist/render-DxVLqqB9.mjs +74 -0
  102. package/dist/render-DxVLqqB9.mjs.map +1 -0
  103. package/dist/renderer-CdLCkJB-.mjs +42 -0
  104. package/dist/renderer-CdLCkJB-.mjs.map +1 -0
  105. package/dist/renderer-CxL4xbkl.mjs +2644 -0
  106. package/dist/renderer-CxL4xbkl.mjs.map +1 -0
  107. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  108. package/dist/search-FvGnswRl.d.mts +49 -0
  109. package/dist/search-FvGnswRl.d.mts.map +1 -0
  110. package/dist/select-CbmAZdpt.d.mts +116 -0
  111. package/dist/select-CbmAZdpt.d.mts.map +1 -0
  112. package/dist/services/logger.d.mts +2 -0
  113. package/dist/services/logger.mjs +71 -0
  114. package/dist/services/logger.mjs.map +1 -0
  115. package/dist/services/notifier.d.mts +17 -0
  116. package/dist/services/notifier.d.mts.map +1 -0
  117. package/dist/services/notifier.mjs +71 -0
  118. package/dist/services/notifier.mjs.map +1 -0
  119. package/dist/services/picker.d.mts +36 -0
  120. package/dist/services/picker.d.mts.map +1 -0
  121. package/dist/services/picker.mjs +137 -0
  122. package/dist/services/picker.mjs.map +1 -0
  123. package/dist/services/prompt.d.mts +30 -0
  124. package/dist/services/prompt.d.mts.map +1 -0
  125. package/dist/services/prompt.mjs +83 -0
  126. package/dist/services/prompt.mjs.map +1 -0
  127. package/dist/shiki-BrjVUkeh.mjs +423 -0
  128. package/dist/shiki-BrjVUkeh.mjs.map +1 -0
  129. package/dist/size-D8JogYLy.mjs +34 -0
  130. package/dist/size-D8JogYLy.mjs.map +1 -0
  131. package/dist/style.d.mts +10 -0
  132. package/dist/style.d.mts.map +1 -0
  133. package/dist/style.mjs +4 -0
  134. package/dist/text.d.mts +33 -0
  135. package/dist/text.d.mts.map +1 -0
  136. package/dist/text.mjs +111 -0
  137. package/dist/text.mjs.map +1 -0
  138. package/dist/theme.config-CU3B50HN.mjs +832 -0
  139. package/dist/theme.config-CU3B50HN.mjs.map +1 -0
  140. package/dist/themes.d.mts +113 -0
  141. package/dist/themes.d.mts.map +1 -0
  142. package/dist/themes.mjs +2 -0
  143. package/dist/tree-C28QOVIK.d.mts +23 -0
  144. package/dist/tree-C28QOVIK.d.mts.map +1 -0
  145. package/dist/types-B9Eh7k2v.d.mts +212 -0
  146. package/dist/types-B9Eh7k2v.d.mts.map +1 -0
  147. package/dist/types-D3jk1rtk.d.mts +140 -0
  148. package/dist/types-D3jk1rtk.d.mts.map +1 -0
  149. package/dist/utils-3M4D_AXZ.mjs +40 -0
  150. package/dist/utils-3M4D_AXZ.mjs.map +1 -0
  151. package/dist/widgets/autocomplete.d.mts +2 -0
  152. package/dist/widgets/autocomplete.mjs +165 -0
  153. package/dist/widgets/autocomplete.mjs.map +1 -0
  154. package/dist/widgets/box.d.mts +2 -0
  155. package/dist/widgets/box.mjs +2 -0
  156. package/dist/widgets/code.d.mts +48 -0
  157. package/dist/widgets/code.d.mts.map +1 -0
  158. package/dist/widgets/code.mjs +59 -0
  159. package/dist/widgets/code.mjs.map +1 -0
  160. package/dist/widgets/completions/actions.d.mts +41 -0
  161. package/dist/widgets/completions/actions.d.mts.map +1 -0
  162. package/dist/widgets/completions/actions.mjs +48 -0
  163. package/dist/widgets/completions/actions.mjs.map +1 -0
  164. package/dist/widgets/completions/files.d.mts +29 -0
  165. package/dist/widgets/completions/files.d.mts.map +1 -0
  166. package/dist/widgets/completions/files.mjs +42 -0
  167. package/dist/widgets/completions/files.mjs.map +1 -0
  168. package/dist/widgets/completions/github.d.mts +64 -0
  169. package/dist/widgets/completions/github.d.mts.map +1 -0
  170. package/dist/widgets/completions/github.mjs +137 -0
  171. package/dist/widgets/completions/github.mjs.map +1 -0
  172. package/dist/widgets/completions/index.d.mts +4 -0
  173. package/dist/widgets/completions/index.mjs +4 -0
  174. package/dist/widgets/diff.d.mts +57 -0
  175. package/dist/widgets/diff.d.mts.map +1 -0
  176. package/dist/widgets/diff.mjs +199 -0
  177. package/dist/widgets/diff.mjs.map +1 -0
  178. package/dist/widgets/divider.d.mts +11 -0
  179. package/dist/widgets/divider.d.mts.map +1 -0
  180. package/dist/widgets/divider.mjs +9 -0
  181. package/dist/widgets/divider.mjs.map +1 -0
  182. package/dist/widgets/image.d.mts +37 -0
  183. package/dist/widgets/image.d.mts.map +1 -0
  184. package/dist/widgets/image.mjs +77 -0
  185. package/dist/widgets/image.mjs.map +1 -0
  186. package/dist/widgets/input.d.mts +2 -0
  187. package/dist/widgets/input.mjs +497 -0
  188. package/dist/widgets/input.mjs.map +1 -0
  189. package/dist/widgets/log.d.mts +2 -0
  190. package/dist/widgets/log.mjs +130 -0
  191. package/dist/widgets/log.mjs.map +1 -0
  192. package/dist/widgets/markdown.d.mts +45 -0
  193. package/dist/widgets/markdown.d.mts.map +1 -0
  194. package/dist/widgets/markdown.mjs +124 -0
  195. package/dist/widgets/markdown.mjs.map +1 -0
  196. package/dist/widgets/overlay.d.mts +2 -0
  197. package/dist/widgets/overlay.mjs +18 -0
  198. package/dist/widgets/overlay.mjs.map +1 -0
  199. package/dist/widgets/picker.d.mts +2 -0
  200. package/dist/widgets/picker.mjs +2 -0
  201. package/dist/widgets/progress.d.mts +48 -0
  202. package/dist/widgets/progress.d.mts.map +1 -0
  203. package/dist/widgets/progress.mjs +48 -0
  204. package/dist/widgets/progress.mjs.map +1 -0
  205. package/dist/widgets/select.d.mts +2 -0
  206. package/dist/widgets/select.mjs +195 -0
  207. package/dist/widgets/select.mjs.map +1 -0
  208. package/dist/widgets/show.d.mts +55 -0
  209. package/dist/widgets/show.d.mts.map +1 -0
  210. package/dist/widgets/show.mjs +85 -0
  211. package/dist/widgets/show.mjs.map +1 -0
  212. package/dist/widgets/spinner.d.mts +65 -0
  213. package/dist/widgets/spinner.d.mts.map +1 -0
  214. package/dist/widgets/spinner.mjs +147 -0
  215. package/dist/widgets/spinner.mjs.map +1 -0
  216. package/dist/widgets/text.d.mts +2 -0
  217. package/dist/widgets/text.mjs +32 -0
  218. package/dist/widgets/text.mjs.map +1 -0
  219. package/dist/widgets/tree.d.mts +2 -0
  220. package/dist/widgets/tree.mjs +73 -0
  221. package/dist/widgets/tree.mjs.map +1 -0
  222. package/dist/widgets/widget.d.mts +49 -0
  223. package/dist/widgets/widget.d.mts.map +1 -0
  224. package/dist/widgets/widget.mjs +40 -0
  225. package/dist/widgets/widget.mjs.map +1 -0
  226. package/dist/worker-DjHuQSbY.mjs +27 -0
  227. package/dist/worker-DjHuQSbY.mjs.map +1 -0
  228. package/package.json +117 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.mts","names":[],"sources":["../../../src/widgets/completions/github.ts"],"mappings":";;;;;;KAWY,aAAa;EACvB;EACA;;;EAGA;EACA;EACA;EACA;IAAW;;;KAGD;KAEA,iBAAiB,aAAa,OAAO,gBAAgB,QAAQ;UAExD;;;EAGf;;;EAGA,UAAU;;;EAGV;;EAEA,QAAQ;;EAER;;;;EAIA,UAAU;;;;;;;;;;;;;;;;;;;;;;;iBAwBI,aAAa,OAAM,sBAA2B,iBAAiB"}
@@ -0,0 +1,137 @@
1
+ import { stringWidth } from "@zaly/shared/ansi";
2
+ import { spawn } from "node:child_process";
3
+ //#region src/widgets/completions/github.ts
4
+ /**
5
+ * Completion source for GitHub issues and pull requests from the repo
6
+ * at `cwd`. Shells out to the `gh` CLI on first use, caches the result,
7
+ * and fuzzy-filters locally on subsequent calls so typing stays snappy.
8
+ *
9
+ * Accepts insert `#123 ` into the input — ready for a markdown-style
10
+ * reference in a chat message or commit subject.
11
+ *
12
+ * ```ts
13
+ * autocomplete({
14
+ * input: "chat-input",
15
+ * sources: {
16
+ * gh: githubSource(),
17
+ * },
18
+ * })
19
+ * ```
20
+ *
21
+ * Silently returns no items when `gh` isn't installed, the user isn't
22
+ * authenticated, or the directory isn't a GitHub repo — so it's safe
23
+ * to wire up unconditionally.
24
+ */
25
+ function githubSource(opts = {}) {
26
+ const cwd = opts.cwd ?? process.cwd();
27
+ const trigger = opts.trigger ?? /(?<=^|\s)#/;
28
+ const prefix = opts.prefix ?? "#";
29
+ const state = opts.state ?? "open";
30
+ const limit = opts.limit ?? 50;
31
+ const fetcher = opts.fetcher ?? defaultFetcher(limit);
32
+ let cache;
33
+ return {
34
+ accept: (item) => `${prefix}${item.number} `,
35
+ async complete(_query, match) {
36
+ cache ??= fetcher(cwd, state).catch(() => []);
37
+ const items = await cache;
38
+ const out = [];
39
+ for (const item of items) {
40
+ const score = match(`#${item.number} ${item.title}`);
41
+ if (!score) continue;
42
+ out.push({
43
+ ...item,
44
+ score
45
+ });
46
+ }
47
+ return out;
48
+ },
49
+ render: defaultRender,
50
+ triggers: [trigger]
51
+ };
52
+ }
53
+ const defaultRender = (item, ctx) => {
54
+ const { style } = ctx;
55
+ const num = `#${item.number}`;
56
+ const stateSlot = stateStyleSlot(item.state);
57
+ const typeLabel = item.type === "pr" ? "pr" : "issue";
58
+ const author = item.author?.login ? `@${item.author.login}` : "";
59
+ const lead = `${style.add("optionName")(num)} ${style.add(stateSlot)(`${item.state}·${typeLabel}`)} ${item.title}`;
60
+ if (author === "") return lead;
61
+ const gap = 2;
62
+ const leadW = stringWidth(lead);
63
+ const hintW = stringWidth(author);
64
+ const padded = ctx.width - leadW - hintW - gap;
65
+ if (padded < 1) return lead;
66
+ return lead + " ".repeat(padded + gap) + style.add("optionDesc")(author);
67
+ };
68
+ function stateStyleSlot(state) {
69
+ if (state === "open") return "success";
70
+ if (state === "merged") return "accent";
71
+ return "muted";
72
+ }
73
+ /** Default fetcher — shells out to `gh issue list` + `gh pr list` in
74
+ * parallel. Either failing is swallowed so a repo with only issues
75
+ * (or only PRs) still works. */
76
+ function defaultFetcher(limit) {
77
+ const fields = "number,title,state,author,url";
78
+ return async (cwd, state) => {
79
+ const [issues, prs] = await Promise.all([runGh(cwd, [
80
+ "issue",
81
+ "list",
82
+ "--state",
83
+ state,
84
+ "--json",
85
+ fields,
86
+ "--limit",
87
+ String(limit)
88
+ ]), runGh(cwd, [
89
+ "pr",
90
+ "list",
91
+ "--state",
92
+ state,
93
+ "--json",
94
+ fields,
95
+ "--limit",
96
+ String(limit)
97
+ ])]);
98
+ return [...parse(issues).map((x) => Object.assign(x, { type: "issue" })), ...parse(prs).map((x) => Object.assign(x, { type: "pr" }))].map((item) => Object.assign(item, { text: `#${item.number} ${item.title}` }));
99
+ };
100
+ }
101
+ function parse(out) {
102
+ try {
103
+ return JSON.parse(out);
104
+ } catch {
105
+ return [];
106
+ }
107
+ }
108
+ function runGh(cwd, args) {
109
+ return new Promise((resolve) => {
110
+ try {
111
+ const child = spawn("gh", args, {
112
+ cwd,
113
+ stdio: [
114
+ "ignore",
115
+ "pipe",
116
+ "ignore"
117
+ ]
118
+ });
119
+ let stdout = "";
120
+ child.stdout.on("data", (chunk) => {
121
+ stdout += chunk.toString("utf8");
122
+ });
123
+ child.on("close", (code) => {
124
+ resolve(code === 0 ? stdout : "");
125
+ });
126
+ child.on("error", () => {
127
+ resolve("");
128
+ });
129
+ } catch {
130
+ resolve("");
131
+ }
132
+ });
133
+ }
134
+ //#endregion
135
+ export { githubSource };
136
+
137
+ //# sourceMappingURL=github.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.mjs","names":[],"sources":["../../../src/widgets/completions/github.ts"],"sourcesContent":["import type { ScoredItem } from \"../../search/matcher.ts\"\nimport type { CompletionSource } from \"../autocomplete.ts\"\nimport type { PickerItem } from \"../picker.ts\"\nimport type { OptionRender } from \"../select.ts\"\n\nimport { stringWidth } from \"@zaly/shared/ansi\"\nimport { spawn } from \"node:child_process\"\n\n/** A GitHub issue or pull request returned by `githubSource`. Shape\n * mirrors the `gh` CLI's `--json number,title,state,author,url`\n * output, with `type` added to distinguish issues from PRs. */\nexport type GithubItem = PickerItem & {\n number: number\n title: string\n /** `\"open\"` / `\"closed\"` for issues; `\"open\"` / `\"closed\"` / `\"merged\"`\n * for PRs. Kept as a string so we don't trip if gh evolves. */\n state: string\n type: \"issue\" | \"pr\"\n url: string\n author?: { login?: string }\n}\n\nexport type GithubState = \"open\" | \"closed\" | \"all\"\n\nexport type GithubFetcher = (cwd: string, state: GithubState) => Promise<GithubItem[]>\n\nexport interface GithubSourceOptions {\n /** Working directory used to locate the repo. `gh` resolves the repo\n * from the git remote of this dir. Default: `process.cwd()`. */\n cwd?: string\n /** Trigger regex. Default: `/(?<=^|\\s)#/` — `#` preceded by start-of-\n * string or whitespace (same lookbehind trick as `filesSource`). */\n trigger?: RegExp\n /** Prepended to the accepted value so the trigger character stays in\n * the input. Default: `\"#\"`. */\n prefix?: string\n /** Which issues / PRs to include. Default: `\"open\"`. */\n state?: GithubState\n /** Per-kind cap passed to `gh`. Default: `50`. */\n limit?: number\n /** Override the fetch implementation. Mainly useful for tests — the\n * default shells out to `gh`. The fetcher is called at most once per\n * `githubSource` instance; results are cached thereafter. */\n fetcher?: GithubFetcher\n}\n\n/**\n * Completion source for GitHub issues and pull requests from the repo\n * at `cwd`. Shells out to the `gh` CLI on first use, caches the result,\n * and fuzzy-filters locally on subsequent calls so typing stays snappy.\n *\n * Accepts insert `#123 ` into the input — ready for a markdown-style\n * reference in a chat message or commit subject.\n *\n * ```ts\n * autocomplete({\n * input: \"chat-input\",\n * sources: {\n * gh: githubSource(),\n * },\n * })\n * ```\n *\n * Silently returns no items when `gh` isn't installed, the user isn't\n * authenticated, or the directory isn't a GitHub repo — so it's safe\n * to wire up unconditionally.\n */\nexport function githubSource(opts: GithubSourceOptions = {}): CompletionSource<GithubItem> {\n const cwd = opts.cwd ?? process.cwd()\n const trigger = opts.trigger ?? /(?<=^|\\s)#/\n const prefix = opts.prefix ?? \"#\"\n const state: GithubState = opts.state ?? \"open\"\n const limit = opts.limit ?? 50\n const fetcher = opts.fetcher ?? defaultFetcher(limit)\n\n // Lazy, single-fetch cache. The Promise is reused by concurrent\n // callers so rapid keystrokes while the first fetch is in flight\n // don't spawn duplicate `gh` processes.\n let cache: Promise<GithubItem[]> | undefined\n\n return {\n accept: (item) => `${prefix}${item.number} `,\n async complete(_query: string, match): Promise<ScoredItem<GithubItem>[]> {\n cache ??= fetcher(cwd, state).catch(() => [])\n const items = await cache\n const out: ScoredItem<GithubItem>[] = []\n for (const item of items) {\n // Match on \"#<num> <title>\" so users can type either digits or\n // words and both hit. Keeps ranking implicit — source order\n // (freshest first from gh) wins when scores tie.\n const target = `#${item.number} ${item.title}`\n const score = match(target)\n if (!score) continue\n out.push({ ...item, score })\n }\n return out\n },\n render: defaultRender,\n triggers: [trigger],\n }\n}\n\nconst defaultRender: OptionRender<GithubItem> = (item, ctx) => {\n const { style } = ctx\n const num = `#${item.number}`\n const stateSlot = stateStyleSlot(item.state)\n const typeLabel = item.type === \"pr\" ? \"pr\" : \"issue\"\n const author = item.author?.login ? `@${item.author.login}` : \"\"\n\n // Layout: [#num] [state/type] title ............. @author\n // All chrome is left-aligned; author is the right-column hint.\n const lead = `${style.add(\"optionName\")(num)} ${style.add(stateSlot)(`${item.state}·${typeLabel}`)} ${item.title}`\n\n if (author === \"\") return lead\n const gap = 2\n const leadW = stringWidth(lead)\n const hintW = stringWidth(author)\n const padded = ctx.width - leadW - hintW - gap\n if (padded < 1) return lead\n return lead + \" \".repeat(padded + gap) + style.add(\"optionDesc\")(author)\n}\n\nfunction stateStyleSlot(state: string): string {\n if (state === \"open\") return \"success\"\n if (state === \"merged\") return \"accent\"\n // \"closed\" and anything unknown → dim.\n return \"muted\"\n}\n\n/** Default fetcher — shells out to `gh issue list` + `gh pr list` in\n * parallel. Either failing is swallowed so a repo with only issues\n * (or only PRs) still works. */\nfunction defaultFetcher(limit: number): GithubFetcher {\n const fields = \"number,title,state,author,url\"\n return async (cwd, state) => {\n const [issues, prs] = await Promise.all([\n runGh(cwd, [\"issue\", \"list\", \"--state\", state, \"--json\", fields, \"--limit\", String(limit)]),\n runGh(cwd, [\"pr\", \"list\", \"--state\", state, \"--json\", fields, \"--limit\", String(limit)]),\n ])\n return [\n ...parse(issues).map((x) => Object.assign(x, { type: \"issue\" as const })),\n ...parse(prs).map((x) => Object.assign(x, { type: \"pr\" as const })),\n ].map((item) => Object.assign(item, { text: `#${item.number} ${item.title}` }))\n }\n}\n\nfunction parse(out: string): Omit<GithubItem, \"type\">[] {\n try {\n return JSON.parse(out) as Omit<GithubItem, \"type\">[]\n } catch {\n return []\n }\n}\n\nfunction runGh(cwd: string, args: string[]): Promise<string> {\n return new Promise((resolve) => {\n try {\n const child = spawn(\"gh\", args, { cwd, stdio: [\"ignore\", \"pipe\", \"ignore\"] })\n let stdout = \"\"\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString(\"utf8\")\n })\n child.on(\"close\", (code) => {\n resolve(code === 0 ? stdout : \"\")\n })\n child.on(\"error\", () => {\n resolve(\"\")\n })\n } catch {\n resolve(\"\")\n }\n })\n}\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { ActionCompletionItem, ActionsSourceOptions, actionsSource } from "./actions.mjs";
2
+ import { FilesSourceOptions, filesSource } from "./files.mjs";
3
+ import { GithubFetcher, GithubItem, GithubSourceOptions, GithubState, githubSource } from "./github.mjs";
4
+ export { ActionCompletionItem, ActionsSourceOptions, FilesSourceOptions, GithubFetcher, GithubItem, GithubSourceOptions, GithubState, actionsSource, filesSource, githubSource };
@@ -0,0 +1,4 @@
1
+ import { actionsSource } from "./actions.mjs";
2
+ import { filesSource } from "./files.mjs";
3
+ import { githubSource } from "./github.mjs";
4
+ export { actionsSource, filesSource, githubSource };
@@ -0,0 +1,57 @@
1
+ import { J as Layout, P as RenderCtx, X as State, nt as Reactive, p as Node } from "../queries-DH3bj74K.mjs";
2
+ import { WrapMode } from "../text.mjs";
3
+ //#region src/widgets/diff.d.ts
4
+ interface DiffState {
5
+ /** Pre-state file content. Plain string or reactive accessor —
6
+ * pass a signal to live-update the diff as either side changes. */
7
+ original: Reactive<string>;
8
+ /** Post-state file content. The widget runs `diffLines` between
9
+ * `original` and `modified` and renders the resulting hunks. */
10
+ modified: Reactive<string>;
11
+ /** Optional file path. Drives the default `lang` (extension or
12
+ * basename — shiki recognises both `.ts` and `Dockerfile`-style
13
+ * names) and `title` when those aren't set explicitly. */
14
+ path?: Reactive<string>;
15
+ /** Language for syntax highlighting (any shiki-bundled name or
16
+ * alias). Defaults from `path` when set. */
17
+ lang?: string;
18
+ /** Lines of surrounding context per hunk. Default: 3. */
19
+ context?: number;
20
+ wrap?: WrapMode;
21
+ }
22
+ /**
23
+ * Show a unified diff between two file contents with syntax-highlighted
24
+ * rows. Added rows carry a green backdrop and `+` prefix; removed rows
25
+ * carry a red backdrop and `-` prefix; context rows show both line
26
+ * numbers with a neutral gutter.
27
+ *
28
+ * Both sides are highlighted as complete files (via shiki) before the
29
+ * diff is assembled, so context around an edit tokenizes the way it
30
+ * would in the actual file — multi-line strings, block comments, etc.
31
+ * don't get truncated by the hunk window.
32
+ */
33
+ declare class Diff extends Node<DiffState> {
34
+ #private;
35
+ constructor(state: DiffState);
36
+ get input(): {
37
+ original: string;
38
+ modified: string;
39
+ };
40
+ protected _render(ctx: RenderCtx): Promise<string[]>;
41
+ layout(): Layout;
42
+ }
43
+ /**
44
+ * Factory for `Diff`.
45
+ *
46
+ * ```ts
47
+ * diff({
48
+ * original: "function foo() {}\n",
49
+ * modified: "function foo(x) {}\n",
50
+ * path: "foo.ts",
51
+ * })
52
+ * ```
53
+ */
54
+ declare function diff(state: State<DiffState>): Diff;
55
+ //#endregion
56
+ export { Diff, DiffState, diff };
57
+ //# sourceMappingURL=diff.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.mts","names":[],"sources":["../../src/widgets/diff.ts"],"mappings":";;;UAeiB;;;EAGf,UAAU;;;EAGV,UAAU;;;;EAIV,OAAO;;;EAGP;;EAEA;EACA,OAAO;;;;;;;;;;;;;cAuBI,aAAa,KAAK;;EAI7B,YAAY,OAAO;MA+Bf;IAAW;IAAkB;;YAOjB,QAAQ,KAAK,YAAY;EAmBhC,UAAU;;;;;;;;;;;;;iBA4BL,KAAK,OAAO,MAAM,aAAa"}
@@ -0,0 +1,199 @@
1
+ import { S as unwrap, h as memo, r as createAsync, w as useContext } from "../reactive-f7NpAi69.mjs";
2
+ import { t as Node } from "../node-BZ7f6HzU.mjs";
3
+ import { calcLayout, countLines } from "../text.mjs";
4
+ import { t as RenderContext } from "../render-DxVLqqB9.mjs";
5
+ import { t as codeToAnsi } from "../shiki-BrjVUkeh.mjs";
6
+ import { splitAnsi, stringWidth, wrapAnsi } from "@zaly/shared/ansi";
7
+ import { basename, extname } from "pathe";
8
+ //#region src/widgets/diff.ts
9
+ /**
10
+ * Show a unified diff between two file contents with syntax-highlighted
11
+ * rows. Added rows carry a green backdrop and `+` prefix; removed rows
12
+ * carry a red backdrop and `-` prefix; context rows show both line
13
+ * numbers with a neutral gutter.
14
+ *
15
+ * Both sides are highlighted as complete files (via shiki) before the
16
+ * diff is assembled, so context around an edit tokenizes the way it
17
+ * would in the actual file — multi-line strings, block comments, etc.
18
+ * don't get truncated by the hunk window.
19
+ */
20
+ var Diff = class extends Node {
21
+ #lang;
22
+ #highlighted;
23
+ constructor(state) {
24
+ super({
25
+ wrap: "word",
26
+ ...state
27
+ });
28
+ this.#lang = memo(() => {
29
+ const path = unwrap(this.state.path);
30
+ return this.state.lang ?? (path !== void 0 ? langFromPath(path) : void 0);
31
+ });
32
+ const context = useContext(RenderContext);
33
+ this.#highlighted = createAsync(async () => {
34
+ const lang = this.#lang();
35
+ const input = this.input;
36
+ if (!lang) return input;
37
+ const theme = context?.style().theme.shiki;
38
+ const [original, modified] = await Promise.all([codeToAnsi(input.original, lang, { theme }), codeToAnsi(input.modified, lang, { theme })]);
39
+ return {
40
+ modified,
41
+ original
42
+ };
43
+ }, { initialValue: this.input });
44
+ }
45
+ #norm(s) {
46
+ return s.replaceAll("\r\n", "\n").replaceAll("\r", "");
47
+ }
48
+ get input() {
49
+ return {
50
+ modified: this.#norm(unwrap(this.state.modified)),
51
+ original: this.#norm(unwrap(this.state.original))
52
+ };
53
+ }
54
+ async _render(ctx) {
55
+ const highlighted = this.#highlighted();
56
+ const input = this.input;
57
+ const layout = this.layout();
58
+ const width = Math.min(ctx.width, layout.width);
59
+ const opts = {
60
+ context: this.state.context ?? 3,
61
+ ctx: {
62
+ ...ctx,
63
+ width
64
+ },
65
+ modified: input.modified.split("\n"),
66
+ modifiedHi: highlighted.modified.split("\n"),
67
+ original: input.original.split("\n"),
68
+ originalHi: highlighted.original.split("\n")
69
+ };
70
+ this.state.wrap ??= "word";
71
+ return await buildDiffRows(opts);
72
+ }
73
+ layout() {
74
+ const input = this.input;
75
+ const original = calcLayout(input.original, { wrap: this.state.wrap });
76
+ const modified = calcLayout(input.modified, { wrap: this.state.wrap });
77
+ const padding = Math.max(1, countLines(unwrap(this.state.original)), countLines(unwrap(this.state.modified))).toString().length * 2 + 4 + 4;
78
+ return {
79
+ minWidth: Math.max(original.minWidth, modified.minWidth) + padding,
80
+ width: Math.max(original.width, modified.width) + padding
81
+ };
82
+ }
83
+ };
84
+ /**
85
+ * Factory for `Diff`.
86
+ *
87
+ * ```ts
88
+ * diff({
89
+ * original: "function foo() {}\n",
90
+ * modified: "function foo(x) {}\n",
91
+ * path: "foo.ts",
92
+ * })
93
+ * ```
94
+ */
95
+ function diff(state) {
96
+ return new Diff(state);
97
+ }
98
+ async function buildDiffRows(opts) {
99
+ const context = opts.context;
100
+ const { diffArrays } = await import("diff");
101
+ const segments = diffArrays(opts.original, opts.modified);
102
+ const { original: origLines, modified: modLines, originalHi: origHi, modifiedHi: modHi, ctx } = opts;
103
+ const ann = [];
104
+ let oI = 0;
105
+ let mI = 0;
106
+ for (const seg of segments) if (seg.added) for (const _ of seg.value) ann.push({
107
+ kind: "add",
108
+ modIdx: mI++
109
+ });
110
+ else if (seg.removed) for (const _ of seg.value) ann.push({
111
+ kind: "remove",
112
+ origIdx: oI++
113
+ });
114
+ else for (const _ of seg.value) ann.push({
115
+ kind: "equal",
116
+ modIdx: mI++,
117
+ origIdx: oI++
118
+ });
119
+ const ranges = [];
120
+ for (let i = 0; i < ann.length; i++) {
121
+ if (ann[i].kind === "equal") continue;
122
+ const start = Math.max(0, i - context);
123
+ const end = Math.min(ann.length - 1, i + context);
124
+ const last = ranges.at(-1);
125
+ if (last && start <= last.end + 1) last.end = Math.max(last.end, end);
126
+ else ranges.push({
127
+ end,
128
+ start
129
+ });
130
+ }
131
+ if (ranges.length === 0) return [];
132
+ const rows = [];
133
+ for (const r of ranges) for (let i = r.start; i <= r.end; i++) {
134
+ const a = ann[i];
135
+ if (a.kind === "equal") rows.push({
136
+ content: modHi[a.modIdx] ?? origHi[a.origIdx] ?? "",
137
+ newNum: a.modIdx + 1,
138
+ origNum: a.origIdx + 1,
139
+ type: "context"
140
+ });
141
+ else if (a.kind === "remove") rows.push({
142
+ content: origHi[a.origIdx] ?? "",
143
+ origNum: a.origIdx + 1,
144
+ type: "remove"
145
+ });
146
+ else rows.push({
147
+ content: modHi[a.modIdx] ?? "",
148
+ newNum: a.modIdx + 1,
149
+ type: "add"
150
+ });
151
+ }
152
+ return renderRows(ctx, rows, origLines.length, modLines.length);
153
+ }
154
+ function renderRows(ctx, rows, origTotal, modTotal) {
155
+ const numWidth = Math.max(String(origTotal).length, String(modTotal).length, 1);
156
+ const s = ctx.style;
157
+ const gutterWidth = numWidth * 2 + 4;
158
+ const contentWidth = Math.max(0, ctx.width - gutterWidth);
159
+ const blankGutter = " ".repeat(gutterWidth - 2) + s.dim("↪ ");
160
+ const styles = {
161
+ add: "diffAdd",
162
+ context: "diffContext",
163
+ remove: "diffDel"
164
+ };
165
+ const out = [];
166
+ for (const r of rows) {
167
+ const origStr = r.type === "add" ? "" : String(r.origNum);
168
+ const newStr = r.type === "remove" ? "" : String(r.newNum);
169
+ const gutterStyle = s.bg(`${styles[r.type]}+10`);
170
+ const gutter = gutterStyle(` ${pad(origStr, numWidth)} ${pad(newStr, numWidth)} `);
171
+ const prefix = r.type === "add" ? " + " : r.type === "remove" ? " - " : " ";
172
+ const innerWidth = Math.max(0, contentWidth - stringWidth(prefix) - 1);
173
+ const wrapped = innerWidth > 0 && stringWidth(r.content) > innerWidth ? splitAnsi(wrapAnsi(r.content, innerWidth, { mode: "word" })) : [r.content];
174
+ const rowStyle = s.bg(styles[r.type]);
175
+ for (let i = 0; i < wrapped.length; i++) {
176
+ const wline = wrapped[i];
177
+ const pw = Math.max(0, innerWidth - stringWidth(wline) + 1);
178
+ const bodyLine = (i === 0 ? prefix : " ") + wline + " ".repeat(pw);
179
+ const styledBody = rowStyle?.(bodyLine) ?? bodyLine;
180
+ out.push((i === 0 ? gutter : gutterStyle(blankGutter)) + styledBody);
181
+ }
182
+ }
183
+ return out;
184
+ }
185
+ function pad(s, width) {
186
+ return s.length >= width ? s : " ".repeat(width - s.length) + s;
187
+ }
188
+ /** Resolve a file path to a shiki language identifier. Falls back from
189
+ * extension to basename — shiki natively recognises `dockerfile`,
190
+ * `makefile`, `cmakelists.txt`, etc. */
191
+ function langFromPath(path) {
192
+ const ext = extname(path).slice(1).toLowerCase();
193
+ if (ext) return ext;
194
+ return basename(path).toLowerCase() || void 0;
195
+ }
196
+ //#endregion
197
+ export { Diff, diff };
198
+
199
+ //# sourceMappingURL=diff.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.mjs","names":["#lang","#highlighted","#norm"],"sources":["../../src/widgets/diff.ts"],"sourcesContent":["import type { RenderCtx } from \"../core/ctx.ts\"\nimport type { Accessor, Reactive } from \"../core/reactive.ts\"\nimport type { Layout, State } from \"../core/state.ts\"\nimport type { WrapMode } from \"../layout/text.ts\"\n\nimport { splitAnsi, stringWidth, wrapAnsi } from \"@zaly/shared/ansi\"\nimport { basename, extname } from \"pathe\"\n// oxlint-disable no-nested-ternary\n// oxlint-disable typescript/no-unnecessary-condition\nimport { Node } from \"../core/node.ts\"\nimport { createAsync, memo, unwrap, useContext } from \"../core/reactive.ts\"\nimport { RenderContext } from \"../core/render.ts\"\nimport { calcLayout, countLines } from \"../layout/text.ts\"\nimport { codeToAnsi } from \"../shiki/shiki.ts\"\n\nexport interface DiffState {\n /** Pre-state file content. Plain string or reactive accessor —\n * pass a signal to live-update the diff as either side changes. */\n original: Reactive<string>\n /** Post-state file content. The widget runs `diffLines` between\n * `original` and `modified` and renders the resulting hunks. */\n modified: Reactive<string>\n /** Optional file path. Drives the default `lang` (extension or\n * basename — shiki recognises both `.ts` and `Dockerfile`-style\n * names) and `title` when those aren't set explicitly. */\n path?: Reactive<string>\n /** Language for syntax highlighting (any shiki-bundled name or\n * alias). Defaults from `path` when set. */\n lang?: string\n /** Lines of surrounding context per hunk. Default: 3. */\n context?: number\n wrap?: WrapMode\n}\n\ntype DiffOpts = {\n original: string[]\n originalHi: string[]\n modified: string[]\n modifiedHi: string[]\n context: number\n ctx: RenderCtx\n}\n\n/**\n * Show a unified diff between two file contents with syntax-highlighted\n * rows. Added rows carry a green backdrop and `+` prefix; removed rows\n * carry a red backdrop and `-` prefix; context rows show both line\n * numbers with a neutral gutter.\n *\n * Both sides are highlighted as complete files (via shiki) before the\n * diff is assembled, so context around an edit tokenizes the way it\n * would in the actual file — multi-line strings, block comments, etc.\n * don't get truncated by the hunk window.\n */\nexport class Diff extends Node<DiffState> {\n #lang: Accessor<string | undefined>\n #highlighted: Accessor<{ original: string; modified: string }>\n\n constructor(state: DiffState) {\n super({ wrap: \"word\", ...state })\n\n this.#lang = memo(() => {\n const path = unwrap(this.state.path)\n return this.state.lang ?? (path !== undefined ? langFromPath(path) : undefined)\n })\n\n const context = useContext(RenderContext)\n this.#highlighted = createAsync(\n async () => {\n const lang = this.#lang()\n const input = this.input\n if (!lang) return input\n const theme = context?.style().theme.shiki\n const [original, modified] = await Promise.all([\n codeToAnsi(input.original, lang, { theme }),\n codeToAnsi(input.modified, lang, { theme }),\n ])\n return { modified, original }\n },\n {\n initialValue: this.input,\n }\n )\n }\n\n #norm(s: string): string {\n return s.replaceAll(\"\\r\\n\", \"\\n\").replaceAll(\"\\r\", \"\")\n }\n\n get input(): { original: string; modified: string } {\n return {\n modified: this.#norm(unwrap(this.state.modified)),\n original: this.#norm(unwrap(this.state.original)),\n }\n }\n\n protected async _render(ctx: RenderCtx): Promise<string[]> {\n const highlighted = this.#highlighted()\n const input = this.input\n const layout = this.layout()\n const width = Math.min(ctx.width, layout.width)\n\n const opts: DiffOpts = {\n context: this.state.context ?? 3,\n ctx: { ...ctx, width },\n modified: input.modified.split(\"\\n\"),\n modifiedHi: highlighted.modified.split(\"\\n\"),\n original: input.original.split(\"\\n\"),\n originalHi: highlighted.original.split(\"\\n\"),\n }\n\n this.state.wrap ??= \"word\"\n return await buildDiffRows(opts)\n }\n\n override layout(): Layout {\n const input = this.input\n const original = calcLayout(input.original, { wrap: this.state.wrap })\n const modified = calcLayout(input.modified, { wrap: this.state.wrap })\n const numWidth = Math.max(\n 1,\n countLines(unwrap(this.state.original)),\n countLines(unwrap(this.state.modified))\n ).toString().length\n const padding = numWidth * 2 + 4 + 4\n return {\n minWidth: Math.max(original.minWidth, modified.minWidth) + padding,\n width: Math.max(original.width, modified.width) + padding,\n }\n }\n}\n\n/**\n * Factory for `Diff`.\n *\n * ```ts\n * diff({\n * original: \"function foo() {}\\n\",\n * modified: \"function foo(x) {}\\n\",\n * path: \"foo.ts\",\n * })\n * ```\n */\nexport function diff(state: State<DiffState>): Diff {\n return new Diff(state)\n}\n\n// ---------- internals ----------\n\ntype DiffRow =\n | { type: \"context\"; origNum: number; newNum: number; content: string }\n | { type: \"remove\"; origNum: number; content: string }\n | { type: \"add\"; newNum: number; content: string }\n\nasync function buildDiffRows(opts: DiffOpts): Promise<string[]> {\n const context = opts.context\n // Async-import keeps cold start snappy — only the diff widget pulls\n // in the library, and it lands at first render rather than at module\n // load.\n const { diffArrays } = await import(\"diff\")\n const segments = diffArrays(opts.original, opts.modified)\n const {\n original: origLines,\n modified: modLines,\n originalHi: origHi,\n modifiedHi: modHi,\n ctx,\n } = opts\n\n // Annotate each line in the diff with its kind + (orig, mod) indices.\n // The flattened list is the row-order the diff would print without\n // any hunking; we compute hunks (with surrounding context) on top.\n interface Annotated {\n kind: \"equal\" | \"remove\" | \"add\"\n origIdx?: number\n modIdx?: number\n }\n const ann: Annotated[] = []\n let oI = 0\n let mI = 0\n for (const seg of segments) {\n if (seg.added) {\n for (const _ of seg.value) ann.push({ kind: \"add\", modIdx: mI++ })\n } else if (seg.removed) {\n for (const _ of seg.value) ann.push({ kind: \"remove\", origIdx: oI++ })\n } else {\n for (const _ of seg.value) ann.push({ kind: \"equal\", modIdx: mI++, origIdx: oI++ })\n }\n }\n\n // Find hunks: ranges around each non-equal line, expanded by `context`,\n // merged when adjacent ranges touch.\n const ranges: { start: number; end: number }[] = []\n for (let i = 0; i < ann.length; i++) {\n if (ann[i].kind === \"equal\") continue\n const start = Math.max(0, i - context)\n const end = Math.min(ann.length - 1, i + context)\n const last = ranges.at(-1)\n if (last && start <= last.end + 1) last.end = Math.max(last.end, end)\n else ranges.push({ end, start })\n }\n if (ranges.length === 0) return [] // nothing changed\n\n // Emit rows for each hunk. (Between hunks we just skip the equal\n // lines — a `…` divider is a future polish.)\n const rows: DiffRow[] = []\n for (const r of ranges) {\n for (let i = r.start; i <= r.end; i++) {\n const a = ann[i]\n if (a.kind === \"equal\") {\n rows.push({\n content: modHi[a.modIdx!] ?? origHi[a.origIdx!] ?? \"\",\n newNum: a.modIdx! + 1,\n origNum: a.origIdx! + 1,\n type: \"context\",\n })\n } else if (a.kind === \"remove\") {\n rows.push({\n content: origHi[a.origIdx!] ?? \"\",\n origNum: a.origIdx! + 1,\n type: \"remove\",\n })\n } else {\n rows.push({\n content: modHi[a.modIdx!] ?? \"\",\n newNum: a.modIdx! + 1,\n type: \"add\",\n })\n }\n }\n }\n\n return renderRows(ctx, rows, origLines.length, modLines.length)\n}\n\nfunction renderRows(\n ctx: RenderCtx,\n rows: DiffRow[],\n origTotal: number,\n modTotal: number\n): string[] {\n const numWidth = Math.max(String(origTotal).length, String(modTotal).length, 1)\n const s = ctx.style\n\n // Two-column gutter: original line number | new line number | space.\n // - Context rows show both numbers.\n // - Remove rows show only the original number; new column blank.\n // - Add rows show only the new number; original column blank.\n // Wrapped continuation lines reuse the row style but emit a blank\n // gutter so the line number isn't repeated per visual row.\n const gutterWidth = numWidth * 2 + 4 // \"<orig> <new> \"\n const contentWidth = Math.max(0, ctx.width - gutterWidth)\n const blankGutter = \" \".repeat(gutterWidth - 2) + s.dim(\"↪ \")\n\n const styles = {\n add: \"diffAdd\",\n context: \"diffContext\",\n remove: \"diffDel\",\n } as const\n\n const out: string[] = []\n for (const r of rows) {\n const origStr = r.type === \"add\" ? \"\" : String(r.origNum)\n const newStr = r.type === \"remove\" ? \"\" : String(r.newNum)\n const gutterStyle = s.bg(`${styles[r.type]}+10`)\n const gutter = gutterStyle(` ${pad(origStr, numWidth)} ${pad(newStr, numWidth)} `)\n\n const prefix = r.type === \"add\" ? \" + \" : r.type === \"remove\" ? \" - \" : \" \"\n const innerWidth = Math.max(0, contentWidth - stringWidth(prefix) - 1)\n\n // Hard-wrap the content. Char-wrap (vs. word-wrap) keeps code\n // diffs honest — splitting by word boundaries would shift columns\n // mid-line and make alignment confusing.\n const wrapped =\n innerWidth > 0 && stringWidth(r.content) > innerWidth\n ? splitAnsi(wrapAnsi(r.content, innerWidth, { mode: \"word\" }))\n : [r.content]\n\n const rowStyle = s.bg(styles[r.type])\n for (let i = 0; i < wrapped.length; i++) {\n const wline = wrapped[i]\n const pw = Math.max(0, innerWidth - stringWidth(wline) + 1)\n const bodyLine = (i === 0 ? prefix : \" \") + wline + \" \".repeat(pw)\n const styledBody = rowStyle?.(bodyLine) ?? bodyLine\n out.push((i === 0 ? gutter : gutterStyle(blankGutter)) + styledBody)\n }\n }\n return out\n}\n\nfunction pad(s: string, width: number): string {\n return s.length >= width ? s : \" \".repeat(width - s.length) + s\n}\n\n/** Resolve a file path to a shiki language identifier. Falls back from\n * extension to basename — shiki natively recognises `dockerfile`,\n * `makefile`, `cmakelists.txt`, etc. */\nfunction langFromPath(path: string): string | undefined {\n const ext = extname(path).slice(1).toLowerCase()\n if (ext) return ext\n const base = basename(path).toLowerCase()\n return base || undefined\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsDA,IAAa,OAAb,cAA0B,KAAgB;CACxC;CACA;CAEA,YAAY,OAAkB;EAC5B,MAAM;GAAE,MAAM;GAAQ,GAAG;EAAM,CAAC;EAEhC,KAAKA,QAAQ,WAAW;GACtB,MAAM,OAAO,OAAO,KAAK,MAAM,IAAI;GACnC,OAAO,KAAK,MAAM,SAAS,SAAS,KAAA,IAAY,aAAa,IAAI,IAAI,KAAA;EACvE,CAAC;EAED,MAAM,UAAU,WAAW,aAAa;EACxC,KAAKC,eAAe,YAClB,YAAY;GACV,MAAM,OAAO,KAAKD,MAAM;GACxB,MAAM,QAAQ,KAAK;GACnB,IAAI,CAAC,MAAM,OAAO;GAClB,MAAM,QAAQ,SAAS,MAAM,CAAC,CAAC,MAAM;GACrC,MAAM,CAAC,UAAU,YAAY,MAAM,QAAQ,IAAI,CAC7C,WAAW,MAAM,UAAU,MAAM,EAAE,MAAM,CAAC,GAC1C,WAAW,MAAM,UAAU,MAAM,EAAE,MAAM,CAAC,CAC5C,CAAC;GACD,OAAO;IAAE;IAAU;GAAS;EAC9B,GACA,EACE,cAAc,KAAK,MACrB,CACF;CACF;CAEA,MAAM,GAAmB;EACvB,OAAO,EAAE,WAAW,QAAQ,IAAI,CAAC,CAAC,WAAW,MAAM,EAAE;CACvD;CAEA,IAAI,QAAgD;EAClD,OAAO;GACL,UAAU,KAAKE,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC;GAChD,UAAU,KAAKA,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC;EAClD;CACF;CAEA,MAAgB,QAAQ,KAAmC;EACzD,MAAM,cAAc,KAAKD,aAAa;EACtC,MAAM,QAAQ,KAAK;EACnB,MAAM,SAAS,KAAK,OAAO;EAC3B,MAAM,QAAQ,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK;EAE9C,MAAM,OAAiB;GACrB,SAAS,KAAK,MAAM,WAAW;GAC/B,KAAK;IAAE,GAAG;IAAK;GAAM;GACrB,UAAU,MAAM,SAAS,MAAM,IAAI;GACnC,YAAY,YAAY,SAAS,MAAM,IAAI;GAC3C,UAAU,MAAM,SAAS,MAAM,IAAI;GACnC,YAAY,YAAY,SAAS,MAAM,IAAI;EAC7C;EAEA,KAAK,MAAM,SAAS;EACpB,OAAO,MAAM,cAAc,IAAI;CACjC;CAEA,SAA0B;EACxB,MAAM,QAAQ,KAAK;EACnB,MAAM,WAAW,WAAW,MAAM,UAAU,EAAE,MAAM,KAAK,MAAM,KAAK,CAAC;EACrE,MAAM,WAAW,WAAW,MAAM,UAAU,EAAE,MAAM,KAAK,MAAM,KAAK,CAAC;EAMrE,MAAM,UALW,KAAK,IACpB,GACA,WAAW,OAAO,KAAK,MAAM,QAAQ,CAAC,GACtC,WAAW,OAAO,KAAK,MAAM,QAAQ,CAAC,CACxC,CAAC,CAAC,SAAS,CAAC,CAAC,SACc,IAAI,IAAI;EACnC,OAAO;GACL,UAAU,KAAK,IAAI,SAAS,UAAU,SAAS,QAAQ,IAAI;GAC3D,OAAO,KAAK,IAAI,SAAS,OAAO,SAAS,KAAK,IAAI;EACpD;CACF;AACF;;;;;;;;;;;;AAaA,SAAgB,KAAK,OAA+B;CAClD,OAAO,IAAI,KAAK,KAAK;AACvB;AASA,eAAe,cAAc,MAAmC;CAC9D,MAAM,UAAU,KAAK;CAIrB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,WAAW,WAAW,KAAK,UAAU,KAAK,QAAQ;CACxD,MAAM,EACJ,UAAU,WACV,UAAU,UACV,YAAY,QACZ,YAAY,OACZ,QACE;CAUJ,MAAM,MAAmB,CAAC;CAC1B,IAAI,KAAK;CACT,IAAI,KAAK;CACT,KAAK,MAAM,OAAO,UAChB,IAAI,IAAI,OACN,KAAK,MAAM,KAAK,IAAI,OAAO,IAAI,KAAK;EAAE,MAAM;EAAO,QAAQ;CAAK,CAAC;MAC5D,IAAI,IAAI,SACb,KAAK,MAAM,KAAK,IAAI,OAAO,IAAI,KAAK;EAAE,MAAM;EAAU,SAAS;CAAK,CAAC;MAErE,KAAK,MAAM,KAAK,IAAI,OAAO,IAAI,KAAK;EAAE,MAAM;EAAS,QAAQ;EAAM,SAAS;CAAK,CAAC;CAMtF,MAAM,SAA2C,CAAC;CAClD,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;EACnC,IAAI,IAAI,EAAE,CAAC,SAAS,SAAS;EAC7B,MAAM,QAAQ,KAAK,IAAI,GAAG,IAAI,OAAO;EACrC,MAAM,MAAM,KAAK,IAAI,IAAI,SAAS,GAAG,IAAI,OAAO;EAChD,MAAM,OAAO,OAAO,GAAG,EAAE;EACzB,IAAI,QAAQ,SAAS,KAAK,MAAM,GAAG,KAAK,MAAM,KAAK,IAAI,KAAK,KAAK,GAAG;OAC/D,OAAO,KAAK;GAAE;GAAK;EAAM,CAAC;CACjC;CACA,IAAI,OAAO,WAAW,GAAG,OAAO,CAAC;CAIjC,MAAM,OAAkB,CAAC;CACzB,KAAK,MAAM,KAAK,QACd,KAAK,IAAI,IAAI,EAAE,OAAO,KAAK,EAAE,KAAK,KAAK;EACrC,MAAM,IAAI,IAAI;EACd,IAAI,EAAE,SAAS,SACb,KAAK,KAAK;GACR,SAAS,MAAM,EAAE,WAAY,OAAO,EAAE,YAAa;GACnD,QAAQ,EAAE,SAAU;GACpB,SAAS,EAAE,UAAW;GACtB,MAAM;EACR,CAAC;OACI,IAAI,EAAE,SAAS,UACpB,KAAK,KAAK;GACR,SAAS,OAAO,EAAE,YAAa;GAC/B,SAAS,EAAE,UAAW;GACtB,MAAM;EACR,CAAC;OAED,KAAK,KAAK;GACR,SAAS,MAAM,EAAE,WAAY;GAC7B,QAAQ,EAAE,SAAU;GACpB,MAAM;EACR,CAAC;CAEL;CAGF,OAAO,WAAW,KAAK,MAAM,UAAU,QAAQ,SAAS,MAAM;AAChE;AAEA,SAAS,WACP,KACA,MACA,WACA,UACU;CACV,MAAM,WAAW,KAAK,IAAI,OAAO,SAAS,CAAC,CAAC,QAAQ,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC9E,MAAM,IAAI,IAAI;CAQd,MAAM,cAAc,WAAW,IAAI;CACnC,MAAM,eAAe,KAAK,IAAI,GAAG,IAAI,QAAQ,WAAW;CACxD,MAAM,cAAc,IAAI,OAAO,cAAc,CAAC,IAAI,EAAE,IAAI,IAAI;CAE5D,MAAM,SAAS;EACb,KAAK;EACL,SAAS;EACT,QAAQ;CACV;CAEA,MAAM,MAAgB,CAAC;CACvB,KAAK,MAAM,KAAK,MAAM;EACpB,MAAM,UAAU,EAAE,SAAS,QAAQ,KAAK,OAAO,EAAE,OAAO;EACxD,MAAM,SAAS,EAAE,SAAS,WAAW,KAAK,OAAO,EAAE,MAAM;EACzD,MAAM,cAAc,EAAE,GAAG,GAAG,OAAO,EAAE,MAAM,IAAI;EAC/C,MAAM,SAAS,YAAY,IAAI,IAAI,SAAS,QAAQ,EAAE,IAAI,IAAI,QAAQ,QAAQ,EAAE,EAAE;EAElF,MAAM,SAAS,EAAE,SAAS,QAAQ,QAAQ,EAAE,SAAS,WAAW,QAAQ;EACxE,MAAM,aAAa,KAAK,IAAI,GAAG,eAAe,YAAY,MAAM,IAAI,CAAC;EAKrE,MAAM,UACJ,aAAa,KAAK,YAAY,EAAE,OAAO,IAAI,aACvC,UAAU,SAAS,EAAE,SAAS,YAAY,EAAE,MAAM,OAAO,CAAC,CAAC,IAC3D,CAAC,EAAE,OAAO;EAEhB,MAAM,WAAW,EAAE,GAAG,OAAO,EAAE,KAAK;EACpC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,QAAQ,QAAQ;GACtB,MAAM,KAAK,KAAK,IAAI,GAAG,aAAa,YAAY,KAAK,IAAI,CAAC;GAC1D,MAAM,YAAY,MAAM,IAAI,SAAS,SAAS,QAAQ,IAAI,OAAO,EAAE;GACnE,MAAM,aAAa,WAAW,QAAQ,KAAK;GAC3C,IAAI,MAAM,MAAM,IAAI,SAAS,YAAY,WAAW,KAAK,UAAU;EACrE;CACF;CACA,OAAO;AACT;AAEA,SAAS,IAAI,GAAW,OAAuB;CAC7C,OAAO,EAAE,UAAU,QAAQ,IAAI,IAAI,OAAO,QAAQ,EAAE,MAAM,IAAI;AAChE;;;;AAKA,SAAS,aAAa,MAAkC;CACtD,MAAM,MAAM,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY;CAC/C,IAAI,KAAK,OAAO;CAEhB,OADa,SAAS,IAAI,CAAC,CAAC,YAClB,KAAK,KAAA;AACjB"}
@@ -0,0 +1,11 @@
1
+ import { H as Text } from "../queries-DH3bj74K.mjs";
2
+ import { f as Style } from "../types-B9Eh7k2v.mjs";
3
+ //#region src/widgets/divider.d.ts
4
+ type DividerState = Style & {
5
+ char?: string;
6
+ length?: number;
7
+ };
8
+ declare function divider(state?: DividerState): Text;
9
+ //#endregion
10
+ export { DividerState, divider };
11
+ //# sourceMappingURL=divider.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"divider.d.mts","names":[],"sources":["../../src/widgets/divider.ts"],"mappings":";;;KAKY,eAAe;EACzB;EACA;;iBAGc,QAAQ,QAAO,eAAsC"}
@@ -0,0 +1,9 @@
1
+ import { text } from "./text.mjs";
2
+ //#region src/widgets/divider.ts
3
+ function divider(state = { style: "divider" }) {
4
+ return text((ctx) => (state.char ?? "─").repeat(state.length ?? ctx.width), state);
5
+ }
6
+ //#endregion
7
+ export { divider };
8
+
9
+ //# sourceMappingURL=divider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"divider.mjs","names":[],"sources":["../../src/widgets/divider.ts"],"sourcesContent":["import type { Style } from \"../style/types.ts\"\nimport type { Text } from \"./text.ts\"\n\nimport { text } from \"./text.ts\"\n\nexport type DividerState = Style & {\n char?: string\n length?: number\n}\n\nexport function divider(state: DividerState = { style: \"divider\" }): Text {\n return text((ctx) => (state.char ?? \"─\").repeat(state.length ?? ctx.width), state)\n}\n"],"mappings":";;AAUA,SAAgB,QAAQ,QAAsB,EAAE,OAAO,UAAU,GAAS;CACxE,OAAO,MAAM,SAAS,MAAM,QAAQ,IAAA,CAAK,OAAO,MAAM,UAAU,IAAI,KAAK,GAAG,KAAK;AACnF"}
@@ -0,0 +1,37 @@
1
+ import { J as Layout, P as RenderCtx, p as Node } from "../queries-DH3bj74K.mjs";
2
+ //#region src/widgets/image.d.ts
3
+ interface ImageState {
4
+ /**
5
+ * Path to an image file. PNG passes through with zero decoding; other
6
+ * formats (JPEG/WebP/GIF/AVIF/SVG) are read via image-meta for
7
+ * dimensions and — for KGP terminals — converted once to a cached
8
+ * temp PNG via sharp. iTerm2 accepts all formats natively.
9
+ */
10
+ src: string;
11
+ /** Display width in terminal cells. Defaults to `ctx.width`. */
12
+ width?: number;
13
+ /**
14
+ * Display height in terminal cells. When omitted, computed from the
15
+ * source aspect ratio + `cellAspect`.
16
+ */
17
+ height?: number;
18
+ /** Fallback text shown on terminals without an image protocol. */
19
+ alt?: string;
20
+ /**
21
+ * Character cell aspect ratio (cellHeight / cellWidth). Most terminals
22
+ * land around 2.0. Tweak if aspect-preserved images look stretched.
23
+ */
24
+ cellAspect?: number;
25
+ }
26
+ declare class Image extends Node<ImageState> {
27
+ #private;
28
+ constructor(state: ImageState);
29
+ get fallback(): string[];
30
+ layout(): Layout;
31
+ protected _render(ctx: RenderCtx): Promise<string[]>;
32
+ }
33
+ declare function image(src: string, style?: Omit<ImageState, "src">): Image;
34
+ declare function image(state: ImageState): Image;
35
+ //#endregion
36
+ export { Image, ImageState, image };
37
+ //# sourceMappingURL=image.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.d.mts","names":[],"sources":["../../src/widgets/image.ts"],"mappings":";;UAWiB;;;;;;;EAOf;;EAEA;;;;;EAKA;;EAEA;;;;;EAKA;;cAGW,cAAc,KAAK;;EAI9B,YAAY,OAAO;MAOf;EAIK,UAAU;YAIH,QAAQ,KAAK,YAAY;;iBAuC3B,MAAM,aAAa,QAAQ,KAAK,qBAAqB;iBACrD,MAAM,OAAO,aAAa"}
@@ -0,0 +1,77 @@
1
+ import { S as unwrap, w as useContext } from "../reactive-f7NpAi69.mjs";
2
+ import { t as Node } from "../node-BZ7f6HzU.mjs";
3
+ import { t as RenderContext } from "../render-DxVLqqB9.mjs";
4
+ import { s as loadKittyGraphics } from "../kitty-JbCJC0iQ.mjs";
5
+ import { fileDetect } from "@zaly/shared/detect";
6
+ import { imageInfo } from "@zaly/shared/image";
7
+ //#region src/widgets/image.ts
8
+ var Image = class extends Node {
9
+ #delete;
10
+ #enabled;
11
+ constructor(state) {
12
+ super(state);
13
+ this.on("unmount", () => this.#delete?.());
14
+ const context = useContext(RenderContext);
15
+ this.#enabled = context?.images;
16
+ }
17
+ get fallback() {
18
+ return [this.state.alt ?? `[Image: ${this.state.src}]`];
19
+ }
20
+ layout() {
21
+ return {
22
+ minWidth: 50,
23
+ width: 100
24
+ };
25
+ }
26
+ async _render(ctx) {
27
+ if (unwrap(this.#enabled) === false) return this.fallback;
28
+ const queries = this.ctx?.input.queries;
29
+ const kitty = queries ? await loadKittyGraphics(queries) : void 0;
30
+ if (!kitty?.supported) return this.fallback;
31
+ const detected = await fileDetect(this.state.src);
32
+ if (detected?.type !== "image") return this.fallback;
33
+ const img = await imageInfo(detected);
34
+ const { cols, rows } = dims(this.state, img, Math.min(ctx.width, 80));
35
+ const t = await kitty.transmitOnce(img);
36
+ if (!t) return this.fallback;
37
+ const p = kitty.placement(t.imageId, {
38
+ cols,
39
+ rows
40
+ });
41
+ if (!p) return this.fallback;
42
+ this.#delete = () => p.placementId ? kitty.deletePlacement(t.imageId, p.placementId) : void 0;
43
+ if (t.transmit) {
44
+ ctx.transmit(t.transmit);
45
+ if (p.inline) ctx.transmit(p.seq);
46
+ }
47
+ if (p.inline) return p.data;
48
+ const ret = [...p.data];
49
+ if (!ret.length) return this.fallback;
50
+ ret[0] = p.seq + ret[0];
51
+ return ret;
52
+ }
53
+ };
54
+ function image(first, style) {
55
+ if (typeof first === "string") return new Image({
56
+ src: first,
57
+ ...style
58
+ });
59
+ return new Image(first);
60
+ }
61
+ function dims(state, meta, available) {
62
+ const cellAspect = state.cellAspect ?? 2;
63
+ const aspect = meta.height / meta.width;
64
+ let cols = state.width;
65
+ let rows = state.height;
66
+ if (cols === void 0 && rows === void 0) cols = Math.max(1, available);
67
+ if (cols !== void 0 && rows === void 0) rows = Math.max(1, Math.round(cols * aspect / cellAspect));
68
+ else if (rows !== void 0 && cols === void 0) cols = Math.max(1, Math.round(rows * cellAspect / aspect));
69
+ return {
70
+ cols: cols ?? available,
71
+ rows: rows ?? 1
72
+ };
73
+ }
74
+ //#endregion
75
+ export { Image, image };
76
+
77
+ //# sourceMappingURL=image.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.mjs","names":["#delete","#enabled"],"sources":["../../src/widgets/image.ts"],"sourcesContent":["import type { RenderCtx } from \"../core/ctx.ts\"\nimport type { Accessor } from \"../core/reactive.ts\"\nimport type { Layout } from \"../core/state.ts\"\n\nimport { fileDetect } from \"@zaly/shared/detect\"\nimport { imageInfo } from \"@zaly/shared/image\"\nimport { Node } from \"../core/node.ts\"\nimport { unwrap, useContext } from \"../core/reactive.ts\"\nimport { RenderContext } from \"../core/render.ts\"\nimport { loadKittyGraphics } from \"../image/kitty.ts\"\n\nexport interface ImageState {\n /**\n * Path to an image file. PNG passes through with zero decoding; other\n * formats (JPEG/WebP/GIF/AVIF/SVG) are read via image-meta for\n * dimensions and — for KGP terminals — converted once to a cached\n * temp PNG via sharp. iTerm2 accepts all formats natively.\n */\n src: string\n /** Display width in terminal cells. Defaults to `ctx.width`. */\n width?: number\n /**\n * Display height in terminal cells. When omitted, computed from the\n * source aspect ratio + `cellAspect`.\n */\n height?: number\n /** Fallback text shown on terminals without an image protocol. */\n alt?: string\n /**\n * Character cell aspect ratio (cellHeight / cellWidth). Most terminals\n * land around 2.0. Tweak if aspect-preserved images look stretched.\n */\n cellAspect?: number\n}\n\nexport class Image extends Node<ImageState> {\n #delete?: () => void\n #enabled?: Accessor<boolean>\n\n constructor(state: ImageState) {\n super(state)\n this.on(\"unmount\", () => this.#delete?.())\n const context = useContext(RenderContext)\n this.#enabled = context?.images\n }\n\n get fallback(): string[] {\n return [this.state.alt ?? `[Image: ${this.state.src}]`]\n }\n\n override layout(): Layout {\n return { minWidth: 50, width: 100 }\n }\n\n protected async _render(ctx: RenderCtx): Promise<string[]> {\n if (unwrap(this.#enabled) === false) return this.fallback\n\n const queries = this.ctx?.input.queries\n const kitty = queries ? await loadKittyGraphics(queries) : undefined\n if (!kitty?.supported) return this.fallback\n\n const detected = await fileDetect(this.state.src)\n if (detected?.type !== \"image\") return this.fallback\n const img = await imageInfo(detected)\n\n const { cols, rows } = dims(this.state, img, Math.min(ctx.width, 80))\n\n const t = await kitty.transmitOnce(img)\n if (!t) return this.fallback\n const p = kitty.placement(t.imageId, { cols, rows })\n if (!p) return this.fallback\n this.#delete = () =>\n p.placementId ? kitty.deletePlacement(t.imageId, p.placementId) : undefined\n // Route the transmit bytes through the side-channel queue (mount\n // ctx → terminal). Cached rows then hold pure placement ANSI,\n // which is safe to reuse across repaints. If we're rendering\n // before mount (rare — tests, headless previews), fall back to\n // inlining the transmit into the lead so the picture still\n // shows correctly the first time.\n if (t.transmit) {\n ctx.transmit(t.transmit)\n if (p.inline) ctx.transmit(p.seq)\n }\n\n if (p.inline) return p.data\n\n const ret = [...p.data]\n if (!ret.length) return this.fallback\n ret[0] = p.seq + ret[0]\n return ret\n }\n}\n\nexport function image(src: string, style?: Omit<ImageState, \"src\">): Image\nexport function image(state: ImageState): Image\nexport function image(first: string | ImageState, style?: Omit<ImageState, \"src\">): Image {\n if (typeof first === \"string\") return new Image({ src: first, ...style })\n return new Image(first)\n}\n\nfunction dims(\n state: ImageState,\n meta: { width: number; height: number },\n available: number\n): { cols: number; rows: number } {\n const cellAspect = state.cellAspect ?? 2\n const aspect = meta.height / meta.width\n let cols = state.width\n let rows = state.height\n if (cols === undefined && rows === undefined) cols = Math.max(1, available)\n if (cols !== undefined && rows === undefined) {\n rows = Math.max(1, Math.round((cols * aspect) / cellAspect))\n } else if (rows !== undefined && cols === undefined) {\n cols = Math.max(1, Math.round((rows * cellAspect) / aspect))\n }\n return { cols: cols ?? available, rows: rows ?? 1 }\n}\n"],"mappings":";;;;;;;AAmCA,IAAa,QAAb,cAA2B,KAAiB;CAC1C;CACA;CAEA,YAAY,OAAmB;EAC7B,MAAM,KAAK;EACX,KAAK,GAAG,iBAAiB,KAAKA,UAAU,CAAC;EACzC,MAAM,UAAU,WAAW,aAAa;EACxC,KAAKC,WAAW,SAAS;CAC3B;CAEA,IAAI,WAAqB;EACvB,OAAO,CAAC,KAAK,MAAM,OAAO,WAAW,KAAK,MAAM,IAAI,EAAE;CACxD;CAEA,SAA0B;EACxB,OAAO;GAAE,UAAU;GAAI,OAAO;EAAI;CACpC;CAEA,MAAgB,QAAQ,KAAmC;EACzD,IAAI,OAAO,KAAKA,QAAQ,MAAM,OAAO,OAAO,KAAK;EAEjD,MAAM,UAAU,KAAK,KAAK,MAAM;EAChC,MAAM,QAAQ,UAAU,MAAM,kBAAkB,OAAO,IAAI,KAAA;EAC3D,IAAI,CAAC,OAAO,WAAW,OAAO,KAAK;EAEnC,MAAM,WAAW,MAAM,WAAW,KAAK,MAAM,GAAG;EAChD,IAAI,UAAU,SAAS,SAAS,OAAO,KAAK;EAC5C,MAAM,MAAM,MAAM,UAAU,QAAQ;EAEpC,MAAM,EAAE,MAAM,SAAS,KAAK,KAAK,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,EAAE,CAAC;EAEpE,MAAM,IAAI,MAAM,MAAM,aAAa,GAAG;EACtC,IAAI,CAAC,GAAG,OAAO,KAAK;EACpB,MAAM,IAAI,MAAM,UAAU,EAAE,SAAS;GAAE;GAAM;EAAK,CAAC;EACnD,IAAI,CAAC,GAAG,OAAO,KAAK;EACpB,KAAKD,gBACH,EAAE,cAAc,MAAM,gBAAgB,EAAE,SAAS,EAAE,WAAW,IAAI,KAAA;EAOpE,IAAI,EAAE,UAAU;GACd,IAAI,SAAS,EAAE,QAAQ;GACvB,IAAI,EAAE,QAAQ,IAAI,SAAS,EAAE,GAAG;EAClC;EAEA,IAAI,EAAE,QAAQ,OAAO,EAAE;EAEvB,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI;EACtB,IAAI,CAAC,IAAI,QAAQ,OAAO,KAAK;EAC7B,IAAI,KAAK,EAAE,MAAM,IAAI;EACrB,OAAO;CACT;AACF;AAIA,SAAgB,MAAM,OAA4B,OAAwC;CACxF,IAAI,OAAO,UAAU,UAAU,OAAO,IAAI,MAAM;EAAE,KAAK;EAAO,GAAG;CAAM,CAAC;CACxE,OAAO,IAAI,MAAM,KAAK;AACxB;AAEA,SAAS,KACP,OACA,MACA,WACgC;CAChC,MAAM,aAAa,MAAM,cAAc;CACvC,MAAM,SAAS,KAAK,SAAS,KAAK;CAClC,IAAI,OAAO,MAAM;CACjB,IAAI,OAAO,MAAM;CACjB,IAAI,SAAS,KAAA,KAAa,SAAS,KAAA,GAAW,OAAO,KAAK,IAAI,GAAG,SAAS;CAC1E,IAAI,SAAS,KAAA,KAAa,SAAS,KAAA,GACjC,OAAO,KAAK,IAAI,GAAG,KAAK,MAAO,OAAO,SAAU,UAAU,CAAC;MACtD,IAAI,SAAS,KAAA,KAAa,SAAS,KAAA,GACxC,OAAO,KAAK,IAAI,GAAG,KAAK,MAAO,OAAO,aAAc,MAAM,CAAC;CAE7D,OAAO;EAAE,MAAM,QAAQ;EAAW,MAAM,QAAQ;CAAE;AACpD"}
@@ -0,0 +1,2 @@
1
+ import { a as InputValue, i as InputState, n as InputAttachment, o as input, r as InputEvents, t as Input } from "../input-B5Vy-iaa.mjs";
2
+ export { Input, InputAttachment, InputEvents, InputState, InputValue, input };