@effect-tui/react 0.15.2 → 2.0.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 (313) hide show
  1. package/README.md +11 -2
  2. package/dist/src/codeblock.d.ts +1 -1
  3. package/dist/src/codeblock.d.ts.map +1 -1
  4. package/dist/src/codeblock.js +2 -2
  5. package/dist/src/codeblock.js.map +1 -1
  6. package/dist/src/components/ListView.d.ts +4 -4
  7. package/dist/src/components/ListView.d.ts.map +1 -1
  8. package/dist/src/components/ListView.js +16 -17
  9. package/dist/src/components/ListView.js.map +1 -1
  10. package/dist/src/components/Markdown.js +3 -3
  11. package/dist/src/components/Markdown.js.map +1 -1
  12. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  13. package/dist/src/components/MultilineTextInput.js +133 -305
  14. package/dist/src/components/MultilineTextInput.js.map +1 -1
  15. package/dist/src/components/TextInput.d.ts.map +1 -1
  16. package/dist/src/components/TextInput.js +51 -98
  17. package/dist/src/components/TextInput.js.map +1 -1
  18. package/dist/src/components/text-editing.d.ts +61 -0
  19. package/dist/src/components/text-editing.d.ts.map +1 -1
  20. package/dist/src/components/text-editing.js +131 -0
  21. package/dist/src/components/text-editing.js.map +1 -1
  22. package/dist/src/console/ConsolePopover.d.ts +7 -1
  23. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  24. package/dist/src/console/ConsolePopover.js +55 -74
  25. package/dist/src/console/ConsolePopover.js.map +1 -1
  26. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  27. package/dist/src/debug/DebugOverlay.js +3 -57
  28. package/dist/src/debug/DebugOverlay.js.map +1 -1
  29. package/dist/src/debug/DiagnosticsPanel.js +1 -1
  30. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  31. package/dist/src/dev.d.ts +5 -117
  32. package/dist/src/dev.d.ts.map +1 -1
  33. package/dist/src/dev.js +3 -333
  34. package/dist/src/dev.js.map +1 -1
  35. package/dist/src/hooks/use-scroll.d.ts +31 -35
  36. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  37. package/dist/src/hooks/use-scroll.js +51 -90
  38. package/dist/src/hooks/use-scroll.js.map +1 -1
  39. package/dist/src/hosts/base.d.ts +13 -2
  40. package/dist/src/hosts/base.d.ts.map +1 -1
  41. package/dist/src/hosts/base.js +74 -2
  42. package/dist/src/hosts/base.js.map +1 -1
  43. package/dist/src/hosts/box.d.ts +2 -2
  44. package/dist/src/hosts/box.d.ts.map +1 -1
  45. package/dist/src/hosts/box.js +29 -2
  46. package/dist/src/hosts/box.js.map +1 -1
  47. package/dist/src/hosts/canvas.d.ts +24 -4
  48. package/dist/src/hosts/canvas.d.ts.map +1 -1
  49. package/dist/src/hosts/canvas.js +107 -41
  50. package/dist/src/hosts/canvas.js.map +1 -1
  51. package/dist/src/hosts/codeblock.d.ts +10 -12
  52. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  53. package/dist/src/hosts/codeblock.js +38 -35
  54. package/dist/src/hosts/codeblock.js.map +1 -1
  55. package/dist/src/hosts/flex-container.d.ts +3 -3
  56. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  57. package/dist/src/hosts/flex-container.js +20 -5
  58. package/dist/src/hosts/flex-container.js.map +1 -1
  59. package/dist/src/hosts/index.d.ts +3 -2
  60. package/dist/src/hosts/index.d.ts.map +1 -1
  61. package/dist/src/hosts/index.js +2 -1
  62. package/dist/src/hosts/index.js.map +1 -1
  63. package/dist/src/hosts/layout-helpers.d.ts +10 -0
  64. package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
  65. package/dist/src/hosts/layout-helpers.js +10 -0
  66. package/dist/src/hosts/layout-helpers.js.map +1 -0
  67. package/dist/src/hosts/leaf.d.ts +14 -0
  68. package/dist/src/hosts/leaf.d.ts.map +1 -0
  69. package/dist/src/hosts/leaf.js +31 -0
  70. package/dist/src/hosts/leaf.js.map +1 -0
  71. package/dist/src/hosts/overlay-item.d.ts +2 -2
  72. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  73. package/dist/src/hosts/overlay-item.js +7 -2
  74. package/dist/src/hosts/overlay-item.js.map +1 -1
  75. package/dist/src/hosts/overlay.d.ts +2 -2
  76. package/dist/src/hosts/overlay.d.ts.map +1 -1
  77. package/dist/src/hosts/overlay.js +6 -9
  78. package/dist/src/hosts/overlay.js.map +1 -1
  79. package/dist/src/hosts/scroll.d.ts +54 -26
  80. package/dist/src/hosts/scroll.d.ts.map +1 -1
  81. package/dist/src/hosts/scroll.js +185 -87
  82. package/dist/src/hosts/scroll.js.map +1 -1
  83. package/dist/src/hosts/single-child.d.ts.map +1 -1
  84. package/dist/src/hosts/single-child.js +2 -0
  85. package/dist/src/hosts/single-child.js.map +1 -1
  86. package/dist/src/hosts/spacer.d.ts +3 -3
  87. package/dist/src/hosts/spacer.d.ts.map +1 -1
  88. package/dist/src/hosts/spacer.js +8 -3
  89. package/dist/src/hosts/spacer.js.map +1 -1
  90. package/dist/src/hosts/text.d.ts +22 -18
  91. package/dist/src/hosts/text.d.ts.map +1 -1
  92. package/dist/src/hosts/text.js +108 -131
  93. package/dist/src/hosts/text.js.map +1 -1
  94. package/dist/src/hosts/vstack.js +1 -1
  95. package/dist/src/hosts/vstack.js.map +1 -1
  96. package/dist/src/hosts/zstack.d.ts +3 -3
  97. package/dist/src/hosts/zstack.d.ts.map +1 -1
  98. package/dist/src/hosts/zstack.js +13 -8
  99. package/dist/src/hosts/zstack.js.map +1 -1
  100. package/dist/src/index.d.ts +2 -2
  101. package/dist/src/index.d.ts.map +1 -1
  102. package/dist/src/internal/dev/hmr.d.ts +20 -0
  103. package/dist/src/internal/dev/hmr.d.ts.map +1 -0
  104. package/dist/src/internal/dev/hmr.js +93 -0
  105. package/dist/src/internal/dev/hmr.js.map +1 -0
  106. package/dist/src/internal/dev/runtime.d.ts +24 -0
  107. package/dist/src/internal/dev/runtime.d.ts.map +1 -0
  108. package/dist/src/internal/dev/runtime.js +135 -0
  109. package/dist/src/internal/dev/runtime.js.map +1 -0
  110. package/dist/src/internal/dev/ui.d.ts +13 -0
  111. package/dist/src/internal/dev/ui.d.ts.map +1 -0
  112. package/dist/src/internal/dev/ui.js +51 -0
  113. package/dist/src/internal/dev/ui.js.map +1 -0
  114. package/dist/src/internal/renderer/context.d.ts +9 -0
  115. package/dist/src/internal/renderer/context.d.ts.map +1 -0
  116. package/dist/src/internal/renderer/context.js +22 -0
  117. package/dist/src/internal/renderer/context.js.map +1 -0
  118. package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
  119. package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
  120. package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
  121. package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
  122. package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
  123. package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
  124. package/dist/src/internal/renderer/core/RendererState.js +70 -0
  125. package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
  126. package/dist/src/internal/renderer/core/index.d.ts +3 -0
  127. package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
  128. package/dist/src/internal/renderer/core/index.js +3 -0
  129. package/dist/src/internal/renderer/core/index.js.map +1 -0
  130. package/dist/src/internal/renderer/index.d.ts +40 -0
  131. package/dist/src/internal/renderer/index.d.ts.map +1 -0
  132. package/dist/src/internal/renderer/index.js +543 -0
  133. package/dist/src/internal/renderer/index.js.map +1 -0
  134. package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
  135. package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
  136. package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
  137. package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
  138. package/dist/src/internal/renderer/input/index.d.ts +2 -0
  139. package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
  140. package/dist/src/internal/renderer/input/index.js +2 -0
  141. package/dist/src/internal/renderer/input/index.js.map +1 -0
  142. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
  143. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
  144. package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
  145. package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
  146. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
  147. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
  148. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
  149. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
  150. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
  151. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
  152. package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
  153. package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
  154. package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
  155. package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
  156. package/dist/src/internal/renderer/lifecycle/index.js +4 -0
  157. package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
  158. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
  159. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  160. package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
  161. package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
  162. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
  163. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
  164. package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
  165. package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
  166. package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
  167. package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
  168. package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
  169. package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
  170. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
  171. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  172. package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
  173. package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
  174. package/dist/src/internal/renderer/modes/index.d.ts +5 -0
  175. package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
  176. package/dist/src/internal/renderer/modes/index.js +4 -0
  177. package/dist/src/internal/renderer/modes/index.js.map +1 -0
  178. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
  179. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
  180. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
  181. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
  182. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
  183. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
  184. package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
  185. package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
  186. package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
  187. package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
  188. package/dist/src/internal/renderer/terminal/index.js +3 -0
  189. package/dist/src/internal/renderer/terminal/index.js.map +1 -0
  190. package/dist/src/internal/renderer/types.d.ts +122 -0
  191. package/dist/src/internal/renderer/types.d.ts.map +1 -0
  192. package/dist/src/internal/renderer/types.js +2 -0
  193. package/dist/src/internal/renderer/types.js.map +1 -0
  194. package/dist/src/motion/hooks.d.ts +1 -1
  195. package/dist/src/motion/hooks.js +1 -1
  196. package/dist/src/reconciler/host-config.js +2 -2
  197. package/dist/src/reconciler/host-config.js.map +1 -1
  198. package/dist/src/reconciler/types.d.ts +5 -1
  199. package/dist/src/reconciler/types.d.ts.map +1 -1
  200. package/dist/src/renderer-context.d.ts +1 -8
  201. package/dist/src/renderer-context.d.ts.map +1 -1
  202. package/dist/src/renderer-context.js +1 -21
  203. package/dist/src/renderer-context.js.map +1 -1
  204. package/dist/src/renderer-types.d.ts +1 -115
  205. package/dist/src/renderer-types.d.ts.map +1 -1
  206. package/dist/src/renderer.d.ts +1 -31
  207. package/dist/src/renderer.d.ts.map +1 -1
  208. package/dist/src/renderer.js +1 -495
  209. package/dist/src/renderer.js.map +1 -1
  210. package/dist/src/test/render-tui.d.ts +3 -3
  211. package/dist/src/test/render-tui.d.ts.map +1 -1
  212. package/dist/src/test/render-tui.js +16 -9
  213. package/dist/src/test/render-tui.js.map +1 -1
  214. package/dist/src/utils/alignment.d.ts +1 -1
  215. package/dist/src/utils/alignment.d.ts.map +1 -1
  216. package/dist/src/utils/alignment.js +0 -2
  217. package/dist/src/utils/alignment.js.map +1 -1
  218. package/dist/src/utils/border.d.ts +1 -1
  219. package/dist/src/utils/border.d.ts.map +1 -1
  220. package/dist/src/utils/border.js +2 -0
  221. package/dist/src/utils/border.js.map +1 -1
  222. package/dist/src/utils/console-helpers.d.ts +19 -0
  223. package/dist/src/utils/console-helpers.d.ts.map +1 -0
  224. package/dist/src/utils/console-helpers.js +61 -0
  225. package/dist/src/utils/console-helpers.js.map +1 -0
  226. package/dist/src/utils/index.d.ts +2 -1
  227. package/dist/src/utils/index.d.ts.map +1 -1
  228. package/dist/src/utils/index.js +2 -1
  229. package/dist/src/utils/index.js.map +1 -1
  230. package/dist/src/utils/styles.d.ts +8 -1
  231. package/dist/src/utils/styles.d.ts.map +1 -1
  232. package/dist/src/utils/styles.js +10 -8
  233. package/dist/src/utils/styles.js.map +1 -1
  234. package/dist/src/utils/text-layout.d.ts +22 -0
  235. package/dist/src/utils/text-layout.d.ts.map +1 -0
  236. package/dist/src/utils/text-layout.js +37 -0
  237. package/dist/src/utils/text-layout.js.map +1 -0
  238. package/dist/src/utils/text-wrap.d.ts +31 -1
  239. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  240. package/dist/src/utils/text-wrap.js +205 -48
  241. package/dist/src/utils/text-wrap.js.map +1 -1
  242. package/dist/src/visualize/index.js +1 -1
  243. package/dist/src/visualize/index.js.map +1 -1
  244. package/dist/tsconfig.tsbuildinfo +1 -1
  245. package/package.json +2 -2
  246. package/src/codeblock.tsx +2 -2
  247. package/src/components/ListView.tsx +21 -23
  248. package/src/components/Markdown.tsx +3 -3
  249. package/src/components/MultilineTextInput.tsx +138 -344
  250. package/src/components/TextInput.tsx +54 -99
  251. package/src/components/text-editing.ts +180 -0
  252. package/src/console/ConsolePopover.tsx +124 -107
  253. package/src/debug/DebugOverlay.ts +15 -74
  254. package/src/debug/DiagnosticsPanel.tsx +1 -1
  255. package/src/dev.tsx +5 -458
  256. package/src/hooks/use-scroll.ts +85 -145
  257. package/src/hosts/base.ts +86 -3
  258. package/src/hosts/box.ts +37 -2
  259. package/src/hosts/canvas.ts +128 -42
  260. package/src/hosts/codeblock.ts +48 -35
  261. package/src/hosts/flex-container.ts +25 -6
  262. package/src/hosts/index.ts +11 -2
  263. package/src/hosts/layout-helpers.ts +20 -0
  264. package/src/hosts/leaf.ts +36 -0
  265. package/src/hosts/overlay-item.ts +8 -2
  266. package/src/hosts/overlay.ts +13 -11
  267. package/src/hosts/scroll.ts +228 -106
  268. package/src/hosts/single-child.ts +2 -0
  269. package/src/hosts/spacer.ts +8 -3
  270. package/src/hosts/text.ts +126 -132
  271. package/src/hosts/vstack.ts +1 -1
  272. package/src/hosts/zstack.ts +14 -9
  273. package/src/index.ts +2 -2
  274. package/src/internal/dev/hmr.ts +101 -0
  275. package/src/internal/dev/runtime.ts +170 -0
  276. package/src/internal/dev/ui.tsx +87 -0
  277. package/src/internal/renderer/context.ts +27 -0
  278. package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
  279. package/src/internal/renderer/index.ts +689 -0
  280. package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
  281. package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
  282. package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
  283. package/src/internal/renderer/lifecycle/index.ts +3 -0
  284. package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
  285. package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
  286. package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
  287. package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
  288. package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
  289. package/src/internal/renderer/terminal/index.ts +2 -0
  290. package/src/internal/renderer/types.ts +129 -0
  291. package/src/motion/hooks.ts +1 -1
  292. package/src/reconciler/host-config.ts +2 -2
  293. package/src/reconciler/types.ts +7 -1
  294. package/src/renderer-context.ts +1 -27
  295. package/src/renderer-types.ts +10 -123
  296. package/src/renderer.ts +1 -619
  297. package/src/test/render-tui.ts +16 -10
  298. package/src/utils/alignment.ts +1 -3
  299. package/src/utils/border.ts +11 -1
  300. package/src/utils/console-helpers.ts +86 -0
  301. package/src/utils/index.ts +15 -1
  302. package/src/utils/styles.ts +16 -4
  303. package/src/utils/text-layout.ts +65 -0
  304. package/src/utils/text-wrap.ts +261 -48
  305. package/src/visualize/index.tsx +1 -1
  306. package/src/renderer/lifecycle/ResizeManager.ts +0 -65
  307. package/src/renderer/lifecycle/index.ts +0 -4
  308. /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
  309. /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
  310. /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
  311. /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
  312. /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
  313. /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
