@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/keys.ts CHANGED
@@ -1,302 +1,482 @@
1
1
  // Minimal key decoder for Node raw input → semantic key messages.
2
2
 
3
+ // SGR mouse button codes (low 2 bits, after masking modifiers)
4
+ const MOUSE_LEFT = 0
5
+ const MOUSE_MIDDLE = 1
6
+ const MOUSE_RIGHT = 2
7
+ const MOUSE_SCROLL_UP = 64
8
+ const MOUSE_SCROLL_DOWN = 65
9
+
10
+ // Modifier bits (subtract 1 from raw value before checking)
11
+ const MOD_SHIFT = 1
12
+ const MOD_ALT = 2
13
+ const MOD_CTRL = 4
14
+ const MOD_META = 8
15
+
16
+ // Common key codes
17
+ const KEY_TAB = 9
18
+ const KEY_ENTER_CR = 13
19
+ const KEY_ESC = 27
20
+ const KEY_SPACE = 32
21
+ const KEY_BS = 127
22
+
3
23
  export type KeyName =
4
- | "up"
5
- | "down"
6
- | "left"
7
- | "right"
8
- | "enter"
9
- | "escape"
10
- | "tab"
11
- | "shift-tab"
12
- | "backspace"
13
- | "delete"
14
- | "home"
15
- | "end"
16
- | "pageup"
17
- | "pagedown"
18
- | "insert"
19
- | "return"
20
- | "space"
21
- | "char" // printable
24
+ | "up"
25
+ | "down"
26
+ | "left"
27
+ | "right"
28
+ | "enter"
29
+ | "escape"
30
+ | "tab"
31
+ | "shift-tab"
32
+ | "backspace"
33
+ | "delete"
34
+ | "home"
35
+ | "end"
36
+ | "pageup"
37
+ | "pagedown"
38
+ | "insert"
39
+ | "return"
40
+ | "space"
41
+ | "char" // printable
22
42
 
23
43
  export type KeyMsg = {
24
- type: "key"
25
- name: KeyName
26
- text?: string // for printable chars
27
- ctrl?: boolean
28
- meta?: boolean
29
- shift?: boolean
30
- /** Optional phase; currently always "press" but left extensible. */
31
- phase?: "press" | "repeat" | "release"
32
- /** Set by renderer to allow handlers to stop further propagation. */
33
- defaultPrevented?: boolean
34
- preventDefault?: () => void
35
- /** Source protocol (raw|csiu|modifyOther|kitty). */
36
- source?: "raw" | "csiu" | "modify-other" | "kitty"
44
+ type: "key"
45
+ name: KeyName
46
+ text?: string // for printable chars
47
+ ctrl?: boolean
48
+ meta?: boolean
49
+ shift?: boolean
50
+ /** Optional phase; currently always "press" but left extensible. */
51
+ phase?: "press" | "repeat" | "release"
52
+ /** Set by renderer to allow handlers to stop further propagation. */
53
+ defaultPrevented?: boolean
54
+ preventDefault?: () => void
55
+ /** Source protocol (raw|csiu|modifyOther|kitty). */
56
+ source?: "raw" | "csiu" | "modify-other" | "kitty"
37
57
  }
38
58
 
59
+ export type MouseButton = "left" | "middle" | "right" | "scroll-up" | "scroll-down" | "none"
60
+
61
+ export type MouseMsg = {
62
+ type: "mouse"
63
+ button: MouseButton
64
+ /** Column (0-based) */
65
+ x: number
66
+ /** Row (0-based) */
67
+ y: number
68
+ /** Press or release */
69
+ action: "press" | "release" | "drag" | "move"
70
+ ctrl?: boolean
71
+ meta?: boolean
72
+ shift?: boolean
73
+ }
74
+
75
+ /** Union of keyboard and mouse input events */
76
+ export type InputMsg = KeyMsg | MouseMsg
77
+
39
78
  const ESC = "\x1b"
40
79
 
