@promptctl/rich-js 0.1.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.
Files changed (158) hide show
  1. package/README.md +29 -15
  2. package/dist/core/cells.d.ts +83 -3
  3. package/dist/core/cells.d.ts.map +1 -1
  4. package/dist/core/cells.js +113 -1
  5. package/dist/core/cells.js.map +1 -1
  6. package/dist/core/console.d.ts +0 -1
  7. package/dist/core/console.d.ts.map +1 -1
  8. package/dist/core/console.js +20 -16
  9. package/dist/core/console.js.map +1 -1
  10. package/dist/core/highlighter.d.ts.map +1 -1
  11. package/dist/core/highlighter.js.map +1 -1
  12. package/dist/core/oklch.d.ts +106 -0
  13. package/dist/core/oklch.d.ts.map +1 -0
  14. package/dist/core/oklch.js +221 -0
  15. package/dist/core/oklch.js.map +1 -0
  16. package/dist/core/render.d.ts +26 -7
  17. package/dist/core/render.d.ts.map +1 -1
  18. package/dist/core/render.js +83 -15
  19. package/dist/core/render.js.map +1 -1
  20. package/dist/core/segment.d.ts +14 -1
  21. package/dist/core/segment.d.ts.map +1 -1
  22. package/dist/core/segment.js +28 -3
  23. package/dist/core/segment.js.map +1 -1
  24. package/dist/core/style.d.ts +11 -2
  25. package/dist/core/style.d.ts.map +1 -1
  26. package/dist/core/style.js +31 -18
  27. package/dist/core/style.js.map +1 -1
  28. package/dist/core/text.d.ts +20 -0
  29. package/dist/core/text.d.ts.map +1 -1
  30. package/dist/core/text.js +31 -0
  31. package/dist/core/text.js.map +1 -1
  32. package/dist/index.d.ts +11 -4
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +12 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/renderables/columns.d.ts.map +1 -1
  37. package/dist/renderables/columns.js +15 -22
  38. package/dist/renderables/columns.js.map +1 -1
  39. package/dist/renderables/layout.d.ts.map +1 -1
  40. package/dist/renderables/layout.js +8 -29
  41. package/dist/renderables/layout.js.map +1 -1
  42. package/dist/renderables/live.d.ts.map +1 -1
  43. package/dist/renderables/live.js +8 -7
  44. package/dist/renderables/live.js.map +1 -1
  45. package/dist/renderables/panel.d.ts +34 -0
  46. package/dist/renderables/panel.d.ts.map +1 -1
  47. package/dist/renderables/panel.js +66 -23
  48. package/dist/renderables/panel.js.map +1 -1
  49. package/dist/template-bindings/helpers.d.ts +25 -0
  50. package/dist/template-bindings/helpers.d.ts.map +1 -0
  51. package/dist/template-bindings/helpers.js +31 -0
  52. package/dist/template-bindings/helpers.js.map +1 -0
  53. package/dist/template-bindings/index.d.ts +57 -18
  54. package/dist/template-bindings/index.d.ts.map +1 -1
  55. package/dist/template-bindings/index.js +83 -18
  56. package/dist/template-bindings/index.js.map +1 -1
  57. package/dist/template-bindings/palette-funcs.d.ts +76 -0
  58. package/dist/template-bindings/palette-funcs.d.ts.map +1 -0
  59. package/dist/template-bindings/palette-funcs.js +157 -0
  60. package/dist/template-bindings/palette-funcs.js.map +1 -0
  61. package/dist/template-bindings/style-funcs.d.ts.map +1 -1
  62. package/dist/template-bindings/style-funcs.js +33 -27
  63. package/dist/template-bindings/style-funcs.js.map +1 -1
  64. package/dist/themes/colorMath.d.ts +40 -0
  65. package/dist/themes/colorMath.d.ts.map +1 -1
  66. package/dist/themes/colorMath.js +82 -1
  67. package/dist/themes/colorMath.js.map +1 -1
  68. package/dist/themes/data/catppuccin-frappe.d.ts +4 -0
  69. package/dist/themes/data/catppuccin-frappe.d.ts.map +1 -0
  70. package/dist/themes/data/catppuccin-frappe.js +159 -0
  71. package/dist/themes/data/catppuccin-frappe.js.map +1 -0
  72. package/dist/themes/data/catppuccin-macchiato.d.ts +4 -0
  73. package/dist/themes/data/catppuccin-macchiato.d.ts.map +1 -0
  74. package/dist/themes/data/catppuccin-macchiato.js +159 -0
  75. package/dist/themes/data/catppuccin-macchiato.js.map +1 -0
  76. package/dist/themes/data/cyberpunk.d.ts +18 -0
  77. package/dist/themes/data/cyberpunk.d.ts.map +1 -0
  78. package/dist/themes/data/cyberpunk.js +192 -0
  79. package/dist/themes/data/cyberpunk.js.map +1 -0
  80. package/dist/themes/data/default.d.ts +4 -0
  81. package/dist/themes/data/default.d.ts.map +1 -0
  82. package/dist/themes/data/default.js +159 -0
  83. package/dist/themes/data/default.js.map +1 -0
  84. package/dist/themes/data/index.d.ts +10 -3
  85. package/dist/themes/data/index.d.ts.map +1 -1
  86. package/dist/themes/data/index.js +24 -7
  87. package/dist/themes/data/index.js.map +1 -1
  88. package/dist/themes/data/svg-export.d.ts +4 -0
  89. package/dist/themes/data/svg-export.d.ts.map +1 -0
  90. package/dist/themes/data/svg-export.js +159 -0
  91. package/dist/themes/data/svg-export.js.map +1 -0
  92. package/dist/themes/data/types.d.ts +4 -3
  93. package/dist/themes/data/types.d.ts.map +1 -1
  94. package/dist/themes/registry.d.ts +29 -2
  95. package/dist/themes/registry.d.ts.map +1 -1
  96. package/dist/themes/registry.js +62 -12
  97. package/dist/themes/registry.js.map +1 -1
  98. package/dist/themes/terminalThemes.d.ts +29 -20
  99. package/dist/themes/terminalThemes.d.ts.map +1 -1
  100. package/dist/themes/terminalThemes.js +50 -249
  101. package/dist/themes/terminalThemes.js.map +1 -1
  102. package/dist/themes/transpose.d.ts +88 -0
  103. package/dist/themes/transpose.d.ts.map +1 -0
  104. package/dist/themes/transpose.js +139 -0
  105. package/dist/themes/transpose.js.map +1 -0
  106. package/dist/widgets/button.d.ts +8 -5
  107. package/dist/widgets/button.d.ts.map +1 -1
  108. package/dist/widgets/button.js +36 -11
  109. package/dist/widgets/button.js.map +1 -1
  110. package/dist/widgets/checkbox.d.ts +1 -2
  111. package/dist/widgets/checkbox.d.ts.map +1 -1
  112. package/dist/widgets/checkbox.js +20 -7
  113. package/dist/widgets/checkbox.js.map +1 -1
  114. package/dist/widgets/dropdown.d.ts +4 -3
  115. package/dist/widgets/dropdown.d.ts.map +1 -1
  116. package/dist/widgets/dropdown.js +109 -27
  117. package/dist/widgets/dropdown.js.map +1 -1
  118. package/dist/widgets/event-router.d.ts +6 -3
  119. package/dist/widgets/event-router.d.ts.map +1 -1
  120. package/dist/widgets/event-router.js +179 -68
  121. package/dist/widgets/event-router.js.map +1 -1
  122. package/dist/widgets/focus-manager.d.ts +2 -1
  123. package/dist/widgets/focus-manager.d.ts.map +1 -1
  124. package/dist/widgets/focus-manager.js +28 -9
  125. package/dist/widgets/focus-manager.js.map +1 -1
  126. package/dist/widgets/index.d.ts +2 -2
  127. package/dist/widgets/index.d.ts.map +1 -1
  128. package/dist/widgets/index.js +1 -1
  129. package/dist/widgets/index.js.map +1 -1
  130. package/dist/widgets/screen.d.ts +2 -0
  131. package/dist/widgets/screen.d.ts.map +1 -1
  132. package/dist/widgets/screen.js +148 -45
  133. package/dist/widgets/screen.js.map +1 -1
  134. package/dist/widgets/slider.d.ts +6 -3
  135. package/dist/widgets/slider.d.ts.map +1 -1
  136. package/dist/widgets/slider.js +34 -10
  137. package/dist/widgets/slider.js.map +1 -1
  138. package/dist/widgets/static-item.d.ts +1 -1
  139. package/dist/widgets/static-item.d.ts.map +1 -1
  140. package/dist/widgets/static-item.js +12 -13
  141. package/dist/widgets/static-item.js.map +1 -1
  142. package/dist/widgets/text-input.d.ts +239 -13
  143. package/dist/widgets/text-input.d.ts.map +1 -1
  144. package/dist/widgets/text-input.js +1004 -74
  145. package/dist/widgets/text-input.js.map +1 -1
  146. package/dist/widgets/toggle.d.ts +1 -2
  147. package/dist/widgets/toggle.d.ts.map +1 -1
  148. package/dist/widgets/toggle.js +20 -8
  149. package/dist/widgets/toggle.js.map +1 -1
  150. package/dist/widgets/types.d.ts +21 -4
  151. package/dist/widgets/types.d.ts.map +1 -1
  152. package/dist/widgets/types.js +19 -0
  153. package/dist/widgets/types.js.map +1 -1
  154. package/dist/widgets/widget-base.d.ts +1 -0
  155. package/dist/widgets/widget-base.d.ts.map +1 -1
  156. package/dist/widgets/widget-base.js +24 -7
  157. package/dist/widgets/widget-base.js.map +1 -1
  158. package/package.json +5 -3
