@effect-tui/core 0.1.0 → 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.
- 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/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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
268
|
-
|
|
447
|
+
code: number,
|
|
448
|
+
mods: { shift?: boolean; ctrl?: boolean; meta?: boolean; source: KeyMsg["source"] },
|
|
269
449
|
): KeyMsg | null {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
}
|