@promptctl/rich-js 0.18.1 → 0.20.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 (169) hide show
  1. package/README.md +11 -8
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +99 -18
  9. package/dist/core/color.js +334 -146
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/noise.d.ts +31 -0
  33. package/dist/core/noise.js +94 -0
  34. package/dist/core/osc8.d.ts +6 -6
  35. package/dist/core/osc8.js +9 -21
  36. package/dist/core/paint.d.ts +107 -0
  37. package/dist/core/paint.js +161 -0
  38. package/dist/core/place.d.ts +1 -1
  39. package/dist/core/place.js +4 -6
  40. package/dist/core/pretty.d.ts +126 -2
  41. package/dist/core/pretty.js +571 -105
  42. package/dist/core/protocol.d.ts +43 -3
  43. package/dist/core/protocol.js +23 -2
  44. package/dist/core/render.d.ts +33 -30
  45. package/dist/core/render.js +43 -83
  46. package/dist/core/segment.d.ts +6 -5
  47. package/dist/core/segment.js +11 -15
  48. package/dist/core/selection.d.ts +12 -0
  49. package/dist/core/selection.js +14 -0
  50. package/dist/core/strip.d.ts +116 -74
  51. package/dist/core/strip.js +266 -217
  52. package/dist/core/style.d.ts +27 -7
  53. package/dist/core/style.js +106 -41
  54. package/dist/core/text.d.ts +43 -16
  55. package/dist/core/text.js +180 -59
  56. package/dist/core/wrap.d.ts +5 -0
  57. package/dist/core/wrap.js +16 -3
  58. package/dist/host/app.d.ts +95 -17
  59. package/dist/host/app.js +201 -61
  60. package/dist/host/index.d.ts +4 -1
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +84 -0
  63. package/dist/host/input.js +498 -0
  64. package/dist/host/terminal-host.d.ts +15 -2
  65. package/dist/host/terminal-host.js +2 -0
  66. package/dist/index.d.ts +26 -14
  67. package/dist/index.js +17 -7
  68. package/dist/node/prompt.d.ts +13 -0
  69. package/dist/node/prompt.js +30 -9
  70. package/dist/node/save.d.ts +4 -3
  71. package/dist/node/save.js +6 -3
  72. package/dist/node/terminal-host.d.ts +10 -2
  73. package/dist/node/terminal-host.js +7 -0
  74. package/dist/node/traceback.d.ts +2 -1
  75. package/dist/node/traceback.js +3 -39
  76. package/dist/renderables/align.js +2 -9
  77. package/dist/renderables/columns.d.ts +45 -29
  78. package/dist/renderables/columns.js +142 -103
  79. package/dist/renderables/constrain.js +6 -12
  80. package/dist/renderables/effect.d.ts +113 -0
  81. package/dist/renderables/effect.js +182 -0
  82. package/dist/renderables/effects.d.ts +252 -0
  83. package/dist/renderables/effects.js +565 -0
  84. package/dist/renderables/embed.d.ts +94 -31
  85. package/dist/renderables/embed.js +141 -45
  86. package/dist/renderables/group.d.ts +6 -3
  87. package/dist/renderables/group.js +5 -0
  88. package/dist/renderables/layout.d.ts +6 -23
  89. package/dist/renderables/layout.js +24 -113
  90. package/dist/renderables/list-view.d.ts +42 -0
  91. package/dist/renderables/list-view.js +78 -0
  92. package/dist/renderables/live.d.ts +40 -16
  93. package/dist/renderables/live.js +96 -83
  94. package/dist/renderables/markdown.d.ts +16 -2
  95. package/dist/renderables/markdown.js +521 -118
  96. package/dist/renderables/padding.d.ts +22 -1
  97. package/dist/renderables/padding.js +30 -24
  98. package/dist/renderables/panel.d.ts +31 -33
  99. package/dist/renderables/panel.js +137 -141
  100. package/dist/renderables/progress.d.ts +110 -16
  101. package/dist/renderables/progress.js +212 -81
  102. package/dist/renderables/progressBar.d.ts +24 -4
  103. package/dist/renderables/progressBar.js +78 -23
  104. package/dist/renderables/prompt.d.ts +37 -15
  105. package/dist/renderables/prompt.js +125 -85
  106. package/dist/renderables/ratio.d.ts +123 -0
  107. package/dist/renderables/ratio.js +213 -0
  108. package/dist/renderables/rule.d.ts +3 -2
  109. package/dist/renderables/rule.js +50 -52
  110. package/dist/renderables/spinner.d.ts +28 -2
  111. package/dist/renderables/spinner.js +52 -19
  112. package/dist/renderables/status.d.ts +13 -7
  113. package/dist/renderables/status.js +15 -29
  114. package/dist/renderables/syntax.d.ts +37 -7
  115. package/dist/renderables/syntax.js +134 -83
  116. package/dist/renderables/table.d.ts +58 -20
  117. package/dist/renderables/table.js +373 -197
  118. package/dist/renderables/traceback.d.ts +5 -4
  119. package/dist/renderables/traceback.js +239 -68
  120. package/dist/renderables/tree.d.ts +10 -2
  121. package/dist/renderables/tree.js +61 -34
  122. package/dist/renderables/viewport.d.ts +30 -0
  123. package/dist/renderables/viewport.js +61 -7
  124. package/dist/template-bindings/color-funcs.d.ts +31 -9
  125. package/dist/template-bindings/color-funcs.js +40 -14
  126. package/dist/template-bindings/helpers.d.ts +4 -2
  127. package/dist/template-bindings/helpers.js +5 -20
  128. package/dist/template-bindings/index.d.ts +29 -17
  129. package/dist/template-bindings/index.js +36 -20
  130. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  131. package/dist/template-bindings/palette-funcs.js +11 -7
  132. package/dist/template-bindings/style-funcs.js +4 -2
  133. package/dist/themes/buildPalette.d.ts +24 -3
  134. package/dist/themes/buildPalette.js +52 -8
  135. package/dist/themes/colorMath.d.ts +29 -17
  136. package/dist/themes/colorMath.js +74 -48
  137. package/dist/themes/colorRef.d.ts +2 -17
  138. package/dist/themes/colorRef.js +13 -31
  139. package/dist/themes/palette.d.ts +24 -0
  140. package/dist/themes/palette.js +46 -3
  141. package/dist/themes/ramp.d.ts +21 -34
  142. package/dist/themes/ramp.js +23 -42
  143. package/dist/themes/registry.js +39 -28
  144. package/dist/widgets/button.d.ts +2 -6
  145. package/dist/widgets/button.js +9 -40
  146. package/dist/widgets/checkbox.d.ts +2 -5
  147. package/dist/widgets/checkbox.js +9 -31
  148. package/dist/widgets/dropdown.d.ts +2 -5
  149. package/dist/widgets/dropdown.js +17 -51
  150. package/dist/widgets/event-router.d.ts +31 -40
  151. package/dist/widgets/event-router.js +52 -483
  152. package/dist/widgets/index.d.ts +2 -1
  153. package/dist/widgets/ink.d.ts +22 -0
  154. package/dist/widgets/ink.js +31 -0
  155. package/dist/widgets/slider.d.ts +2 -5
  156. package/dist/widgets/slider.js +11 -34
  157. package/dist/widgets/text-input.d.ts +19 -10
  158. package/dist/widgets/text-input.js +156 -147
  159. package/dist/widgets/themed-widget.d.ts +20 -0
  160. package/dist/widgets/themed-widget.js +89 -0
  161. package/dist/widgets/toggle.d.ts +2 -5
  162. package/dist/widgets/toggle.js +8 -31
  163. package/dist/widgets/types.d.ts +8 -25
  164. package/dist/widgets/types.js +8 -0
  165. package/dist/widgets/widget-app.d.ts +24 -7
  166. package/dist/widgets/widget-app.js +35 -18
  167. package/dist/widgets/widget-base.d.ts +2 -0
  168. package/dist/widgets/widget-base.js +9 -2
  169. package/package.json +31 -17
