@effect-tui/core 0.1.1 → 0.1.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 (271) hide show
  1. package/README.md +31 -11
  2. package/dist/ansi.d.ts +127 -32
  3. package/dist/ansi.d.ts.map +1 -1
  4. package/dist/ansi.js +159 -37
  5. package/dist/ansi.js.map +1 -1
  6. package/dist/colors.d.ts +139 -0
  7. package/dist/colors.d.ts.map +1 -0
  8. package/dist/colors.js +339 -0
  9. package/dist/colors.js.map +1 -0
  10. package/dist/index.d.ts +6 -10
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -11
  13. package/dist/index.js.map +1 -1
  14. package/dist/keys.d.ts +21 -0
  15. package/dist/keys.d.ts.map +1 -1
  16. package/dist/keys.js +199 -58
  17. package/dist/keys.js.map +1 -1
  18. package/dist/layout/axis-helpers.d.ts +19 -0
  19. package/dist/layout/axis-helpers.d.ts.map +1 -0
  20. package/dist/layout/axis-helpers.js +19 -0
  21. package/dist/layout/axis-helpers.js.map +1 -0
  22. package/dist/output.d.ts +59 -0
  23. package/dist/output.d.ts.map +1 -0
  24. package/dist/output.js +142 -0
  25. package/dist/output.js.map +1 -0
  26. package/dist/render/buffer.d.ts.map +1 -1
  27. package/dist/render/buffer.js +6 -25
  28. package/dist/render/buffer.js.map +1 -1
  29. package/dist/render/graphemes.d.ts +15 -0
  30. package/dist/render/graphemes.d.ts.map +1 -0
  31. package/dist/render/graphemes.js +28 -0
  32. package/dist/render/graphemes.js.map +1 -0
  33. package/dist/render/measure.d.ts +1 -0
  34. package/dist/render/measure.d.ts.map +1 -1
  35. package/dist/render/measure.js +14 -36
  36. package/dist/render/measure.js.map +1 -1
  37. package/dist/render/palette.d.ts.map +1 -1
  38. package/dist/render/palette.js +26 -1
  39. package/dist/render/palette.js.map +1 -1
  40. package/dist/render/segmenter.d.ts +8 -0
  41. package/dist/render/segmenter.d.ts.map +1 -0
  42. package/dist/render/segmenter.js +23 -0
  43. package/dist/render/segmenter.js.map +1 -0
  44. package/dist/render/surface.d.ts +6 -32
  45. package/dist/render/surface.d.ts.map +1 -1
  46. package/dist/render/surface.js +11 -80
  47. package/dist/render/surface.js.map +1 -1
  48. package/dist/runtime/backend_node.d.ts.map +1 -1
  49. package/dist/runtime/backend_node.js.map +1 -1
  50. package/dist/tailwind-colors.d.ts +291 -0
  51. package/dist/tailwind-colors.d.ts.map +1 -0
  52. package/dist/tailwind-colors.js +291 -0
  53. package/dist/tailwind-colors.js.map +1 -0
  54. package/dist/types.d.ts +15 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +3 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +55 -55
  59. package/src/ansi.ts +201 -73
  60. package/src/colors.ts +468 -0
  61. package/src/index.ts +28 -14
  62. package/src/keys.ts +467 -287
  63. package/src/layout/axis-helpers.ts +33 -0
  64. package/src/output.ts +175 -0
  65. package/src/render/buffer.ts +161 -184
  66. package/src/render/graphemes.ts +34 -0
  67. package/src/render/measure.ts +15 -38
  68. package/src/render/palette.ts +98 -77
  69. package/src/render/segmenter.ts +27 -0
  70. package/src/render/surface.ts +139 -225
  71. package/src/runtime/backend_node.ts +71 -71
  72. package/src/tailwind-colors.ts +295 -0
  73. package/src/types.ts +18 -0
  74. package/dist/anim.d.ts +0 -4
  75. package/dist/anim.d.ts.map +0 -1
  76. package/dist/anim.js +0 -5
  77. package/dist/anim.js.map +0 -1
  78. package/dist/layout/linearStack.d.ts +0 -17
  79. package/dist/layout/linearStack.d.ts.map +0 -1
  80. package/dist/layout/linearStack.js +0 -86
  81. package/dist/layout/linearStack.js.map +0 -1
  82. package/dist/motion-value.d.ts +0 -58
  83. package/dist/motion-value.d.ts.map +0 -1
  84. package/dist/motion-value.js +0 -250
  85. package/dist/motion-value.js.map +0 -1
  86. package/dist/present/display.d.ts +0 -58
  87. package/dist/present/display.d.ts.map +0 -1
  88. package/dist/present/display.js +0 -168
  89. package/dist/present/display.js.map +0 -1
  90. package/dist/present/writers/fullscreen.d.ts +0 -19
  91. package/dist/present/writers/fullscreen.d.ts.map +0 -1
  92. package/dist/present/writers/fullscreen.js +0 -55
  93. package/dist/present/writers/fullscreen.js.map +0 -1
  94. package/dist/present/writers/inline.d.ts +0 -20
  95. package/dist/present/writers/inline.d.ts.map +0 -1
  96. package/dist/present/writers/inline.js +0 -92
  97. package/dist/present/writers/inline.js.map +0 -1
  98. package/dist/render/color-utils.d.ts +0 -18
  99. package/dist/render/color-utils.d.ts.map +0 -1
  100. package/dist/render/color-utils.js +0 -58
  101. package/dist/render/color-utils.js.map +0 -1
  102. package/dist/render/diff.d.ts +0 -30
  103. package/dist/render/diff.d.ts.map +0 -1
  104. package/dist/render/diff.js +0 -83
  105. package/dist/render/diff.js.map +0 -1
  106. package/dist/spring-physics.d.ts +0 -36
  107. package/dist/spring-physics.d.ts.map +0 -1
  108. package/dist/spring-physics.js +0 -113
  109. package/dist/spring-physics.js.map +0 -1
  110. package/dist/spring.d.ts +0 -73
  111. package/dist/spring.d.ts.map +0 -1
  112. package/dist/spring.js +0 -136
  113. package/dist/spring.js.map +0 -1
  114. package/dist/ui/containers/canvas.d.ts +0 -13
  115. package/dist/ui/containers/canvas.d.ts.map +0 -1
  116. package/dist/ui/containers/canvas.js +0 -16
  117. package/dist/ui/containers/canvas.js.map +0 -1
  118. package/dist/ui/containers/geometry-reader.d.ts +0 -17
  119. package/dist/ui/containers/geometry-reader.d.ts.map +0 -1
  120. package/dist/ui/containers/geometry-reader.js +0 -24
  121. package/dist/ui/containers/geometry-reader.js.map +0 -1
  122. package/dist/ui/containers/hstack.d.ts +0 -12
  123. package/dist/ui/containers/hstack.d.ts.map +0 -1
  124. package/dist/ui/containers/hstack.js +0 -28
  125. package/dist/ui/containers/hstack.js.map +0 -1
  126. package/dist/ui/containers/scroll.d.ts +0 -28
  127. package/dist/ui/containers/scroll.d.ts.map +0 -1
  128. package/dist/ui/containers/scroll.js +0 -97
  129. package/dist/ui/containers/scroll.js.map +0 -1
  130. package/dist/ui/containers/shared.d.ts +0 -12
  131. package/dist/ui/containers/shared.d.ts.map +0 -1
  132. package/dist/ui/containers/shared.js +0 -19
  133. package/dist/ui/containers/shared.js.map +0 -1
  134. package/dist/ui/containers/vstack.d.ts +0 -12
  135. package/dist/ui/containers/vstack.d.ts.map +0 -1
  136. package/dist/ui/containers/vstack.js +0 -28
  137. package/dist/ui/containers/vstack.js.map +0 -1
  138. package/dist/ui/containers/zstack.d.ts +0 -14
  139. package/dist/ui/containers/zstack.d.ts.map +0 -1
  140. package/dist/ui/containers/zstack.js +0 -36
  141. package/dist/ui/containers/zstack.js.map +0 -1
  142. package/dist/ui/core/geometry-store.d.ts +0 -22
  143. package/dist/ui/core/geometry-store.d.ts.map +0 -1
  144. package/dist/ui/core/geometry-store.js +0 -29
  145. package/dist/ui/core/geometry-store.js.map +0 -1
  146. package/dist/ui/core/geometry.d.ts +0 -34
  147. package/dist/ui/core/geometry.d.ts.map +0 -1
  148. package/dist/ui/core/geometry.js +0 -14
  149. package/dist/ui/core/geometry.js.map +0 -1
  150. package/dist/ui/core/view.d.ts +0 -25
  151. package/dist/ui/core/view.d.ts.map +0 -1
  152. package/dist/ui/core/view.js +0 -34
  153. package/dist/ui/core/view.js.map +0 -1
  154. package/dist/ui/index.d.ts +0 -44
  155. package/dist/ui/index.d.ts.map +0 -1
  156. package/dist/ui/index.js +0 -39
  157. package/dist/ui/index.js.map +0 -1
  158. package/dist/ui/inlinetext.d.ts +0 -24
  159. package/dist/ui/inlinetext.d.ts.map +0 -1
  160. package/dist/ui/inlinetext.js +0 -131
  161. package/dist/ui/inlinetext.js.map +0 -1
  162. package/dist/ui/install.d.ts +0 -22
  163. package/dist/ui/install.d.ts.map +0 -1
  164. package/dist/ui/install.js +0 -66
  165. package/dist/ui/install.js.map +0 -1
  166. package/dist/ui/markdown.d.ts +0 -40
  167. package/dist/ui/markdown.d.ts.map +0 -1
  168. package/dist/ui/markdown.js +0 -351
  169. package/dist/ui/markdown.js.map +0 -1
  170. package/dist/ui/modifiers/border.d.ts +0 -33
  171. package/dist/ui/modifiers/border.d.ts.map +0 -1
  172. package/dist/ui/modifiers/border.js +0 -82
  173. package/dist/ui/modifiers/border.js.map +0 -1
  174. package/dist/ui/modifiers/fill.d.ts +0 -14
  175. package/dist/ui/modifiers/fill.d.ts.map +0 -1
  176. package/dist/ui/modifiers/fill.js +0 -25
  177. package/dist/ui/modifiers/fill.js.map +0 -1
  178. package/dist/ui/modifiers/frame.d.ts +0 -23
  179. package/dist/ui/modifiers/frame.d.ts.map +0 -1
  180. package/dist/ui/modifiers/frame.js +0 -54
  181. package/dist/ui/modifiers/frame.js.map +0 -1
  182. package/dist/ui/modifiers/offset.d.ts +0 -15
  183. package/dist/ui/modifiers/offset.d.ts.map +0 -1
  184. package/dist/ui/modifiers/offset.js +0 -21
  185. package/dist/ui/modifiers/offset.js.map +0 -1
  186. package/dist/ui/modifiers/opacity.d.ts +0 -15
  187. package/dist/ui/modifiers/opacity.d.ts.map +0 -1
  188. package/dist/ui/modifiers/opacity.js +0 -95
  189. package/dist/ui/modifiers/opacity.js.map +0 -1
  190. package/dist/ui/modifiers/padding.d.ts +0 -20
  191. package/dist/ui/modifiers/padding.d.ts.map +0 -1
  192. package/dist/ui/modifiers/padding.js +0 -36
  193. package/dist/ui/modifiers/padding.js.map +0 -1
  194. package/dist/ui/modifiers/styled.d.ts +0 -14
  195. package/dist/ui/modifiers/styled.d.ts.map +0 -1
  196. package/dist/ui/modifiers/styled.js +0 -26
  197. package/dist/ui/modifiers/styled.js.map +0 -1
  198. package/dist/ui/primitives/rectangle.d.ts +0 -15
  199. package/dist/ui/primitives/rectangle.d.ts.map +0 -1
  200. package/dist/ui/primitives/rectangle.js +0 -23
  201. package/dist/ui/primitives/rectangle.js.map +0 -1
  202. package/dist/ui/primitives/spacer.d.ts +0 -13
  203. package/dist/ui/primitives/spacer.d.ts.map +0 -1
  204. package/dist/ui/primitives/spacer.js +0 -16
  205. package/dist/ui/primitives/spacer.js.map +0 -1
  206. package/dist/ui/primitives/text.d.ts +0 -15
  207. package/dist/ui/primitives/text.d.ts.map +0 -1
  208. package/dist/ui/primitives/text.js +0 -79
  209. package/dist/ui/primitives/text.js.map +0 -1
  210. package/dist/ui/primitives/wrapped-text.d.ts +0 -30
  211. package/dist/ui/primitives/wrapped-text.d.ts.map +0 -1
  212. package/dist/ui/primitives/wrapped-text.js +0 -117
  213. package/dist/ui/primitives/wrapped-text.js.map +0 -1
  214. package/dist/ui/shinytext.d.ts +0 -66
  215. package/dist/ui/shinytext.d.ts.map +0 -1
  216. package/dist/ui/shinytext.js +0 -99
  217. package/dist/ui/shinytext.js.map +0 -1
  218. package/dist/ui/text/layout.d.ts +0 -35
  219. package/dist/ui/text/layout.d.ts.map +0 -1
  220. package/dist/ui/text/layout.js +0 -102
  221. package/dist/ui/text/layout.js.map +0 -1
  222. package/dist/ui/textinput.d.ts +0 -140
  223. package/dist/ui/textinput.d.ts.map +0 -1
  224. package/dist/ui/textinput.js +0 -402
  225. package/dist/ui/textinput.js.map +0 -1
  226. package/dist/ui/view-constructors.d.ts +0 -72
  227. package/dist/ui/view-constructors.d.ts.map +0 -1
  228. package/dist/ui/view-constructors.js +0 -74
  229. package/dist/ui/view-constructors.js.map +0 -1
  230. package/src/anim.ts +0 -5
  231. package/src/layout/linearStack.ts +0 -115
  232. package/src/motion-value.ts +0 -335
  233. package/src/present/display.ts +0 -206
  234. package/src/present/writers/fullscreen.ts +0 -58
  235. package/src/present/writers/inline.ts +0 -101
  236. package/src/render/color-utils.ts +0 -60
  237. package/src/render/diff.ts +0 -95
  238. package/src/spring-physics.ts +0 -151
  239. package/src/spring.ts +0 -234
  240. package/src/ui/__snapshots__/wrappedtext.test.ts.snap +0 -57
  241. package/src/ui/containers/canvas.ts +0 -18
  242. package/src/ui/containers/geometry-reader.ts +0 -32
  243. package/src/ui/containers/hstack.ts +0 -33
  244. package/src/ui/containers/scroll.ts +0 -106
  245. package/src/ui/containers/shared.ts +0 -27
  246. package/src/ui/containers/vstack.ts +0 -34
  247. package/src/ui/containers/zstack.ts +0 -37
  248. package/src/ui/core/geometry-store.ts +0 -42
  249. package/src/ui/core/geometry.ts +0 -30
  250. package/src/ui/core/view.ts +0 -49
  251. package/src/ui/index.ts +0 -84
  252. package/src/ui/inlinetext.ts +0 -135
  253. package/src/ui/install.ts +0 -110
  254. package/src/ui/markdown.test.ts +0 -74
  255. package/src/ui/markdown.ts +0 -388
  256. package/src/ui/modifiers/border.ts +0 -100
  257. package/src/ui/modifiers/fill.ts +0 -28
  258. package/src/ui/modifiers/frame.ts +0 -74
  259. package/src/ui/modifiers/offset.ts +0 -23
  260. package/src/ui/modifiers/opacity.ts +0 -93
  261. package/src/ui/modifiers/padding.ts +0 -53
  262. package/src/ui/modifiers/styled.ts +0 -31
  263. package/src/ui/primitives/rectangle.ts +0 -25
  264. package/src/ui/primitives/spacer.ts +0 -18
  265. package/src/ui/primitives/text.ts +0 -85
  266. package/src/ui/primitives/wrapped-text.ts +0 -131
  267. package/src/ui/shinytext.ts +0 -159
  268. package/src/ui/text/layout.ts +0 -119
  269. package/src/ui/textinput.ts +0 -496
  270. package/src/ui/view-constructors.ts +0 -96
  271. package/src/ui/wrappedtext.test.ts +0 -138
