@effect-tui/core 0.1.1 → 0.1.5
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.
- package/README.md +31 -11
- package/dist/ansi.d.ts +127 -32
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +159 -37
- package/dist/ansi.js.map +1 -1
- package/dist/colors.d.ts +139 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +339 -0
- package/dist/colors.js.map +1 -0
- package/dist/index.d.ts +6 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +21 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +199 -58
- package/dist/keys.js.map +1 -1
- package/dist/layout/axis-helpers.d.ts +19 -0
- package/dist/layout/axis-helpers.d.ts.map +1 -0
- package/dist/layout/axis-helpers.js +19 -0
- package/dist/layout/axis-helpers.js.map +1 -0
- package/dist/output.d.ts +59 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +142 -0
- package/dist/output.js.map +1 -0
- package/dist/render/buffer.d.ts.map +1 -1
- package/dist/render/buffer.js +6 -25
- package/dist/render/buffer.js.map +1 -1
- package/dist/render/graphemes.d.ts +15 -0
- package/dist/render/graphemes.d.ts.map +1 -0
- package/dist/render/graphemes.js +28 -0
- package/dist/render/graphemes.js.map +1 -0
- package/dist/render/measure.d.ts +1 -0
- package/dist/render/measure.d.ts.map +1 -1
- package/dist/render/measure.js +14 -36
- package/dist/render/measure.js.map +1 -1
- package/dist/render/palette.d.ts.map +1 -1
- package/dist/render/palette.js +26 -1
- package/dist/render/palette.js.map +1 -1
- package/dist/render/segmenter.d.ts +8 -0
- package/dist/render/segmenter.d.ts.map +1 -0
- package/dist/render/segmenter.js +23 -0
- package/dist/render/segmenter.js.map +1 -0
- package/dist/render/surface.d.ts +6 -32
- package/dist/render/surface.d.ts.map +1 -1
- package/dist/render/surface.js +11 -80
- package/dist/render/surface.js.map +1 -1
- package/dist/runtime/backend_node.d.ts.map +1 -1
- package/dist/runtime/backend_node.js.map +1 -1
- package/dist/tailwind-colors.d.ts +291 -0
- package/dist/tailwind-colors.d.ts.map +1 -0
- package/dist/tailwind-colors.js +291 -0
- package/dist/tailwind-colors.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -55
- package/src/ansi.ts +201 -73
- package/src/colors.ts +468 -0
- package/src/index.ts +28 -14
- package/src/keys.ts +467 -287
- package/src/layout/axis-helpers.ts +33 -0
- package/src/output.ts +175 -0
- package/src/render/buffer.ts +161 -184
- package/src/render/graphemes.ts +34 -0
- package/src/render/measure.ts +15 -38
- package/src/render/palette.ts +98 -77
- package/src/render/segmenter.ts +27 -0
- package/src/render/surface.ts +139 -225
- package/src/runtime/backend_node.ts +71 -71
- package/src/tailwind-colors.ts +295 -0
- package/src/types.ts +18 -0
- package/dist/anim.d.ts +0 -4
- package/dist/anim.d.ts.map +0 -1
- package/dist/anim.js +0 -5
- package/dist/anim.js.map +0 -1
- package/dist/layout/linearStack.d.ts +0 -17
- package/dist/layout/linearStack.d.ts.map +0 -1
- package/dist/layout/linearStack.js +0 -86
- package/dist/layout/linearStack.js.map +0 -1
- package/dist/motion-value.d.ts +0 -58
- package/dist/motion-value.d.ts.map +0 -1
- package/dist/motion-value.js +0 -250
- package/dist/motion-value.js.map +0 -1
- package/dist/present/display.d.ts +0 -58
- package/dist/present/display.d.ts.map +0 -1
- package/dist/present/display.js +0 -168
- package/dist/present/display.js.map +0 -1
- package/dist/present/writers/fullscreen.d.ts +0 -19
- package/dist/present/writers/fullscreen.d.ts.map +0 -1
- package/dist/present/writers/fullscreen.js +0 -55
- package/dist/present/writers/fullscreen.js.map +0 -1
- package/dist/present/writers/inline.d.ts +0 -20
- package/dist/present/writers/inline.d.ts.map +0 -1
- package/dist/present/writers/inline.js +0 -92
- package/dist/present/writers/inline.js.map +0 -1
- package/dist/render/color-utils.d.ts +0 -18
- package/dist/render/color-utils.d.ts.map +0 -1
- package/dist/render/color-utils.js +0 -58
- package/dist/render/color-utils.js.map +0 -1
- package/dist/render/diff.d.ts +0 -30
- package/dist/render/diff.d.ts.map +0 -1
- package/dist/render/diff.js +0 -83
- package/dist/render/diff.js.map +0 -1
- package/dist/spring-physics.d.ts +0 -36
- package/dist/spring-physics.d.ts.map +0 -1
- package/dist/spring-physics.js +0 -113
- package/dist/spring-physics.js.map +0 -1
- package/dist/spring.d.ts +0 -73
- package/dist/spring.d.ts.map +0 -1
- package/dist/spring.js +0 -136
- package/dist/spring.js.map +0 -1
- package/dist/ui/containers/canvas.d.ts +0 -13
- package/dist/ui/containers/canvas.d.ts.map +0 -1
- package/dist/ui/containers/canvas.js +0 -16
- package/dist/ui/containers/canvas.js.map +0 -1
- package/dist/ui/containers/geometry-reader.d.ts +0 -17
- package/dist/ui/containers/geometry-reader.d.ts.map +0 -1
- package/dist/ui/containers/geometry-reader.js +0 -24
- package/dist/ui/containers/geometry-reader.js.map +0 -1
- package/dist/ui/containers/hstack.d.ts +0 -12
- package/dist/ui/containers/hstack.d.ts.map +0 -1
- package/dist/ui/containers/hstack.js +0 -28
- package/dist/ui/containers/hstack.js.map +0 -1
- package/dist/ui/containers/scroll.d.ts +0 -28
- package/dist/ui/containers/scroll.d.ts.map +0 -1
- package/dist/ui/containers/scroll.js +0 -97
- package/dist/ui/containers/scroll.js.map +0 -1
- package/dist/ui/containers/shared.d.ts +0 -12
- package/dist/ui/containers/shared.d.ts.map +0 -1
- package/dist/ui/containers/shared.js +0 -19
- package/dist/ui/containers/shared.js.map +0 -1
- package/dist/ui/containers/vstack.d.ts +0 -12
- package/dist/ui/containers/vstack.d.ts.map +0 -1
- package/dist/ui/containers/vstack.js +0 -28
- package/dist/ui/containers/vstack.js.map +0 -1
- package/dist/ui/containers/zstack.d.ts +0 -14
- package/dist/ui/containers/zstack.d.ts.map +0 -1
- package/dist/ui/containers/zstack.js +0 -36
- package/dist/ui/containers/zstack.js.map +0 -1
- package/dist/ui/core/geometry-store.d.ts +0 -22
- package/dist/ui/core/geometry-store.d.ts.map +0 -1
- package/dist/ui/core/geometry-store.js +0 -29
- package/dist/ui/core/geometry-store.js.map +0 -1
- package/dist/ui/core/geometry.d.ts +0 -34
- package/dist/ui/core/geometry.d.ts.map +0 -1
- package/dist/ui/core/geometry.js +0 -14
- package/dist/ui/core/geometry.js.map +0 -1
- package/dist/ui/core/view.d.ts +0 -25
- package/dist/ui/core/view.d.ts.map +0 -1
- package/dist/ui/core/view.js +0 -34
- package/dist/ui/core/view.js.map +0 -1
- package/dist/ui/index.d.ts +0 -44
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js +0 -39
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/inlinetext.d.ts +0 -24
- package/dist/ui/inlinetext.d.ts.map +0 -1
- package/dist/ui/inlinetext.js +0 -131
- package/dist/ui/inlinetext.js.map +0 -1
- package/dist/ui/install.d.ts +0 -22
- package/dist/ui/install.d.ts.map +0 -1
- package/dist/ui/install.js +0 -66
- package/dist/ui/install.js.map +0 -1
- package/dist/ui/markdown.d.ts +0 -40
- package/dist/ui/markdown.d.ts.map +0 -1
- package/dist/ui/markdown.js +0 -351
- package/dist/ui/markdown.js.map +0 -1
- package/dist/ui/modifiers/border.d.ts +0 -33
- package/dist/ui/modifiers/border.d.ts.map +0 -1
- package/dist/ui/modifiers/border.js +0 -82
- package/dist/ui/modifiers/border.js.map +0 -1
- package/dist/ui/modifiers/fill.d.ts +0 -14
- package/dist/ui/modifiers/fill.d.ts.map +0 -1
- package/dist/ui/modifiers/fill.js +0 -25
- package/dist/ui/modifiers/fill.js.map +0 -1
- package/dist/ui/modifiers/frame.d.ts +0 -23
- package/dist/ui/modifiers/frame.d.ts.map +0 -1
- package/dist/ui/modifiers/frame.js +0 -54
- package/dist/ui/modifiers/frame.js.map +0 -1
- package/dist/ui/modifiers/offset.d.ts +0 -15
- package/dist/ui/modifiers/offset.d.ts.map +0 -1
- package/dist/ui/modifiers/offset.js +0 -21
- package/dist/ui/modifiers/offset.js.map +0 -1
- package/dist/ui/modifiers/opacity.d.ts +0 -15
- package/dist/ui/modifiers/opacity.d.ts.map +0 -1
- package/dist/ui/modifiers/opacity.js +0 -95
- package/dist/ui/modifiers/opacity.js.map +0 -1
- package/dist/ui/modifiers/padding.d.ts +0 -20
- package/dist/ui/modifiers/padding.d.ts.map +0 -1
- package/dist/ui/modifiers/padding.js +0 -36
- package/dist/ui/modifiers/padding.js.map +0 -1
- package/dist/ui/modifiers/styled.d.ts +0 -14
- package/dist/ui/modifiers/styled.d.ts.map +0 -1
- package/dist/ui/modifiers/styled.js +0 -26
- package/dist/ui/modifiers/styled.js.map +0 -1
- package/dist/ui/primitives/rectangle.d.ts +0 -15
- package/dist/ui/primitives/rectangle.d.ts.map +0 -1
- package/dist/ui/primitives/rectangle.js +0 -23
- package/dist/ui/primitives/rectangle.js.map +0 -1
- package/dist/ui/primitives/spacer.d.ts +0 -13
- package/dist/ui/primitives/spacer.d.ts.map +0 -1
- package/dist/ui/primitives/spacer.js +0 -16
- package/dist/ui/primitives/spacer.js.map +0 -1
- package/dist/ui/primitives/text.d.ts +0 -15
- package/dist/ui/primitives/text.d.ts.map +0 -1
- package/dist/ui/primitives/text.js +0 -79
- package/dist/ui/primitives/text.js.map +0 -1
- package/dist/ui/primitives/wrapped-text.d.ts +0 -30
- package/dist/ui/primitives/wrapped-text.d.ts.map +0 -1
- package/dist/ui/primitives/wrapped-text.js +0 -117
- package/dist/ui/primitives/wrapped-text.js.map +0 -1
- package/dist/ui/shinytext.d.ts +0 -66
- package/dist/ui/shinytext.d.ts.map +0 -1
- package/dist/ui/shinytext.js +0 -99
- package/dist/ui/shinytext.js.map +0 -1
- package/dist/ui/text/layout.d.ts +0 -35
- package/dist/ui/text/layout.d.ts.map +0 -1
- package/dist/ui/text/layout.js +0 -102
- package/dist/ui/text/layout.js.map +0 -1
- package/dist/ui/textinput.d.ts +0 -140
- package/dist/ui/textinput.d.ts.map +0 -1
- package/dist/ui/textinput.js +0 -402
- package/dist/ui/textinput.js.map +0 -1
- package/dist/ui/view-constructors.d.ts +0 -72
- package/dist/ui/view-constructors.d.ts.map +0 -1
- package/dist/ui/view-constructors.js +0 -74
- package/dist/ui/view-constructors.js.map +0 -1
- package/src/anim.ts +0 -5
- package/src/layout/linearStack.ts +0 -115
- package/src/motion-value.ts +0 -335
- package/src/present/display.ts +0 -206
- package/src/present/writers/fullscreen.ts +0 -58
- package/src/present/writers/inline.ts +0 -101
- package/src/render/color-utils.ts +0 -60
- package/src/render/diff.ts +0 -95
- package/src/spring-physics.ts +0 -151
- package/src/spring.ts +0 -234
- package/src/ui/__snapshots__/wrappedtext.test.ts.snap +0 -57
- package/src/ui/containers/canvas.ts +0 -18
- package/src/ui/containers/geometry-reader.ts +0 -32
- package/src/ui/containers/hstack.ts +0 -33
- package/src/ui/containers/scroll.ts +0 -106
- package/src/ui/containers/shared.ts +0 -27
- package/src/ui/containers/vstack.ts +0 -34
- package/src/ui/containers/zstack.ts +0 -37
- package/src/ui/core/geometry-store.ts +0 -42
- package/src/ui/core/geometry.ts +0 -30
- package/src/ui/core/view.ts +0 -49
- package/src/ui/index.ts +0 -84
- package/src/ui/inlinetext.ts +0 -135
- package/src/ui/install.ts +0 -110
- package/src/ui/markdown.test.ts +0 -74
- package/src/ui/markdown.ts +0 -388
- package/src/ui/modifiers/border.ts +0 -100
- package/src/ui/modifiers/fill.ts +0 -28
- package/src/ui/modifiers/frame.ts +0 -74
- package/src/ui/modifiers/offset.ts +0 -23
- package/src/ui/modifiers/opacity.ts +0 -93
- package/src/ui/modifiers/padding.ts +0 -53
- package/src/ui/modifiers/styled.ts +0 -31
- package/src/ui/primitives/rectangle.ts +0 -25
- package/src/ui/primitives/spacer.ts +0 -18
- package/src/ui/primitives/text.ts +0 -85
- package/src/ui/primitives/wrapped-text.ts +0 -131
- package/src/ui/shinytext.ts +0 -159
- package/src/ui/text/layout.ts +0 -119
- package/src/ui/textinput.ts +0 -496
- package/src/ui/view-constructors.ts +0 -96
- package/src/ui/wrappedtext.test.ts +0 -138
package/src/ui/text/layout.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/* text/layout.ts — Shared text layout utilities for wrapping and caret mapping
|
|
2
|
-
*
|
|
3
|
-
* Single source of truth for:
|
|
4
|
-
* - Computing wrapped visual lines (first-line width vs subsequent)
|
|
5
|
-
* - Mapping between grapheme cursor index and visual caret coordinates
|
|
6
|
-
* - Mapping from visual cell X back to cursor index (for vertical nav)
|
|
7
|
-
*
|
|
8
|
-
* These helpers mirror renderer behavior by delegating to render/measure.ts
|
|
9
|
-
* (displayWidth, graphemes, sliceByWidth), ensuring edit-time and render-time
|
|
10
|
-
* wrapping boundaries are identical.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { displayWidth, graphemes, sliceByWidth } from "../../render/measure.js"
|
|
14
|
-
|
|
15
|
-
export type WrappedLine = { text: string; graphemes: string[]; start: number; width: number }
|
|
16
|
-
export type Wrapped = { lines: Array<WrappedLine> }
|
|
17
|
-
|
|
18
|
-
export type WrapOptions = {
|
|
19
|
-
widthFirst: number
|
|
20
|
-
widthOther: number
|
|
21
|
-
wordWrap: boolean
|
|
22
|
-
breakWords: boolean
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Compute wrapped lines with widths and grapheme start offsets (soft + hard breaks). */
|
|
26
|
-
export function wrapText(value: string, opts: WrapOptions): Wrapped {
|
|
27
|
-
const lines: Array<{ graphemes: string[]; start: number; width: number }> = []
|
|
28
|
-
const widthFor = (isFirstVisual: boolean) => Math.max(0, isFirstVisual ? opts.widthFirst : opts.widthOther)
|
|
29
|
-
|
|
30
|
-
let gOffset = 0 // grapheme index from start of whole value
|
|
31
|
-
let isFirstVisual = true
|
|
32
|
-
|
|
33
|
-
// Split logical lines by explicit newlines
|
|
34
|
-
const logicals = value.split("\n")
|
|
35
|
-
for (let li = 0; li < logicals.length; li++) {
|
|
36
|
-
const logical = logicals[li]
|
|
37
|
-
let rest = logical
|
|
38
|
-
// Even for empty logical lines, emit one visual line (empty)
|
|
39
|
-
if (rest.length === 0) {
|
|
40
|
-
lines.push({ graphemes: [], start: gOffset, width: 0 })
|
|
41
|
-
if (li < logicals.length - 1) gOffset += 1 // the "\n" grapheme
|
|
42
|
-
isFirstVisual = false
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
while (rest.length > 0) {
|
|
46
|
-
const maxW = widthFor(isFirstVisual)
|
|
47
|
-
const { text } = sliceByWidth(rest, maxW)
|
|
48
|
-
const used = text.length > 0 ? text : rest.slice(0, 1) // ensure progress even when maxW == 0
|
|
49
|
-
const usedGs = graphemes(used)
|
|
50
|
-
const w = displayWidth(used)
|
|
51
|
-
lines.push({ graphemes: usedGs, start: gOffset, width: w })
|
|
52
|
-
gOffset += usedGs.length
|
|
53
|
-
rest = rest.slice(used.length)
|
|
54
|
-
isFirstVisual = false
|
|
55
|
-
if (used.length === 0) break
|
|
56
|
-
}
|
|
57
|
-
if (li < logicals.length - 1) gOffset += 1 // account for newline between logical segments
|
|
58
|
-
}
|
|
59
|
-
return { lines: lines.map((l) => ({ ...l, text: l.graphemes.join("") })) }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Locate the visual line and column within that line for a grapheme cursor. */
|
|
63
|
-
export function findVisualPos(wrap: Wrapped, cursor: number): { lineIdx: number; colInLine: number } {
|
|
64
|
-
for (let i = 0; i < wrap.lines.length; i++) {
|
|
65
|
-
const ln = wrap.lines[i]
|
|
66
|
-
const start = ln.start
|
|
67
|
-
// Use next line's start as upper bound (accounts for newline gaps)
|
|
68
|
-
const nextStart = i + 1 < wrap.lines.length ? wrap.lines[i + 1].start : Infinity
|
|
69
|
-
|
|
70
|
-
// Cursor in range [start, nextStart) belongs to this line
|
|
71
|
-
if (cursor >= start && cursor < nextStart) {
|
|
72
|
-
// For empty lines, cursor must be exactly at start
|
|
73
|
-
if (ln.graphemes.length === 0) {
|
|
74
|
-
return { lineIdx: i, colInLine: 0 }
|
|
75
|
-
}
|
|
76
|
-
const col = Math.min(cursor - start, ln.graphemes.length)
|
|
77
|
-
return { lineIdx: i, colInLine: col }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Fallback: end of last line
|
|
81
|
-
const last = wrap.lines[wrap.lines.length - 1]
|
|
82
|
-
return { lineIdx: wrap.lines.length - 1, colInLine: last.graphemes.length }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Sum display widths of graphemes up to a column to get visual cell X. */
|
|
86
|
-
export function cellXWithinLine(wrap: Wrapped, lineIdx: number, colInLine: number): number {
|
|
87
|
-
const ln = wrap.lines[lineIdx]
|
|
88
|
-
const gs = ln.graphemes
|
|
89
|
-
let x = 0
|
|
90
|
-
for (let i = 0; i < Math.min(colInLine, gs.length); i++) x += displayWidth(gs[i])
|
|
91
|
-
return x
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Map a visual cell X back to a cursor index on a given line (nearest-left by centers). */
|
|
95
|
-
export function cursorFromCellX(wrap: Wrapped, lineIdx: number, x: number): number {
|
|
96
|
-
const ln = wrap.lines[lineIdx]
|
|
97
|
-
const gs = ln.graphemes
|
|
98
|
-
let acc = 0
|
|
99
|
-
for (let i = 0; i < gs.length; i++) {
|
|
100
|
-
const w = displayWidth(gs[i])
|
|
101
|
-
const center = acc + w / 2
|
|
102
|
-
if (x < center) return ln.start + i
|
|
103
|
-
acc += w
|
|
104
|
-
}
|
|
105
|
-
return ln.start + gs.length
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Map cursor index to absolute caret coordinates using provided anchors. */
|
|
109
|
-
export function caretXYFromCursor(
|
|
110
|
-
wrap: Wrapped,
|
|
111
|
-
cursor: number,
|
|
112
|
-
anchors: { xFirst: number; xOther: number },
|
|
113
|
-
): { x: number; y: number } {
|
|
114
|
-
const { lineIdx, colInLine } = findVisualPos(wrap, cursor)
|
|
115
|
-
const baseX = lineIdx === 0 ? anchors.xFirst : anchors.xOther
|
|
116
|
-
const x = baseX + cellXWithinLine(wrap, lineIdx, colInLine)
|
|
117
|
-
const y = lineIdx
|
|
118
|
-
return { x, y }
|
|
119
|
-
}
|
package/src/ui/textinput.ts
DELETED
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
/* textinput.ts — single-line TextInput component and edit helpers */
|
|
2
|
-
|
|
3
|
-
import type { Palette, Surface, StyleSpec } from "../render/surface.js"
|
|
4
|
-
import type { Rect } from "./core/geometry.js"
|
|
5
|
-
import { View } from "./core/view.js"
|
|
6
|
-
import { geometryStore } from "./core/geometry-store.js"
|
|
7
|
-
import { Colors } from "../render/surface.js"
|
|
8
|
-
import { wrapText, findVisualPos, cellXWithinLine, cursorFromCellX, caretXYFromCursor } from "./text/layout.js"
|
|
9
|
-
import { Schema } from "effect"
|
|
10
|
-
import type { KeyMsg } from "../keys.js"
|
|
11
|
-
|
|
12
|
-
/** Geometry info captured from last render. */
|
|
13
|
-
export type TextInputGeom = { firstW: number; wrapW: number }
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* TextInputState — immutable state for text input with built-in editing.
|
|
17
|
-
*
|
|
18
|
-
* Geometry is captured from render and used automatically in edit().
|
|
19
|
-
* This eliminates the need to manually pass wrap widths.
|
|
20
|
-
*/
|
|
21
|
-
export class TextInputState extends Schema.Class<TextInputState>("TextInputState")({
|
|
22
|
-
value: Schema.String,
|
|
23
|
-
cursor: Schema.Number,
|
|
24
|
-
focused: Schema.optionalWith(Schema.Boolean, { default: () => false }),
|
|
25
|
-
_vcol: Schema.optionalWith(Schema.UndefinedOr(Schema.Number), { default: () => undefined }),
|
|
26
|
-
_geom: Schema.optionalWith(Schema.Unknown, { default: () => undefined }),
|
|
27
|
-
}) {
|
|
28
|
-
/** Create a new TextInputState with sensible defaults. */
|
|
29
|
-
static of(value: string, opts?: { cursor?: number; focused?: boolean }): TextInputState {
|
|
30
|
-
return new TextInputState({
|
|
31
|
-
value,
|
|
32
|
-
cursor: opts?.cursor ?? value.length,
|
|
33
|
-
focused: opts?.focused ?? false,
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Create empty focused state. */
|
|
38
|
-
static empty(focused = true): TextInputState {
|
|
39
|
-
return new TextInputState({ value: "", cursor: 0, focused })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Edit state in response to a key press.
|
|
44
|
-
* Uses geometry captured from last render for accurate multiline navigation.
|
|
45
|
-
*/
|
|
46
|
-
edit(key: KeyMsg, opts?: { multiline?: boolean }): { state: TextInputState; quit?: boolean } {
|
|
47
|
-
const geom = this._geom as TextInputGeom | undefined
|
|
48
|
-
return editTextInputCore(this, key, {
|
|
49
|
-
multiline: opts?.multiline ?? false,
|
|
50
|
-
wrapWidth: geom?.wrapW ?? Infinity,
|
|
51
|
-
firstLineWidth: geom?.firstW ?? Infinity,
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// --- Immutable setters ---
|
|
56
|
-
|
|
57
|
-
withValue(value: string, cursor?: number): TextInputState {
|
|
58
|
-
return new TextInputState({
|
|
59
|
-
...this,
|
|
60
|
-
value,
|
|
61
|
-
cursor: cursor ?? Math.min(this.cursor, [...value].length),
|
|
62
|
-
_vcol: undefined,
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
withCursor(cursor: number): TextInputState {
|
|
67
|
-
const max = [...this.value].length
|
|
68
|
-
return new TextInputState({
|
|
69
|
-
...this,
|
|
70
|
-
cursor: Math.max(0, Math.min(max, cursor)),
|
|
71
|
-
_vcol: undefined,
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
focus(): TextInputState {
|
|
76
|
-
return this.focused ? this : new TextInputState({ ...this, focused: true })
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
blur(): TextInputState {
|
|
80
|
-
return !this.focused ? this : new TextInputState({ ...this, focused: false })
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
clear(): TextInputState {
|
|
84
|
-
return new TextInputState({ ...this, value: "", cursor: 0, _vcol: undefined })
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Internal: capture geometry from render. Called by TextInput view. */
|
|
88
|
-
_captureGeom(geom: TextInputGeom): void {
|
|
89
|
-
;(this as any)._geom = geom
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Legacy plain-object type for backward compatibility. */
|
|
94
|
-
export type TextInputStatePlain = {
|
|
95
|
-
value: string
|
|
96
|
-
cursor: number
|
|
97
|
-
focused?: boolean
|
|
98
|
-
_vcol?: number
|
|
99
|
-
_geom?: TextInputGeom
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type TextInputOptions = {
|
|
103
|
-
placeholder?: string
|
|
104
|
-
/** Optional left prompt character(s), e.g. ">" */
|
|
105
|
-
prompt?: string
|
|
106
|
-
/** Optional styles, merged with inherited styles */
|
|
107
|
-
style?: {
|
|
108
|
-
base?: StyleSpec
|
|
109
|
-
placeholder?: StyleSpec // e.g. { fg: Colors.gray(12) }
|
|
110
|
-
prompt?: StyleSpec
|
|
111
|
-
caret?: StyleSpec // e.g. { inverse: true }
|
|
112
|
-
}
|
|
113
|
-
/** Enable multiline input and rendering (supports newlines and wrapping). */
|
|
114
|
-
multiline?: boolean
|
|
115
|
-
/** Word-wrapping preferences when multiline (defaults: wordWrap=true, breakWords=true). */
|
|
116
|
-
wrap?: {
|
|
117
|
-
wordWrap?: boolean
|
|
118
|
-
breakWords?: boolean
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** State that can be passed to TextInput: class instance or plain object. */
|
|
123
|
-
export type TextInputStateLike = TextInputState | TextInputStatePlain
|
|
124
|
-
|
|
125
|
-
/** A minimal single-line text input view (no border). */
|
|
126
|
-
export class TextInput extends View {
|
|
127
|
-
constructor(
|
|
128
|
-
readonly state: TextInputStateLike,
|
|
129
|
-
readonly opts?: TextInputOptions,
|
|
130
|
-
) {
|
|
131
|
-
super()
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
protected measureContent(maxW: number, maxH: number) {
|
|
135
|
-
const w = Math.max(0, maxW)
|
|
136
|
-
if (!this.opts?.multiline) {
|
|
137
|
-
// Greedy single line
|
|
138
|
-
return { w, h: 1 }
|
|
139
|
-
}
|
|
140
|
-
// Multiline: compute wrapped lines to determine natural height (clamped by maxH)
|
|
141
|
-
const { lines } = wrapText(this.state.value, {
|
|
142
|
-
widthFirst: Math.max(0, w - (this.opts?.prompt ? displayWidth(this.opts?.prompt ?? "") : 0)),
|
|
143
|
-
widthOther: w,
|
|
144
|
-
wordWrap: this.opts?.wrap?.wordWrap ?? true,
|
|
145
|
-
breakWords: this.opts?.wrap?.breakWords ?? true,
|
|
146
|
-
})
|
|
147
|
-
const h = Math.min(Math.max(1, lines.length), maxH)
|
|
148
|
-
return { w, h }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
protected renderContent(s: Surface, pal: Palette, rect: Rect) {
|
|
152
|
-
const styles = this.opts?.style ?? {}
|
|
153
|
-
const idBase = pal.id(styles.base)
|
|
154
|
-
const idPlaceholder = pal.id(styles.placeholder ?? { fg: Colors.gray(12) })
|
|
155
|
-
const idCaret = pal.id(styles.caret ?? { inverse: true })
|
|
156
|
-
|
|
157
|
-
const w = Math.max(0, rect.w)
|
|
158
|
-
if (w <= 0 || rect.h <= 0) return
|
|
159
|
-
|
|
160
|
-
const promptText = this.opts?.prompt ?? ""
|
|
161
|
-
const prefix = promptText ? `${promptText}` : ""
|
|
162
|
-
const prefixW = displayWidth(prefix)
|
|
163
|
-
if (prefixW > 0) s.drawText(rect.x, rect.y, prefix, pal.id(styles.prompt), w)
|
|
164
|
-
|
|
165
|
-
const contentX = rect.x + prefixW
|
|
166
|
-
const contentW = Math.max(0, w - prefixW)
|
|
167
|
-
if (contentW <= 0) return
|
|
168
|
-
|
|
169
|
-
const valueEmpty = this.state.value.length === 0
|
|
170
|
-
const placeholderText = this.opts?.placeholder ?? ""
|
|
171
|
-
|
|
172
|
-
// Capture geometry into state for accurate edit calculations
|
|
173
|
-
const geom: TextInputGeom = {
|
|
174
|
-
firstW: contentW,
|
|
175
|
-
wrapW: this.opts?.multiline ? w : contentW,
|
|
176
|
-
}
|
|
177
|
-
if (this.state instanceof TextInputState) {
|
|
178
|
-
this.state._captureGeom(geom)
|
|
179
|
-
} else {
|
|
180
|
-
;(this.state as TextInputStatePlain)._geom = geom
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!this.opts?.multiline) {
|
|
184
|
-
// Single-line path (back-compat)
|
|
185
|
-
const text = valueEmpty ? placeholderText : this.state.value
|
|
186
|
-
const isPlaceholder = valueEmpty && text.length > 0
|
|
187
|
-
const textStyle = isPlaceholder ? idPlaceholder : idBase
|
|
188
|
-
s.drawText(contentX, rect.y, text, textStyle, contentW)
|
|
189
|
-
|
|
190
|
-
if (this.state.focused) {
|
|
191
|
-
const cx0 = this.state.cursor | 0
|
|
192
|
-
const cx = Math.max(0, Math.min(contentW - 1, cx0))
|
|
193
|
-
const arr = [...text]
|
|
194
|
-
const ch = arr[cx] ?? " "
|
|
195
|
-
s.drawText(contentX + cx, rect.y, ch, idCaret, 1)
|
|
196
|
-
}
|
|
197
|
-
// Publish geometry snapshot if id is attached (legacy)
|
|
198
|
-
if ((this as any)._id) {
|
|
199
|
-
geometryStore.setInputGeom((this as any)._id, {
|
|
200
|
-
firstW: contentW,
|
|
201
|
-
wrapW: contentW,
|
|
202
|
-
xFirst: contentX,
|
|
203
|
-
xOther: contentX,
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
return
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Multiline path: wrap and render multiple rows
|
|
210
|
-
const wrap = wrapText(this.state.value, {
|
|
211
|
-
widthFirst: contentW,
|
|
212
|
-
widthOther: w,
|
|
213
|
-
wordWrap: this.opts?.wrap?.wordWrap ?? true,
|
|
214
|
-
breakWords: this.opts?.wrap?.breakWords ?? true,
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
// Draw placeholder on first line only when empty
|
|
218
|
-
if (valueEmpty && placeholderText) {
|
|
219
|
-
s.drawText(contentX, rect.y, placeholderText, idPlaceholder, contentW)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Determine cursor location in visual coordinates
|
|
223
|
-
let caretAbsX = contentX
|
|
224
|
-
let caretAbsY = rect.y
|
|
225
|
-
if (this.state.focused) {
|
|
226
|
-
const { x, y } = caretXYFromCursor(wrap, this.state.cursor, {
|
|
227
|
-
xFirst: contentX,
|
|
228
|
-
xOther: rect.x,
|
|
229
|
-
})
|
|
230
|
-
caretAbsX = x
|
|
231
|
-
caretAbsY = y + rect.y
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Render lines (clip to rect.h)
|
|
235
|
-
const maxLines = Math.min(rect.h, wrap.lines.length)
|
|
236
|
-
for (let i = 0; i < maxLines; i++) {
|
|
237
|
-
const line = wrap.lines[i]
|
|
238
|
-
const baseX = i === 0 ? contentX : rect.x
|
|
239
|
-
const maxW = i === 0 ? contentW : w
|
|
240
|
-
const text = line.text
|
|
241
|
-
if (text.length > 0) s.drawText(baseX, rect.y + i, text, idBase, maxW)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Publish input geometry for this id (legacy)
|
|
245
|
-
if ((this as any)._id) {
|
|
246
|
-
geometryStore.setInputGeom((this as any)._id, {
|
|
247
|
-
firstW: contentW,
|
|
248
|
-
wrapW: w,
|
|
249
|
-
xFirst: contentX,
|
|
250
|
-
xOther: rect.x,
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Caret on top
|
|
255
|
-
if (this.state.focused) {
|
|
256
|
-
// Determine caret character (either actual char under cursor or space)
|
|
257
|
-
const { lineIdx, colInLine } = findVisualPos(wrap, this.state.cursor)
|
|
258
|
-
const line = wrap.lines[lineIdx]
|
|
259
|
-
const ch = line?.graphemes[colInLine] ?? " "
|
|
260
|
-
s.drawText(caretAbsX, caretAbsY, ch, idCaret, 1)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// Builder contribution for the View object
|
|
265
|
-
export type ViewTextInputExt = {
|
|
266
|
-
textInput(state: TextInputStateLike, opts?: TextInputOptions): View
|
|
267
|
-
}
|
|
268
|
-
export const viewTextInput: ViewTextInputExt = {
|
|
269
|
-
textInput(state: TextInputStateLike, opts?: TextInputOptions): View {
|
|
270
|
-
return new TextInput(state, opts)
|
|
271
|
-
},
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// --- Text editing helpers ---
|
|
275
|
-
|
|
276
|
-
export type TextInputEditResult = {
|
|
277
|
-
state: TextInputState
|
|
278
|
-
quit?: boolean
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export type TextInputEditOptions = {
|
|
282
|
-
/** Enable multiline editing semantics (enter inserts newline with shift). */
|
|
283
|
-
multiline?: boolean
|
|
284
|
-
/** Visual wrap width for subsequent lines. If absent, only explicit newlines are considered. */
|
|
285
|
-
wrapWidth?: number
|
|
286
|
-
/** Visual wrap width for first line (after prompt). Defaults to wrapWidth. */
|
|
287
|
-
firstLineWidth?: number
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Core edit function. Prefer using TextInputState.edit() for automatic geometry. */
|
|
291
|
-
function editTextInputCore(state: TextInputStateLike, key: KeyMsg, opts?: TextInputEditOptions): TextInputEditResult {
|
|
292
|
-
const { value, cursor, focused, _geom } = state
|
|
293
|
-
const vcol = state._vcol as number | undefined
|
|
294
|
-
const len = [...value].length
|
|
295
|
-
|
|
296
|
-
const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n))
|
|
297
|
-
|
|
298
|
-
// Create new state preserving geometry
|
|
299
|
-
const make = (updates: { value?: string; cursor?: number; _vcol?: number }): TextInputState =>
|
|
300
|
-
new TextInputState({
|
|
301
|
-
value: updates.value ?? value,
|
|
302
|
-
cursor: updates.cursor ?? cursor,
|
|
303
|
-
focused,
|
|
304
|
-
_vcol: updates._vcol,
|
|
305
|
-
_geom,
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
// Unchanged state (preserve as-is, converting to class if needed)
|
|
309
|
-
const unchanged = (): TextInputState =>
|
|
310
|
-
state instanceof TextInputState ? state : new TextInputState({ value, cursor, focused, _vcol: vcol, _geom })
|
|
311
|
-
|
|
312
|
-
const isSpace = (ch: string) => /\s/.test(ch)
|
|
313
|
-
const prevWord = (arr: string[], pos: number) => {
|
|
314
|
-
let i = clamp(pos, 0, arr.length)
|
|
315
|
-
if (i === 0) return 0
|
|
316
|
-
while (i > 0 && isSpace(arr[i - 1])) i--
|
|
317
|
-
while (i > 0 && !isSpace(arr[i - 1])) i--
|
|
318
|
-
return i
|
|
319
|
-
}
|
|
320
|
-
const nextWord = (arr: string[], pos: number) => {
|
|
321
|
-
let i = clamp(pos, 0, arr.length)
|
|
322
|
-
if (i === arr.length) return i
|
|
323
|
-
while (i < arr.length && !isSpace(arr[i])) i++
|
|
324
|
-
while (i < arr.length && isSpace(arr[i])) i++
|
|
325
|
-
return i
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const multiline = !!opts?.multiline
|
|
329
|
-
const wrapWidth = opts?.wrapWidth
|
|
330
|
-
const firstWidth = opts?.firstLineWidth ?? wrapWidth
|
|
331
|
-
|
|
332
|
-
const getWrap = () =>
|
|
333
|
-
wrapText(value, {
|
|
334
|
-
widthFirst: firstWidth ?? Infinity,
|
|
335
|
-
widthOther: wrapWidth ?? Infinity,
|
|
336
|
-
wordWrap: true,
|
|
337
|
-
breakWords: true,
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
switch (key.name) {
|
|
341
|
-
case "left":
|
|
342
|
-
if (key.meta) {
|
|
343
|
-
const arr = [...value]
|
|
344
|
-
return { state: make({ cursor: prevWord(arr, cursor) }) }
|
|
345
|
-
}
|
|
346
|
-
return { state: make({ cursor: clamp(cursor - 1, 0, len) }) }
|
|
347
|
-
|
|
348
|
-
case "right":
|
|
349
|
-
if (key.meta) {
|
|
350
|
-
const arr = [...value]
|
|
351
|
-
return { state: make({ cursor: nextWord(arr, cursor) }) }
|
|
352
|
-
}
|
|
353
|
-
return { state: make({ cursor: clamp(cursor + 1, 0, len) }) }
|
|
354
|
-
|
|
355
|
-
case "home":
|
|
356
|
-
return { state: make({ cursor: 0 }) }
|
|
357
|
-
|
|
358
|
-
case "end":
|
|
359
|
-
return { state: make({ cursor: len }) }
|
|
360
|
-
|
|
361
|
-
case "up": {
|
|
362
|
-
if (!multiline) return { state: unchanged() }
|
|
363
|
-
const wrap = getWrap()
|
|
364
|
-
const pos = findVisualPos(wrap, cursor)
|
|
365
|
-
if (pos.lineIdx <= 0) return { state: unchanged() }
|
|
366
|
-
const curX = vcol ?? cellXWithinLine(wrap, pos.lineIdx, pos.colInLine)
|
|
367
|
-
const target = cursorFromCellX(wrap, pos.lineIdx - 1, curX)
|
|
368
|
-
return { state: make({ cursor: target, _vcol: curX }) }
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
case "down": {
|
|
372
|
-
if (!multiline) return { state: unchanged() }
|
|
373
|
-
const wrap = getWrap()
|
|
374
|
-
const pos = findVisualPos(wrap, cursor)
|
|
375
|
-
if (pos.lineIdx >= wrap.lines.length - 1) return { state: unchanged() }
|
|
376
|
-
const curX = vcol ?? cellXWithinLine(wrap, pos.lineIdx, pos.colInLine)
|
|
377
|
-
const target = cursorFromCellX(wrap, pos.lineIdx + 1, curX)
|
|
378
|
-
return { state: make({ cursor: target, _vcol: curX }) }
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
case "backspace": {
|
|
382
|
-
if (key.meta) {
|
|
383
|
-
if (cursor <= 0) return { state: unchanged() }
|
|
384
|
-
const arr = [...value]
|
|
385
|
-
const start = prevWord(arr, cursor)
|
|
386
|
-
arr.splice(start, cursor - start)
|
|
387
|
-
return { state: make({ value: arr.join(""), cursor: start }) }
|
|
388
|
-
}
|
|
389
|
-
if (cursor <= 0) return { state: unchanged() }
|
|
390
|
-
const arr = [...value]
|
|
391
|
-
arr.splice(cursor - 1, 1)
|
|
392
|
-
return { state: make({ value: arr.join(""), cursor: cursor - 1 }) }
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
case "delete": {
|
|
396
|
-
if (key.meta) {
|
|
397
|
-
if (cursor <= 0) return { state: unchanged() }
|
|
398
|
-
const arr = [...value]
|
|
399
|
-
arr.splice(0, cursor)
|
|
400
|
-
return { state: make({ value: arr.join(""), cursor: 0 }) }
|
|
401
|
-
}
|
|
402
|
-
if (cursor >= len) return { state: unchanged() }
|
|
403
|
-
const arr = [...value]
|
|
404
|
-
arr.splice(cursor, 1)
|
|
405
|
-
return { state: make({ value: arr.join(""), cursor }) }
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
case "enter":
|
|
409
|
-
case "return": {
|
|
410
|
-
if (multiline) {
|
|
411
|
-
const arr = [...value]
|
|
412
|
-
arr.splice(cursor, 0, "\n")
|
|
413
|
-
return { state: make({ value: arr.join(""), cursor: cursor + 1 }) }
|
|
414
|
-
}
|
|
415
|
-
return { state: unchanged() }
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
case "space": {
|
|
419
|
-
return editTextInputCore(state, { ...key, name: "char", text: " " }, opts)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
case "char": {
|
|
423
|
-
if (!key.text) return { state: unchanged() }
|
|
424
|
-
|
|
425
|
-
// Ctrl-C: clear or quit
|
|
426
|
-
if (key.ctrl && key.text === "c") {
|
|
427
|
-
if (value.length > 0) {
|
|
428
|
-
return { state: make({ value: "", cursor: 0 }) }
|
|
429
|
-
}
|
|
430
|
-
return { state: unchanged(), quit: true }
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Ctrl-U: delete to start
|
|
434
|
-
if (key.ctrl && key.text === "u") {
|
|
435
|
-
if (cursor <= 0) return { state: unchanged() }
|
|
436
|
-
const arr = [...value]
|
|
437
|
-
arr.splice(0, cursor)
|
|
438
|
-
return { state: make({ value: arr.join(""), cursor: 0 }) }
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Ctrl-W: delete previous word
|
|
442
|
-
if (key.ctrl && key.text === "w") {
|
|
443
|
-
const arr = [...value]
|
|
444
|
-
const start = prevWord(arr, cursor)
|
|
445
|
-
arr.splice(start, cursor - start)
|
|
446
|
-
return { state: make({ value: arr.join(""), cursor: start }) }
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Meta-b/B: previous word
|
|
450
|
-
if (key.meta && (key.text === "b" || key.text === "B")) {
|
|
451
|
-
const arr = [...value]
|
|
452
|
-
return { state: make({ cursor: prevWord(arr, cursor) }) }
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Meta-f/F: next word
|
|
456
|
-
if (key.meta && (key.text === "f" || key.text === "F")) {
|
|
457
|
-
const arr = [...value]
|
|
458
|
-
return { state: make({ cursor: nextWord(arr, cursor) }) }
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Meta-d/D: delete next word
|
|
462
|
-
if (key.meta && (key.text === "d" || key.text === "D")) {
|
|
463
|
-
const arr = [...value]
|
|
464
|
-
const end = nextWord(arr, cursor)
|
|
465
|
-
if (end === cursor) return { state: unchanged() }
|
|
466
|
-
arr.splice(cursor, end - cursor)
|
|
467
|
-
return { state: make({ value: arr.join(""), cursor }) }
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (key.meta) return { state: unchanged() }
|
|
471
|
-
|
|
472
|
-
// Insert character
|
|
473
|
-
const arr = [...value]
|
|
474
|
-
arr.splice(cursor, 0, key.text)
|
|
475
|
-
return { state: make({ value: arr.join(""), cursor: cursor + 1 }) }
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
default:
|
|
479
|
-
return { state: unchanged() }
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Edit text input state with a key event.
|
|
485
|
-
* @deprecated Use TextInputState.edit() instead for automatic geometry handling.
|
|
486
|
-
*/
|
|
487
|
-
export function editTextInput(
|
|
488
|
-
state: TextInputStateLike,
|
|
489
|
-
key: KeyMsg,
|
|
490
|
-
opts?: TextInputEditOptions,
|
|
491
|
-
): TextInputEditResult {
|
|
492
|
-
return editTextInputCore(state, key, opts)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// --- Internal helpers for wrapping-aware cursor movement & rendering ---
|
|
496
|
-
import { displayWidth } from "../render/measure.js"
|