@@ -7,12 +7,15 @@ import {
7
7
  deleteCharBackward,
8
8
  deleteCharForward,
9
9
  deleteWordBackward,
10
+ deleteWordForward,
10
11
  insertText,
11
12
  killToEnd,
12
13
  killToStart,
13
14
  matchNextWord,
14
15
  matchPrevWord,
16
+ resolveTextInputAction,
15
17
  type TextState,
18
+ type TextKeyEvent,
16
19
  transposeChars,
17
20
  } from "./text-editing.js"
18
21
 
@@ -97,17 +100,20 @@ export function TextInput({
97
100
  }, [value, cursorPos])
98
101
 
99
102
  const handleKey = useCallback(
100
- (key: { name: string; text?: string; ctrl?: boolean; meta?: boolean; shift?: boolean }) => {
103
+ (key: TextKeyEvent) => {
101
104
  if (!focused) return
102
105
 
106
+ const action = resolveTextInputAction(key)
107
+ if (!action) return
108
+
103
109
  const state: TextState = { text: value, cursor: cursorPos, killRing }
104
110
 
105
111
  // Helper to apply an edit result
106
- const applyEdit = (result: { state: TextState; changed: boolean }) => {
112
+ const applyEdit = (result: { state: TextState; changed: boolean }, options?: { keepKillRing?: boolean }) => {
107
113
  if (result.changed) {
108
114
  onChange(result.state.text)
109
115
  setCursorPos(result.state.cursor)
110
- if (result.state.killRing !== killRing) {
116
+ if (!options?.keepKillRing && result.state.killRing !== killRing) {
111
117
  setKillRing(result.state.killRing)
112
118
  }
113
119
  }
@@ -126,114 +132,63 @@ export function TextInput({
126
132
  setCursorPos(match ? cursorPos + match.length : value.length)
127
133
  }
128
134
 
129
- switch (key.name) {
130
- case "left":
131
- if (key.meta) {
132
- moveToPrevWord()
133
- } else {
134
- setCursorPos(Math.max(0, cursorPos - 1))
135
- }
135
+ switch (action.type) {
136
+ case "move-left":
137
+ setCursorPos(Math.max(0, cursorPos - 1))
136
138
  break
137
-
138
- case "right":
139
- if (key.meta) {
140
- moveToNextWord()
141
- } else {
142
- setCursorPos(Math.min(value.length, cursorPos + 1))
143
- }
139
+ case "move-right":
140
+ setCursorPos(Math.min(value.length, cursorPos + 1))
144
141
  break
145
-
146
- case "home":
142
+ case "move-word-left":
143
+ moveToPrevWord()
144
+ break
145
+ case "move-word-right":
146
+ moveToNextWord()
147
+ break
148
+ case "move-start":
149
+ case "move-doc-start":
147
150
  setCursorPos(0)
148
151
  break
149
-
150
- case "end":
152
+ case "move-end":
153
+ case "move-doc-end":
151
154
  setCursorPos(value.length)
152
155
  break
153
-
154
- case "backspace":
155
- if (key.meta) {
156
- // Option+Backspace: Delete to previous word boundary
157
- const beforeCursor = value.slice(0, cursorPos)
158
- const match = matchPrevWord(beforeCursor)
159
- if (match) {
160
- const newPos = cursorPos - match.length
161
- onChange(value.slice(0, newPos) + value.slice(cursorPos))
162
- setCursorPos(newPos)
163
- } else if (cursorPos > 0) {
164
- onChange(value.slice(cursorPos))
165
- setCursorPos(0)
166
- }
167
- } else {
168
- applyEdit(deleteCharBackward(state))
169
- }
156
+ case "delete-backward":
157
+ applyEdit(deleteCharBackward(state))
170
158
  break
171
-
172
- case "delete":
173
- if (key.meta) {
174
- // Option+Delete: Delete to next word boundary
175
- const afterCursor = value.slice(cursorPos)
176
- const match = matchNextWord(afterCursor)
177
- if (match) {
178
- onChange(value.slice(0, cursorPos) + value.slice(cursorPos + match.length))
179
- } else if (cursorPos < value.length) {
180
- onChange(value.slice(0, cursorPos))
181
- }
182
- } else {
183
- applyEdit(deleteCharForward(state))
184
- }
159
+ case "delete-forward":
160
+ applyEdit(deleteCharForward(state))
161
+ break
162
+ case "delete-word-backward":
163
+ applyEdit(deleteWordBackward(state), { keepKillRing: action.scope === "line" })
164
+ break
165
+ case "delete-word-forward":
166
+ applyEdit(deleteWordForward(state), { keepKillRing: action.scope === "line" })
167
+ break
168
+ case "kill-to-end":
169
+ applyEdit(killToEnd(state))
170
+ break
171
+ case "kill-to-start":
172
+ applyEdit(killToStart(state))
173
+ break
174
+ case "transpose":
175
+ applyEdit(transposeChars(state))
176
+ break
177
+ case "yank":
178
+ applyEdit(insertText(state, killRing))
179
+ break
180
+ case "insert":
181
+ applyEdit(insertText(state, action.text))
185
182
  break
186
-
187
183
  case "enter":
184
+ case "submit":
188
185
  onSubmit?.(value)
189
186
  break
190
-
191
- case "escape":
187
+ case "cancel":
192
188
  onCancel?.()
193
189
  break
194
-
195
- case "char":
196
- case "space":
197
- if (key.ctrl && key.text) {
198
- // Emacs-style keybindings
199
- switch (key.text) {
200
- case "a":
201
- setCursorPos(0)
202
- break
203
- case "e":
204
- setCursorPos(value.length)
205
- break
206
- case "b":
207
- setCursorPos(Math.max(0, cursorPos - 1))
208
- break
209
- case "f":
210
- setCursorPos(Math.min(value.length, cursorPos + 1))
211
- break
212
- case "d":
213
- applyEdit(deleteCharForward(state))
214
- break
215
- case "h":
216
- applyEdit(deleteCharBackward(state))
217
- break
218
- case "k":
219
- applyEdit(killToEnd(state))
220
- break
221
- case "u":
222
- applyEdit(killToStart(state))
223
- break
224
- case "w":
225
- applyEdit(deleteWordBackward(state))
226
- break
227
- case "t":
228
- applyEdit(transposeChars(state))
229
- break
230
- case "y":
231
- applyEdit(insertText(state, killRing))
232
- break
233
- }
234
- } else if (key.text && !key.meta) {
235
- applyEdit(insertText(state, key.text))
236
- }
190
+ case "move-up":
191
+ case "move-down":
237
192
  break
238
193
  }
239
194
  },
@@ -268,7 +223,7 @@ export function TextInput({
268
223
 
269
224
  // Clear the line with background color
270
225
  if (bg !== undefined) {
271
- ctx.fill(0, 0, ctx.width, 1, " ", { bg })
226
+ ctx.fillRect(0, 0, ctx.width, 1, " ", { bg })
272
227
  }
273
228
 
274
229
  // Calculate scroll offset to keep cursor visible
@@ -25,6 +25,40 @@ export interface MultilineState {
25
25
  killRing: string
26
26
  }
27
27
 
28
+ export interface TextKeyEvent {
29
+ name: string
30
+ text?: string
31
+ ctrl?: boolean
32
+ meta?: boolean
33
+ shift?: boolean
34
+ }
35
+
36
+ export type WordScope = "line" | "document"
37
+
38
+ export type TextInputAction =
39
+ | { type: "move-left" }
40
+ | { type: "move-right" }
41
+ | { type: "move-up" }
42
+ | { type: "move-down" }
43
+ | { type: "move-start" }
44
+ | { type: "move-end" }
45
+ | { type: "move-doc-start" }
46
+ | { type: "move-doc-end" }
47
+ | { type: "move-word-left" }
48
+ | { type: "move-word-right" }
49
+ | { type: "delete-backward" }
50
+ | { type: "delete-forward" }
51
+ | { type: "delete-word-backward"; scope: WordScope }
52
+ | { type: "delete-word-forward"; scope: WordScope }
53
+ | { type: "kill-to-end" }
54
+ | { type: "kill-to-start" }
55
+ | { type: "transpose" }
56
+ | { type: "yank" }
57
+ | { type: "insert"; text: string }
58
+ | { type: "enter" }
59
+ | { type: "submit" }
60
+ | { type: "cancel" }
61
+
28
62
  /** Result of an edit operation */
29
63
  export interface EditResult<T> {
30
64
  state: T
@@ -37,6 +71,79 @@ export interface EditResult<T> {
37
71
 
38
72
  export { matchNextWord, matchPrevWord }
39
73
 
74
+ // ============================================================================
75
+ // Keybinding resolution (shared by TextInput and MultilineTextInput)
76
+ // ============================================================================
77
+
78
+ export function resolveTextInputAction(key: TextKeyEvent): TextInputAction | null {
79
+ switch (key.name) {
80
+ case "left":
81
+ return key.meta ? { type: "move-word-left" } : { type: "move-left" }
82
+ case "right":
83
+ return key.meta ? { type: "move-word-right" } : { type: "move-right" }
84
+ case "up":
85
+ return key.meta ? { type: "move-doc-start" } : { type: "move-up" }
86
+ case "down":
87
+ return key.meta ? { type: "move-doc-end" } : { type: "move-down" }
88
+ case "home":
89
+ return { type: "move-start" }
90
+ case "end":
91
+ return { type: "move-end" }
92
+ case "backspace":
93
+ return key.meta
94
+ ? { type: "delete-word-backward", scope: "line" }
95
+ : { type: "delete-backward" }
96
+ case "delete":
97
+ return key.meta
98
+ ? { type: "delete-word-forward", scope: "line" }
99
+ : { type: "delete-forward" }
100
+ case "enter":
101
+ return key.ctrl || key.meta ? { type: "submit" } : { type: "enter" }
102
+ case "escape":
103
+ return { type: "cancel" }
104
+ case "char":
105
+ case "space": {
106
+ if (key.ctrl && key.text) {
107
+ switch (key.text) {
108
+ case "a":
109
+ return { type: "move-start" }
110
+ case "e":
111
+ return { type: "move-end" }
112
+ case "b":
113
+ return { type: "move-left" }
114
+ case "f":
115
+ return { type: "move-right" }
116
+ case "n":
117
+ return { type: "move-down" }
118
+ case "p":
119
+ return { type: "move-up" }
120
+ case "d":
121
+ return { type: "delete-forward" }
122
+ case "h":
123
+ return { type: "delete-backward" }
124
+ case "k":
125
+ return { type: "kill-to-end" }
126
+ case "u":
127
+ return { type: "kill-to-start" }
128
+ case "w":
129
+ return { type: "delete-word-backward", scope: "document" }
130
+ case "t":
131
+ return { type: "transpose" }
132
+ case "y":
133
+ return { type: "yank" }
134
+ }
135
+ }
136
+
137
+ if (key.text && !key.meta) {
138
+ return { type: "insert", text: key.text }
139
+ }
140
+ break
141
+ }
142
+ }
143
+
144
+ return null
145
+ }
146
+
40
147
  // ============================================================================
41
148
  // Single-line operations (for TextInput)
42
149
  // ============================================================================
@@ -64,6 +171,35 @@ export function deleteWordBackward(state: TextState): EditResult<TextState> {
64
171
  }
65
172
  }
66
173
 
174
+ /** Delete word forward */
175
+ export function deleteWordForward(state: TextState): EditResult<TextState> {
176
+ const { text, cursor } = state
177
+ if (cursor >= text.length) {
178
+ return { state, changed: false }
179
+ }
180
+
181
+ const afterCursor = text.slice(cursor)
182
+ const match = matchNextWord(afterCursor)
183
+
184
+ if (match) {
185
+ const newText = text.slice(0, cursor) + text.slice(cursor + match.length)
186
+ return {
187
+ state: { text: newText, cursor, killRing: match },
188
+ changed: true,
189
+ }
190
+ }
191
+
192
+ const killed = text.slice(cursor)
193
+ if (!killed) {
194
+ return { state, changed: false }
195
+ }
196
+
197
+ return {
198
+ state: { text: text.slice(0, cursor), cursor, killRing: killed },
199
+ changed: true,
200
+ }
201
+ }
202
+
67
203
  /** Kill to end of line */
68
204
  export function killToEnd(state: TextState): EditResult<TextState> {
69
205
  const { text, cursor } = state
@@ -247,6 +383,50 @@ export function deleteWordBackwardMultiline(state: MultilineState): EditResult<M
247
383
  return { state, changed: false }
248
384
  }
249
385
 
386
+ /** Delete word forward in multiline (line-scoped) */
387
+ export function deleteWordForwardMultiline(state: MultilineState): EditResult<MultilineState> {
388
+ const { lines, cursor } = state
389
+ const line = lines[cursor.row]
390
+ const lineLen = graphemes(line).length
391
+
392
+ if (cursor.col < lineLen) {
393
+ const charIdx = graphemeColToCharIdx(line, cursor.col)
394
+ const afterCursor = line.slice(charIdx)
395
+ const match = matchNextWord(afterCursor)
396
+ const newLines = [...lines]
397
+
398
+ if (match) {
399
+ const newLine = line.slice(0, charIdx) + line.slice(charIdx + match.length)
400
+ newLines[cursor.row] = newLine
401
+ return {
402
+ state: {
403
+ lines: newLines,
404
+ cursor,
405
+ killRing: match,
406
+ },
407
+ changed: true,
408
+ }
409
+ }
410
+
411
+ const killed = line.slice(charIdx)
412
+ if (!killed) {
413
+ return { state, changed: false }
414
+ }
415
+
416
+ newLines[cursor.row] = line.slice(0, charIdx)
417
+ return {
418
+ state: {
419
+ lines: newLines,
420
+ cursor,
421
+ killRing: killed,
422
+ },
423
+ changed: true,
424
+ }
425
+ }
426
+
427
+ return { state, changed: false }
428
+ }
429
+
250
430
  /** Kill to end of line in multiline */
251
431
  export function killToEndMultiline(state: MultilineState): EditResult<MultilineState> {
252
432
  const { lines, cursor } = state