80
+ // ─────────────────────────────────────────────────────────────
81
+ // CSI sequence parsing (xterm-style modified keys)
82
+ // ─────────────────────────────────────────────────────────────
83
+
84
+ // CSI pattern: ESC [ <params> <final>
85
+ // Examples: ESC[A (up), ESC[1;2A (shift+up), ESC[5~ (pageup)
86
+ const CSI_RE = /^\x1b\[([0-9;]*)([A-Za-z~])$/
87
+
88
+ // Map CSI final character to key name
89
+ const CSI_ARROW_MAP: Record<string, KeyName> = {
90
+ A: "up",
91
+ B: "down",
92
+ C: "right",
93
+ D: "left",
94
+ H: "home",
95
+ F: "end",
96
+ }
97
+
98
+ // Map CSI ~ sequences to key names (e.g., ESC[5~ = pageup)
99
+ const CSI_TILDE_MAP: Record<number, KeyName> = {
100
+ 2: "insert",
101
+ 3: "delete",
102
+ 5: "pageup",
103
+ 6: "pagedown",
104
+ }
105
+
106
+ /**
107
+ * Parse xterm modifier parameter into boolean flags.
108
+ * xterm uses 1+bitmask encoding: 2=shift, 3=alt, 4=shift+alt, 5=ctrl, etc.
109
+ * Bits: 0=shift, 1=alt/meta, 2=ctrl, 3=meta (extended)
110
+ */
111
+ function parseModifiers(modParam: number): { shift: boolean; meta: boolean; ctrl: boolean } {
112
+ const bits = modParam - 1
113
+ return {
114
+ shift: !!(bits & MOD_SHIFT),
115
+ meta: !!(bits & MOD_ALT), // Alt is typically mapped to meta
116
+ ctrl: !!(bits & MOD_CTRL),
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Try to decode a CSI sequence with optional modifiers.
122
+ * Handles: arrows (A/B/C/D), home/end (H/F), and tilde sequences (2~/3~/5~/6~)
123
+ */
124
+ function decodeCsiSequence(s: string): KeyMsg | null {
125
+ const match = CSI_RE.exec(s)
126
+ if (!match) return null
127
+
128
+ const params = match[1] ? match[1].split(";").map(Number) : []
129
+ const final = match[2]
130
+
131
+ // Handle arrow keys and home/end (A/B/C/D/H/F)
132
+ const arrowName = CSI_ARROW_MAP[final]
133
+ if (arrowName) {
134
+ // Get modifier from 2nd param (ESC[1;2A) or 1st if only one (rare)
135
+ const modParam = params.length >= 2 ? params[1] : params.length === 1 && params[0] >= 2 ? params[0] : null
136
+ const mods = modParam ? parseModifiers(modParam) : { shift: false, meta: false, ctrl: false }
137
+
138
+ return {
139
+ type: "key",
140
+ name: arrowName,
141
+ ...(mods.shift && { shift: true }),
142
+ ...(mods.meta && { meta: true }),
143
+ ...(mods.ctrl && { ctrl: true }),
144
+ }
145
+ }
146
+
147
+ // Handle tilde sequences (ESC[5~, ESC[3;5~ for ctrl+delete, etc.)
148
+ if (final === "~") {
149
+ const keyCode = params[0]
150
+ const tildeName = keyCode !== undefined ? CSI_TILDE_MAP[keyCode] : undefined
151
+ if (tildeName) {
152
+ const modParam = params.length >= 2 ? params[1] : null
153
+ const mods = modParam ? parseModifiers(modParam) : { shift: false, meta: false, ctrl: false }
154
+
155
+ return {
156
+ type: "key",
157
+ name: tildeName,
158
+ ...(mods.shift && { shift: true }),
159
+ ...(mods.meta && { meta: true }),
160
+ ...(mods.ctrl && { ctrl: true }),
161
+ }
162
+ }
163
+ }
164
+
165
+ return null
166
+ }
167
+
168
+ // SGR mouse event regex
169
+ const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g
170
+
171
+ /**
172
+ * Parse SGR mouse button code into button name and modifiers.
173
+ * Code format: low 2 bits = button (0-2), bits 2-5 = modifiers, bit 5 = motion, bits 6-7 = scroll
174
+ */
175
+ function parseSgrMouse(code: number, cx: number, cy: number, isRelease: boolean): MouseMsg {
176
+ // Scroll events (bits 6-7)
177
+ if (code >= 64) {
178
+ return {
179
+ type: "mouse",
180
+ button: code === MOUSE_SCROLL_UP ? "scroll-up" : "scroll-down",
181
+ x: cx - 1, // Convert to 0-based
182
+ y: cy - 1,
183
+ action: "press", // Scroll doesn't have release
184
+ shift: Boolean(code & 4),
185
+ meta: Boolean(code & 8),
186
+ ctrl: Boolean(code & 16),
187
+ }
188
+ }
189
+
190
+ // Button events
191
+ const buttonBits = code & 3
192
+ const isMotion = Boolean(code & 32)
193
+
194
+ let button: MouseButton
195
+ if (buttonBits === MOUSE_LEFT) button = "left"
196
+ else if (buttonBits === MOUSE_MIDDLE) button = "middle"
197
+ else if (buttonBits === MOUSE_RIGHT) button = "right"
198
+ else button = "none"
199
+
200
+ let action: MouseMsg["action"]
201
+ if (isMotion) {
202
+ action = button === "none" ? "move" : "drag"
203
+ } else {
204
+ action = isRelease ? "release" : "press"
205
+ }
206
+
207
+ return {
208
+ type: "mouse",
209
+ button,
210
+ x: cx - 1,
211
+ y: cy - 1,
212
+ action,
213
+ shift: Boolean(code & 4),
214
+ meta: Boolean(code & 8),
215
+ ctrl: Boolean(code & 16),
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Decode input buffer into keyboard and mouse events.
221
+ * Use this instead of decodeKeys if you want mouse support.
222
+ */
223
+ export function* decodeInput(buf: Buffer): Iterable<InputMsg> {
224
+ let s = buf.toString("utf8")
225
+
226
+ // Extract all SGR mouse events first
227
+ for (const m of s.matchAll(SGR_MOUSE_RE)) {
228
+ const code = parseInt(m[1], 10)
229
+ const cx = parseInt(m[2], 10)
230
+ const cy = parseInt(m[3], 10)
231
+ const isRelease = m[4] === "m"
232
+ const mouse = parseSgrMouse(code, cx, cy, isRelease)
233
+
234
+ // Yield the mouse event
235
+ yield mouse
236
+
237
+ // For scroll wheel, ALSO emit legacy KeyMsg for backward compatibility
238
+ // (useScroll listens for pageup/pagedown with meta: true)
239
+ if (mouse.button === "scroll-up") {
240
+ yield { type: "key", name: "pageup", meta: true } as KeyMsg
241
+ } else if (mouse.button === "scroll-down") {
242
+ yield { type: "key", name: "pagedown", meta: true } as KeyMsg
243
+ }
244
+ }
245
+
246
+ // Strip mouse sequences and process remaining as keys
247
+ s = s.replace(SGR_MOUSE_RE, "")
248
+ if (s.length > 0) {
249
+ yield* decodeKeys(Buffer.from(s, "utf8"))
250
+ }
251
+ }
252
+
41
253
  export function* decodeKeys(buf: Buffer): Iterable<KeyMsg> {
42
- // Handle control characters (0x00-0x1F)
43
- // These are generated by Ctrl+letter combinations
44
- if (buf.length === 1 && buf[0] !== undefined && buf[0] < 0x20) {
45
- const byte = buf[0]
46
-
47
- // Special control characters with specific meanings
48
- switch (byte) {
49
- case 0x03: // Ctrl-C (ETX - End of Text)
50
- yield { type: "key", name: "char", text: "c", ctrl: true }
51
- return
52
- case 0x04: // Ctrl-D (EOT - End of Transmission)
53
- yield { type: "key", name: "char", text: "d", ctrl: true }
54
- return
55
- case 0x17: // Ctrl-W (ETB - End of Transmission Block)
56
- yield { type: "key", name: "char", text: "w", ctrl: true }
57
- return
58
- case 0x1a: // Ctrl-Z (SUB - Substitute)
59
- yield { type: "key", name: "char", text: "z", ctrl: true }
60
- return
61
- case 0x0c: // Ctrl-L (FF - Form Feed)
62
- yield { type: "key", name: "char", text: "l", ctrl: true }
63
- return
64
- case 0x09: // Tab (HT - Horizontal Tab)
65
- yield { type: "key", name: "tab" }
66
- return
67
- case 0x0d: // Enter (CR - Carriage Return)
68
- yield { type: "key", name: "enter" }
69
- return
70
- case 0x0a: // Line Feed (LF)
71
- yield { type: "key", name: "enter" }
72
- return
73
- case 0x08: // Backspace (BS)
74
- yield { type: "key", name: "backspace" }
75
- return
76
- case 0x1b: // Escape (ESC)
77
- yield { type: "key", name: "escape" }
78
- return
79
- default:
80
- // Other control characters: convert back to letter
81
- // Ctrl-A = 0x01, Ctrl-B = 0x02, etc.
82
- if (byte >= 0x01 && byte <= 0x1a) {
83
- const letter = String.fromCharCode(byte + 96) // 0x01 + 96 = 'a'
84
- yield { type: "key", name: "char", text: letter, ctrl: true }
85
- } else {
86
- // Unknown control character, emit raw
87
- yield { type: "key", name: "char", text: String.fromCharCode(byte), ctrl: true }
88
- }
89
- return
90
- }
91
- }
92
-
93
- // Handle 0x7F (DEL) separately as it's outside the control character range
94
- if (buf.length === 1 && buf[0] === 0x7f) {
95
- yield { type: "key", name: "backspace" }
96
- return
97
- }
98
-
99
- let s = buf.toString("utf8")
100
-
101
- // Handle one or more SGR mouse reports possibly coalesced in a single chunk.
102
- // Format: ESC [ < Cb ; Cx ; Cy (M|m) — terminals vary on the trailing letter; accept any A-Z.
103
- const sgrRe = /\x1b\x5b<(?<cb>\d+);(?<cx>\d+);(?<cy>\d+)(?<updown>[A-Za-z])/g
104
- let matchedAny = false
105
- for (const m of s.matchAll(sgrRe)) {
106
- matchedAny = true
107
- const code = parseInt(m.groups?.cb ?? "0", 10)
108
- if (code === 64) {
109
- yield { type: "key", name: "pageup", meta: true }
110
- continue
111
- }
112
- if (code === 65) {
113
- yield { type: "key", name: "pagedown", meta: true }
114
- }
115
- // ignore other mouse events
116
- }
117
- if (matchedAny) {
118
- // Strip SGR mouse sequences so they never leak into the input as text
119
- // Create a fresh regex since the global one was consumed by matchAll()
120
- s = s.replace(/\x1b\x5b<(?<cb>\d+);(?<cx>\d+);(?<cy>\d+)(?<updown>[A-Za-z])/g, "")
121
- if (s.length === 0) return
122
- }
123
-
124
- // Legacy single SGR report path (when only one arrives)
125
- const sgr = /^\x1b\[<(?<cb>\d+);(?<cx>\d+);(?<cy>\d+)(?<updown>[A-Za-z])$/.exec(s)
126
- if (sgr?.groups) {
127
- const code = parseInt(sgr.groups.cb ?? "0", 10)
128
- if (code === 64) {
129
- yield { type: "key", name: "pageup", meta: true }
130
- return
131
- }
132
- if (code === 65) {
133
- yield { type: "key", name: "pagedown", meta: true }
134
- return
135
- }
136
- return
137
- }
138
-
139
- // Meta+Backspace (Option+Backspace on macOS terminals) — often ESC + DEL / BS
140
- if (s === `${ESC}\x7f` || s === `${ESC}\b` || s === `${ESC}\x08`) {
141
- yield { type: "key", name: "backspace", meta: true }
142
- return
143
- }
144
-
145
- // Alt/Meta + Enter (many terminals send ESC + CR/LF)
146
- if (s === `${ESC}\r` || s === `${ESC}\n`) {
147
- yield { type: "key", name: "enter", meta: true }
148
- return
149
- }
150
-
151
- // modifyOtherKeys (xterm / some terminals): CSI 27;modifier;code~
152
- const moo = /^\x1b\[27;(?<mod>\d+);(?<code>\d+)~$/.exec(s)
153
- if (moo?.groups) {
154
- const mod = parseInt(moo.groups.mod ?? "1", 10) - 1
155
- const code = parseInt(moo.groups.code ?? "0", 10)
156
- const shift = !!(mod & 1)
157
- const alt = !!(mod & 2)
158
- const ctrl = !!(mod & 4)
159
- const meta = !!(mod & 8) || alt
160
- const key = fromCharCode(code, { shift, ctrl, meta, source: "modify-other" })
161
- if (key) {
162
- yield key
163
- return
164
- }
165
- }
166
-
167
- // CSI-u (modern modifyOtherKeys): CSI <code> ; <modifier> u
168
- const csiu = /^\x1b\[(?<code>\d+);(?<mod>\d+)u$/.exec(s)
169
- if (csiu?.groups) {
170
- const mod = parseInt(csiu.groups.mod ?? "1", 10) - 1
171
- const code = parseInt(csiu.groups.code ?? "0", 10)
172
- const shift = !!(mod & 1)
173
- const alt = !!(mod & 2)
174
- const ctrl = !!(mod & 4)
175
- const meta = !!(mod & 8) || alt
176
- const key = fromCharCode(code, { shift, ctrl, meta, source: "csiu" })
177
- if (key) {
178
- yield key
179
- return
180
- }
181
- }
182
-
183
- // Escape (lone ESC) - handle after meta combinations
184
- if (s === ESC) {
185
- yield { type: "key", name: "escape" }
186
- return
187
- }
188
-
189
- // Single printable
190
- if (s.length === 1 && s !== ESC && s !== "\x7f") {
191
- if (s === " ") {
192
- yield { type: "key", name: "space", text: " " }
193
- return
194
- }
195
- if (s >= " ") {
196
- yield { type: "key", name: "char", text: s }
197
- return
198
- }
199
- }
200
-
201
- // Common escapes
202
- switch (s) {
203
- case `${ESC}[A`:
204
- yield { type: "key", name: "up" }
205
- return
206
- case `${ESC}[B`:
207
- yield { type: "key", name: "down" }
208
- return
209
- case `${ESC}[C`:
210
- yield { type: "key", name: "right" }
211
- return
212
- case `${ESC}[D`:
213
- yield { type: "key", name: "left" }
214
- return
215
- case `${ESC}[1;3C`:
216
- yield { type: "key", name: "right", meta: true }
217
- return
218
- case `${ESC}[1;3D`:
219
- yield { type: "key", name: "left", meta: true }
220
- return
221
- case `${ESC}OP`: // F1 (ignored)
222
- return
223
- case `${ESC}[H`:
224
- yield { type: "key", name: "home" }
225
- return
226
- case `${ESC}[F`:
227
- yield { type: "key", name: "end" }
228
- return
229
- case `${ESC}[5~`:
230
- yield { type: "key", name: "pageup" }
231
- return
232
- case `${ESC}[6~`:
233
- yield { type: "key", name: "pagedown" }
234
- return
235
- case `${ESC}[2~`:
236
- yield { type: "key", name: "insert" }
237
- return
238
- case `${ESC}[3~`:
239
- yield { type: "key", name: "delete" }
240
- return
241
- case `${ESC}[3;3~`: // Meta+Delete (best-effort for some terminals)
242
- yield { type: "key", name: "delete", meta: true }
243
- return
244
- case `${ESC}[Z`:
245
- yield { type: "key", name: "shift-tab", shift: true }
246
- return
247
- }
248
-
249
- // Alt/Meta + char (ESC <char>)
250
- if (s.length === 2 && s[0] === ESC && s[1] && s[1] >= " ") {
251
- const ch = s[1]
252
- if (ch === " ") {
253
- yield { type: "key", name: "space", text: " ", meta: true }
254
- return
255
- }
256
- yield { type: "key", name: "char", text: ch, meta: true }
257
- return
258
- }
259
-
260
- // Fallback: emit printable chars individually
261
- for (const ch of s) {
262
- if (ch >= " ") yield { type: "key", name: "char", text: ch }
263
- }
254
+ // Handle control characters (0x00-0x1F)
255
+ // These are generated by Ctrl+letter combinations
256
+ if (buf.length === 1 && buf[0] !== undefined && buf[0] < 0x20) {
257
+ const byte = buf[0]
258
+
259
+ // Special control characters with specific meanings
260
+ switch (byte) {
261
+ case 0x03: // Ctrl-C (ETX - End of Text)
262
+ yield { type: "key", name: "char", text: "c", ctrl: true }
263
+ return
264
+ case 0x04: // Ctrl-D (EOT - End of Transmission)
265
+ yield { type: "key", name: "char", text: "d", ctrl: true }
266
+ return
267
+ case 0x17: // Ctrl-W (ETB - End of Transmission Block)
268
+ yield { type: "key", name: "char", text: "w", ctrl: true }
269
+ return
270
+ case 0x1a: // Ctrl-Z (SUB - Substitute)
271
+ yield { type: "key", name: "char", text: "z", ctrl: true }
272
+ return
273
+ case 0x0c: // Ctrl-L (FF - Form Feed)
274
+ yield { type: "key", name: "char", text: "l", ctrl: true }
275
+ return
276
+ case 0x09: // Tab (HT - Horizontal Tab)
277
+ yield { type: "key", name: "tab" }
278
+ return
279
+ case 0x0d: // Enter (CR - Carriage Return)
280
+ yield { type: "key", name: "enter" }
281
+ return
282
+ case 0x0a: // Line Feed (LF)
283
+ yield { type: "key", name: "enter" }
284
+ return
285
+ case 0x08: // Backspace (BS)
286
+ yield { type: "key", name: "backspace" }
287
+ return
288
+ case 0x1b: // Escape (ESC)
289
+ yield { type: "key", name: "escape" }
290
+ return
291
+ default:
292
+ // Other control characters: convert back to letter
293
+ // Ctrl-A = 0x01, Ctrl-B = 0x02, etc.
294
+ if (byte >= 0x01 && byte <= 0x1a) {
295
+ const letter = String.fromCharCode(byte + 96) // 0x01 + 96 = 'a'
296
+ yield { type: "key", name: "char", text: letter, ctrl: true }
297
+ } else {
298
+ // Unknown control character, emit raw
299
+ yield { type: "key", name: "char", text: String.fromCharCode(byte), ctrl: true }
300
+ }
301
+ return
302
+ }
303
+ }
304
+
305
+ // Handle 0x7F (DEL) separately as it's outside the control character range
306
+ if (buf.length === 1 && buf[0] === 0x7f) {
307
+ yield { type: "key", name: "backspace" }
308
+ return
309
+ }
310
+
311
+ let s = buf.toString("utf8")
312
+
313
+ // Handle one or more SGR mouse reports possibly coalesced in a single chunk.
314
+ // Format: ESC [ < Cb ; Cx ; Cy (M|m) — terminals vary on the trailing letter; accept any A-Z.
315
+ const sgrRe = /\x1b\x5b<(?<cb>\d+);(?<cx>\d+);(?<cy>\d+)(?<updown>[A-Za-z])/g
316
+ let matchedAny = false
317
+ for (const m of s.matchAll(sgrRe)) {
318
+ matchedAny = true
319
+ const code = parseInt(m.groups?.cb ?? "0", 10)
320
+ if (code === MOUSE_SCROLL_UP) {
321
+ yield { type: "key", name: "pageup", meta: true }
322
+ continue
323
+ }
324
+ if (code === MOUSE_SCROLL_DOWN) {
325
+ yield { type: "key", name: "pagedown", meta: true }
326
+ }
327
+ // ignore other mouse events
328
+ }
329
+ if (matchedAny) {
330
+ // Strip SGR mouse sequences so they never leak into the input as text
331
+ // Create a fresh regex since the global one was consumed by matchAll()
332
+ s = s.replace(/\x1b\x5b<(?<cb>\d+);(?<cx>\d+);(?<cy>\d+)(?<updown>[A-Za-z])/g, "")
333
+ if (s.length === 0) return
334
+ }
335
+
336
+ // Legacy single SGR report path (when only one arrives)
337
+ const sgr = /^\x1b\[<(?<cb>\d+);(?<cx>\d+);(?<cy>\d+)(?<updown>[A-Za-z])$/.exec(s)
338
+ if (sgr?.groups) {
339
+ const code = parseInt(sgr.groups.cb ?? "0", 10)
340
+ if (code === MOUSE_SCROLL_UP) {
341
+ yield { type: "key", name: "pageup", meta: true }
342
+ return
343
+ }
344
+ if (code === MOUSE_SCROLL_DOWN) {
345
+ yield { type: "key", name: "pagedown", meta: true }
346
+ return
347
+ }
348
+ return
349
+ }
350
+
351
+ // Meta+Backspace (Option+Backspace on macOS terminals) — often ESC + DEL / BS
352
+ if (s === `${ESC}\x7f` || s === `${ESC}\b` || s === `${ESC}\x08`) {
353
+ yield { type: "key", name: "backspace", meta: true }
354
+ return
355
+ }
356
+
357
+ // Alt/Meta + Enter (many terminals send ESC + CR/LF)
358
+ if (s === `${ESC}\r` || s === `${ESC}\n`) {
359
+ yield { type: "key", name: "enter", meta: true }
360
+ return
361
+ }
362
+
363
+ // modifyOtherKeys (xterm / some terminals): CSI 27;modifier;code~
364
+ const moo = /^\x1b\[27;(?<mod>\d+);(?<code>\d+)~$/.exec(s)
365
+ if (moo?.groups) {
366
+ const mod = parseInt(moo.groups.mod ?? "1", 10) - 1
367
+ const code = parseInt(moo.groups.code ?? "0", 10)
368
+ const shift = !!(mod & MOD_SHIFT)
369
+ const alt = !!(mod & MOD_ALT)
370
+ const ctrl = !!(mod & MOD_CTRL)
371
+ const meta = !!(mod & MOD_META) || alt
372
+ const key = fromCharCode(code, { shift, ctrl, meta, source: "modify-other" })
373
+ if (key) {
374
+ yield key
375
+ return
376
+ }
377
+ }
378
+
379
+ // CSI-u (modern modifyOtherKeys): CSI <code> ; <modifier> u
380
+ const csiu = /^\x1b\[(?<code>\d+);(?<mod>\d+)u$/.exec(s)
381
+ if (csiu?.groups) {
382
+ const mod = parseInt(csiu.groups.mod ?? "1", 10) - 1
383
+ const code = parseInt(csiu.groups.code ?? "0", 10)
384
+ const shift = !!(mod & MOD_SHIFT)
385
+ const alt = !!(mod & MOD_ALT)
386
+ const ctrl = !!(mod & MOD_CTRL)
387
+ const meta = !!(mod & MOD_META) || alt
388
+ const key = fromCharCode(code, { shift, ctrl, meta, source: "csiu" })
389
+ if (key) {
390
+ yield key
391
+ return
392
+ }
393
+ }
394
+
395
+ // Escape (lone ESC) - handle after meta combinations
396
+ if (s === ESC) {
397
+ yield { type: "key", name: "escape" }
398
+ return
399
+ }
400
+
401
+ // Single printable
402
+ if (s.length === 1 && s !== ESC && s !== "\x7f") {
403
+ if (s === " ") {
404
+ yield { type: "key", name: "space", text: " " }
405
+ return
406
+ }
407
+ if (s >= " ") {
408
+ yield { type: "key", name: "char", text: s }
409
+ return
410
+ }
411
+ }
412
+
413
+ // Try generic CSI parser first (handles all modifier combinations)
414
+ const csiKey = decodeCsiSequence(s)
415
+ if (csiKey) {
416
+ yield csiKey
417
+ return
418
+ }
419
+
420
+ // Special cases not handled by generic CSI parser
421
+ switch (s) {
422
+ case `${ESC}OP`: // F1 (ignored)
423
+ return
424
+ case `${ESC}[Z`:
425
+ yield { type: "key", name: "shift-tab", shift: true }
426
+ return
427
+ }
428
+
429
+ // Alt/Meta + char (ESC <char>)
430
+ if (s.length === 2 && s[0] === ESC && s[1] && s[1] >= " ") {
431
+ const ch = s[1]
432
+ if (ch === " ") {
433
+ yield { type: "key", name: "space", text: " ", meta: true }
434
+ return
435
+ }
436
+ yield { type: "key", name: "char", text: ch, meta: true }
437
+ return
438
+ }
439
+
440
+ // Fallback: emit printable chars individually
441
+ for (const ch of s) {
442
+ if (ch >= " ") yield { type: "key", name: "char", text: ch }
443
+ }
264
444
  }
265
445
 
266
446
  function fromCharCode(
267
- code: number,
268
- mods: { shift?: boolean; ctrl?: boolean; meta?: boolean; source: KeyMsg["source"] },
447
+ code: number,
448
+ mods: { shift?: boolean; ctrl?: boolean; meta?: boolean; source: KeyMsg["source"] },
269
449
  ): KeyMsg | null {
270
- const { shift, ctrl, meta, source } = mods
271
- const common: Omit<KeyMsg, "name"> = {
272
- type: "key",
273
- ctrl,
274
- meta,
275
- shift,
276
- phase: "press",
277
- source,
278
- }
279
-
280
- switch (code) {
281
- case 13:
282
- return { ...common, name: "enter" }
283
- case 27:
284
- return { ...common, name: "escape" }
285
- case 9:
286
- return { ...common, name: "tab" }
287
- case 32:
288
- return { ...common, name: "space", text: " " }
289
- case 127:
290
- case 8:
291
- return { ...common, name: "backspace" }
292
- }
293
-
294
- if (code >= 0 && code < 32) {
295
- // Control character -> map to letter when possible
296
- const letter = String.fromCharCode(code + 96)
297
- return { ...common, name: "char", text: letter, ctrl: true }
298
- }
299
-
300
- const ch = String.fromCharCode(code)
301
- return { ...common, name: "char", text: ch }
450
+ const { shift, ctrl, meta, source } = mods
451
+ const common: Omit<KeyMsg, "name"> = {
452
+ type: "key",
453
+ ctrl,
454
+ meta,
455
+ shift,
456
+ phase: "press",
457
+ source,
458
+ }
459
+
460
+ switch (code) {
461
+ case KEY_ENTER_CR:
462
+ return { ...common, name: "enter" }
463
+ case KEY_ESC:
464
+ return { ...common, name: "escape" }
465
+ case KEY_TAB:
466
+ return { ...common, name: "tab" }
467
+ case KEY_SPACE:
468
+ return { ...common, name: "space", text: " " }
469
+ case KEY_BS:
470
+ case 8: // Backspace (legacy)
471
+ return { ...common, name: "backspace" }
472
+ }
473
+
474
+ if (code >= 0 && code < 32) {
475
+ // Control character -> map to letter when possible
476
+ const letter = String.fromCharCode(code + 96)
477
+ return { ...common, name: "char", text: letter, ctrl: true }
478
+ }
479
+
480
+ const ch = String.fromCharCode(code)
481
+ return { ...common, name: "char", text: ch }
302
482
  }