@@ -22,10 +22,15 @@
22
22
  * [LAW:types-are-the-program] Three integer spaces coexist: `CellCol` (terminal
23
23
  * cell columns), `CodeUnit` (JS UTF-16 indices), and `CodePoint` (code-unit
24
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
25
+ * `CodeUnit`). `cursorPosition` is `CodePoint`, and stronger: its setter
26
+ * parses any offset to a grapheme-cluster boundary. Visual positions are `CellCol`. The three are never
27
27
  * interchangeable — the type system enforces this at every crossing point.
28
28
  *
29
+ * [LAW:one-source-of-truth] The cursor moves, deletes and draws one grapheme
30
+ * cluster at a time (`nextGrapheme` / `prevGrapheme`) — the unit `cellLen`
31
+ * measures — so `👍🏽` is one step and one cursor cell, never a glyph and a
32
+ * stray skin-tone swatch.
33
+ *
29
34
  * Keymap (readline / emacs compatible):
30
35
  *
31
36
  * ─── motion ───
@@ -89,14 +94,13 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
89
94
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
90
95
  done = true;
91
96
  };
92
- import { observable, action, observableRef } from "mobx";
97
+ import { observable, action, observableRef, runInAction } from "mobx";
93
98
  import { Segment } from "../core/segment.js";
94
99
  import { Style } from "../core/style.js";
95
- import { ColorSpec } from "../core/color.js";
96
- import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, nextCodePoint, prevCodePoint, } from "../core/cells.js";
97
- import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
100
+ import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
98
101
  import { drawable } from "../core/protocol.js";
99
- import { WidgetBase } from "./widget-base.js";
102
+ import { ThemedWidget } from "./themed-widget.js";
103
+ import { ink } from "./ink.js";
100
104
  /**
101
105
  * Character-greedy soft wrap. Breaks at any character once `firstWidth`
102
106
  * (or `continuationWidth` for continuation rows) cells are exhausted. The
@@ -131,11 +135,18 @@ const NEWLINE_GLYPH = "↵";
131
135
  const ASCII_NEWLINE_GLYPH = "~";
132
136
  const MASK_GLYPH = "•";
133
137
  const ASCII_MASK_GLYPH = "*";
138
+ // Text a key may insert: anything but the C0 controls, DEL and the C1
139
+ // controls, which a terminal reads as escape sequences (U+009B is CSI).
140
+ const PRINTABLE = /^[^\x00-\x1f\x7f-\x9f]+$/u;
134
141
  // Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
135
142
  // readline default (alphanumeric + underscore), which is what users typing
136
143
  // shell commands or code identifiers expect.
137
144
  const WORD_CHAR_RE = /[A-Za-z0-9_]/;
138
145
  const WHITESPACE_RE = /\s/;
146
+ /** CRLF and lone CR as LF, the line break `value` holds. */
147
+ function toLf(text) {
148
+ return text.replace(/\r\n?/g, "\n");
149
+ }
139
150
  function isWordChar(c) {
140
151
  return c !== undefined && WORD_CHAR_RE.test(c);
141
152
  }