@@ -1,16 +1,59 @@
1
1
  /**
2
- * TextInput widget — single-line editable text field with cursor.
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
- * Visual states:
8
- * normal — "[value______]" with brackets bracketing fixed-width content
9
- * focused — same width with cursor cell (reverse video) at cursorPosition
10
- * placeholder — focused + empty value: dimmed placeholder text shown,
11
- * cursor at position 0
12
- * password — every value char rendered as "•"
13
- * disabled — entire segment dimmed
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
- this.cursorPosition = this.value.length;
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
- switch (event.key) {
120
- case "left":
121
- this.cursorPosition = Math.max(0, this.cursorPosition - 1);
122
- return;
123
- case "right":
124
- this.cursorPosition = Math.min(this.value.length, this.cursorPosition + 1);
125
- return;
126
- case "home":
127
- this.cursorPosition = 0;
128
- return;
129
- case "end":
130
- this.cursorPosition = this.value.length;
131
- return;
132
- case "backspace":
133
- if (this.cursorPosition > 0) {
134
- this.value = this.value.slice(0, this.cursorPosition - 1) + this.value.slice(this.cursorPosition);
135
- this.cursorPosition -= 1;
136
- this.emitChange();
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
- return;
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
- // Printable insertion: single-character event.character with no modifier.
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
- if (this._maxLength !== undefined && this.value.length >= this._maxLength)
156
- return;
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
- // Click position relative to the content area (skip the leading "[" bracket).
174
- const relX = event.x - b.x - 1;
175
- this.cursorPosition = Math.max(0, Math.min(this.value.length, relX));
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
- // [LAW:dataflow-not-control-flow] content width is data — derived from
188
- // value/placeholder length, MIN_CONTENT_WIDTH floor, maxWidth ceiling.
189
- const maxAvailable = Math.max(MIN_CONTENT_WIDTH, options.maxWidth - 2);
190
- const desiredWidth = Math.max(MIN_CONTENT_WIDTH, rawDisplay.length, this.cursorPosition + 1);
191
- const contentWidth = Math.min(maxAvailable, desiredWidth);
192
- // Slide window to keep cursor visible when content overflows.
193
- const startIdx = Math.max(0, Math.min(rawDisplay.length - contentWidth, this.cursorPosition - contentWidth + 1));
194
- const sliced = rawDisplay.slice(Math.max(0, startIdx), Math.max(0, startIdx) + contentWidth);
195
- const display = sliced.padEnd(contentWidth, " ");
196
- const cursorDisplayIdx = this.cursorPosition - Math.max(0, startIdx);
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
- // Cursor cell paints on a full primary bg → use on-primary for fg so
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 && cursorDisplayIdx >= 0 && cursorDisplayIdx < contentWidth) {
213
- const before = display.slice(0, cursorDisplayIdx);
214
- const at = display.slice(cursorDisplayIdx, cursorDisplayIdx + 1);
215
- const after = display.slice(cursorDisplayIdx + 1);
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.length > 0 ? at : " ", cursorStyle));
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.length, this.placeholder.length) + 2);
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);