@promptctl/rich-js 0.2.0 → 0.3.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.
- package/README.md +29 -15
- package/dist/core/cells.d.ts +83 -3
- package/dist/core/cells.d.ts.map +1 -1
- package/dist/core/cells.js +113 -1
- package/dist/core/cells.js.map +1 -1
- package/dist/core/console.d.ts +0 -1
- package/dist/core/console.d.ts.map +1 -1
- package/dist/core/console.js +20 -16
- package/dist/core/console.js.map +1 -1
- package/dist/core/highlighter.d.ts.map +1 -1
- package/dist/core/highlighter.js.map +1 -1
- package/dist/core/oklch.d.ts +106 -0
- package/dist/core/oklch.d.ts.map +1 -0
- package/dist/core/oklch.js +221 -0
- package/dist/core/oklch.js.map +1 -0
- package/dist/core/render.d.ts +26 -7
- package/dist/core/render.d.ts.map +1 -1
- package/dist/core/render.js +83 -15
- package/dist/core/render.js.map +1 -1
- package/dist/core/segment.d.ts +14 -1
- package/dist/core/segment.d.ts.map +1 -1
- package/dist/core/segment.js +28 -3
- package/dist/core/segment.js.map +1 -1
- package/dist/core/style.d.ts +11 -2
- package/dist/core/style.d.ts.map +1 -1
- package/dist/core/style.js +31 -18
- package/dist/core/style.js.map +1 -1
- package/dist/core/text.d.ts +20 -0
- package/dist/core/text.d.ts.map +1 -1
- package/dist/core/text.js +31 -0
- package/dist/core/text.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/renderables/columns.d.ts.map +1 -1
- package/dist/renderables/columns.js +15 -22
- package/dist/renderables/columns.js.map +1 -1
- package/dist/renderables/layout.d.ts.map +1 -1
- package/dist/renderables/layout.js +8 -29
- package/dist/renderables/layout.js.map +1 -1
- package/dist/renderables/live.d.ts.map +1 -1
- package/dist/renderables/live.js +8 -7
- package/dist/renderables/live.js.map +1 -1
- package/dist/renderables/panel.d.ts +34 -0
- package/dist/renderables/panel.d.ts.map +1 -1
- package/dist/renderables/panel.js +66 -23
- package/dist/renderables/panel.js.map +1 -1
- package/dist/template-bindings/index.d.ts +35 -0
- package/dist/template-bindings/index.d.ts.map +1 -1
- package/dist/template-bindings/index.js +61 -0
- package/dist/template-bindings/index.js.map +1 -1
- package/dist/template-bindings/style-funcs.d.ts.map +1 -1
- package/dist/template-bindings/style-funcs.js +32 -0
- package/dist/template-bindings/style-funcs.js.map +1 -1
- package/dist/themes/colorMath.d.ts +40 -0
- package/dist/themes/colorMath.d.ts.map +1 -1
- package/dist/themes/colorMath.js +82 -1
- package/dist/themes/colorMath.js.map +1 -1
- package/dist/themes/data/catppuccin-frappe.d.ts +4 -0
- package/dist/themes/data/catppuccin-frappe.d.ts.map +1 -0
- package/dist/themes/data/catppuccin-frappe.js +159 -0
- package/dist/themes/data/catppuccin-frappe.js.map +1 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +4 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts.map +1 -0
- package/dist/themes/data/catppuccin-macchiato.js +159 -0
- package/dist/themes/data/catppuccin-macchiato.js.map +1 -0
- package/dist/themes/data/cyberpunk.d.ts +18 -0
- package/dist/themes/data/cyberpunk.d.ts.map +1 -0
- package/dist/themes/data/cyberpunk.js +192 -0
- package/dist/themes/data/cyberpunk.js.map +1 -0
- package/dist/themes/data/default.d.ts +4 -0
- package/dist/themes/data/default.d.ts.map +1 -0
- package/dist/themes/data/default.js +159 -0
- package/dist/themes/data/default.js.map +1 -0
- package/dist/themes/data/index.d.ts +10 -3
- package/dist/themes/data/index.d.ts.map +1 -1
- package/dist/themes/data/index.js +24 -7
- package/dist/themes/data/index.js.map +1 -1
- package/dist/themes/data/svg-export.d.ts +4 -0
- package/dist/themes/data/svg-export.d.ts.map +1 -0
- package/dist/themes/data/svg-export.js +159 -0
- package/dist/themes/data/svg-export.js.map +1 -0
- package/dist/themes/data/types.d.ts +4 -3
- package/dist/themes/data/types.d.ts.map +1 -1
- package/dist/themes/registry.d.ts +29 -2
- package/dist/themes/registry.d.ts.map +1 -1
- package/dist/themes/registry.js +62 -12
- package/dist/themes/registry.js.map +1 -1
- package/dist/themes/terminalThemes.d.ts +29 -20
- package/dist/themes/terminalThemes.d.ts.map +1 -1
- package/dist/themes/terminalThemes.js +50 -249
- package/dist/themes/terminalThemes.js.map +1 -1
- package/dist/themes/transpose.d.ts +88 -0
- package/dist/themes/transpose.d.ts.map +1 -0
- package/dist/themes/transpose.js +139 -0
- package/dist/themes/transpose.js.map +1 -0
- package/dist/widgets/button.d.ts +8 -5
- package/dist/widgets/button.d.ts.map +1 -1
- package/dist/widgets/button.js +36 -11
- package/dist/widgets/button.js.map +1 -1
- package/dist/widgets/checkbox.d.ts +1 -2
- package/dist/widgets/checkbox.d.ts.map +1 -1
- package/dist/widgets/checkbox.js +20 -7
- package/dist/widgets/checkbox.js.map +1 -1
- package/dist/widgets/dropdown.d.ts +4 -3
- package/dist/widgets/dropdown.d.ts.map +1 -1
- package/dist/widgets/dropdown.js +109 -27
- package/dist/widgets/dropdown.js.map +1 -1
- package/dist/widgets/event-router.d.ts +6 -3
- package/dist/widgets/event-router.d.ts.map +1 -1
- package/dist/widgets/event-router.js +179 -68
- package/dist/widgets/event-router.js.map +1 -1
- package/dist/widgets/focus-manager.d.ts +2 -1
- package/dist/widgets/focus-manager.d.ts.map +1 -1
- package/dist/widgets/focus-manager.js +28 -9
- package/dist/widgets/focus-manager.js.map +1 -1
- package/dist/widgets/index.d.ts +2 -2
- package/dist/widgets/index.d.ts.map +1 -1
- package/dist/widgets/index.js +1 -1
- package/dist/widgets/index.js.map +1 -1
- package/dist/widgets/screen.d.ts +2 -0
- package/dist/widgets/screen.d.ts.map +1 -1
- package/dist/widgets/screen.js +148 -45
- package/dist/widgets/screen.js.map +1 -1
- package/dist/widgets/slider.d.ts +6 -3
- package/dist/widgets/slider.d.ts.map +1 -1
- package/dist/widgets/slider.js +34 -10
- package/dist/widgets/slider.js.map +1 -1
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.d.ts.map +1 -1
- package/dist/widgets/static-item.js +12 -13
- package/dist/widgets/static-item.js.map +1 -1
- package/dist/widgets/text-input.d.ts +239 -13
- package/dist/widgets/text-input.d.ts.map +1 -1
- package/dist/widgets/text-input.js +1004 -74
- package/dist/widgets/text-input.js.map +1 -1
- package/dist/widgets/toggle.d.ts +1 -2
- package/dist/widgets/toggle.d.ts.map +1 -1
- package/dist/widgets/toggle.js +20 -8
- package/dist/widgets/toggle.js.map +1 -1
- package/dist/widgets/types.d.ts +21 -4
- package/dist/widgets/types.d.ts.map +1 -1
- package/dist/widgets/types.js +19 -0
- package/dist/widgets/types.js.map +1 -1
- package/dist/widgets/widget-base.d.ts +1 -0
- package/dist/widgets/widget-base.d.ts.map +1 -1
- package/dist/widgets/widget-base.js +24 -7
- package/dist/widgets/widget-base.js.map +1 -1
- package/package.json +5 -3
|
@@ -1,16 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TextInput widget —
|
|
3
|
-
* [LAW:dataflow-not-control-flow] cursor is a data position; when focused,
|
|
4
|
-
* exactly one cell is rendered with reverse-video styling on every frame.
|
|
5
|
-
* [LAW:one-type-per-behavior] shared infrastructure inherited from WidgetBase.
|
|
2
|
+
* TextInput widget — editable text field with cursor.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - single-line (default): Enter submits; newlines in value are rendered
|
|
6
|
+
* as a literal `↵` glyph if they ever appear.
|
|
7
|
+
* - multi-line (`multiline: true`): Enter inserts a `\n`; `value` may
|
|
8
|
+
* contain logical line breaks; Up/Down navigate
|
|
9
|
+
* between logical lines preserving column intent.
|
|
10
|
+
*
|
|
11
|
+
* [LAW:dataflow-not-control-flow] cursor and value are observable data; the
|
|
12
|
+
* keymap dispatches to small `@action` mutators that all flow through the
|
|
13
|
+
* same value/cursor update path. There is no per-key bespoke branch in the
|
|
14
|
+
* render loop or the change-emit path.
|
|
15
|
+
*
|
|
16
|
+
* [LAW:one-source-of-truth] The cursor's logical line — and therefore "what
|
|
17
|
+
* Home/End/Up/Down mean" — is derived from `value` and `cursorPosition` via
|
|
18
|
+
* `_lineStart()` / `_lineEnd()`. Multi-line behavior is the same code path
|
|
19
|
+
* as single-line; with no `\n` in `value`, line bounds collapse to value
|
|
20
|
+
* bounds, so single-line semantics are recovered without a special case.
|
|
21
|
+
*
|
|
22
|
+
* [LAW:types-are-the-program] Three integer spaces coexist: `CellCol` (terminal
|
|
23
|
+
* cell columns), `CodeUnit` (JS UTF-16 indices), and `CodePoint` (code-unit
|
|
24
|
+
* offsets that are additionally on a Unicode code-point boundary — a subtype of
|
|
25
|
+
* `CodeUnit`). `cursorPosition` is `CodePoint`; it is never inside a surrogate
|
|
26
|
+
* pair by construction. Visual positions are `CellCol`. The three are never
|
|
27
|
+
* interchangeable — the type system enforces this at every crossing point.
|
|
28
|
+
*
|
|
29
|
+
* Keymap (readline / emacs compatible):
|
|
30
|
+
*
|
|
31
|
+
* ─── motion ───
|
|
32
|
+
* left | Ctrl+B char left
|
|
33
|
+
* right | Ctrl+F char right
|
|
34
|
+
* up | Ctrl+P line up (preserves preferred column)
|
|
35
|
+
* down | Ctrl+N line down (preserves preferred column)
|
|
36
|
+
* home | Ctrl+A line start
|
|
37
|
+
* end | Ctrl+E line end
|
|
38
|
+
* Ctrl+Home document start
|
|
39
|
+
* Ctrl+End document end
|
|
40
|
+
* Ctrl+Left | Alt+Left | Alt+B word left
|
|
41
|
+
* Ctrl+Right | Alt+Right | Alt+F word right
|
|
42
|
+
*
|
|
43
|
+
* ─── editing ───
|
|
44
|
+
* backspace | Ctrl+H delete char back
|
|
45
|
+
* delete | Ctrl+D delete char forward
|
|
46
|
+
* Ctrl+W | Alt+Backspace delete word back (whitespace-bounded; readline parity)
|
|
47
|
+
* Alt+D delete word forward
|
|
48
|
+
* Ctrl+U kill to line start (stores in kill buffer)
|
|
49
|
+
* Ctrl+K kill to line end (stores in kill buffer)
|
|
50
|
+
* Ctrl+Y yank kill buffer at cursor
|
|
51
|
+
* Ctrl+T transpose chars (swap pre-cursor/at-cursor, advance)
|
|
52
|
+
* enter submit (single-line) or insert `\n` (multiline)
|
|
53
|
+
*
|
|
54
|
+
* Motion and editing primitives are also exposed as public methods
|
|
55
|
+
* (`moveCharLeft`, `killLineForward`, etc.) so a host can bind custom keys
|
|
56
|
+
* or invoke them programmatically.
|
|
14
57
|
*/
|
|
15
58
|
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
16
59
|
var useValue = arguments.length > 2;
|
|
@@ -50,11 +93,59 @@ import { observable, action } from "mobx";
|
|
|
50
93
|
import { Segment } from "../core/segment.js";
|
|
51
94
|
import { Style } from "../core/style.js";
|
|
52
95
|
import { ColorSpec } from "../core/color.js";
|
|
96
|
+
import { cellLen, setCellSize, splitText, cellFitFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, nextCodePoint, prevCodePoint, } from "../core/cells.js";
|
|
53
97
|
import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
|
|
54
98
|
import { WidgetBase } from "./widget-base.js";
|
|
99
|
+
/**
|
|
100
|
+
* Character-greedy soft wrap. Breaks at any character once `firstWidth`
|
|
101
|
+
* (or `continuationWidth` for continuation rows) cells are exhausted. The
|
|
102
|
+
* textarea default when a consumer says "wrap, I don't care how" — no syntax
|
|
103
|
+
* awareness, just fits the line to the width.
|
|
104
|
+
*
|
|
105
|
+
* Wide characters (CJK, emoji) are treated as atomic: a char that would
|
|
106
|
+
* straddle the budget is moved to the next row rather than split. When the
|
|
107
|
+
* budget is too narrow to fit even one character, that character is
|
|
108
|
+
* force-taken to guarantee progress (avoids an infinite loop).
|
|
109
|
+
*/
|
|
110
|
+
export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
|
|
111
|
+
if (line.length === 0)
|
|
112
|
+
return [{ content: "", start: asCodePoint(0) }];
|
|
113
|
+
const rows = [];
|
|
114
|
+
let pos = asCodePoint(0);
|
|
115
|
+
let isFirst = true;
|
|
116
|
+
while (pos < line.length) {
|
|
117
|
+
const cap = isFirst ? firstWidth : continuationWidth;
|
|
118
|
+
if (cap <= 0)
|
|
119
|
+
break;
|
|
120
|
+
let end = cellFitFrom(line, pos, cap);
|
|
121
|
+
if (end === pos) {
|
|
122
|
+
// Leading character exceeds cap — force-take one code point to guarantee
|
|
123
|
+
// progress when the terminal is extremely narrow.
|
|
124
|
+
end = nextCodePoint(line, pos);
|
|
125
|
+
if (end === pos)
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
rows.push({ content: line.slice(pos, end), start: pos });
|
|
129
|
+
pos = end;
|
|
130
|
+
isFirst = false;
|
|
131
|
+
}
|
|
132
|
+
return rows;
|
|
133
|
+
};
|
|
55
134
|
const MIN_CONTENT_WIDTH = 8;
|
|
135
|
+
const NEWLINE_GLYPH = "↵";
|
|
136
|
+
// Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
|
|
137
|
+
// readline default (alphanumeric + underscore), which is what users typing
|
|
138
|
+
// shell commands or code identifiers expect.
|
|
139
|
+
const WORD_CHAR_RE = /[A-Za-z0-9_]/;
|
|
140
|
+
const WHITESPACE_RE = /\s/;
|
|
141
|
+
function isWordChar(c) {
|
|
142
|
+
return c !== undefined && WORD_CHAR_RE.test(c);
|
|
143
|
+
}
|
|
144
|
+
function isWhitespace(c) {
|
|
145
|
+
return c !== undefined && WHITESPACE_RE.test(c);
|
|
146
|
+
}
|
|
56
147
|
let TextInput = (() => {
|
|
57
|
-
var _a;
|
|
148
|
+
var _a, _b, _c, _d;
|
|
58
149
|
let _classSuper = WidgetBase;
|
|
59
150
|
let _instanceExtraInitializers = [];
|
|
60
151
|
let _value_decorators;
|
|
@@ -66,24 +157,96 @@ let TextInput = (() => {
|
|
|
66
157
|
let _placeholder_decorators;
|
|
67
158
|
let _placeholder_initializers = [];
|
|
68
159
|
let _placeholder_extraInitializers = [];
|
|
160
|
+
let _indicatorStyleOverride_decorators;
|
|
161
|
+
let _indicatorStyleOverride_initializers = [];
|
|
162
|
+
let _indicatorStyleOverride_extraInitializers = [];
|
|
163
|
+
let _cursorStyleOverride_decorators;
|
|
164
|
+
let _cursorStyleOverride_initializers = [];
|
|
165
|
+
let _cursorStyleOverride_extraInitializers = [];
|
|
166
|
+
let _contentStyleOverride_decorators;
|
|
167
|
+
let _contentStyleOverride_initializers = [];
|
|
168
|
+
let _contentStyleOverride_extraInitializers = [];
|
|
69
169
|
let _handleKey_decorators;
|
|
70
170
|
let _handleMouse_decorators;
|
|
71
171
|
let _setHovered_decorators;
|
|
172
|
+
let _moveCharLeft_decorators;
|
|
173
|
+
let _moveCharRight_decorators;
|
|
174
|
+
let _moveLineUp_decorators;
|
|
175
|
+
let _moveLineDown_decorators;
|
|
176
|
+
let _moveLineStart_decorators;
|
|
177
|
+
let _moveLineEnd_decorators;
|
|
178
|
+
let _moveDocStart_decorators;
|
|
179
|
+
let _moveDocEnd_decorators;
|
|
180
|
+
let _moveWordLeft_decorators;
|
|
181
|
+
let _moveWordRight_decorators;
|
|
182
|
+
let _deleteCharBack_decorators;
|
|
183
|
+
let _deleteCharForward_decorators;
|
|
184
|
+
let _deleteWordBack_decorators;
|
|
185
|
+
let _deleteWordForward_decorators;
|
|
186
|
+
let _killLineBack_decorators;
|
|
187
|
+
let _killLineForward_decorators;
|
|
188
|
+
let _yank_decorators;
|
|
189
|
+
let _transposeChars_decorators;
|
|
190
|
+
let __insertText_decorators;
|
|
72
191
|
return class TextInput extends _classSuper {
|
|
73
192
|
static {
|
|
74
193
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
75
194
|
_value_decorators = [observable];
|
|
76
195
|
_cursorPosition_decorators = [observable];
|
|
77
196
|
_placeholder_decorators = [(_a = observable).ref.bind(_a)];
|
|
197
|
+
_indicatorStyleOverride_decorators = [(_b = observable).ref.bind(_b)];
|
|
198
|
+
_cursorStyleOverride_decorators = [(_c = observable).ref.bind(_c)];
|
|
199
|
+
_contentStyleOverride_decorators = [(_d = observable).ref.bind(_d)];
|
|
78
200
|
_handleKey_decorators = [action];
|
|
79
201
|
_handleMouse_decorators = [action];
|
|
80
202
|
_setHovered_decorators = [action];
|
|
203
|
+
_moveCharLeft_decorators = [action];
|
|
204
|
+
_moveCharRight_decorators = [action];
|
|
205
|
+
_moveLineUp_decorators = [action];
|
|
206
|
+
_moveLineDown_decorators = [action];
|
|
207
|
+
_moveLineStart_decorators = [action];
|
|
208
|
+
_moveLineEnd_decorators = [action];
|
|
209
|
+
_moveDocStart_decorators = [action];
|
|
210
|
+
_moveDocEnd_decorators = [action];
|
|
211
|
+
_moveWordLeft_decorators = [action];
|
|
212
|
+
_moveWordRight_decorators = [action];
|
|
213
|
+
_deleteCharBack_decorators = [action];
|
|
214
|
+
_deleteCharForward_decorators = [action];
|
|
215
|
+
_deleteWordBack_decorators = [action];
|
|
216
|
+
_deleteWordForward_decorators = [action];
|
|
217
|
+
_killLineBack_decorators = [action];
|
|
218
|
+
_killLineForward_decorators = [action];
|
|
219
|
+
_yank_decorators = [action];
|
|
220
|
+
_transposeChars_decorators = [action];
|
|
221
|
+
__insertText_decorators = [action];
|
|
81
222
|
__esDecorate(this, null, _value_decorators, { kind: "accessor", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
|
|
82
223
|
__esDecorate(this, null, _cursorPosition_decorators, { kind: "accessor", name: "cursorPosition", static: false, private: false, access: { has: obj => "cursorPosition" in obj, get: obj => obj.cursorPosition, set: (obj, value) => { obj.cursorPosition = value; } }, metadata: _metadata }, _cursorPosition_initializers, _cursorPosition_extraInitializers);
|
|
83
224
|
__esDecorate(this, null, _placeholder_decorators, { kind: "accessor", name: "placeholder", static: false, private: false, access: { has: obj => "placeholder" in obj, get: obj => obj.placeholder, set: (obj, value) => { obj.placeholder = value; } }, metadata: _metadata }, _placeholder_initializers, _placeholder_extraInitializers);
|
|
225
|
+
__esDecorate(this, null, _indicatorStyleOverride_decorators, { kind: "accessor", name: "indicatorStyleOverride", static: false, private: false, access: { has: obj => "indicatorStyleOverride" in obj, get: obj => obj.indicatorStyleOverride, set: (obj, value) => { obj.indicatorStyleOverride = value; } }, metadata: _metadata }, _indicatorStyleOverride_initializers, _indicatorStyleOverride_extraInitializers);
|
|
226
|
+
__esDecorate(this, null, _cursorStyleOverride_decorators, { kind: "accessor", name: "cursorStyleOverride", static: false, private: false, access: { has: obj => "cursorStyleOverride" in obj, get: obj => obj.cursorStyleOverride, set: (obj, value) => { obj.cursorStyleOverride = value; } }, metadata: _metadata }, _cursorStyleOverride_initializers, _cursorStyleOverride_extraInitializers);
|
|
227
|
+
__esDecorate(this, null, _contentStyleOverride_decorators, { kind: "accessor", name: "contentStyleOverride", static: false, private: false, access: { has: obj => "contentStyleOverride" in obj, get: obj => obj.contentStyleOverride, set: (obj, value) => { obj.contentStyleOverride = value; } }, metadata: _metadata }, _contentStyleOverride_initializers, _contentStyleOverride_extraInitializers);
|
|
84
228
|
__esDecorate(this, null, _handleKey_decorators, { kind: "method", name: "handleKey", static: false, private: false, access: { has: obj => "handleKey" in obj, get: obj => obj.handleKey }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
85
229
|
__esDecorate(this, null, _handleMouse_decorators, { kind: "method", name: "handleMouse", static: false, private: false, access: { has: obj => "handleMouse" in obj, get: obj => obj.handleMouse }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
86
230
|
__esDecorate(this, null, _setHovered_decorators, { kind: "method", name: "setHovered", static: false, private: false, access: { has: obj => "setHovered" in obj, get: obj => obj.setHovered }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
231
|
+
__esDecorate(this, null, _moveCharLeft_decorators, { kind: "method", name: "moveCharLeft", static: false, private: false, access: { has: obj => "moveCharLeft" in obj, get: obj => obj.moveCharLeft }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
232
|
+
__esDecorate(this, null, _moveCharRight_decorators, { kind: "method", name: "moveCharRight", static: false, private: false, access: { has: obj => "moveCharRight" in obj, get: obj => obj.moveCharRight }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
233
|
+
__esDecorate(this, null, _moveLineUp_decorators, { kind: "method", name: "moveLineUp", static: false, private: false, access: { has: obj => "moveLineUp" in obj, get: obj => obj.moveLineUp }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
234
|
+
__esDecorate(this, null, _moveLineDown_decorators, { kind: "method", name: "moveLineDown", static: false, private: false, access: { has: obj => "moveLineDown" in obj, get: obj => obj.moveLineDown }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
235
|
+
__esDecorate(this, null, _moveLineStart_decorators, { kind: "method", name: "moveLineStart", static: false, private: false, access: { has: obj => "moveLineStart" in obj, get: obj => obj.moveLineStart }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
236
|
+
__esDecorate(this, null, _moveLineEnd_decorators, { kind: "method", name: "moveLineEnd", static: false, private: false, access: { has: obj => "moveLineEnd" in obj, get: obj => obj.moveLineEnd }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
237
|
+
__esDecorate(this, null, _moveDocStart_decorators, { kind: "method", name: "moveDocStart", static: false, private: false, access: { has: obj => "moveDocStart" in obj, get: obj => obj.moveDocStart }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
238
|
+
__esDecorate(this, null, _moveDocEnd_decorators, { kind: "method", name: "moveDocEnd", static: false, private: false, access: { has: obj => "moveDocEnd" in obj, get: obj => obj.moveDocEnd }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
239
|
+
__esDecorate(this, null, _moveWordLeft_decorators, { kind: "method", name: "moveWordLeft", static: false, private: false, access: { has: obj => "moveWordLeft" in obj, get: obj => obj.moveWordLeft }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
240
|
+
__esDecorate(this, null, _moveWordRight_decorators, { kind: "method", name: "moveWordRight", static: false, private: false, access: { has: obj => "moveWordRight" in obj, get: obj => obj.moveWordRight }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
241
|
+
__esDecorate(this, null, _deleteCharBack_decorators, { kind: "method", name: "deleteCharBack", static: false, private: false, access: { has: obj => "deleteCharBack" in obj, get: obj => obj.deleteCharBack }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
242
|
+
__esDecorate(this, null, _deleteCharForward_decorators, { kind: "method", name: "deleteCharForward", static: false, private: false, access: { has: obj => "deleteCharForward" in obj, get: obj => obj.deleteCharForward }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
243
|
+
__esDecorate(this, null, _deleteWordBack_decorators, { kind: "method", name: "deleteWordBack", static: false, private: false, access: { has: obj => "deleteWordBack" in obj, get: obj => obj.deleteWordBack }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
244
|
+
__esDecorate(this, null, _deleteWordForward_decorators, { kind: "method", name: "deleteWordForward", static: false, private: false, access: { has: obj => "deleteWordForward" in obj, get: obj => obj.deleteWordForward }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
245
|
+
__esDecorate(this, null, _killLineBack_decorators, { kind: "method", name: "killLineBack", static: false, private: false, access: { has: obj => "killLineBack" in obj, get: obj => obj.killLineBack }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
246
|
+
__esDecorate(this, null, _killLineForward_decorators, { kind: "method", name: "killLineForward", static: false, private: false, access: { has: obj => "killLineForward" in obj, get: obj => obj.killLineForward }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
247
|
+
__esDecorate(this, null, _yank_decorators, { kind: "method", name: "yank", static: false, private: false, access: { has: obj => "yank" in obj, get: obj => obj.yank }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
248
|
+
__esDecorate(this, null, _transposeChars_decorators, { kind: "method", name: "transposeChars", static: false, private: false, access: { has: obj => "transposeChars" in obj, get: obj => obj.transposeChars }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
249
|
+
__esDecorate(this, null, __insertText_decorators, { kind: "method", name: "_insertText", static: false, private: false, access: { has: obj => "_insertText" in obj, get: obj => obj._insertText }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
87
250
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
88
251
|
}
|
|
89
252
|
id = __runInitializers(this, _instanceExtraInitializers);
|
|
@@ -100,66 +263,255 @@ let TextInput = (() => {
|
|
|
100
263
|
_theme = __runInitializers(this, _placeholder_extraInitializers);
|
|
101
264
|
_maxLength;
|
|
102
265
|
_password;
|
|
266
|
+
_multiline;
|
|
267
|
+
_wrap;
|
|
268
|
+
_continuationMarker;
|
|
269
|
+
_markerWidth;
|
|
270
|
+
_maxRows;
|
|
271
|
+
_minRows;
|
|
272
|
+
_scrollIndicator;
|
|
273
|
+
#indicatorStyleOverride_accessor_storage = __runInitializers(this, _indicatorStyleOverride_initializers, void 0);
|
|
274
|
+
get indicatorStyleOverride() { return this.#indicatorStyleOverride_accessor_storage; }
|
|
275
|
+
set indicatorStyleOverride(value) { this.#indicatorStyleOverride_accessor_storage = value; }
|
|
276
|
+
#cursorStyleOverride_accessor_storage = (__runInitializers(this, _indicatorStyleOverride_extraInitializers), __runInitializers(this, _cursorStyleOverride_initializers, void 0));
|
|
277
|
+
get cursorStyleOverride() { return this.#cursorStyleOverride_accessor_storage; }
|
|
278
|
+
set cursorStyleOverride(value) { this.#cursorStyleOverride_accessor_storage = value; }
|
|
279
|
+
#contentStyleOverride_accessor_storage = (__runInitializers(this, _cursorStyleOverride_extraInitializers), __runInitializers(this, _contentStyleOverride_initializers, void 0));
|
|
280
|
+
get contentStyleOverride() { return this.#contentStyleOverride_accessor_storage; }
|
|
281
|
+
set contentStyleOverride(value) { this.#contentStyleOverride_accessor_storage = value; }
|
|
282
|
+
/**
|
|
283
|
+
* Last computed visual-row decomposition. Cached at the end of `render()`
|
|
284
|
+
* so vertical motion (Up/Down) can step row-by-row without re-running the
|
|
285
|
+
* wrap strategy. Null before the first render — vertical motion falls
|
|
286
|
+
* back to logical-line motion in that case.
|
|
287
|
+
*
|
|
288
|
+
* [LAW:dataflow-not-control-flow] One source of truth for "what does Up
|
|
289
|
+
* mean right now": the row table the renderer just produced. No parallel
|
|
290
|
+
* "where would the cursor go" math; both the renderer and the keymap
|
|
291
|
+
* read from the same array.
|
|
292
|
+
*/
|
|
293
|
+
_visualRows = (__runInitializers(this, _contentStyleOverride_extraInitializers), null);
|
|
294
|
+
// [LAW:types-are-the-program] `_preferredColumn` is a cell column (not a
|
|
295
|
+
// code-unit index) — vertical motion navigates the visual surface, not the
|
|
296
|
+
// string's byte layout. Reset to null on every non-vertical motion or edit
|
|
297
|
+
// so the next Up/Down captures a fresh intent. Without this, Down through a
|
|
298
|
+
// short line then back Up would land at the short-line column instead of the
|
|
299
|
+
// original.
|
|
300
|
+
_preferredColumn = null;
|
|
301
|
+
// Single-slot kill buffer used by Ctrl+U / Ctrl+K → Ctrl+Y. Not a full
|
|
302
|
+
// kill ring (no Alt+Y rotation) — sufficient for the common cut/paste
|
|
303
|
+
// pattern that's the readline 90% case.
|
|
304
|
+
_killBuffer = "";
|
|
305
|
+
// [LAW:one-source-of-truth] Top visual row of the scroll viewport.
|
|
306
|
+
// Persisted across renders so the cursor can move *within* the viewport
|
|
307
|
+
// without dragging it along — the viewport scrolls only when the cursor
|
|
308
|
+
// would leave it. Without this, recomputing scroll from cursor position
|
|
309
|
+
// alone pins the cursor to the viewport's bottom edge whenever
|
|
310
|
+
// `cursorRow >= maxRows`, making every subsequent Up/Down scroll in
|
|
311
|
+
// lockstep with the cursor.
|
|
312
|
+
_scrollStart = 0;
|
|
313
|
+
// [LAW:one-source-of-truth] Viewport scroll offset in cells for the
|
|
314
|
+
// single-line renderer. Captured by _renderSingleLine and read by
|
|
315
|
+
// handleMouse so the hit-test uses the same visual origin the user sees.
|
|
316
|
+
_singleLineViewportStart = asCellCol(0);
|
|
317
|
+
multiline;
|
|
103
318
|
constructor(options = {}) {
|
|
104
319
|
super();
|
|
105
320
|
this.id = options.id ?? `text-input-${Math.random().toString(36).slice(2, 8)}`;
|
|
106
321
|
this.value = options.value ?? "";
|
|
107
322
|
this.placeholder = options.placeholder ?? "";
|
|
108
|
-
|
|
323
|
+
// [LAW:dataflow-not-control-flow] Initial cursor position is data, not
|
|
324
|
+
// a render-time branch. Single-line inputs follow the pre-filled-form
|
|
325
|
+
// convention (cursor at end — typing continues the value). Multiline
|
|
326
|
+
// inputs follow the textarea convention (cursor at start — pre-loaded
|
|
327
|
+
// content is read top-to-bottom, and the viewport's "scroll into view"
|
|
328
|
+
// logic does nothing because row 0 is already inside the initial window).
|
|
329
|
+
this.cursorPosition = (options.multiline ?? false) ? asCodePoint(0) : asCodePoint(this.value.length);
|
|
109
330
|
this.disabled = options.disabled ?? false;
|
|
110
331
|
this._theme = options.theme ?? DEFAULT_TERMINAL_THEME;
|
|
111
332
|
this._maxLength = options.maxLength;
|
|
112
333
|
this._password = options.password ?? false;
|
|
334
|
+
this._multiline = options.multiline ?? false;
|
|
335
|
+
this._wrap = options.wrap;
|
|
336
|
+
this._continuationMarker = options.continuationMarker ?? "↳ ";
|
|
337
|
+
this._markerWidth = asCellCol(cellLen(this._continuationMarker));
|
|
338
|
+
this._maxRows = options.maxRows;
|
|
339
|
+
this._minRows = options.minRows;
|
|
340
|
+
this._scrollIndicator = options.scrollIndicator ?? "arrows";
|
|
341
|
+
this.indicatorStyleOverride = options.indicatorStyle;
|
|
342
|
+
this.cursorStyleOverride = options.cursorStyle;
|
|
343
|
+
this.contentStyleOverride = options.contentStyle;
|
|
344
|
+
this.multiline = this._multiline;
|
|
113
345
|
}
|
|
114
346
|
setTheme(theme) { this._theme = theme; }
|
|
115
347
|
// --- Event handlers ---
|
|
116
348
|
handleKey(event) {
|
|
117
349
|
if (this.disabled)
|
|
118
350
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
case "delete":
|
|
140
|
-
if (this.cursorPosition < this.value.length) {
|
|
141
|
-
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
|
|
142
|
-
this.emitChange();
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
case "enter":
|
|
351
|
+
// ─── editing keys (modifier-conscious) ───
|
|
352
|
+
if (event.key === "backspace") {
|
|
353
|
+
if (event.meta || event.ctrl)
|
|
354
|
+
this.deleteWordBack();
|
|
355
|
+
else
|
|
356
|
+
this.deleteCharBack();
|
|
357
|
+
event.stop();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (event.key === "delete") {
|
|
361
|
+
this.deleteCharForward();
|
|
362
|
+
event.stop();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (event.key === "enter") {
|
|
366
|
+
if (this._multiline && !event.ctrl)
|
|
367
|
+
this._insertText("\n");
|
|
368
|
+
else
|
|
146
369
|
this.emitSubmit();
|
|
147
|
-
|
|
370
|
+
event.stop();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Escape is intentionally ignored — text-input does not stop it so
|
|
374
|
+
// higher-level UI (dialogs, modals) can react to it.
|
|
375
|
+
if (event.key === "escape")
|
|
376
|
+
return;
|
|
377
|
+
// ─── unmodified motion ───
|
|
378
|
+
if (!event.ctrl && !event.meta) {
|
|
379
|
+
switch (event.key) {
|
|
380
|
+
case "left":
|
|
381
|
+
this.moveCharLeft();
|
|
382
|
+
event.stop();
|
|
383
|
+
return;
|
|
384
|
+
case "right":
|
|
385
|
+
this.moveCharRight();
|
|
386
|
+
event.stop();
|
|
387
|
+
return;
|
|
388
|
+
case "up":
|
|
389
|
+
this.moveLineUp();
|
|
390
|
+
event.stop();
|
|
391
|
+
return;
|
|
392
|
+
case "down":
|
|
393
|
+
this.moveLineDown();
|
|
394
|
+
event.stop();
|
|
395
|
+
return;
|
|
396
|
+
case "home":
|
|
397
|
+
this.moveLineStart();
|
|
398
|
+
event.stop();
|
|
399
|
+
return;
|
|
400
|
+
case "end":
|
|
401
|
+
this.moveLineEnd();
|
|
402
|
+
event.stop();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ─── Ctrl-modified motion + readline editing ───
|
|
407
|
+
if (event.ctrl && !event.meta) {
|
|
408
|
+
switch (event.key) {
|
|
409
|
+
case "left":
|
|
410
|
+
this.moveWordLeft();
|
|
411
|
+
event.stop();
|
|
412
|
+
return;
|
|
413
|
+
case "right":
|
|
414
|
+
this.moveWordRight();
|
|
415
|
+
event.stop();
|
|
416
|
+
return;
|
|
417
|
+
case "home":
|
|
418
|
+
this.moveDocStart();
|
|
419
|
+
event.stop();
|
|
420
|
+
return;
|
|
421
|
+
case "end":
|
|
422
|
+
this.moveDocEnd();
|
|
423
|
+
event.stop();
|
|
424
|
+
return;
|
|
425
|
+
case "a":
|
|
426
|
+
this.moveLineStart();
|
|
427
|
+
event.stop();
|
|
428
|
+
return;
|
|
429
|
+
case "e":
|
|
430
|
+
this.moveLineEnd();
|
|
431
|
+
event.stop();
|
|
432
|
+
return;
|
|
433
|
+
case "b":
|
|
434
|
+
this.moveCharLeft();
|
|
435
|
+
event.stop();
|
|
436
|
+
return;
|
|
437
|
+
case "f":
|
|
438
|
+
this.moveCharRight();
|
|
439
|
+
event.stop();
|
|
440
|
+
return;
|
|
441
|
+
case "p":
|
|
442
|
+
this.moveLineUp();
|
|
443
|
+
event.stop();
|
|
444
|
+
return;
|
|
445
|
+
case "n":
|
|
446
|
+
this.moveLineDown();
|
|
447
|
+
event.stop();
|
|
448
|
+
return;
|
|
449
|
+
case "d":
|
|
450
|
+
this.deleteCharForward();
|
|
451
|
+
event.stop();
|
|
452
|
+
return;
|
|
453
|
+
case "h":
|
|
454
|
+
this.deleteCharBack();
|
|
455
|
+
event.stop();
|
|
456
|
+
return;
|
|
457
|
+
case "w":
|
|
458
|
+
this.deleteWordBack();
|
|
459
|
+
event.stop();
|
|
460
|
+
return;
|
|
461
|
+
case "u":
|
|
462
|
+
this.killLineBack();
|
|
463
|
+
event.stop();
|
|
464
|
+
return;
|
|
465
|
+
case "k":
|
|
466
|
+
this.killLineForward();
|
|
467
|
+
event.stop();
|
|
468
|
+
return;
|
|
469
|
+
case "y":
|
|
470
|
+
this.yank();
|
|
471
|
+
event.stop();
|
|
472
|
+
return;
|
|
473
|
+
case "t":
|
|
474
|
+
this.transposeChars();
|
|
475
|
+
event.stop();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
148
478
|
}
|
|
149
|
-
//
|
|
479
|
+
// ─── Alt-modified (meta) motion + editing ───
|
|
480
|
+
if (event.meta && !event.ctrl) {
|
|
481
|
+
switch (event.key) {
|
|
482
|
+
case "left":
|
|
483
|
+
this.moveWordLeft();
|
|
484
|
+
event.stop();
|
|
485
|
+
return;
|
|
486
|
+
case "right":
|
|
487
|
+
this.moveWordRight();
|
|
488
|
+
event.stop();
|
|
489
|
+
return;
|
|
490
|
+
case "b":
|
|
491
|
+
this.moveWordLeft();
|
|
492
|
+
event.stop();
|
|
493
|
+
return;
|
|
494
|
+
case "f":
|
|
495
|
+
this.moveWordRight();
|
|
496
|
+
event.stop();
|
|
497
|
+
return;
|
|
498
|
+
case "d":
|
|
499
|
+
this.deleteWordForward();
|
|
500
|
+
event.stop();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// ─── printable insertion ───
|
|
505
|
+
// A single-char `event.character` with no command modifiers is text to
|
|
506
|
+
// insert. Ctrl/meta-modified keys produce empty `character` from the
|
|
507
|
+
// router (see event-router.ts) so they never reach this branch.
|
|
150
508
|
if (event.character.length === 1 &&
|
|
151
509
|
!event.ctrl &&
|
|
152
510
|
!event.meta &&
|
|
153
511
|
event.character >= " " &&
|
|
154
512
|
event.character !== "\x7f") {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.value =
|
|
158
|
-
this.value.slice(0, this.cursorPosition) +
|
|
159
|
-
event.character +
|
|
160
|
-
this.value.slice(this.cursorPosition);
|
|
161
|
-
this.cursorPosition += 1;
|
|
162
|
-
this.emitChange();
|
|
513
|
+
this._insertText(event.character);
|
|
514
|
+
event.stop();
|
|
163
515
|
}
|
|
164
516
|
}
|
|
165
517
|
handleMouse(event) {
|
|
@@ -170,30 +522,400 @@ let TextInput = (() => {
|
|
|
170
522
|
const b = this.bounds;
|
|
171
523
|
if (!b)
|
|
172
524
|
return;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
525
|
+
if (this._multiline) {
|
|
526
|
+
// Multiline has no brackets; rows may have a leading continuation marker.
|
|
527
|
+
// Use event.y to pick the visual row, then project event.x into its content.
|
|
528
|
+
const rows = this._visualRows;
|
|
529
|
+
if (!rows || rows.length === 0)
|
|
530
|
+
return;
|
|
531
|
+
const relY = event.y - b.y;
|
|
532
|
+
const rawRowIdx = this._scrollStart + relY;
|
|
533
|
+
// Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
|
|
534
|
+
if (rawRowIdx >= rows.length) {
|
|
535
|
+
this.cursorPosition = asCodePoint(this.value.length);
|
|
536
|
+
this._preferredColumn = null;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const rowIdx = Math.max(0, rawRowIdx);
|
|
540
|
+
const row = rows[rowIdx];
|
|
541
|
+
// Continuation rows start after the marker; non-continuation rows start at column 0.
|
|
542
|
+
const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
|
|
543
|
+
const relX = asCellCol(Math.max(0, event.x - b.x - contentXOff));
|
|
544
|
+
const pos = cellColToCodeUnitOffset(row.content, relX);
|
|
545
|
+
const absPos = row.valueStart + pos;
|
|
546
|
+
const chu = absPos < this.value.length ? this.value.charCodeAt(absPos) : 0;
|
|
547
|
+
const prevChu = absPos > 0 ? this.value.charCodeAt(absPos - 1) : 0;
|
|
548
|
+
this.cursorPosition = asCodePoint(chu >= 0xDC00 && chu <= 0xDFFF && prevChu >= 0xD800 && prevChu <= 0xDBFF
|
|
549
|
+
? absPos + 1
|
|
550
|
+
: absPos);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
// [LAW:types-are-the-program] relX is a cell-column offset into the full
|
|
554
|
+
// display string. Add _singleLineViewportStart (the scroll offset captured
|
|
555
|
+
// by _renderSingleLine) so a click on a scrolled viewport maps to the
|
|
556
|
+
// correct code-unit in the underlying value.
|
|
557
|
+
const relX = asCellCol(Math.max(0, event.x - b.x - 1) + this._singleLineViewportStart);
|
|
558
|
+
const displayForHitTest = this._password
|
|
559
|
+
? "•".repeat(this.value.length)
|
|
560
|
+
: this.value.indexOf("\n") >= 0
|
|
561
|
+
? this.value.replace(/\n/g, NEWLINE_GLYPH)
|
|
562
|
+
: this.value;
|
|
563
|
+
const pos = cellColToCodeUnitOffset(displayForHitTest, relX);
|
|
564
|
+
// Snap forward only when pos lands on a valid surrogate pair, never on a
|
|
565
|
+
// lone low surrogate (malformed string). Verify the preceding code unit is
|
|
566
|
+
// the high-surrogate half before advancing.
|
|
567
|
+
const chu = pos < this.value.length ? this.value.charCodeAt(pos) : 0;
|
|
568
|
+
const prevChu = pos > 0 ? this.value.charCodeAt(pos - 1) : 0;
|
|
569
|
+
this.cursorPosition = asCodePoint(chu >= 0xDC00 && chu <= 0xDFFF && prevChu >= 0xD800 && prevChu <= 0xDBFF
|
|
570
|
+
? pos + 1
|
|
571
|
+
: pos);
|
|
572
|
+
}
|
|
573
|
+
this._preferredColumn = null;
|
|
176
574
|
}
|
|
177
575
|
// --- Hover mutator (router fast-path) ---
|
|
178
576
|
setHovered(value) { this.hovered = value; }
|
|
577
|
+
// ─── Public motion primitives ───────────────────────────────────────────
|
|
578
|
+
moveCharLeft() {
|
|
579
|
+
this.cursorPosition = prevCodePoint(this.value, this.cursorPosition);
|
|
580
|
+
this._preferredColumn = null;
|
|
581
|
+
}
|
|
582
|
+
moveCharRight() {
|
|
583
|
+
this.cursorPosition = nextCodePoint(this.value, this.cursorPosition);
|
|
584
|
+
this._preferredColumn = null;
|
|
585
|
+
}
|
|
586
|
+
moveLineUp() {
|
|
587
|
+
// Prefer the on-screen visual decomposition when it exists — wraps,
|
|
588
|
+
// marker offsets, and scroll alignment are all already accounted for.
|
|
589
|
+
// Fall back to logical-line motion only when no render has occurred yet
|
|
590
|
+
// (e.g. cursor motion before mount).
|
|
591
|
+
if (this._visualRows !== null && this._visualRows.length > 1) {
|
|
592
|
+
const rowIdx = this._cursorVisualRow();
|
|
593
|
+
if (rowIdx === 0)
|
|
594
|
+
return;
|
|
595
|
+
const col = this._preferredColumn ?? this._cursorVisualCol();
|
|
596
|
+
const target = this._visualRows[rowIdx - 1];
|
|
597
|
+
this.cursorPosition = asCodePoint(target.valueStart + this._clampColForRow(col, rowIdx - 1));
|
|
598
|
+
this._preferredColumn = col;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const lineStart = this._lineStart();
|
|
602
|
+
if (lineStart === 0)
|
|
603
|
+
return;
|
|
604
|
+
// Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
|
|
605
|
+
const col = this._preferredColumn ?? asCellCol(cellLen(this.value.slice(lineStart, this.cursorPosition)));
|
|
606
|
+
const prevLineEnd = lineStart - 1;
|
|
607
|
+
let prevLineStart = prevLineEnd;
|
|
608
|
+
while (prevLineStart > 0 && this.value[prevLineStart - 1] !== "\n")
|
|
609
|
+
prevLineStart--;
|
|
610
|
+
const prevLineContent = this.value.slice(prevLineStart, prevLineEnd);
|
|
611
|
+
this.cursorPosition = asCodePoint(prevLineStart + cellColToCodeUnitOffset(prevLineContent, asCellCol(Math.min(col, cellLen(prevLineContent)))));
|
|
612
|
+
this._preferredColumn = col;
|
|
613
|
+
}
|
|
614
|
+
moveLineDown() {
|
|
615
|
+
if (this._visualRows !== null && this._visualRows.length > 1) {
|
|
616
|
+
const rowIdx = this._cursorVisualRow();
|
|
617
|
+
if (rowIdx === this._visualRows.length - 1)
|
|
618
|
+
return;
|
|
619
|
+
const col = this._preferredColumn ?? this._cursorVisualCol();
|
|
620
|
+
this.cursorPosition = asCodePoint(this._visualRows[rowIdx + 1].valueStart + this._clampColForRow(col, rowIdx + 1));
|
|
621
|
+
this._preferredColumn = col;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const lineEnd = this._lineEnd();
|
|
625
|
+
if (lineEnd === this.value.length)
|
|
626
|
+
return;
|
|
627
|
+
const lineStart = this._lineStart();
|
|
628
|
+
// Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
|
|
629
|
+
const col = this._preferredColumn ?? asCellCol(cellLen(this.value.slice(lineStart, this.cursorPosition)));
|
|
630
|
+
const nextLineStart = lineEnd + 1;
|
|
631
|
+
let nextLineEnd = nextLineStart;
|
|
632
|
+
while (nextLineEnd < this.value.length && this.value[nextLineEnd] !== "\n")
|
|
633
|
+
nextLineEnd++;
|
|
634
|
+
const nextLineContent = this.value.slice(nextLineStart, nextLineEnd);
|
|
635
|
+
this.cursorPosition = asCodePoint(nextLineStart + cellColToCodeUnitOffset(nextLineContent, asCellCol(Math.min(col, cellLen(nextLineContent)))));
|
|
636
|
+
this._preferredColumn = col;
|
|
637
|
+
}
|
|
638
|
+
// Clamp `col` (a cell column) to the target visual row's content, returning
|
|
639
|
+
// the code-unit offset within that row. When the row IS followed by a
|
|
640
|
+
// continuation of the same logical line, we clamp to the last valid
|
|
641
|
+
// code-point boundary strictly inside the row (which may be more than
|
|
642
|
+
// one cell before the visual end when the last code point is wide) so the
|
|
643
|
+
// cursor stays strictly inside the target row — landing at the boundary
|
|
644
|
+
// would cause `_cursorVisualRow` to resolve to the *later* row, making
|
|
645
|
+
// every subsequent Up/Down stick at the boundary.
|
|
646
|
+
//
|
|
647
|
+
// For rows NOT followed by a continuation (last wrap row, or any unwrapped
|
|
648
|
+
// logical line), the end-of-line position is valid — there is no adjacent
|
|
649
|
+
// continuation row to collide with.
|
|
650
|
+
_clampColForRow(col, targetIdx) {
|
|
651
|
+
const rows = this._visualRows;
|
|
652
|
+
const target = rows[targetIdx];
|
|
653
|
+
const nextIsContinuation = targetIdx + 1 < rows.length && rows[targetIdx + 1].isContinuation;
|
|
654
|
+
const contentCellWidth = asCellCol(cellLen(target.content));
|
|
655
|
+
const capCells = nextIsContinuation
|
|
656
|
+
? asCellCol(Math.max(0, contentCellWidth - 1))
|
|
657
|
+
: contentCellWidth;
|
|
658
|
+
return cellColToCodeUnitOffset(target.content, asCellCol(Math.min(col, capCells)));
|
|
659
|
+
}
|
|
660
|
+
// Locate which cached visual row the cursor sits on. Returns the row index
|
|
661
|
+
// for use in `moveLineUp`/`moveLineDown`; caller must guard `_visualRows`.
|
|
662
|
+
_cursorVisualRow() {
|
|
663
|
+
const rows = this._visualRows;
|
|
664
|
+
let idx = 0;
|
|
665
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
666
|
+
if (rows[i].valueStart <= this.cursorPosition) {
|
|
667
|
+
idx = i;
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return idx;
|
|
672
|
+
}
|
|
673
|
+
// Returns the cursor's cell column within its current visual row.
|
|
674
|
+
_cursorVisualCol() {
|
|
675
|
+
const rows = this._visualRows;
|
|
676
|
+
const idx = this._cursorVisualRow();
|
|
677
|
+
const row = rows[idx];
|
|
678
|
+
return asCellCol(cellLen(row.content.slice(0, this.cursorPosition - row.valueStart)));
|
|
679
|
+
}
|
|
680
|
+
moveLineStart() {
|
|
681
|
+
this.cursorPosition = this._lineStart();
|
|
682
|
+
this._preferredColumn = null;
|
|
683
|
+
}
|
|
684
|
+
moveLineEnd() {
|
|
685
|
+
this.cursorPosition = this._lineEnd();
|
|
686
|
+
this._preferredColumn = null;
|
|
687
|
+
}
|
|
688
|
+
moveDocStart() {
|
|
689
|
+
this.cursorPosition = asCodePoint(0);
|
|
690
|
+
this._preferredColumn = null;
|
|
691
|
+
}
|
|
692
|
+
moveDocEnd() {
|
|
693
|
+
this.cursorPosition = asCodePoint(this.value.length);
|
|
694
|
+
this._preferredColumn = null;
|
|
695
|
+
}
|
|
696
|
+
moveWordLeft() {
|
|
697
|
+
let p = this.cursorPosition;
|
|
698
|
+
while (p > 0) {
|
|
699
|
+
const prev = prevCodePoint(this.value, p);
|
|
700
|
+
if (isWordChar(this.value.slice(prev, p)))
|
|
701
|
+
break;
|
|
702
|
+
p = prev;
|
|
703
|
+
}
|
|
704
|
+
while (p > 0) {
|
|
705
|
+
const prev = prevCodePoint(this.value, p);
|
|
706
|
+
if (!isWordChar(this.value.slice(prev, p)))
|
|
707
|
+
break;
|
|
708
|
+
p = prev;
|
|
709
|
+
}
|
|
710
|
+
this.cursorPosition = p;
|
|
711
|
+
this._preferredColumn = null;
|
|
712
|
+
}
|
|
713
|
+
moveWordRight() {
|
|
714
|
+
let p = this.cursorPosition;
|
|
715
|
+
while (p < this.value.length) {
|
|
716
|
+
const next = nextCodePoint(this.value, p);
|
|
717
|
+
if (isWordChar(this.value.slice(p, next)))
|
|
718
|
+
break;
|
|
719
|
+
p = next;
|
|
720
|
+
}
|
|
721
|
+
while (p < this.value.length) {
|
|
722
|
+
const next = nextCodePoint(this.value, p);
|
|
723
|
+
if (!isWordChar(this.value.slice(p, next)))
|
|
724
|
+
break;
|
|
725
|
+
p = next;
|
|
726
|
+
}
|
|
727
|
+
this.cursorPosition = p;
|
|
728
|
+
this._preferredColumn = null;
|
|
729
|
+
}
|
|
730
|
+
// ─── Public editing primitives ──────────────────────────────────────────
|
|
731
|
+
deleteCharBack() {
|
|
732
|
+
if (this.cursorPosition === 0)
|
|
733
|
+
return;
|
|
734
|
+
const newPos = prevCodePoint(this.value, this.cursorPosition);
|
|
735
|
+
this.value = this.value.slice(0, newPos) + this.value.slice(this.cursorPosition);
|
|
736
|
+
this.cursorPosition = newPos;
|
|
737
|
+
this._preferredColumn = null;
|
|
738
|
+
this.emitChange();
|
|
739
|
+
}
|
|
740
|
+
deleteCharForward() {
|
|
741
|
+
if (this.cursorPosition >= this.value.length)
|
|
742
|
+
return;
|
|
743
|
+
const nextPos = nextCodePoint(this.value, this.cursorPosition);
|
|
744
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(nextPos);
|
|
745
|
+
this._preferredColumn = null;
|
|
746
|
+
this.emitChange();
|
|
747
|
+
}
|
|
748
|
+
deleteWordBack() {
|
|
749
|
+
// Readline `unix-word-rubout` semantics: delete back to nearest whitespace,
|
|
750
|
+
// skipping trailing whitespace first so successive Ctrl+W at "foo bar |"
|
|
751
|
+
// → "foo |" → "|" rather than getting stuck on the trailing space.
|
|
752
|
+
let p = this.cursorPosition;
|
|
753
|
+
while (p > 0) {
|
|
754
|
+
const prev = prevCodePoint(this.value, p);
|
|
755
|
+
if (!isWhitespace(this.value.slice(prev, p)))
|
|
756
|
+
break;
|
|
757
|
+
p = prev;
|
|
758
|
+
}
|
|
759
|
+
while (p > 0) {
|
|
760
|
+
const prev = prevCodePoint(this.value, p);
|
|
761
|
+
if (isWhitespace(this.value.slice(prev, p)))
|
|
762
|
+
break;
|
|
763
|
+
p = prev;
|
|
764
|
+
}
|
|
765
|
+
if (p === this.cursorPosition)
|
|
766
|
+
return;
|
|
767
|
+
this._killBuffer = this.value.slice(p, this.cursorPosition);
|
|
768
|
+
this.value = this.value.slice(0, p) + this.value.slice(this.cursorPosition);
|
|
769
|
+
this.cursorPosition = p;
|
|
770
|
+
this._preferredColumn = null;
|
|
771
|
+
this.emitChange();
|
|
772
|
+
}
|
|
773
|
+
deleteWordForward() {
|
|
774
|
+
let p = this.cursorPosition;
|
|
775
|
+
while (p < this.value.length) {
|
|
776
|
+
const next = nextCodePoint(this.value, p);
|
|
777
|
+
if (isWordChar(this.value.slice(p, next)))
|
|
778
|
+
break;
|
|
779
|
+
p = next;
|
|
780
|
+
}
|
|
781
|
+
while (p < this.value.length) {
|
|
782
|
+
const next = nextCodePoint(this.value, p);
|
|
783
|
+
if (!isWordChar(this.value.slice(p, next)))
|
|
784
|
+
break;
|
|
785
|
+
p = next;
|
|
786
|
+
}
|
|
787
|
+
if (p === this.cursorPosition)
|
|
788
|
+
return;
|
|
789
|
+
this._killBuffer = this.value.slice(this.cursorPosition, p);
|
|
790
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(p);
|
|
791
|
+
this._preferredColumn = null;
|
|
792
|
+
this.emitChange();
|
|
793
|
+
}
|
|
794
|
+
killLineBack() {
|
|
795
|
+
const start = this._lineStart();
|
|
796
|
+
if (start === this.cursorPosition)
|
|
797
|
+
return;
|
|
798
|
+
this._killBuffer = this.value.slice(start, this.cursorPosition);
|
|
799
|
+
this.value = this.value.slice(0, start) + this.value.slice(this.cursorPosition);
|
|
800
|
+
this.cursorPosition = start;
|
|
801
|
+
this._preferredColumn = null;
|
|
802
|
+
this.emitChange();
|
|
803
|
+
}
|
|
804
|
+
killLineForward() {
|
|
805
|
+
const end = this._lineEnd();
|
|
806
|
+
if (end > this.cursorPosition) {
|
|
807
|
+
this._killBuffer = this.value.slice(this.cursorPosition, end);
|
|
808
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(end);
|
|
809
|
+
this._preferredColumn = null;
|
|
810
|
+
this.emitChange();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
// Already at line end → kill the trailing `\n` (join next line).
|
|
814
|
+
if (this.cursorPosition < this.value.length) {
|
|
815
|
+
this._killBuffer = "\n";
|
|
816
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
|
|
817
|
+
this._preferredColumn = null;
|
|
818
|
+
this.emitChange();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
yank() {
|
|
822
|
+
if (this._killBuffer.length === 0)
|
|
823
|
+
return;
|
|
824
|
+
this._insertText(this._killBuffer);
|
|
825
|
+
}
|
|
826
|
+
transposeChars() {
|
|
827
|
+
// Readline `transpose-chars`: swap the char before the cursor with the
|
|
828
|
+
// char at the cursor and advance. At end-of-value, swap the trailing
|
|
829
|
+
// two chars without advancing. At position 0, no-op.
|
|
830
|
+
const len = this.value.length;
|
|
831
|
+
if (len < 2 || this.cursorPosition === 0)
|
|
832
|
+
return;
|
|
833
|
+
const p = this.cursorPosition;
|
|
834
|
+
if (p === len) {
|
|
835
|
+
// End-of-value: swap the trailing two code points; cursor stays.
|
|
836
|
+
const cp2 = prevCodePoint(this.value, p);
|
|
837
|
+
const cp1 = prevCodePoint(this.value, cp2);
|
|
838
|
+
this.value = this.value.slice(0, cp1) + this.value.slice(cp2) + this.value.slice(cp1, cp2);
|
|
839
|
+
this._preferredColumn = null;
|
|
840
|
+
this.emitChange();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Normal case: find full code-point substrings for the char before and at cursor.
|
|
844
|
+
const cpBefore = prevCodePoint(this.value, p);
|
|
845
|
+
const cpAfter = nextCodePoint(this.value, p);
|
|
846
|
+
const charBefore = this.value.slice(cpBefore, p);
|
|
847
|
+
const charAt = this.value.slice(p, cpAfter);
|
|
848
|
+
this.value = this.value.slice(0, cpBefore) + charAt + charBefore + this.value.slice(cpAfter);
|
|
849
|
+
this.cursorPosition = asCodePoint(cpAfter);
|
|
850
|
+
this._preferredColumn = null;
|
|
851
|
+
this.emitChange();
|
|
852
|
+
}
|
|
853
|
+
// ─── Internal helpers ───────────────────────────────────────────────────
|
|
854
|
+
_lineStart() {
|
|
855
|
+
let p = this.cursorPosition;
|
|
856
|
+
while (p > 0 && this.value[p - 1] !== "\n")
|
|
857
|
+
p--;
|
|
858
|
+
return asCodePoint(p);
|
|
859
|
+
}
|
|
860
|
+
_lineEnd() {
|
|
861
|
+
let p = this.cursorPosition;
|
|
862
|
+
while (p < this.value.length && this.value[p] !== "\n")
|
|
863
|
+
p++;
|
|
864
|
+
return asCodePoint(p);
|
|
865
|
+
}
|
|
866
|
+
_insertText(text) {
|
|
867
|
+
let toInsert = text;
|
|
868
|
+
if (this._maxLength !== undefined) {
|
|
869
|
+
const room = this._maxLength - this.value.length;
|
|
870
|
+
if (room <= 0)
|
|
871
|
+
return;
|
|
872
|
+
if (toInsert.length > room)
|
|
873
|
+
toInsert = toInsert.slice(0, room);
|
|
874
|
+
}
|
|
875
|
+
this.value =
|
|
876
|
+
this.value.slice(0, this.cursorPosition) +
|
|
877
|
+
toInsert +
|
|
878
|
+
this.value.slice(this.cursorPosition);
|
|
879
|
+
this.cursorPosition = asCodePoint(this.cursorPosition + toInsert.length);
|
|
880
|
+
this._preferredColumn = null;
|
|
881
|
+
this.emitChange();
|
|
882
|
+
}
|
|
179
883
|
// --- Rendering ---
|
|
180
884
|
render(options) {
|
|
885
|
+
if (this._multiline)
|
|
886
|
+
return this._renderMultiline(options);
|
|
887
|
+
return this._renderSingleLine(options);
|
|
888
|
+
}
|
|
889
|
+
_renderSingleLine(options) {
|
|
181
890
|
const showPlaceholder = this.focused && this.value.length === 0 && this.placeholder.length > 0;
|
|
891
|
+
// [LAW:dataflow-not-control-flow] Single rawDisplay value derives from
|
|
892
|
+
// mode (password / placeholder / plain). NEWLINE_GLYPH substitution maps
|
|
893
|
+
// every code unit in value 1:1 to rawDisplay so cursorPosition (a code-
|
|
894
|
+
// unit index) is a valid index into rawDisplay regardless of mode.
|
|
182
895
|
const rawDisplay = showPlaceholder
|
|
183
896
|
? this.placeholder
|
|
184
897
|
: this._password
|
|
185
898
|
? "•".repeat(this.value.length)
|
|
186
|
-
: this.value
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
899
|
+
: this.value.indexOf("\n") >= 0
|
|
900
|
+
? this.value.replace(/\n/g, NEWLINE_GLYPH)
|
|
901
|
+
: this.value;
|
|
902
|
+
// Measure in cell space — cursorPosition is a code-unit index; convert to
|
|
903
|
+
// cell column for all viewport arithmetic.
|
|
904
|
+
const rawCellWidth = asCellCol(cellLen(rawDisplay));
|
|
905
|
+
const cursorCellCol = asCellCol(cellLen(rawDisplay.slice(0, this.cursorPosition)));
|
|
906
|
+
const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, options.maxWidth - 2));
|
|
907
|
+
const desiredWidth = asCellCol(Math.max(MIN_CONTENT_WIDTH, rawCellWidth, cursorCellCol + 1));
|
|
908
|
+
const contentWidth = asCellCol(Math.min(maxAvailable, desiredWidth));
|
|
909
|
+
// Slide viewport (in cells) to keep cursor visible when content overflows.
|
|
910
|
+
const startCell = asCellCol(Math.max(0, Math.min(rawCellWidth - contentWidth, cursorCellCol - contentWidth + 1)));
|
|
911
|
+
const [, afterStart] = splitText(rawDisplay, startCell);
|
|
912
|
+
// splitText snaps backward when startCell falls mid-wide-char, so afterStart
|
|
913
|
+
// may start 1 cell earlier than requested. actualStartCell is the real offset.
|
|
914
|
+
const actualStartCell = asCellCol(rawCellWidth - cellLen(afterStart));
|
|
915
|
+
this._singleLineViewportStart = actualStartCell;
|
|
916
|
+
const [visible] = splitText(afterStart, contentWidth);
|
|
917
|
+
const display = setCellSize(visible, contentWidth);
|
|
918
|
+
const cursorDisplayCellCol = asCellCol(cursorCellCol - actualStartCell);
|
|
197
919
|
const bracketStyle = this.disabled
|
|
198
920
|
? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
|
|
199
921
|
: new Style({ color: this.resolvePalette("foreground") });
|
|
@@ -201,21 +923,24 @@ let TextInput = (() => {
|
|
|
201
923
|
? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
|
|
202
924
|
: showPlaceholder
|
|
203
925
|
? new Style({ color: this.resolvePalette("foreground"), dim: true })
|
|
204
|
-
: new Style({ color: this.resolvePalette("foreground") });
|
|
205
|
-
|
|
206
|
-
// contrast is WCAG-correct across all themes, not theme-bg-dependent.
|
|
207
|
-
const cursorStyle = new Style({
|
|
926
|
+
: this.contentStyleOverride ?? new Style({ color: this.resolvePalette("foreground") });
|
|
927
|
+
const cursorStyle = this.cursorStyleOverride ?? new Style({
|
|
208
928
|
color: this.resolvePalette("on-primary"),
|
|
209
929
|
bgcolor: this.resolvePalette("primary"),
|
|
210
930
|
});
|
|
211
931
|
const segments = [new Segment("[", bracketStyle)];
|
|
212
|
-
if (this.focused && !this.disabled &&
|
|
213
|
-
const before = display
|
|
214
|
-
|
|
215
|
-
const
|
|
932
|
+
if (this.focused && !this.disabled && cursorDisplayCellCol >= 0 && cursorDisplayCellCol < contentWidth) {
|
|
933
|
+
const [before, rest] = splitText(display, cursorDisplayCellCol);
|
|
934
|
+
let firstCh = "";
|
|
935
|
+
for (const ch of rest) {
|
|
936
|
+
firstCh = ch;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
const at = firstCh || " ";
|
|
940
|
+
const after = rest.slice(firstCh.length);
|
|
216
941
|
if (before.length > 0)
|
|
217
942
|
segments.push(new Segment(before, contentStyle));
|
|
218
|
-
segments.push(new Segment(at
|
|
943
|
+
segments.push(new Segment(at, cursorStyle));
|
|
219
944
|
if (after.length > 0)
|
|
220
945
|
segments.push(new Segment(after, contentStyle));
|
|
221
946
|
}
|
|
@@ -225,11 +950,216 @@ let TextInput = (() => {
|
|
|
225
950
|
segments.push(new Segment("]", bracketStyle));
|
|
226
951
|
return segments;
|
|
227
952
|
}
|
|
953
|
+
_renderMultiline(options) {
|
|
954
|
+
// [LAW:dataflow-not-control-flow] Decompose `value` into visual rows
|
|
955
|
+
// exactly once per render, cache the result, then drive both display
|
|
956
|
+
// *and* cursor projection from the same array. Vertical motion reads
|
|
957
|
+
// from the cache so Up/Down step through whatever the user actually
|
|
958
|
+
// sees, including soft-wrap continuations.
|
|
959
|
+
const visualRows = this._computeVisualRows(options.maxWidth);
|
|
960
|
+
this._visualRows = visualRows;
|
|
961
|
+
// Scroll window: keep `_scrollStart` (the viewport's top row) stable
|
|
962
|
+
// across renders, adjusting only when the cursor would leave the
|
|
963
|
+
// viewport. This is the textarea-conventional "viewport follows cursor
|
|
964
|
+
// when it has to" behavior — cursor moves freely *within* the visible
|
|
965
|
+
// rows; the window scrolls only at the edges. When `maxRows` is unset,
|
|
966
|
+
// render every row.
|
|
967
|
+
const total = visualRows.length;
|
|
968
|
+
const cursorRow = this._cursorVisualRow();
|
|
969
|
+
let scrollStart = 0;
|
|
970
|
+
let visibleCount = total;
|
|
971
|
+
if (this._maxRows !== undefined && total > this._maxRows) {
|
|
972
|
+
const maxStart = total - this._maxRows;
|
|
973
|
+
if (cursorRow < this._scrollStart) {
|
|
974
|
+
this._scrollStart = cursorRow;
|
|
975
|
+
}
|
|
976
|
+
else if (cursorRow >= this._scrollStart + this._maxRows) {
|
|
977
|
+
this._scrollStart = cursorRow - this._maxRows + 1;
|
|
978
|
+
}
|
|
979
|
+
this._scrollStart = Math.max(0, Math.min(maxStart, this._scrollStart));
|
|
980
|
+
scrollStart = this._scrollStart;
|
|
981
|
+
visibleCount = this._maxRows;
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
this._scrollStart = 0;
|
|
985
|
+
}
|
|
986
|
+
// Pad with empty rows when the value is shorter than `minRows`.
|
|
987
|
+
let padRows = 0;
|
|
988
|
+
if (this._minRows !== undefined && total < this._minRows && this._maxRows === undefined) {
|
|
989
|
+
padRows = this._minRows - total;
|
|
990
|
+
}
|
|
991
|
+
const contentStyle = this.disabled
|
|
992
|
+
? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
|
|
993
|
+
: this.contentStyleOverride ?? new Style({ color: this.resolvePalette("foreground") });
|
|
994
|
+
const markerStyle = new Style({ color: this.resolvePalette("foreground"), dim: true });
|
|
995
|
+
const cursorStyle = this.cursorStyleOverride ?? new Style({
|
|
996
|
+
color: this.resolvePalette("on-primary"),
|
|
997
|
+
bgcolor: this.resolvePalette("primary"),
|
|
998
|
+
});
|
|
999
|
+
// Scroll-direction arrows appear in the rightmost cell of the first/last
|
|
1000
|
+
// visible row, *only* when scroll is actually possible in that direction.
|
|
1001
|
+
// _emitRowContent reserves the indicator's cells so content never
|
|
1002
|
+
// collides with the arrow. Only emitted in `"arrows"` mode; other modes
|
|
1003
|
+
// leave the content area untouched and rely on the host to surface scroll
|
|
1004
|
+
// state externally (e.g. Panel border accessory).
|
|
1005
|
+
const scrollable = this._maxRows !== undefined && total > this._maxRows;
|
|
1006
|
+
const arrowsMode = this._scrollIndicator === "arrows";
|
|
1007
|
+
const canScrollUp = arrowsMode && scrollable && this._scrollStart > 0;
|
|
1008
|
+
const canScrollDown = arrowsMode && scrollable && this._scrollStart + this._maxRows < total;
|
|
1009
|
+
const indicatorStyle = this.indicatorStyleOverride ?? new Style({ color: this.resolvePalette("primary") });
|
|
1010
|
+
const segments = [];
|
|
1011
|
+
const showCursor = this.focused && !this.disabled;
|
|
1012
|
+
for (let i = 0; i < visibleCount; i++) {
|
|
1013
|
+
const rowIdx = scrollStart + i;
|
|
1014
|
+
const row = visualRows[rowIdx];
|
|
1015
|
+
if (i > 0)
|
|
1016
|
+
segments.push(new Segment("\n"));
|
|
1017
|
+
if (row.isContinuation) {
|
|
1018
|
+
segments.push(new Segment(this._continuationMarker, markerStyle));
|
|
1019
|
+
}
|
|
1020
|
+
let indicator;
|
|
1021
|
+
if (i === 0 && canScrollUp) {
|
|
1022
|
+
indicator = { ch: "▲", style: indicatorStyle };
|
|
1023
|
+
}
|
|
1024
|
+
else if (i === visibleCount - 1 && canScrollDown) {
|
|
1025
|
+
indicator = { ch: "▼", style: indicatorStyle };
|
|
1026
|
+
}
|
|
1027
|
+
// When a continuation marker was emitted, the row's printable width is
|
|
1028
|
+
// reduced by the marker width — the indicator column is still at
|
|
1029
|
+
// `options.maxWidth - 1` in the panel's view, which is
|
|
1030
|
+
// `options.maxWidth - markerWidth - 1` columns past the marker.
|
|
1031
|
+
const rowPrintWidth = row.isContinuation
|
|
1032
|
+
? asCellCol(Math.max(0, options.maxWidth - this._markerWidth))
|
|
1033
|
+
: asCellCol(options.maxWidth);
|
|
1034
|
+
this._emitRowContent(segments, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle, indicator, rowPrintWidth);
|
|
1035
|
+
}
|
|
1036
|
+
// Trailing empty rows for minRows padding (no cursor, no marker).
|
|
1037
|
+
// Emit a whitespace segment of the full content width so Segment.splitLines
|
|
1038
|
+
// does not drop the row — an empty segment before a newline would produce
|
|
1039
|
+
// an invisible row that the renderer discards.
|
|
1040
|
+
for (let i = 0; i < padRows; i++) {
|
|
1041
|
+
segments.push(new Segment("\n"));
|
|
1042
|
+
segments.push(new Segment(setCellSize("", asCellCol(options.maxWidth)), contentStyle));
|
|
1043
|
+
}
|
|
1044
|
+
return segments;
|
|
1045
|
+
}
|
|
1046
|
+
_emitRowContent(out, row, cursorOnRow, contentStyle, cursorStyle, indicator, rowPrintWidth) {
|
|
1047
|
+
const content = row.content;
|
|
1048
|
+
// cursorCol is a code-unit offset into row.content; both cursorPosition
|
|
1049
|
+
// and valueStart are CodePoint, so their difference lands on a code-point
|
|
1050
|
+
// boundary within content and is safe for string slicing.
|
|
1051
|
+
const cursorCol = cursorOnRow ? this.cursorPosition - row.valueStart : -1;
|
|
1052
|
+
// Fast path: no indicator on this row. Let the wrapper pad to width.
|
|
1053
|
+
if (indicator === undefined) {
|
|
1054
|
+
if (!cursorOnRow) {
|
|
1055
|
+
if (content.length > 0)
|
|
1056
|
+
out.push(new Segment(content, contentStyle));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const before = content.slice(0, cursorCol);
|
|
1060
|
+
const nextCp = nextCodePoint(content, asCodePoint(cursorCol));
|
|
1061
|
+
const at = content.slice(cursorCol, nextCp) || " ";
|
|
1062
|
+
const after = content.slice(nextCp);
|
|
1063
|
+
if (before.length > 0)
|
|
1064
|
+
out.push(new Segment(before, contentStyle));
|
|
1065
|
+
out.push(new Segment(at, cursorStyle));
|
|
1066
|
+
if (after.length > 0)
|
|
1067
|
+
out.push(new Segment(after, contentStyle));
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
// Indicator path: reserve the indicator's cells at the end of the row so
|
|
1071
|
+
// wide characters never collide with it. Content fills [0, contentCellWidth)
|
|
1072
|
+
// cells; the indicator occupies the remaining cells.
|
|
1073
|
+
const indicatorWidth = asCellCol(cellLen(indicator.ch));
|
|
1074
|
+
const contentCellWidth = asCellCol(Math.max(0, rowPrintWidth - indicatorWidth));
|
|
1075
|
+
// Clip row content to the non-indicator region and pad to exact cell width.
|
|
1076
|
+
const [visibleContent] = splitText(content, contentCellWidth);
|
|
1077
|
+
const paddedContent = setCellSize(visibleContent, contentCellWidth);
|
|
1078
|
+
// Map the code-unit cursor offset to a cell column within this row.
|
|
1079
|
+
const cursorCellColInRow = cursorOnRow && cursorCol >= 0
|
|
1080
|
+
? cellLen(content.slice(0, cursorCol))
|
|
1081
|
+
: -1;
|
|
1082
|
+
if (cursorCellColInRow >= 0 && cursorCellColInRow < contentCellWidth) {
|
|
1083
|
+
const [before, rest] = splitText(paddedContent, asCellCol(cursorCellColInRow));
|
|
1084
|
+
let firstCh = "";
|
|
1085
|
+
for (const ch of rest) {
|
|
1086
|
+
firstCh = ch;
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
const at = firstCh || " ";
|
|
1090
|
+
const after = rest.slice(firstCh.length);
|
|
1091
|
+
if (before.length > 0)
|
|
1092
|
+
out.push(new Segment(before, contentStyle));
|
|
1093
|
+
out.push(new Segment(at, cursorStyle));
|
|
1094
|
+
if (after.length > 0)
|
|
1095
|
+
out.push(new Segment(after, contentStyle));
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
if (paddedContent.length > 0)
|
|
1099
|
+
out.push(new Segment(paddedContent, contentStyle));
|
|
1100
|
+
}
|
|
1101
|
+
out.push(new Segment(indicator.ch, indicator.style));
|
|
1102
|
+
}
|
|
1103
|
+
_computeVisualRows(maxWidth) {
|
|
1104
|
+
const firstWidth = asCellCol(Math.max(1, maxWidth));
|
|
1105
|
+
const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
|
|
1106
|
+
const rows = [];
|
|
1107
|
+
const lines = this.value.split("\n");
|
|
1108
|
+
let pos = 0;
|
|
1109
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1110
|
+
const line = lines[li];
|
|
1111
|
+
if (this._wrap !== undefined) {
|
|
1112
|
+
const wrapRows = this._wrap(line, { firstWidth, continuationWidth });
|
|
1113
|
+
if (wrapRows.length === 0) {
|
|
1114
|
+
rows.push({ content: "", valueStart: asCodePoint(pos), isContinuation: false });
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
for (let ri = 0; ri < wrapRows.length; ri++) {
|
|
1118
|
+
const wr = wrapRows[ri];
|
|
1119
|
+
rows.push({
|
|
1120
|
+
content: wr.content,
|
|
1121
|
+
valueStart: asCodePoint(pos + wr.start),
|
|
1122
|
+
isContinuation: ri > 0,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
rows.push({ content: line, valueStart: asCodePoint(pos), isContinuation: false });
|
|
1129
|
+
}
|
|
1130
|
+
pos += line.length + 1; // +1 for the \n separator
|
|
1131
|
+
}
|
|
1132
|
+
return rows;
|
|
1133
|
+
}
|
|
228
1134
|
measure(_options) {
|
|
229
1135
|
const minimum = MIN_CONTENT_WIDTH + 2;
|
|
230
|
-
const maximum = Math.max(minimum, Math.max(this.value
|
|
1136
|
+
const maximum = Math.max(minimum, Math.max(cellLen(this.value), cellLen(this.placeholder)) + 2);
|
|
231
1137
|
return { minimum, maximum };
|
|
232
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* `[X/Y]` scroll-position string when there's actually something to
|
|
1141
|
+
* scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
|
|
1142
|
+
* the total visual row count. Reads cached state populated by the most
|
|
1143
|
+
* recent `render()` — call from a `Panel.bottomRightAccessory` thunk so
|
|
1144
|
+
* it evaluates after content has been rendered for the current frame.
|
|
1145
|
+
*
|
|
1146
|
+
* Returns `undefined` when:
|
|
1147
|
+
* - `scrollIndicator` is not `"indices"`, or
|
|
1148
|
+
* - `maxRows` is unset, or total visual rows ≤ maxRows (nothing to
|
|
1149
|
+
* scroll), or
|
|
1150
|
+
* - no render has happened yet (cache is empty).
|
|
1151
|
+
*/
|
|
1152
|
+
get scrollIndicatorText() {
|
|
1153
|
+
if (this._scrollIndicator !== "indices")
|
|
1154
|
+
return undefined;
|
|
1155
|
+
if (this._maxRows === undefined)
|
|
1156
|
+
return undefined;
|
|
1157
|
+
const rows = this._visualRows;
|
|
1158
|
+
if (rows === null || rows.length <= this._maxRows)
|
|
1159
|
+
return undefined;
|
|
1160
|
+
const cursorRow1 = this._cursorVisualRow() + 1;
|
|
1161
|
+
return `[${cursorRow1}/${rows.length}]`;
|
|
1162
|
+
}
|
|
233
1163
|
// --- Palette resolution ---
|
|
234
1164
|
resolvePalette(key) {
|
|
235
1165
|
const rgba = this._theme.palette.get(key);
|