@@ -143,14 +154,14 @@ function isWhitespace(c) {
143
154
  return c !== undefined && WHITESPACE_RE.test(c);
144
155
  }
145
156
  let TextInput = (() => {
146
- let _classSuper = WidgetBase;
157
+ let _classSuper = ThemedWidget;
147
158
  let _instanceExtraInitializers = [];
148
- let _value_decorators;
149
- let _value_initializers = [];
150
- let _value_extraInitializers = [];
151
- let _cursorPosition_decorators;
152
- let _cursorPosition_initializers = [];
153
- let _cursorPosition_extraInitializers = [];
159
+ let __value_decorators;
160
+ let __value_initializers = [];
161
+ let __value_extraInitializers = [];
162
+ let __cursor_decorators;
163
+ let __cursor_initializers = [];
164
+ let __cursor_extraInitializers = [];
154
165
  let _placeholder_decorators;
155
166
  let _placeholder_initializers = [];
156
167
  let _placeholder_extraInitializers = [];
@@ -188,8 +199,8 @@ let TextInput = (() => {
188
199
  return class TextInput extends _classSuper {
189
200
  static {
190
201
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
191
- _value_decorators = [observable];
192
- _cursorPosition_decorators = [observable];
202
+ __value_decorators = [observable];
203
+ __cursor_decorators = [observable];
193
204
  _placeholder_decorators = [observableRef];
194
205
  _indicatorStyleOverride_decorators = [observableRef];
195
206
  _cursorStyleOverride_decorators = [observableRef];
@@ -216,8 +227,8 @@ let TextInput = (() => {
216
227
  _yank_decorators = [action];
217
228
  _transposeChars_decorators = [action];
218
229
  __insertText_decorators = [action];
219
- __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);
220
- __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);
230
+ __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);
231
+ __esDecorate(this, null, __cursor_decorators, { kind: "accessor", name: "_cursor", static: false, private: false, access: { has: obj => "_cursor" in obj, get: obj => obj._cursor, set: (obj, value) => { obj._cursor = value; } }, metadata: _metadata }, __cursor_initializers, __cursor_extraInitializers);
221
232
  __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);
222
233
  __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);
223
234
  __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);
@@ -248,17 +259,31 @@ let TextInput = (() => {
248
259
  }
249
260
  id = __runInitializers(this, _instanceExtraInitializers);
250
261
  focusable = true;
251
- #value_accessor_storage = __runInitializers(this, _value_initializers, void 0);
252
- get value() { return this.#value_accessor_storage; }
253
- set value(value) { this.#value_accessor_storage = value; }
254
- #cursorPosition_accessor_storage = (__runInitializers(this, _value_extraInitializers), __runInitializers(this, _cursorPosition_initializers, void 0));
255
- get cursorPosition() { return this.#cursorPosition_accessor_storage; }
256
- set cursorPosition(value) { this.#cursorPosition_accessor_storage = value; }
257
- #placeholder_accessor_storage = (__runInitializers(this, _cursorPosition_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
262
+ #_value_accessor_storage = __runInitializers(this, __value_initializers, void 0);
263
+ get _value() { return this.#_value_accessor_storage; }
264
+ set _value(value) { this.#_value_accessor_storage = value; }
265
+ #_cursor_accessor_storage = (__runInitializers(this, __value_extraInitializers), __runInitializers(this, __cursor_initializers, void 0));
266
+ get _cursor() { return this.#_cursor_accessor_storage; }
267
+ set _cursor(value) { this.#_cursor_accessor_storage = value; }
268
+ // [LAW:single-enforcer] Every write to either half of the pair passes
269
+ // `graphemeBoundary`, so the cursor is a cluster boundary inside `value`
270
+ // whoever set it last — a caller replacing `value`, a click, an insert that
271
+ // grew the cluster before it. A line break is `\n`, as in an HTML
272
+ // textarea's value: `\r\n` is one cluster, and a cursor could not rest at
273
+ // its line's end.
274
+ get value() { return this._value; }
275
+ set value(v) {
276
+ runInAction(() => {
277
+ this._value = toLf(v);
278
+ this._cursor = graphemeBoundary(this._value, this._cursor);
279
+ });
280
+ }
281
+ get cursorPosition() { return this._cursor; }
282
+ set cursorPosition(p) { this._cursor = graphemeBoundary(this._value, p); }
283
+ #placeholder_accessor_storage = (__runInitializers(this, __cursor_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
258
284
  get placeholder() { return this.#placeholder_accessor_storage; }
259
285
  set placeholder(value) { this.#placeholder_accessor_storage = value; }
260
- _theme = __runInitializers(this, _placeholder_extraInitializers);
261
- _maxLength;
286
+ _maxLength = __runInitializers(this, _placeholder_extraInitializers);
262
287
  _password;
263
288
  _multiline;
264
289
  _wrap;
@@ -311,11 +336,12 @@ let TextInput = (() => {
311
336
  // single-line renderer. Captured by _renderSingleLine and read by
312
337
  // handleMouse so the hit-test uses the same visual origin the user sees.
313
338
  _singleLineViewportStart = asCellCol(0);
339
+ _singleLineAsciiOnly = false;
314
340
  multiline;
315
341
  constructor(options = {}) {
316
- super();
342
+ super(options.theme);
317
343
  this.id = options.id ?? `text-input-${Math.random().toString(36).slice(2, 8)}`;
318
- this.value = options.value ?? "";
344
+ this._value = toLf(options.value ?? "");
319
345
  this.placeholder = options.placeholder ?? "";
320
346
  // [LAW:dataflow-not-control-flow] Initial cursor position is data, not
321
347
  // a render-time branch. Single-line inputs follow the pre-filled-form
@@ -323,9 +349,8 @@ let TextInput = (() => {
323
349
  // inputs follow the textarea convention (cursor at start — pre-loaded
324
350
  // content is read top-to-bottom, and the viewport's "scroll into view"
325
351
  // logic does nothing because row 0 is already inside the initial window).
326
- this.cursorPosition = (options.multiline ?? false) ? asCodePoint(0) : asCodePoint(this.value.length);
352
+ this._cursor = graphemeBoundary(this._value, asCodeUnit((options.multiline ?? false) ? 0 : this._value.length));
327
353
  this.disabled = options.disabled ?? false;
328
- this._theme = options.theme ?? DEFAULT_TERMINAL_THEME;
329
354
  this._maxLength = options.maxLength;
330
355
  this._password = options.password ?? false;
331
356
  this._multiline = options.multiline ?? false;
@@ -340,7 +365,6 @@ let TextInput = (() => {
340
365
  this.contentStyleOverride = options.contentStyle;
341
366
  this.multiline = this._multiline;
342
367
  }
343
- setTheme(theme) { this._theme = theme; }
344
368
  // --- Event handlers ---
345
369
  handleKey(event) {
346
370
  if (this.disabled)
@@ -499,14 +523,12 @@ let TextInput = (() => {
499
523
  }
500
524
  }
501
525
  // ─── printable insertion ───
502
- // A single-char `event.character` with no command modifiers is text to
526
+ // A non-control `event.character` with no command modifiers is text to
503
527
  // insert. Ctrl/meta-modified keys produce empty `character` from the
504
- // router (see event-router.ts) so they never reach this branch.
505
- if (event.character.length === 1 &&
506
- !event.ctrl &&
507
- !event.meta &&
508
- event.character >= " " &&
509
- event.character !== "\x7f") {
528
+ // router (see event-router.ts) so they never reach this branch. The router
529
+ // sends one code point per key, so `👍🏽` arrives as `👍` then `🏽`, and an
530
+ // astral code point is two UTF-16 units — a `.length === 1` test drops it.
531
+ if (PRINTABLE.test(event.character) && !event.ctrl && !event.meta) {
510
532
  this._insertText(event.character);
511
533
  event.stop();
512
534
  }
@@ -525,7 +547,7 @@ let TextInput = (() => {
525
547
  const rawRowIdx = this._scrollStart + event.y;
526
548
  // Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
527
549
  if (rawRowIdx >= rows.length) {
528
- this.cursorPosition = asCodePoint(this.value.length);
550
+ this.cursorPosition = asCodeUnit(this.value.length);
529
551
  this._preferredColumn = null;
530
552
  return;
531
553
  }
@@ -534,13 +556,7 @@ let TextInput = (() => {
534
556
  // Continuation rows start after the marker; non-continuation rows start at column 0.
535
557
  const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
536
558
  const relX = asCellCol(Math.max(0, event.x - contentXOff));
537
- const pos = cellColToCodeUnitOffset(row.content, relX);
538
- const absPos = row.valueStart + pos;
539
- const chu = absPos < this.value.length ? this.value.charCodeAt(absPos) : 0;
540
- const prevChu = absPos > 0 ? this.value.charCodeAt(absPos - 1) : 0;
541
- this.cursorPosition = asCodePoint(chu >= 0xDC00 && chu <= 0xDFFF && prevChu >= 0xD800 && prevChu <= 0xDBFF
542
- ? absPos + 1
543
- : absPos);
559
+ this.cursorPosition = asCodeUnit(row.valueStart + cellColToCodeUnitOffset(row.content, relX));
544
560
  }
545
561
  else {
546
562
  // [LAW:types-are-the-program] relX is a cell-column offset into the full
@@ -548,20 +564,19 @@ let TextInput = (() => {
548
564
  // by _renderSingleLine) so a click on a scrolled viewport maps to the
549
565
  // correct code-unit in the underlying value.
550
566
  const relX = asCellCol(Math.max(0, event.x - 1) + this._singleLineViewportStart);
551
- const displayForHitTest = this._password
552
- ? MASK_GLYPH.repeat(this.value.length)
553
- : this.value.indexOf("\n") >= 0
554
- ? this.value.replace(/\n/g, NEWLINE_GLYPH)
555
- : this.value;
556
- const pos = cellColToCodeUnitOffset(displayForHitTest, relX);
557
- // Snap forward only when pos lands on a valid surrogate pair, never on a
558
- // lone low surrogate (malformed string). Verify the preceding code unit is
559
- // the high-surrogate half before advancing.
560
- const chu = pos < this.value.length ? this.value.charCodeAt(pos) : 0;
561
- const prevChu = pos > 0 ? this.value.charCodeAt(pos - 1) : 0;
562
- this.cursorPosition = asCodePoint(chu >= 0xDC00 && chu <= 0xDFFF && prevChu >= 0xD800 && prevChu <= 0xDBFF
563
- ? pos + 1
564
- : pos);
567
+ // The glyphs of `value` whose shown cells lie wholly left of the click,
568
+ // shown as the last render showed them.
569
+ const drawn = { asciiOnly: this._singleLineAsciiOnly };
570
+ let at = 0;
571
+ let col = 0;
572
+ for (const g of graphemes(this.value)) {
573
+ const w = cellLen(this._shown(g, drawn));
574
+ if (col + w > relX)
575
+ break;
576
+ col += w;
577
+ at += g.length;
578
+ }
579
+ this.cursorPosition = asCodeUnit(at);
565
580
  }
566
581
  this._preferredColumn = null;
567
582
  }
@@ -569,11 +584,11 @@ let TextInput = (() => {
569
584
  setHovered(value) { this.hovered = value; }
570
585
  // ─── Public motion primitives ───────────────────────────────────────────
571
586
  moveCharLeft() {
572
- this.cursorPosition = prevCodePoint(this.value, this.cursorPosition);
587
+ this.cursorPosition = prevGrapheme(this.value, this.cursorPosition);
573
588
  this._preferredColumn = null;
574
589
  }
575
590
  moveCharRight() {
576
- this.cursorPosition = nextCodePoint(this.value, this.cursorPosition);
591
+ this.cursorPosition = nextGrapheme(this.value, this.cursorPosition);
577
592
  this._preferredColumn = null;
578
593
  }
579
594
  moveLineUp() {
@@ -587,7 +602,7 @@ let TextInput = (() => {
587
602
  return;
588
603
  const col = this._preferredColumn ?? this._cursorVisualCol();
589
604
  const target = this._visualRows[rowIdx - 1];
590
- this.cursorPosition = asCodePoint(target.valueStart + this._clampColForRow(col, rowIdx - 1));
605
+ this.cursorPosition = asCodeUnit(target.valueStart + this._clampColForRow(col, rowIdx - 1));
591
606
  this._preferredColumn = col;
592
607
  return;
593
608
  }
@@ -601,7 +616,7 @@ let TextInput = (() => {
601
616
  while (prevLineStart > 0 && this.value[prevLineStart - 1] !== "\n")
602
617
  prevLineStart--;
603
618
  const prevLineContent = this.value.slice(prevLineStart, prevLineEnd);
604
- this.cursorPosition = asCodePoint(prevLineStart + cellColToCodeUnitOffset(prevLineContent, asCellCol(Math.min(col, cellLen(prevLineContent)))));
619
+ this.cursorPosition = asCodeUnit(prevLineStart + cellColToCodeUnitOffset(prevLineContent, asCellCol(Math.min(col, cellLen(prevLineContent)))));
605
620
  this._preferredColumn = col;
606
621
  }
607
622
  moveLineDown() {
@@ -610,7 +625,7 @@ let TextInput = (() => {
610
625
  if (rowIdx === this._visualRows.length - 1)
611
626
  return;
612
627
  const col = this._preferredColumn ?? this._cursorVisualCol();
613
- this.cursorPosition = asCodePoint(this._visualRows[rowIdx + 1].valueStart + this._clampColForRow(col, rowIdx + 1));
628
+ this.cursorPosition = asCodeUnit(this._visualRows[rowIdx + 1].valueStart + this._clampColForRow(col, rowIdx + 1));
614
629
  this._preferredColumn = col;
615
630
  return;
616
631
  }
@@ -625,14 +640,14 @@ let TextInput = (() => {
625
640
  while (nextLineEnd < this.value.length && this.value[nextLineEnd] !== "\n")
626
641
  nextLineEnd++;
627
642
  const nextLineContent = this.value.slice(nextLineStart, nextLineEnd);
628
- this.cursorPosition = asCodePoint(nextLineStart + cellColToCodeUnitOffset(nextLineContent, asCellCol(Math.min(col, cellLen(nextLineContent)))));
643
+ this.cursorPosition = asCodeUnit(nextLineStart + cellColToCodeUnitOffset(nextLineContent, asCellCol(Math.min(col, cellLen(nextLineContent)))));
629
644
  this._preferredColumn = col;
630
645
  }
631
646
  // Clamp `col` (a cell column) to the target visual row's content, returning
632
647
  // the code-unit offset within that row. When the row IS followed by a
633
648
  // continuation of the same logical line, we clamp to the last valid
634
- // code-point boundary strictly inside the row (which may be more than
635
- // one cell before the visual end when the last code point is wide) so the
649
+ // cluster boundary strictly inside the row (which may be more than
650
+ // one cell before the visual end when the last glyph is wide) so the
636
651
  // cursor stays strictly inside the target row — landing at the boundary
637
652
  // would cause `_cursorVisualRow` to resolve to the *later* row, making
638
653
  // every subsequent Up/Down stick at the boundary.
@@ -679,23 +694,23 @@ let TextInput = (() => {
679
694
  this._preferredColumn = null;
680
695
  }
681
696
  moveDocStart() {
682
- this.cursorPosition = asCodePoint(0);
697
+ this.cursorPosition = asCodeUnit(0);
683
698
  this._preferredColumn = null;
684
699
  }
685
700
  moveDocEnd() {
686
- this.cursorPosition = asCodePoint(this.value.length);
701
+ this.cursorPosition = asCodeUnit(this.value.length);
687
702
  this._preferredColumn = null;
688
703
  }
689
704
  moveWordLeft() {
690
705
  let p = this.cursorPosition;
691
706
  while (p > 0) {
692
- const prev = prevCodePoint(this.value, p);
707
+ const prev = prevGrapheme(this.value, p);
693
708
  if (isWordChar(this.value.slice(prev, p)))
694
709
  break;
695
710
  p = prev;
696
711
  }
697
712
  while (p > 0) {
698
- const prev = prevCodePoint(this.value, p);
713
+ const prev = prevGrapheme(this.value, p);
699
714
  if (!isWordChar(this.value.slice(prev, p)))
700
715
  break;
701
716
  p = prev;
@@ -706,13 +721,13 @@ let TextInput = (() => {
706
721
  moveWordRight() {
707
722
  let p = this.cursorPosition;
708
723
  while (p < this.value.length) {
709
- const next = nextCodePoint(this.value, p);
724
+ const next = nextGrapheme(this.value, p);
710
725
  if (isWordChar(this.value.slice(p, next)))
711
726
  break;
712
727
  p = next;
713
728
  }
714
729
  while (p < this.value.length) {
715
- const next = nextCodePoint(this.value, p);
730
+ const next = nextGrapheme(this.value, p);
716
731
  if (!isWordChar(this.value.slice(p, next)))
717
732
  break;
718
733
  p = next;
@@ -724,7 +739,7 @@ let TextInput = (() => {
724
739
  deleteCharBack() {
725
740
  if (this.cursorPosition === 0)
726
741
  return;
727
- const newPos = prevCodePoint(this.value, this.cursorPosition);
742
+ const newPos = prevGrapheme(this.value, this.cursorPosition);
728
743
  this.value = this.value.slice(0, newPos) + this.value.slice(this.cursorPosition);
729
744
  this.cursorPosition = newPos;
730
745
  this._preferredColumn = null;
@@ -733,7 +748,7 @@ let TextInput = (() => {
733
748
  deleteCharForward() {
734
749
  if (this.cursorPosition >= this.value.length)
735
750
  return;
736
- const nextPos = nextCodePoint(this.value, this.cursorPosition);
751
+ const nextPos = nextGrapheme(this.value, this.cursorPosition);
737
752
  this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(nextPos);
738
753
  this._preferredColumn = null;
739
754
  this.emitChange();
@@ -744,13 +759,13 @@ let TextInput = (() => {
744
759
  // → "foo |" → "|" rather than getting stuck on the trailing space.
745
760
  let p = this.cursorPosition;
746
761
  while (p > 0) {
747
- const prev = prevCodePoint(this.value, p);
762
+ const prev = prevGrapheme(this.value, p);
748
763
  if (!isWhitespace(this.value.slice(prev, p)))
749
764
  break;
750
765
  p = prev;
751
766
  }
752
767
  while (p > 0) {
753
- const prev = prevCodePoint(this.value, p);
768
+ const prev = prevGrapheme(this.value, p);
754
769
  if (isWhitespace(this.value.slice(prev, p)))
755
770
  break;
756
771
  p = prev;
@@ -766,13 +781,13 @@ let TextInput = (() => {
766
781
  deleteWordForward() {
767
782
  let p = this.cursorPosition;
768
783
  while (p < this.value.length) {
769
- const next = nextCodePoint(this.value, p);
784
+ const next = nextGrapheme(this.value, p);
770
785
  if (isWordChar(this.value.slice(p, next)))
771
786
  break;
772
787
  p = next;
773
788
  }
774
789
  while (p < this.value.length) {
775
- const next = nextCodePoint(this.value, p);
790
+ const next = nextGrapheme(this.value, p);
776
791
  if (!isWordChar(this.value.slice(p, next)))
777
792
  break;
778
793
  p = next;
@@ -820,26 +835,28 @@ let TextInput = (() => {
820
835
  // Readline `transpose-chars`: swap the char before the cursor with the
821
836
  // char at the cursor and advance. At end-of-value, swap the trailing
822
837
  // two chars without advancing. At position 0, no-op.
823
- const len = this.value.length;
824
- if (len < 2 || this.cursorPosition === 0)
838
+ if (this.cursorPosition === 0)
825
839
  return;
826
840
  const p = this.cursorPosition;
827
- if (p === len) {
828
- // End-of-value: swap the trailing two code points; cursor stays.
829
- const cp2 = prevCodePoint(this.value, p);
830
- const cp1 = prevCodePoint(this.value, cp2);
841
+ if (p === this.value.length) {
842
+ // End-of-value: swap the trailing two glyphs; cursor stays. One glyph
843
+ // has nothing to swap with.
844
+ const cp2 = prevGrapheme(this.value, p);
845
+ if (cp2 === 0)
846
+ return;
847
+ const cp1 = prevGrapheme(this.value, cp2);
831
848
  this.value = this.value.slice(0, cp1) + this.value.slice(cp2) + this.value.slice(cp1, cp2);
832
849
  this._preferredColumn = null;
833
850
  this.emitChange();
834
851
  return;
835
852
  }
836
- // Normal case: find full code-point substrings for the char before and at cursor.
837
- const cpBefore = prevCodePoint(this.value, p);
838
- const cpAfter = nextCodePoint(this.value, p);
853
+ // Normal case: the whole glyphs before and at the cursor.
854
+ const cpBefore = prevGrapheme(this.value, p);
855
+ const cpAfter = nextGrapheme(this.value, p);
839
856
  const charBefore = this.value.slice(cpBefore, p);
840
857
  const charAt = this.value.slice(p, cpAfter);
841
858
  this.value = this.value.slice(0, cpBefore) + charAt + charBefore + this.value.slice(cpAfter);
842
- this.cursorPosition = asCodePoint(cpAfter);
859
+ this.cursorPosition = cpAfter;
843
860
  this._preferredColumn = null;
844
861
  this.emitChange();
845
862
  }
@@ -860,42 +877,58 @@ let TextInput = (() => {
860
877
  let toInsert = text;
861
878
  if (this._maxLength !== undefined) {
862
879
  const room = this._maxLength - this.value.length;
863
- if (room <= 0)
864
- return;
865
- if (toInsert.length > room)
866
- toInsert = toInsert.slice(0, room);
880
+ // Cut between glyphs: the longest run of whole clusters within `room`.
881
+ let fit = 0;
882
+ for (const g of graphemes(toInsert)) {
883
+ if (fit + g.length > room)
884
+ break;
885
+ fit += g.length;
886
+ }
887
+ toInsert = toInsert.slice(0, fit);
867
888
  }
889
+ if (toInsert.length === 0)
890
+ return;
891
+ // The end is taken before the write: the value setter has already moved
892
+ // the cursor past a cluster the insert grew.
893
+ const end = asCodeUnit(this.cursorPosition + toInsert.length);
868
894
  this.value =
869
895
  this.value.slice(0, this.cursorPosition) +
870
896
  toInsert +
871
897
  this.value.slice(this.cursorPosition);
872
- this.cursorPosition = asCodePoint(this.cursorPosition + toInsert.length);
898
+ this.cursorPosition = end;
873
899
  this._preferredColumn = null;
874
900
  this.emitChange();
875
901
  }
876
902
  // --- Rendering ---
903
+ // [LAW:one-source-of-truth] The style a value is drawn in is also what a
904
+ // short row is padded with, so a caller's `contentStyle` fills the row.
905
+ get ground() {
906
+ return this.disabled
907
+ ? new Style(this.disabledInk)
908
+ : this.contentStyleOverride ?? super.ground;
909
+ }
877
910
  draw(options) {
878
911
  if (this._multiline)
879
912
  return this._renderMultiline(options);
880
913
  return this._renderSingleLine(options);
881
914
  }
915
+ // [LAW:dataflow-not-control-flow] How a stretch of `value` is shown in one
916
+ // line derives from mode: one mask cell per glyph, or `\n` as a visible
917
+ // glyph. The render and the click hit-test both read it.
918
+ _shown(text, options) {
919
+ return this._password
920
+ ? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(graphemes(text).length)
921
+ : text.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH));
922
+ }
882
923
  _renderSingleLine(options) {
883
924
  const showPlaceholder = this.focused && this.value.length === 0 && this.placeholder.length > 0;
884
- // [LAW:dataflow-not-control-flow] Single rawDisplay value derives from
885
- // mode (password / placeholder / plain). NEWLINE_GLYPH substitution maps
886
- // every code unit in value 1:1 to rawDisplay so cursorPosition (a code-
887
- // unit index) is a valid index into rawDisplay regardless of mode.
888
- const rawDisplay = showPlaceholder
889
- ? this.placeholder
890
- : this._password
891
- ? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(this.value.length)
892
- : this.value.indexOf("\n") >= 0
893
- ? this.value.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH))
894
- : this.value;
895
- // Measure in cell space — cursorPosition is a code-unit index; convert to
896
- // cell column for all viewport arithmetic.
925
+ // The cursor's column is the width of the shown text before it, whatever
926
+ // the mode — a placeholder is shown only over an empty value.
927
+ this._singleLineAsciiOnly = options.asciiOnly ?? false;
928
+ const shown = (text) => this._shown(text, options);
929
+ const rawDisplay = showPlaceholder ? this.placeholder : shown(this.value);
897
930
  const rawCellWidth = asCellCol(cellLen(rawDisplay));
898
- const cursorCellCol = asCellCol(cellLen(rawDisplay.slice(0, this.cursorPosition)));
931
+ const cursorCellCol = asCellCol(cellLen(shown(this.value.slice(0, this.cursorPosition))));
899
932
  const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, options.maxWidth - 2));
900
933
  const desiredWidth = asCellCol(Math.max(MIN_CONTENT_WIDTH, rawCellWidth, cursorCellCol + 1));
901
934
  const contentWidth = asCellCol(Math.min(maxAvailable, desiredWidth));
@@ -910,25 +943,16 @@ let TextInput = (() => {
910
943
  const display = setCellSize(visible, contentWidth);
911
944
  const cursorDisplayCellCol = asCellCol(cursorCellCol - actualStartCell);
912
945
  const bracketStyle = this.disabled
913
- ? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
914
- : new Style({ color: this.resolvePalette("foreground") });
915
- const contentStyle = this.disabled
916
- ? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
917
- : showPlaceholder
918
- ? new Style({ color: this.resolvePalette("foreground"), dim: true })
919
- : this.contentStyleOverride ?? new Style({ color: this.resolvePalette("foreground") });
920
- const cursorStyle = this.cursorStyleOverride ?? new Style({
921
- color: this.resolvePalette("on-primary"),
922
- bgcolor: this.resolvePalette("primary"),
923
- });
946
+ ? new Style(this.disabledInk)
947
+ : new Style(ink(this.theme, "foreground", "background"));
948
+ const contentStyle = showPlaceholder && !this.disabled
949
+ ? new Style({ ...ink(this.theme, "foreground", "background"), dim: true })
950
+ : this.ground;
951
+ const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
924
952
  const segments = [new Segment("[", bracketStyle)];
925
953
  if (this.focused && !this.disabled && cursorDisplayCellCol >= 0 && cursorDisplayCellCol < contentWidth) {
926
954
  const [before, rest] = splitText(display, cursorDisplayCellCol);
927
- let firstCh = "";
928
- for (const ch of rest) {
929
- firstCh = ch;
930
- break;
931
- }
955
+ const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
932
956
  const at = firstCh || " ";
933
957
  const after = rest.slice(firstCh.length);
934
958
  if (before.length > 0)
@@ -981,14 +1005,9 @@ let TextInput = (() => {
981
1005
  if (this._minRows !== undefined && total < this._minRows && this._maxRows === undefined) {
982
1006
  padRows = this._minRows - total;
983
1007
  }
984
- const contentStyle = this.disabled
985
- ? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
986
- : this.contentStyleOverride ?? new Style({ color: this.resolvePalette("foreground") });
987
- const markerStyle = new Style({ color: this.resolvePalette("foreground"), dim: true });
988
- const cursorStyle = this.cursorStyleOverride ?? new Style({
989
- color: this.resolvePalette("on-primary"),
990
- bgcolor: this.resolvePalette("primary"),
991
- });
1008
+ const contentStyle = this.ground;
1009
+ const markerStyle = new Style({ ...ink(this.theme, "foreground", "background"), dim: true });
1010
+ const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
992
1011
  // Scroll-direction arrows appear in the rightmost cell of the first/last
993
1012
  // visible row, *only* when scroll is actually possible in that direction.
994
1013
  // _emitRowContent reserves the indicator's cells so content never
@@ -999,7 +1018,7 @@ let TextInput = (() => {
999
1018
  const arrowsMode = this._scrollIndicator === "arrows";
1000
1019
  const canScrollUp = arrowsMode && scrollable && this._scrollStart > 0;
1001
1020
  const canScrollDown = arrowsMode && scrollable && this._scrollStart + this._maxRows < total;
1002
- const indicatorStyle = this.indicatorStyleOverride ?? new Style({ color: this.resolvePalette("primary") });
1021
+ const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
1003
1022
  const segments = [];
1004
1023
  const showCursor = this.focused && !this.disabled;
1005
1024
  for (let i = 0; i < visibleCount; i++) {
@@ -1051,7 +1070,7 @@ let TextInput = (() => {
1051
1070
  return;
1052
1071
  }
1053
1072
  const before = content.slice(0, cursorCol);
1054
- const nextCp = nextCodePoint(content, asCodePoint(cursorCol));
1073
+ const nextCp = nextGrapheme(content, asCodePoint(cursorCol));
1055
1074
  const at = content.slice(cursorCol, nextCp) || " ";
1056
1075
  const after = content.slice(nextCp);
1057
1076
  if (before.length > 0)
@@ -1075,11 +1094,7 @@ let TextInput = (() => {
1075
1094
  : -1;
1076
1095
  if (cursorCellColInRow >= 0 && cursorCellColInRow < contentCellWidth) {
1077
1096
  const [before, rest] = splitText(paddedContent, asCellCol(cursorCellColInRow));
1078
- let firstCh = "";
1079
- for (const ch of rest) {
1080
- firstCh = ch;
1081
- break;
1082
- }
1097
+ const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
1083
1098
  const at = firstCh || " ";
1084
1099
  const after = rest.slice(firstCh.length);
1085
1100
  if (before.length > 0)
@@ -1154,12 +1169,6 @@ let TextInput = (() => {
1154
1169
  const cursorRow1 = this._cursorVisualRow() + 1;
1155
1170
  return `[${cursorRow1}/${rows.length}]`;
1156
1171
  }
1157
- // --- Palette resolution ---
1158
- resolvePalette(key) {
1159
- const rgba = this._theme.palette.get(key);
1160
- // [LAW:no-defensive-null-guards] palette must contain all keys.
1161
- return ColorSpec.fromRgba(rgba);
1162
- }
1163
1172
  };
1164
1173
  })();
1165
1174
  export { TextInput };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ThemedWidget — a widget that draws every cell it owns from one theme's
3
+ * palette: the interactive widgets, as against `StaticItem`, which shows
4
+ * content it did not draw.
5
+ *
6
+ * [LAW:one-type-per-behavior] The theme, how it is swapped, what a disabled
7
+ * widget looks like, and the ground a padded row stands on are the same for
8
+ * every such widget, so they live here once.
9
+ */
10
+ import { Style } from "../core/style.js";
11
+ import type { TerminalTheme } from "../core/color.js";
12
+ import { WidgetBase } from "./widget-base.js";
13
+ import { ink } from "./ink.js";
14
+ export declare abstract class ThemedWidget extends WidgetBase {
15
+ protected accessor theme: TerminalTheme;
16
+ constructor(theme: TerminalTheme | undefined);
17
+ setTheme(theme: TerminalTheme): void;
18
+ protected get ground(): Style;
19
+ protected get disabledInk(): ReturnType<typeof ink>;
20
+ }