package/src/ui/install.ts DELETED
@@ -1,110 +0,0 @@
1
- /* install.ts — Installer for View modifier chainers
2
- *
3
- * This module installs chainable modifier methods on View.prototype using
4
- * a clean installer pattern with TypeScript declaration merging.
5
- */
6
-
7
- import { View } from "./core/view.js"
8
- import type { BorderOptions } from "./modifiers/border.js"
9
- import type { FrameSpec } from "./modifiers/frame.js"
10
- import type { StyleSpec, ColorLike } from "../render/surface.js"
11
- import { parseColor } from "../render/surface.js"
12
-
13
- // Import all modifier classes
14
- import { Border } from "./modifiers/border.js"
15
- import { Offset } from "./modifiers/offset.js"
16
- import { Fill } from "./modifiers/fill.js"
17
- import { Opacity } from "./modifiers/opacity.js"
18
- import { Padding } from "./modifiers/padding.js"
19
- import { Frame } from "./modifiers/frame.js"
20
- import { Styled } from "./modifiers/styled.js"
21
-
22
- // Define modifier methods with proper typing
23
- const modifierMethods = {
24
- // Layout modifiers
25
- border(this: View, opts?: BorderOptions): View {
26
- return new Border(this, opts)
27
- },
28
-
29
- offset(this: View, dx = 0, dy = 0): View {
30
- return new Offset(this, dx, dy)
31
- },
32
-
33
- fillWidth(this: View): View {
34
- return new Fill(this, "horizontal")
35
- },
36
-
37
- fillHeight(this: View): View {
38
- return new Fill(this, "vertical")
39
- },
40
-
41
- padding(this: View, t: number, r?: number, b?: number, l?: number): View {
42
- return new Padding(this, t, r, b, l)
43
- },
44
-
45
- frame(this: View, opts: FrameSpec): View {
46
- return new Frame(this, opts)
47
- },
48
-
49
- // Style modifiers
50
- style(this: View, spec?: StyleSpec): View {
51
- return spec ? new Styled(this, spec) : this
52
- },
53
-
54
- fg(this: View, color: ColorLike): View {
55
- return new Styled(this, { fg: parseColor(color) })
56
- },
57
-
58
- bg(this: View, color: ColorLike): View {
59
- return new Styled(this, { bg: parseColor(color) })
60
- },
61
-
62
- bold(this: View, on = true): View {
63
- return new Styled(this, { bold: on })
64
- },
65
-
66
- italic(this: View, on = true): View {
67
- return new Styled(this, { italic: on })
68
- },
69
-
70
- underline(this: View, on = true): View {
71
- return new Styled(this, { underline: on })
72
- },
73
-
74
- inverse(this: View, on = true): View {
75
- return new Styled(this, { inverse: on })
76
- },
77
-
78
- // Visual effect modifiers
79
- opacity(this: View, alpha: number): View {
80
- return new Opacity(this, alpha)
81
- },
82
- }
83
-
84
- // Install methods on View prototype
85
- Object.assign(View.prototype, modifierMethods)
86
-
87
- // TypeScript declaration merging to add method signatures
88
- declare module "./core/view.js" {
89
- interface View {
90
- // Layout modifiers
91
- border(opts?: BorderOptions): View
92
- offset(dx?: number, dy?: number): View
93
- fillWidth(): View
94
- fillHeight(): View
95
- padding(t: number, r?: number, b?: number, l?: number): View
96
- frame(opts: FrameSpec): View
97
-
98
- // Style modifiers
99
- style(spec?: StyleSpec): View
100
- fg(color: ColorLike): View
101
- bg(color: ColorLike): View
102
- bold(on?: boolean): View
103
- italic(on?: boolean): View
104
- underline(on?: boolean): View
105
- inverse(on?: boolean): View
106
-
107
- // Visual effect modifiers
108
- opacity(alpha: number): View
109
- }
110
- }
@@ -1,74 +0,0 @@
1
- import { describe, expect, it } from "@effect/vitest"
2
- import { Surface, Palette } from "../render/surface"
3
- import { Markdown } from "./markdown"
4
-
5
- describe("Markdown", () => {
6
- it("renders paragraph with inline formatting", () => {
7
- const md = new Markdown("This is **bold** and *italic*, plus `code` and ~~strike~~ and [link](https://ex.com).")
8
- const palette = new Palette()
9
- const m = md.measure(120, 10)
10
- const surface = new Surface(120, m.h)
11
- md.render(surface, palette, { x: 0, y: 0, w: 120, h: m.h })
12
- const output = surface.toString()
13
- expect(output).toBe("This is bold and italic, plus code and strike and link (https://ex.com).")
14
- })
15
-
16
- it("renders heading and paragraph with spacing", () => {
17
- const text = "# Title\n\nBody with *italic* and **bold**."
18
- const md = new Markdown(text)
19
- const palette = new Palette()
20
- const m = md.measure(80, 10)
21
- const surface = new Surface(80, m.h)
22
- md.render(surface, palette, { x: 0, y: 0, w: 80, h: m.h })
23
- const output = surface.toString({ trimEnd: true })
24
- expect(output).toBe("Title\n\nBody with italic and bold.")
25
- })
26
-
27
- it("renders unordered and ordered lists with inline", () => {
28
- const text = ["- first **item**", "- second with [link](u)", "", "1. one", "2. two and *italic*"].join("\n")
29
- const md = new Markdown(text)
30
- const palette = new Palette()
31
- const m = md.measure(80, 20)
32
- const surface = new Surface(80, m.h)
33
- md.render(surface, palette, { x: 0, y: 0, w: 80, h: m.h })
34
- const output = surface.toString({ trimEnd: true })
35
- const expected = ["• first item", "", "• second with link (u)", "", "1. one", "", "2. two and italic"].join("\n")
36
- expect(output).toBe(expected)
37
- })
38
-
39
- it("supports • bullets and 1) ordered markers", () => {
40
- const text = ["• apple", "• banana", "", "1) first", "2) second"].join("\n")
41
- const md = new Markdown(text)
42
- const palette = new Palette()
43
- const m = md.measure(80, 20)
44
- const surface = new Surface(80, m.h)
45
- md.render(surface, palette, { x: 0, y: 0, w: 80, h: m.h })
46
- const output = surface.toString({ trimEnd: true })
47
- const expected = ["• apple", "", "• banana", "", "1. first", "", "2. second"].join("\n")
48
- expect(output).toBe(expected)
49
- })
50
-
51
- it("renders blockquote with prefix", () => {
52
- const text = ["> quoted *text* with [ref](x)", "> and more ~~stuff~~"].join("\n")
53
- const md = new Markdown(text)
54
- const palette = new Palette()
55
- const m = md.measure(80, 10)
56
- const surface = new Surface(80, m.h)
57
- md.render(surface, palette, { x: 0, y: 0, w: 80, h: m.h })
58
- const output = surface.toString({ trimEnd: true })
59
- const expected = ["│ quoted text with ref (x)", "│ and more stuff"].join("\n")
60
- expect(output).toBe(expected)
61
- })
62
-
63
- it("renders fenced code blocks (no inline parsing inside)", () => {
64
- const text = ["```ts", "const x = 1 // **not bold**", "console.log(x) // [link](u)", "```"].join("\n")
65
- const md = new Markdown(text)
66
- const palette = new Palette()
67
- const m = md.measure(80, 10)
68
- const surface = new Surface(80, m.h)
69
- md.render(surface, palette, { x: 0, y: 0, w: 80, h: m.h })
70
- const output = surface.toString({ trimEnd: true })
71
- const expected = ["const x = 1 // **not bold**", "console.log(x) // [link](u)"].join("\n")
72
- expect(output).toBe(expected)
73
- })
74
- })
@@ -1,388 +0,0 @@
1
- import type { Palette, Surface, StyleSpec, ColorLike } from "../render/surface.js"
2
- import { Colors, mergeStyle } from "../render/surface.js"
3
- import { View } from "./core/view.js"
4
- import type { Rect } from "./core/geometry.js"
5
- import { VStack } from "./containers/vstack.js"
6
- import { WrappedText } from "./primitives/wrapped-text.js"
7
- import { InlineText, type TextFragment } from "./inlinetext.js"
8
-
9
- export type MarkdownOptions = {
10
- spacing?: number // vertical space between blocks (lines)
11
- styles?: {
12
- heading?: Partial<Record<1 | 2 | 3 | 4 | 5 | 6, StyleSpec>>
13
- paragraph?: StyleSpec
14
- code?: StyleSpec
15
- codeBorder?: {
16
- kind?: "none" | "rounded" | "square" | "ascii"
17
- color?: ColorLike
18
- padding?: number | { x?: number; y?: number }
19
- }
20
- quote?: StyleSpec
21
- listBullet?: StyleSpec
22
- }
23
- }
24
-
25
- type Block =
26
- | { type: "heading"; level: number; text: string }
27
- | { type: "paragraph"; text: string }
28
- | { type: "code"; lang?: string; text: string }
29
- | { type: "list"; ordered: boolean; items: string[] }
30
- | { type: "quote"; text: string }
31
-
32
- /** Minimal Markdown renderer for terminal UI: block-level only. */
33
- export class Markdown extends View {
34
- constructor(
35
- readonly text: string,
36
- readonly opts: MarkdownOptions = {},
37
- ) {
38
- super()
39
- }
40
-
41
- private parseBlocks(): Block[] {
42
- const lines = this.text.replace(/\r\n?/g, "\n").split("\n")
43
- const blocks: Block[] = []
44
- let i = 0
45
- const isBlank = (s: string) => /^\s*$/.test(s)
46
-
47
- while (i < lines.length) {
48
- const line = lines[i]
49
- if (isBlank(line)) {
50
- i++
51
- continue
52
- }
53
-
54
- // Code fence
55
- const fence = line.match(/^```(?:(\w+))?\s*$/)
56
- if (fence) {
57
- const lang = fence[1]
58
- const buf: string[] = []
59
- i++
60
- while (i < lines.length && !/^```\s*$/.test(lines[i])) {
61
- buf.push(lines[i])
62
- i++
63
- }
64
- // skip closing fence if present
65
- if (i < lines.length && /^```\s*$/.test(lines[i])) i++
66
- blocks.push({ type: "code", lang, text: buf.join("\n") })
67
- continue
68
- }
69
-
70
- // Heading
71
- const h = line.match(/^(#{1,6})\s+(.*)$/)
72
- if (h) {
73
- blocks.push({ type: "heading", level: h[1].length, text: h[2] })
74
- i++
75
- continue
76
- }
77
-
78
- // Blockquote (consume contiguous ">" lines)
79
- if (/^>\s?/.test(line)) {
80
- const buf: string[] = []
81
- while (i < lines.length && /^>\s?/.test(lines[i])) {
82
- buf.push(lines[i].replace(/^>\s?/, ""))
83
- i++
84
- }
85
- blocks.push({ type: "quote", text: buf.join("\n") })
86
- continue
87
- }
88
-
89
- // List (unordered or ordered) — support -,*,+, • and 1., 1)
90
- let m = line.match(/^\s*([-*+]|•)\s+(.*)$/)
91
- let ordered = false
92
- if (!m) {
93
- const om = line.match(/^\s*(\d+)[.)]\s+(.*)$/)
94
- if (om) {
95
- ordered = true
96
- m = [om[0], om[1], om[2]] as any
97
- }
98
- }
99
- if (m) {
100
- const items: string[] = []
101
- while (i < lines.length) {
102
- const l = lines[i]
103
- const um = l.match(/^\s*([-*+]|•)\s+(.*)$/)
104
- const om = l.match(/^\s*(\d+)[.)]\s+(.*)$/)
105
- if ((ordered && om) || (!ordered && um)) {
106
- items.push((ordered ? om?.[2] : um?.[2])?.trimEnd() ?? "")
107
- i++
108
- } else if (isBlank(l)) {
109
- i++
110
- break
111
- } else {
112
- break
113
- }
114
- }
115
- blocks.push({ type: "list", ordered, items })
116
- continue
117
- }
118
-
119
- // Paragraph — consume until blank or next block
120
- const buf: string[] = [line]
121
- i++
122
- while (i < lines.length) {
123
- const l = lines[i]
124
- if (isBlank(l)) {
125
- i++
126
- break
127
- }
128
- if (
129
- /^```/.test(l) ||
130
- /^(#{1,6})\s+/.test(l) ||
131
- /^>\s?/.test(l) ||
132
- /^\s*([-*+])\s+/.test(l) ||
133
- /^\s*\d+\.\s+/.test(l)
134
- ) {
135
- break
136
- }
137
- buf.push(l)
138
- i++
139
- }
140
- blocks.push({ type: "paragraph", text: buf.join("\n") })
141
- }
142
-
143
- return blocks
144
- }
145
-
146
- private parseInline(text: string, base?: StyleSpec): TextFragment[] {
147
- // Recursive descent for bold/italic/strike/link; code spans take precedence and are not parsed inside.
148
- const frags: TextFragment[] = []
149
- const push = (t: string, style?: StyleSpec) => {
150
- if (t.length === 0) return
151
- frags.push({ text: t, style })
152
- }
153
- const walk = (s: string, inherited?: StyleSpec) => {
154
- let i = 0
155
- while (i < s.length) {
156
- // Find nearest marker
157
- const idxs: Array<{ i: number; kind: string }> = []
158
- const find = (re: RegExp, kind: string) => {
159
- const m = re.exec(s.slice(i))
160
- if (m && m.index >= 0) idxs.push({ i: i + m.index, kind })
161
- }
162
- // Prioritize code and link first
163
- find(/`/, "code")
164
- find(/\[/, "link")
165
- find(/\*\*|__/, "bold")
166
- find(/\*|_/, "italic")
167
- find(/~~/, "strike")
168
- if (idxs.length === 0) {
169
- push(s.slice(i), inherited)
170
- break
171
- }
172
- idxs.sort((a, b) => a.i - b.i)
173
- const next = idxs[0]
174
- if (next.i > i) push(s.slice(i, next.i), inherited)
175
- i = next.i
176
- switch (next.kind) {
177
- case "code": {
178
- const j = s.indexOf("`", i + 1)
179
- if (j === -1) {
180
- push(s.slice(i), inherited)
181
- i = s.length
182
- } else {
183
- const inner = s.slice(i + 1, j)
184
- push(inner, mergeStyle(inherited, { bg: Colors.gray(2), fg: Colors.gray(15) }))
185
- i = j + 1
186
- }
187
- break
188
- }
189
- case "link": {
190
- const close = s.indexOf("]", i + 1)
191
- if (close !== -1 && s[close + 1] === "(") {
192
- const end = s.indexOf(")", close + 2)
193
- if (end !== -1) {
194
- const label = s.slice(i + 1, close)
195
- const url = s.slice(close + 2, end)
196
- // Style label as link; append a thin gray url in parentheses
197
- const linkStyle = mergeStyle(inherited, { fg: Colors.brightBlue, underline: true })
198
- walk(label, linkStyle)
199
- push(` (${url})`, mergeStyle(inherited, { fg: Colors.gray(12), underline: false }))
200
- i = end + 1
201
- break
202
- }
203
- }
204
- // Fallback: treat '[' as literal
205
- push("[", inherited)
206
- i += 1
207
- break
208
- }
209
- case "bold": {
210
- const marker = s.slice(i, i + 2)
211
- const j = s.indexOf(marker, i + 2)
212
- if (j === -1) {
213
- push(s.slice(i), inherited)
214
- i = s.length
215
- } else {
216
- const inner = s.slice(i + 2, j)
217
- walk(inner, mergeStyle(inherited, { bold: true }))
218
- i = j + 2
219
- }
220
- break
221
- }
222
- case "strike": {
223
- const j = s.indexOf("~~", i + 2)
224
- if (j === -1) {
225
- push(s.slice(i), inherited)
226
- i = s.length
227
- } else {
228
- const inner = s.slice(i + 2, j)
229
- walk(inner, mergeStyle(inherited, { fg: Colors.gray(10) }))
230
- i = j + 2
231
- }
232
- break
233
- }
234
- case "italic": {
235
- const marker = s[i]
236
- const j = s.indexOf(marker, i + 1)
237
- if (j === -1) {
238
- push(s.slice(i), inherited)
239
- i = s.length
240
- } else {
241
- const inner = s.slice(i + 1, j)
242
- walk(inner, mergeStyle(inherited, { italic: true }))
243
- i = j + 1
244
- }
245
- break
246
- }
247
- }
248
- }
249
- }
250
- walk(text, base)
251
- return frags
252
- }
253
-
254
- private blockViews(_maxW: number): View[] {
255
- const spacing = this.opts.spacing ?? 1
256
- const nodes: View[] = []
257
- const blocks = this.parseBlocks()
258
-
259
- const styles = this.opts.styles ?? {}
260
- const headingDefault: Partial<Record<number, StyleSpec>> = {
261
- 1: { fg: Colors.brightCyan, bold: true },
262
- 2: { fg: Colors.brightBlue, bold: true },
263
- 3: { fg: Colors.brightMagenta, bold: true },
264
- }
265
-
266
- for (const b of blocks) {
267
- switch (b.type) {
268
- case "heading": {
269
- const style =
270
- styles.heading?.[b.level as 1 | 2 | 3 | 4 | 5 | 6] ?? (headingDefault[b.level] as StyleSpec | undefined)
271
- const fr = this.parseInline(b.text, style)
272
- nodes.push(new InlineText(fr, { wordWrap: true, breakWords: true }))
273
- break
274
- }
275
- case "paragraph": {
276
- const fr = this.parseInline(b.text, styles.paragraph)
277
- nodes.push(new InlineText(fr, { wordWrap: true, breakWords: true }))
278
- break
279
- }
280
- case "code": {
281
- let node: View = new WrappedText(b.text, { wordWrap: false }).style(
282
- styles.code ?? { fg: Colors.gray(15), bg: Colors.gray(2) },
283
- )
284
- // Use border if requested
285
- if (this.opts.styles?.codeBorder) {
286
- node = node.border({
287
- kind: this.opts.styles.codeBorder.kind ?? "rounded",
288
- color: this.opts.styles.codeBorder.color ?? Colors.gray(8),
289
- padding: this.opts.styles.codeBorder.padding ?? { x: 1, y: 0 },
290
- })
291
- }
292
- nodes.push(node)
293
- break
294
- }
295
- case "quote": {
296
- // Custom node draws a left bar and renders wrapped lines with a 2-col indent.
297
- class QuoteBlock extends View {
298
- constructor(readonly child: View) {
299
- super()
300
- }
301
- protected measureContent(maxW: number, maxH: number) {
302
- const innerW = Math.max(0, maxW - 2)
303
- const m = this.child.measure(innerW, maxH)
304
- return { w: Math.min(maxW, m.w + 2), h: m.h }
305
- }
306
- protected renderContent(s: Surface, pal: Palette, rect: Rect) {
307
- const idBar = pal.id({ fg: Colors.gray(10) })
308
- for (let yy = 0; yy < rect.h; yy++) s.drawCP(rect.x, rect.y + yy, "│".codePointAt(0)!, idBar)
309
- const r = { x: rect.x + 2, y: rect.y, w: Math.max(0, rect.w - 2), h: rect.h }
310
- this.child.render(s, pal, r)
311
- }
312
- }
313
- const quoteStyle = styles.quote ?? { fg: Colors.gray(14) }
314
- const rows = b.text.split("\n").map(
315
- (line) =>
316
- new InlineText(this.parseInline(line, quoteStyle), {
317
- wordWrap: true,
318
- breakWords: true,
319
- }),
320
- )
321
- const body = new VStack(rows, 0)
322
- nodes.push(new QuoteBlock(body))
323
- break
324
- }
325
- case "list": {
326
- const bulletStyle = styles.listBullet ?? { fg: Colors.gray(12), bold: true }
327
- b.items.forEach((item, idx) => {
328
- const mark = b.ordered ? `${idx + 1}. ` : "• "
329
- const fragments: TextFragment[] = [
330
- { text: mark, style: bulletStyle },
331
- ...this.parseInline(item, { fg: Colors.white }),
332
- ]
333
- const row = new InlineText(fragments, { wordWrap: true, breakWords: true })
334
- nodes.push(row)
335
- // Add explicit spacer between list items to keep things readable
336
- if (idx < b.items.length - 1 && spacing > 0) nodes.push(new WrappedText("", { wordWrap: false }))
337
- })
338
- break
339
- }
340
- }
341
- // Vertical spacing between blocks
342
- if (spacing > 0) nodes.push(new WrappedText("", { wordWrap: false }))
343
- }
344
-
345
- // Trim trailing spacer
346
- if (nodes.length > 0) {
347
- // Heuristic: empty WrappedText measures height 1 and no content; leave one blank line at most
348
- nodes.pop()
349
- }
350
-
351
- return nodes
352
- }
353
-
354
- protected measureContent(maxW: number, maxH: number) {
355
- const nodes = this.blockViews(maxW)
356
- let w = 0
357
- let h = 0
358
- for (const n of nodes) {
359
- const m = n.measure(maxW, Math.max(0, maxH - h))
360
- w = Math.max(w, m.w)
361
- h = Math.min(maxH, h + m.h)
362
- if (h >= maxH) break
363
- }
364
- return { w: Math.min(maxW, w), h }
365
- }
366
-
367
- protected renderContent(s: Surface, pal: Palette, rect: Rect) {
368
- const nodes = this.blockViews(rect.w)
369
- let y = rect.y
370
- for (const n of nodes) {
371
- if (y >= rect.y + rect.h) break
372
- const m = n.measure(rect.w, Math.max(0, rect.y + rect.h - y))
373
- n.render(s, pal, { x: rect.x, y, w: Math.min(rect.w, m.w), h: Math.min(rect.h, m.h) })
374
- y += m.h
375
- }
376
- }
377
- }
378
-
379
- // Builder contribution for the View object
380
- export type ViewMarkdownExt = {
381
- markdown(text: string, opts?: MarkdownOptions): View
382
- }
383
-
384
- export const viewMarkdown: ViewMarkdownExt = {
385
- markdown(text: string, opts?: MarkdownOptions): View {
386
- return new Markdown(text, opts)
387
- },
388
- }
@@ -1,100 +0,0 @@
1
- import type { Palette, Surface, StyleSpec, ColorLike } from "../../render/surface.js"
2
- import { Colors, parseColor } from "../../render/surface.js"
3
- import { View } from "../core/view.js"
4
- import type { Rect } from "../core/geometry.js"
5
-
6
- export type BorderKind = "none" | "rounded" | "square" | "ascii"
7
- export type BorderOptions = {
8
- kind?: BorderKind
9
- color?: ColorLike
10
- padding?: number | { x?: number; y?: number }
11
- chars?: Partial<{ tl: string; tr: string; bl: string; br: string; h: string; v: string }>
12
- style?: StyleSpec
13
- }
14
-
15
- function resolveBorderPadding(p?: BorderOptions["padding"]): { x: number; y: number } {
16
- if (typeof p === "number") return { x: Math.max(0, p | 0), y: Math.max(0, p | 0) }
17
- return { x: Math.max(0, p?.x ?? 0), y: Math.max(0, p?.y ?? 0) }
18
- }
19
-
20
- function borderKindChars(kind: BorderKind): Required<BorderOptions>["chars"] {
21
- switch (kind) {
22
- case "rounded":
23
- return { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" }
24
- case "square":
25
- return { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" }
26
- case "ascii":
27
- return { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" }
28
- default:
29
- return { tl: "", tr: "", bl: "", br: "", h: "", v: "" }
30
- }
31
- }
32
-
33
- export class Border extends View {
34
- constructor(
35
- readonly child: View,
36
- readonly opts?: BorderOptions,
37
- ) {
38
- super()
39
- }
40
-
41
- private get thickness() {
42
- return this.opts?.kind === "none" ? 0 : 1
43
- }
44
-
45
- protected measureContent(maxW: number, maxH: number) {
46
- const t = this.thickness
47
- const pad = resolveBorderPadding(this.opts?.padding)
48
- const m = this.child.measure(Math.max(0, maxW - t * 2 - pad.x * 2), Math.max(0, maxH - t * 2 - pad.y * 2))
49
- return {
50
- w: Math.min(maxW, m.w + t * 2 + pad.x * 2),
51
- h: Math.min(maxH, m.h + t * 2 + pad.y * 2),
52
- }
53
- }
54
-
55
- protected renderContent(s: Surface, pal: Palette, rect: Rect) {
56
- const k = this.opts?.kind ?? "square"
57
- const t = this.thickness
58
- const pad = resolveBorderPadding(this.opts?.padding)
59
- const baseChars = borderKindChars(k)
60
- const chars = { ...baseChars, ...(this.opts?.chars ?? {}) }
61
-
62
- const safeChars = {
63
- tl: (chars.tl ?? baseChars.tl) as string,
64
- tr: (chars.tr ?? baseChars.tr) as string,
65
- bl: (chars.bl ?? baseChars.bl) as string,
66
- br: (chars.br ?? baseChars.br) as string,
67
- h: (chars.h ?? baseChars.h) as string,
68
- v: (chars.v ?? baseChars.v) as string,
69
- }
70
-
71
- const borderStyle = this.opts?.style ?? {}
72
- const color = this.opts?.color != null ? parseColor(this.opts.color) : Colors.gray(8)
73
- const idBorder = pal.id({ ...borderStyle, fg: color ?? borderStyle.fg })
74
-
75
- const w = rect.w
76
- const h = rect.h
77
- if (w <= 0 || h <= 0) return
78
-
79
- if (t === 1 && w >= 2 && h >= 2) {
80
- s.drawCP(rect.x, rect.y, safeChars.tl.codePointAt(0)!, idBorder)
81
- s.fillRect(rect.x + 1, rect.y, w - 2, 1, safeChars.h.codePointAt(0)!, idBorder)
82
- s.drawCP(rect.x + w - 1, rect.y, safeChars.tr.codePointAt(0)!, idBorder)
83
- for (let yy = rect.y + 1; yy < rect.y + h - 1; yy++) {
84
- s.drawCP(rect.x, yy, safeChars.v.codePointAt(0)!, idBorder)
85
- s.drawCP(rect.x + w - 1, yy, safeChars.v.codePointAt(0)!, idBorder)
86
- }
87
- s.drawCP(rect.x, rect.y + h - 1, safeChars.bl.codePointAt(0)!, idBorder)
88
- s.fillRect(rect.x + 1, rect.y + h - 1, w - 2, 1, safeChars.h.codePointAt(0)!, idBorder)
89
- s.drawCP(rect.x + w - 1, rect.y + h - 1, safeChars.br.codePointAt(0)!, idBorder)
90
- }
91
-
92
- const inner: Rect = {
93
- x: rect.x + t + pad.x,
94
- y: rect.y + t + pad.y,
95
- w: Math.max(0, rect.w - (t + pad.x) * 2),
96
- h: Math.max(0, rect.h - (t + pad.y) * 2),
97
- }
98
- this.child.render(s, pal, inner)
99
- }
100
- }
@@ -1,28 +0,0 @@
1
- import type { Palette, Surface } from "../../render/surface.js"
2
- import { View } from "../core/view.js"
3
- import type { Rect } from "../core/geometry.js"
4
-
5
- // Fill wrapper (SwiftUI's .frame(maxWidth: .infinity) / .frame(maxHeight: .infinity))
6
- export class Fill extends View {
7
- constructor(
8
- readonly child: View,
9
- readonly axis: "horizontal" | "vertical",
10
- ) {
11
- super()
12
- }
13
-
14
- protected measureContent(maxW: number, maxH: number) {
15
- const childMeasure = this.child.measure(maxW, maxH)
16
-
17
- // Fill the requested axis to maximum available space
18
- if (this.axis === "horizontal") {
19
- return { w: maxW, h: childMeasure.h }
20
- } else {
21
- return { w: childMeasure.w, h: maxH }
22
- }
23
- }
24
-
25
- protected renderContent(s: Surface, pal: Palette, rect: Rect) {
26
- this.child.render(s, pal, rect)
27
- }
28
- }