@promptctl/rich-js 0.18.0 → 0.19.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 (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Traceback — renders error tracebacks with formatting.
3
+ *
4
+ * [LAW:effects-at-boundaries] Pure rendering: a caught value in, `Segment`s out.
5
+ * Installing this as the process-wide crash handler touches `process.on` and
6
+ * `process.exit`, so that lives behind the node seam as `installTraceback` in
7
+ * `src/node/traceback.ts` — which is what keeps this module, and therefore the
8
+ * main barrel, importable in a browser.
9
+ *
10
+ * [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
11
+ * reads. There is no `showLocals`: an `Error` carries a stack of locations and
12
+ * nothing of the values in scope at them. The one way to get those in node —
13
+ * an inspector session pausing on exceptions — would have to pause on every
14
+ * throw, caught ones included, to serve `new Traceback(caughtError)`, and it
15
+ * cannot exist in a browser at all. Nor is there a `width` or `theme`: those
16
+ * sized and coloured a source-code excerpt this renderer does not produce.
17
+ */
18
+ import { Segment } from "../core/segment.js";
19
+ import type { Renderable, RenderOptions } from "../core/protocol.js";
20
+ export interface TracebackOptions {
21
+ suppress?: string[];
22
+ maxFrames?: number;
23
+ }
24
+ export declare class Traceback implements Renderable {
25
+ /** What was caught, as it was handed in. */
26
+ readonly error: unknown;
27
+ readonly maxFrames: number;
28
+ readonly suppress: string[];
29
+ private readonly caught;
30
+ constructor(error: unknown, options?: TracebackOptions);
31
+ render(options: RenderOptions): Iterable<Segment>;
32
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Traceback — renders error tracebacks with formatting.
3
+ *
4
+ * [LAW:effects-at-boundaries] Pure rendering: a caught value in, `Segment`s out.
5
+ * Installing this as the process-wide crash handler touches `process.on` and
6
+ * `process.exit`, so that lives behind the node seam as `installTraceback` in
7
+ * `src/node/traceback.ts` — which is what keeps this module, and therefore the
8
+ * main barrel, importable in a browser.
9
+ *
10
+ * [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
11
+ * reads. There is no `showLocals`: an `Error` carries a stack of locations and
12
+ * nothing of the values in scope at them. The one way to get those in node —
13
+ * an inspector session pausing on exceptions — would have to pause on every
14
+ * throw, caught ones included, to serve `new Traceback(caughtError)`, and it
15
+ * cannot exist in a browser at all. Nor is there a `width` or `theme`: those
16
+ * sized and coloured a source-code excerpt this renderer does not produce.
17
+ */
18
+ import { Segment } from "../core/segment.js";
19
+ import { RichText } from "../core/text.js";
20
+ import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
21
+ import { layoutPadding } from "./padding.js";
22
+ /**
23
+ * [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
24
+ * JavaScript lets a program throw any value — and the report. Anything with a
25
+ * string `name` and a `message` is reported as the error it presents itself
26
+ * as: that is every native error from any realm, which `instanceof Error`
27
+ * would refuse for one from a `node:vm` context or an iframe, and any thrower
28
+ * shaped like one. A message that is not a string — a native error's, assigned
29
+ * after construction — is formatted by `Pretty` under the error's own name.
30
+ * Anything else is reported as the value it is, under `NonError`, formatted by
31
+ * `Pretty` so it reads the same in a browser as in node — with the frames of a
32
+ * stack it carries, when it carries one.
33
+ */
34
+ function readCaught(value) {
35
+ const fields = readFields(value);
36
+ if (fields !== undefined && typeof fields.name === "string" && fields.hasMessage) {
37
+ const { name, message } = fields;
38
+ const text = typeof message === "string" ? message : undefined;
39
+ return {
40
+ name: name || "Error",
41
+ message: text === undefined
42
+ ? prettyMessage(message)
43
+ : () => new RichText(text, { style: "traceback.text", end: "" }),
44
+ frames: parseStack(fields.stack, name, text),
45
+ };
46
+ }
47
+ return { name: "NonError", message: prettyMessage(value), frames: parseStack(fields?.stack ?? "", undefined, undefined) };
48
+ }
49
+ /**
50
+ * The fields the report reads off a caught object, or `undefined` when it is
51
+ * not an object or reading them throws — a getter that throws, a `Proxy` whose
52
+ * trap does. Such a value is reported under `NonError`, where `Pretty` reads
53
+ * it again and marks the read that threw as `[Threw: …]`, so the fault stays
54
+ * in the report instead of taking the crash handler down with it.
55
+ */
56
+ function readFields(value) {
57
+ if (typeof value !== "object" || value === null)
58
+ return undefined;
59
+ try {
60
+ const record = value;
61
+ const stack = record.stack;
62
+ return {
63
+ name: record.name,
64
+ hasMessage: "message" in record,
65
+ message: record.message,
66
+ stack: typeof stack === "string" ? stack : "",
67
+ };
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ function prettyMessage(value) {
74
+ const pretty = new Pretty(value, UNSEEN_DATA_BOUNDS);
75
+ return (options) => pretty.toText(options);
76
+ }
77
+ /** The first line V8 writes into an error's stack: `String(error)` at the throw. */
78
+ function header(name, message) {
79
+ if (!name)
80
+ return message;
81
+ if (!message)
82
+ return name;
83
+ return `${name}: ${message}`;
84
+ }
85
+ /**
86
+ * Where the header that opens an error's stack ends, or `undefined` when the
87
+ * stack does not open with the header the error now has.
88
+ *
89
+ * V8 writes `String(error)`; node writes its own errors' code into the name
90
+ * there (`RangeError [ERR_X]: message`), so after the name may come one line of
91
+ * anything before `: ` and the message.
92
+ */
93
+ function headerEnd(stack, name, message) {
94
+ const endsLine = (at) => at === stack.length || stack[at] === "\n";
95
+ const plain = header(name, message);
96
+ if (stack.startsWith(plain) && endsLine(plain.length))
97
+ return plain.length;
98
+ if (!name || !stack.startsWith(name))
99
+ return undefined;
100
+ const tail = `: ${message}`;
101
+ const at = stack.indexOf(tail, name.length);
102
+ if (at < 0 || stack.slice(name.length, at).includes("\n"))
103
+ return undefined;
104
+ return endsLine(at + tail.length) ? at + tail.length : undefined;
105
+ }
106
+ /**
107
+ * Whether a stack line is shaped as V8's header — a name, then `:` or nothing —
108
+ * rather than as a SpiderMonkey or JavaScriptCore frame, whose `@` comes before
109
+ * the first `:` of its location. Read by shape, not by the error's name,
110
+ * because the name may have been reassigned after V8 wrote the header too.
111
+ */
112
+ function isHeaderShaped(line) {
113
+ const colon = line.indexOf(":");
114
+ return !(colon < 0 ? line : line.slice(0, colon)).includes("@");
115
+ }
116
+ /**
117
+ * The frames of a stack, in order.
118
+ *
119
+ * V8 opens a stack with the error's header, and a message can span lines that
120
+ * look like frames, so the header comes off first, as the exact text it is.
121
+ * When it does not open the stack, the message was reassigned after the throw
122
+ * and what precedes the first V8 frame is that stale header — all of it, when
123
+ * V8 recorded no frame, as under `Error.stackTraceLimit = 0`. SpiderMonkey and
124
+ * JavaScriptCore write no header, so a stack that opens with one of their
125
+ * frames is all frames. A value that is not an error has no name, so its stack is
126
+ * read from its first V8 frame, or whole when it has none.
127
+ */
128
+ function parseStack(stack, name, message) {
129
+ const end = name !== undefined && message !== undefined ? headerEnd(stack, name, message) : undefined;
130
+ const lines = stack.slice(end ?? 0).split("\n").map((line) => line.trim()).filter((line) => line !== "");
131
+ if (end !== undefined)
132
+ return lines.map(parseFrame);
133
+ const firstV8 = lines.findIndex((line) => V8_FRAME.test(line));
134
+ if (firstV8 >= 0)
135
+ return lines.slice(firstV8).map(parseFrame);
136
+ return name !== undefined && lines[0] !== undefined && isHeaderShaped(lines[0]) ? [] : lines.map(parseFrame);
137
+ }
138
+ /**
139
+ * V8 writes `at [async] [fn (]location[)]`. The `async` marker is not part of
140
+ * any name, so it is read and dropped; the lookahead keeps `at async (…)` — a
141
+ * function named `async` — a name.
142
+ */
143
+ const V8_FRAME = /^at\s+(?:async\s+(?!\())?(.*)$/;
144
+ /**
145
+ * SpiderMonkey and JavaScriptCore write `[fn]@location`, Firefox prefixing an
146
+ * awaited call's name with a cause up to a `*` (`async*`, `promise callback*`),
147
+ * which is dropped as V8's `async` is.
148
+ */
149
+ const AT_SIGN_FRAME = /^(?:[^@]*\*)?([^@]*)@(.*)$/;
150
+ function parseFrame(text) {
151
+ const v8 = V8_FRAME.exec(text);
152
+ if (v8) {
153
+ const body = v8[1];
154
+ const open = openingParen(body);
155
+ return open > 0
156
+ ? locate(body.slice(0, open).trim() || undefined, body.slice(open + 1, -1))
157
+ : locate(undefined, body);
158
+ }
159
+ const atSign = AT_SIGN_FRAME.exec(text);
160
+ if (atSign)
161
+ return locate(atSign[1] || undefined, atSign[2]);
162
+ return { function: undefined, location: text, line: undefined };
163
+ }
164
+ /**
165
+ * Where the parenthesised location that closes a V8 frame opens, or -1 when
166
+ * the frame does not end in one. Matched from the end, because an eval
167
+ * frame's location nests parentheses: `eval (eval at f (file:8:22), …)`.
168
+ */
169
+ function openingParen(body) {
170
+ if (!body.endsWith(")"))
171
+ return -1;
172
+ let depth = 0;
173
+ for (let i = body.length - 1; i >= 0; i--) {
174
+ if (body[i] === ")")
175
+ depth++;
176
+ else if (body[i] === "(" && --depth === 0)
177
+ return i;
178
+ }
179
+ return -1;
180
+ }
181
+ /**
182
+ * The shapes a location takes, each read as its file and line, first match
183
+ * wins. Code run by `eval` or `new Function` has no file of its own, so its
184
+ * frame is placed at the call that ran it, which is where the source is: V8
185
+ * nests that site inside the location (`eval at f (file:8:22), <anonymous>:1:1`,
186
+ * innermost first-parenthesised for an eval inside an eval), and Firefox appends
187
+ * the code to it (`file line 8 > eval:1:1`).
188
+ */
189
+ const LOCATIONS = [
190
+ /^eval at [^]*?\(([^()]+):(\d+):\d+\)/,
191
+ /^(.*?) line (\d+) > (?:eval|Function)/,
192
+ /^(.*?):(\d+)(?::\d+)?$/,
193
+ ];
194
+ function locate(fn, location) {
195
+ for (const shape of LOCATIONS) {
196
+ const match = shape.exec(location);
197
+ if (match)
198
+ return { function: fn, location: match[1], line: parseInt(match[2], 10) };
199
+ }
200
+ return { function: fn, location, line: undefined };
201
+ }
202
+ export class Traceback {
203
+ /** What was caught, as it was handed in. */
204
+ error;
205
+ maxFrames;
206
+ suppress;
207
+ caught;
208
+ constructor(error, options) {
209
+ this.error = error;
210
+ this.caught = readCaught(error);
211
+ this.maxFrames = options?.maxFrames ?? 100;
212
+ this.suppress = options?.suppress ?? [];
213
+ }
214
+ // [LAW:single-enforcer] The report is `RichText`, so fitting it to the width
215
+ // is the one fitting every text in the library gets: a line wider than the
216
+ // report wraps, and a location wider than the report folds across lines
217
+ // whole, as Rich's does. Written as bare segments, each line was cropped at
218
+ // the edge instead, and a frame lost its file and line to a long path.
219
+ *render(options) {
220
+ const header = new RichText("", { end: "" });
221
+ const { name, message } = this.caught;
222
+ header.append(name, "traceback.exc_type");
223
+ header.append(": ");
224
+ // The message starts after the name, so a value `Pretty` lays out fits the
225
+ // width that is left on that line rather than overrunning it.
226
+ header.append(message({ ...options, maxWidth: Math.max(1, options.maxWidth - header.cellLength) }));
227
+ header.append("\n\n");
228
+ yield* header.render(options);
229
+ // A suppressed frame keeps its place and shows its file and line only: it
230
+ // is the same frame with no function name to show.
231
+ const frames = this.caught.frames.map((frame) => this.suppress.some((s) => frame.location.includes(s)) ? { ...frame, function: undefined } : frame);
232
+ // One entry per row of the frame block, so a report with no frames has no
233
+ // rows: empty text is one line, as in Rich, and a block built as one text
234
+ // drew a blank indented row under every frameless report.
235
+ let entries;
236
+ if (this.maxFrames > 0 && frames.length > this.maxFrames) {
237
+ // Spend the budget exactly: the tail takes `maxFrames - head` rather than
238
+ // a second `head`, so an odd budget shows every frame it counts, and it
239
+ // slices from an index because `slice(-0)` is the whole array.
240
+ const head = Math.floor(this.maxFrames / 2);
241
+ const omitted = frames.length - this.maxFrames;
242
+ entries = [
243
+ ...frames.slice(0, head).map(frameEntry),
244
+ new RichText(`... ${omitted} frames omitted ...`, { style: "traceback.text", end: "" }),
245
+ ...frames.slice(frames.length - (this.maxFrames - head)).map(frameEntry),
246
+ ];
247
+ }
248
+ else {
249
+ entries = frames.map(frameEntry);
250
+ }
251
+ // The frames are one block set in from the report's edge, so a frame that
252
+ // wraps continues under itself instead of at the edge, where it read as a
253
+ // frame of its own. The width is divided as `Padding` divides it; the rows
254
+ // are not padded out on the right, as `Padding`'s are, because trailing
255
+ // blanks on every line of a crash report are bytes nobody asked for.
256
+ const { left, contentWidth } = layoutPadding(options.maxWidth, 2, 0);
257
+ const indent = new Segment(" ".repeat(left));
258
+ for (const entry of entries) {
259
+ for (const line of Segment.splitLines(entry.render({ ...options, maxWidth: contentWidth }))) {
260
+ yield indent;
261
+ yield* line;
262
+ yield Segment.line();
263
+ }
264
+ }
265
+ }
266
+ }
267
+ function frameEntry(frame) {
268
+ const entry = new RichText("", { end: "" });
269
+ if (frame.function) {
270
+ entry.append(frame.function, "bold");
271
+ entry.append(" ");
272
+ }
273
+ entry.append(frame.location, "dim");
274
+ if (frame.line !== undefined) {
275
+ entry.append(":");
276
+ entry.append(String(frame.line), "traceback.offset");
277
+ }
278
+ return entry;
279
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tree — hierarchical view with guide lines.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Style } from "../core/style.js";
6
+ import { RichText } from "../core/text.js";
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
+ export interface TreeOptions {
9
+ expanded?: boolean;
10
+ hideRoot?: boolean;
11
+ guide_style?: string | Style;
12
+ style?: string | Style;
13
+ }
14
+ export declare class Tree implements Renderable, Measurable {
15
+ readonly label: Renderable;
16
+ readonly children: Tree[];
17
+ expanded: boolean;
18
+ readonly hideRoot: boolean;
19
+ readonly guideStyle: string | Style;
20
+ readonly style: string | Style;
21
+ constructor(label: string | RichText | Renderable, options?: TreeOptions);
22
+ add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
23
+ render(rawOptions: RenderOptions): Iterable<Segment>;
24
+ /**
25
+ * The rows `render` and `measure` both read, entered in exactly one place so
26
+ * the two cannot start the walk differently. A hidden root takes its own row
27
+ * and the column its children hang from with it, as Rich does — its children
28
+ * stand at the left edge, and its guide style still reaches their guides.
29
+ */
30
+ private _walk;
31
+ /**
32
+ * The tree walked into the rows it emits, one row per label, guides first.
33
+ *
34
+ * [LAW:one-source-of-truth] The walk that decides which rows exist and what
35
+ * leads them is here and only here. `render` turns each row into segments and
36
+ * `measure` reads each row's width off the same sequence, so the two cannot
37
+ * come to disagree about how many rows there are or how wide the guides on
38
+ * them run — which they would the moment a second walk existed.
39
+ *
40
+ * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
41
+ * this one recursion: its children get the columns above it, settled, plus a
42
+ * branch of their own, so no depth is drawn by a rule of its own. A node's
43
+ * style and its guide style are handed down the same way, as one more entry
44
+ * on the stack its ancestors built.
45
+ */
46
+ private _rows;
47
+ /**
48
+ * One row of the tree: its label in whatever width the guides left, each of
49
+ * the label's lines led by the guides.
50
+ *
51
+ * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
52
+ * Before this, each call site emitted its guides and then handed the label
53
+ * `options` unchanged — the label was told it had the whole outer width while
54
+ * the guides had already spent four cells of it, so a tree at maxWidth 5
55
+ * emitted rows of 9. That is the same defect Panel and Table were fixed for,
56
+ * and the same fix: divide the requested width once and let every part read
57
+ * its share off the division. `options` arrives parsed from `render`.
58
+ *
59
+ * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
60
+ * depending on label renderables to invent trailing newlines: the label's
61
+ * output is cut into lines and every line ends where Tree ends it. A label
62
+ * that emits nothing still holds its row, and one that ends in a newline of
63
+ * its own gets the blank row that line is, as in Rich — padded to the label's
64
+ * width when the label is justified.
65
+ *
66
+ * The node's style sits under the label's own, as Rich's `Styled` puts it, so
67
+ * markup in the label still wins; its background alone reaches under the
68
+ * guides, so a row with a background is filled from its first guide to the
69
+ * end of its label.
70
+ */
71
+ private _renderRow;
72
+ measure(options: RenderOptions): {
73
+ minimum: number;
74
+ maximum: number;
75
+ };
76
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tree — hierarchical view with guide lines.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { cellFit, cellLen, asCellCol } from "../core/cells.js";
6
+ import { Style, NULL_STYLE } from "../core/style.js";
7
+ import { embed } from "./embed.js";
8
+ import { drawable, getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
+ import { Measurement } from "../core/measure.js";
10
+ /** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
11
+ const GUIDE_CELLS = 4;
12
+ const LIGHT_GUIDES = { fork: "├── ", end: "└── ", continue: "│ ", space: " " };
13
+ const HEAVY_GUIDES = { fork: "┣━━ ", end: "┗━━ ", continue: "┃ ", space: " " };
14
+ const DOUBLE_GUIDES = { fork: "╠══ ", end: "╚══ ", continue: "║ ", space: " " };
15
+ const ASCII_GUIDES = { fork: "+-- ", end: "`-- ", continue: "| ", space: " " };
16
+ /**
17
+ * The guide attributes a glyph set stands for, switched off on the guide itself
18
+ * as Rich's `remove_guide_styles` does: a bold guide is drawn heavy, not bold.
19
+ */
20
+ const REMOVE_GUIDE_STYLES = new Style({ bold: false, underline2: false });
21
+ const guideGlyphs = (glyphs) => Object.values(glyphs).join("");
22
+ /**
23
+ * The glyphs a column of guide is drawn with, read off its resolved guide style
24
+ * as Rich does: heavy when bold, double when underline2, light otherwise — and
25
+ * ASCII whenever the terminal cannot draw the chosen set.
26
+ */
27
+ function glyphsFor(options, style) {
28
+ const weight = style.bold === true ? HEAVY_GUIDES : style.underline2 === true ? DOUBLE_GUIDES : LIGHT_GUIDES;
29
+ return drawable(options, weight, ASCII_GUIDES, guideGlyphs);
30
+ }
31
+ /** How wide a label wants to be. */
32
+ function labelWidth(options, label) {
33
+ return Measurement.get(options, label).maximum;
34
+ }
35
+ export class Tree {
36
+ label;
37
+ children;
38
+ expanded;
39
+ hideRoot;
40
+ guideStyle;
41
+ style;
42
+ constructor(label, options) {
43
+ this.label = embed(label);
44
+ this.children = [];
45
+ this.expanded = options?.expanded !== false;
46
+ this.hideRoot = options?.hideRoot ?? false;
47
+ this.guideStyle = options?.guide_style ?? NULL_STYLE;
48
+ this.style = options?.style ?? NULL_STYLE;
49
+ }
50
+ add(label, options) {
51
+ const child = new Tree(label, {
52
+ guide_style: options?.guide_style,
53
+ style: options?.style,
54
+ expanded: options?.expanded,
55
+ });
56
+ this.children.push(child);
57
+ return child;
58
+ }
59
+ *render(rawOptions) {
60
+ const options = withBoundedWidth(rawOptions, this);
61
+ for (const row of this._walk()) {
62
+ yield* this._renderRow(options, row);
63
+ }
64
+ }
65
+ /**
66
+ * The rows `render` and `measure` both read, entered in exactly one place so
67
+ * the two cannot start the walk differently. A hidden root takes its own row
68
+ * and the column its children hang from with it, as Rich does — its children
69
+ * stand at the left edge, and its guide style still reaches their guides.
70
+ */
71
+ _walk() {
72
+ const rows = [...this._rows([], [], [])];
73
+ return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
74
+ }
75
+ /**
76
+ * The tree walked into the rows it emits, one row per label, guides first.
77
+ *
78
+ * [LAW:one-source-of-truth] The walk that decides which rows exist and what
79
+ * leads them is here and only here. `render` turns each row into segments and
80
+ * `measure` reads each row's width off the same sequence, so the two cannot
81
+ * come to disagree about how many rows there are or how wide the guides on
82
+ * them run — which they would the moment a second walk existed.
83
+ *
84
+ * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
85
+ * this one recursion: its children get the columns above it, settled, plus a
86
+ * branch of their own, so no depth is drawn by a rule of its own. A node's
87
+ * style and its guide style are handed down the same way, as one more entry
88
+ * on the stack its ancestors built.
89
+ */
90
+ *_rows(guides, inheritedGuideStyles, inheritedStyles) {
91
+ const styles = [...inheritedStyles, this.style];
92
+ yield { guides, label: this.label, styles };
93
+ const children = this.expanded ? this.children : [];
94
+ const guideStyles = [...inheritedGuideStyles, this.guideStyle];
95
+ const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
96
+ for (let i = 0; i < children.length; i++) {
97
+ const branch = i === children.length - 1
98
+ ? { first: "end", rest: "space", styles: guideStyles }
99
+ : { first: "fork", rest: "continue", styles: guideStyles };
100
+ yield* children[i]._rows([...above, branch], guideStyles, styles);
101
+ }
102
+ }
103
+ /**
104
+ * One row of the tree: its label in whatever width the guides left, each of
105
+ * the label's lines led by the guides.
106
+ *
107
+ * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
108
+ * Before this, each call site emitted its guides and then handed the label
109
+ * `options` unchanged — the label was told it had the whole outer width while
110
+ * the guides had already spent four cells of it, so a tree at maxWidth 5
111
+ * emitted rows of 9. That is the same defect Panel and Table were fixed for,
112
+ * and the same fix: divide the requested width once and let every part read
113
+ * its share off the division. `options` arrives parsed from `render`.
114
+ *
115
+ * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
116
+ * depending on label renderables to invent trailing newlines: the label's
117
+ * output is cut into lines and every line ends where Tree ends it. A label
118
+ * that emits nothing still holds its row, and one that ends in a newline of
119
+ * its own gets the blank row that line is, as in Rich — padded to the label's
120
+ * width when the label is justified.
121
+ *
122
+ * The node's style sits under the label's own, as Rich's `Styled` puts it, so
123
+ * markup in the label still wins; its background alone reaches under the
124
+ * guides, so a row with a background is filled from its first guide to the
125
+ * end of its label.
126
+ */
127
+ *_renderRow(options, row) {
128
+ const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
129
+ const style = resolve(row.styles);
130
+ const drawn = row.guides.map((guide) => {
131
+ const guideStyle = resolve(guide.styles);
132
+ return {
133
+ glyphs: glyphsFor(options, guideStyle),
134
+ style: style.backgroundStyle.add(guideStyle).add(REMOVE_GUIDE_STYLES),
135
+ };
136
+ });
137
+ const width = Math.max(0, options.maxWidth - row.guides.length * GUIDE_CELLS);
138
+ // The guides are cropped by `cellFit`; the label is cropped too, so a label
139
+ // that ignores the width it is handed cannot push the row past the offer the
140
+ // guides were fitted into. No highlighter: a label is drawn plain, as
141
+ // Rich's `Tree` hands its labels `highlight=False`.
142
+ const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
143
+ const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(row.label.render(labelOptions), width), style));
144
+ if (lines.length === 0)
145
+ lines.push([]);
146
+ for (let i = 0; i < lines.length; i++) {
147
+ let left = options.maxWidth;
148
+ for (let g = 0; g < row.guides.length; g++) {
149
+ const { glyphs, style: guideStyle } = drawn[g];
150
+ const piece = cellFit(glyphs[i === 0 ? row.guides[g].first : row.guides[g].rest], asCellCol(left));
151
+ if (piece.length > 0)
152
+ yield new Segment(piece, guideStyle);
153
+ left -= cellLen(piece);
154
+ }
155
+ yield* lines[i];
156
+ yield Segment.line();
157
+ }
158
+ }
159
+ measure(options) {
160
+ // The ceiling is parsed for the same reason the rows are: an unparsed NaN
161
+ // ceiling yields a range no parent layout can divide.
162
+ const parsed = withCellWidth(options);
163
+ const ceiling = parsed.maxWidth;
164
+ // The widest row the walk emits, not the offer. Reported as the offer, a
165
+ // tree told any parent that asked "I want all of it" — so `Panel` in fit
166
+ // mode drew a 40-cell frame around nine cells of tree, and an unbounded
167
+ // offer came back unbounded.
168
+ let natural = 0;
169
+ for (const row of this._walk()) {
170
+ natural = Math.max(natural, row.guides.length * GUIDE_CELLS + labelWidth(parsed, row.label));
171
+ }
172
+ const maximum = Math.min(natural, ceiling);
173
+ // A row is its guides plus at least one cell of label — but never more than
174
+ // this tree turned out to want, or the floor would sit above its own ceiling.
175
+ return { minimum: Math.min(4, maximum), maximum };
176
+ }
177
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Viewport — a fixed number of rows onto a taller renderable, scrolled by an
3
+ * offset that cannot leave the content.
4
+ *
5
+ * Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
6
+ * when it is given one; otherwise its own configured rows, or the content's
7
+ * full length when it has none, capped by any ceiling. It renders its content
8
+ * with no height, because the content's natural length is the scroll extent,
9
+ * and pads or crops what comes back to its rows — its rows are its own height,
10
+ * not a region someone else will shape.
11
+ *
12
+ * Its width is the width it is given, in both directions: each row is cropped
13
+ * or padded to it, so content that ignores its width cannot spill past the
14
+ * viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
15
+ * down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
16
+ * has a gutter zero cells wide, and every row takes the same path either way.
17
+ *
18
+ * [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
19
+ * against exists before a render: the rows come from the budget the render is
20
+ * handed, and the content's length from rendering it at the width it is handed.
21
+ * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
+ * are called. Each queues a move, and the next render resolves the queue in
23
+ * call order, clamping after every move, against the rows and length that
24
+ * render found. Calling an operation before the first render, or three of them
25
+ * between two renders, is the same code path as calling one.
26
+ *
27
+ * Every cell of its rows, gutter included, is stamped as its own
28
+ * (`Segment.anchorLines`), around whatever anchors its content drew. So the
29
+ * composed frame names the viewport under any of its cells however deep it is
30
+ * nested, and a cell of a widget inside it names both: that is how the wheel
31
+ * finds the viewport to scroll. `canScrollBy` is how it passes over one that
32
+ * cannot move that way, to the viewport around it.
33
+ */
34
+ import { Segment } from "../core/segment.js";
35
+ import type { Style } from "../core/style.js";
36
+ import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
37
+ /** How one part of a scrollbar draws each row it covers. */
38
+ export interface ScrollbarPart {
39
+ readonly glyph: string;
40
+ /** A style, or a theme name resolved when the viewport renders. */
41
+ readonly style: string | Style;
42
+ }
43
+ /**
44
+ * The look of a scrollbar: the thumb, over the rows of the track that stand for
45
+ * the lines in view, and the track, over the rest. Its gutter is as wide as the
46
+ * wider of the two glyphs.
47
+ */
48
+ export interface Scrollbar {
49
+ readonly thumb: ScrollbarPart;
50
+ readonly track: ScrollbarPart;
51
+ }
52
+ /** A heavy line for the thumb on a light line for the track. */
53
+ export declare const SCROLLBAR: Scrollbar;
54
+ export interface ViewportOptions {
55
+ /**
56
+ * The rows shown when the viewport is given no region. Absent, it shows the
57
+ * content's full length. A ceiling caps either.
58
+ */
59
+ rows?: number;
60
+ /** A scrollbar down the right edge. Absent, the content has the full width. */
61
+ scrollbar?: Scrollbar;
62
+ }
63
+ export declare class Viewport implements Renderable, Measurable, Scrollable {
64
+ /**
65
+ * What the viewport shows. Replace it to show new content from the same
66
+ * scroll position — a view rebuilt every frame keeps one `Viewport`.
67
+ */
68
+ content: Renderable;
69
+ readonly rows: number | undefined;
70
+ readonly scrollbar: Scrollbar;
71
+ private _offset;
72
+ private _extent;
73
+ private _moves;
74
+ constructor(content: Renderable, options?: ViewportOptions);
75
+ /**
76
+ * The first content line the last render showed. A move requested since is
77
+ * not reflected until the next render resolves it.
78
+ */
79
+ get offset(): number;
80
+ /** Scroll so `line` is the first line shown. */
81
+ scrollTo(line: number): void;
82
+ /** Scroll by `lines`: down when positive, up when negative. */
83
+ scrollBy(lines: number): void;
84
+ /**
85
+ * Whether `scrollBy(lines)` would move the viewport from where the moves
86
+ * queued so far leave it, against the rows and length the last render
87
+ * found.
88
+ */
89
+ canScrollBy(lines: number): boolean;
90
+ /**
91
+ * Scroll the least distance that shows lines `start` up to but not
92
+ * including `end` — lines of the content as it renders at the viewport's
93
+ * width, so an item that wraps spans more than one. A range already in view does not move; one taller than
94
+ * the viewport shows its first line at the top.
95
+ */
96
+ ensureVisible(start: number, end: number): void;
97
+ /**
98
+ * The width its content renders at, out of `width`: what the scrollbar's
99
+ * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
100
+ */
101
+ contentWidth(width: number): number;
102
+ render(rawOptions: RenderOptions): Iterable<Segment>;
103
+ /**
104
+ * [LAW:one-source-of-truth] Where the queued moves leave the offset against
105
+ * `extent`: what a render commits, and what `canScrollBy` asks from.
106
+ * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
107
+ * queued moves: content that shrank since the last render pulls it back.
108
+ */
109
+ private resolve;
110
+ measure(rawOptions: RenderOptions): {
111
+ minimum: number;
112
+ maximum: number;
113
+ };
114
+ }