@promptctl/rich-js 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -26
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +0 -1
- package/dist/core/box.js +6 -18
- package/dist/core/cells.d.ts +16 -5
- package/dist/core/cells.js +31 -10
- package/dist/core/color.d.ts +4 -8
- package/dist/core/color.js +28 -19
- package/dist/core/console.d.ts +13 -4
- package/dist/core/console.js +91 -36
- package/dist/core/export-html.d.ts +40 -5
- package/dist/core/export-html.js +111 -17
- package/dist/core/markup.d.ts +27 -5
- package/dist/core/markup.js +282 -170
- package/dist/core/measure.d.ts +1 -1
- package/dist/core/measure.js +2 -1
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +3 -0
- package/dist/core/pretty.js +5 -1
- package/dist/core/protocol.d.ts +136 -6
- package/dist/core/protocol.js +64 -2
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +2 -2
- package/dist/core/segment.d.ts +23 -4
- package/dist/core/segment.js +75 -16
- package/dist/core/spinnerData.d.ts +2 -0
- package/dist/core/spinnerData.js +3 -1
- package/dist/core/strip.d.ts +3 -7
- package/dist/core/strip.js +32 -35
- package/dist/core/style.d.ts +25 -0
- package/dist/core/style.js +61 -89
- package/dist/core/text.d.ts +16 -15
- package/dist/core/text.js +62 -76
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +4 -2
- package/dist/host/index.js +2 -1
- package/dist/host/terminal-host.d.ts +43 -7
- package/dist/host/terminal-host.js +57 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/node/terminal-host.d.ts +31 -2
- package/dist/node/terminal-host.js +69 -2
- package/dist/node/traceback.d.ts +16 -6
- package/dist/node/traceback.js +30 -8
- package/dist/renderables/align.d.ts +2 -2
- package/dist/renderables/align.js +6 -19
- package/dist/renderables/columns.js +4 -20
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/group.js +3 -1
- package/dist/renderables/layout.js +31 -34
- package/dist/renderables/live.d.ts +6 -3
- package/dist/renderables/live.js +89 -45
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.js +6 -4
- package/dist/renderables/padding.js +3 -2
- package/dist/renderables/panel.js +26 -41
- package/dist/renderables/progress.js +5 -8
- package/dist/renderables/progressBar.js +6 -5
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +9 -9
- package/dist/renderables/spinner.d.ts +5 -3
- package/dist/renderables/spinner.js +14 -8
- package/dist/renderables/syntax.d.ts +1 -1
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +26 -14
- package/dist/renderables/table.js +239 -141
- package/dist/renderables/traceback.js +8 -9
- package/dist/renderables/tree.d.ts +17 -3
- package/dist/renderables/tree.js +62 -71
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.js +11 -13
- package/dist/template-bindings/index.js +4 -2
- package/dist/template-bindings/palette-funcs.js +6 -16
- package/dist/template-bindings/style-funcs.js +4 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.js +19 -0
- package/dist/themes/data/atom-one-light.js +19 -0
- package/dist/themes/data/catppuccin-frappe.js +19 -0
- package/dist/themes/data/catppuccin-latte.js +19 -0
- package/dist/themes/data/catppuccin-macchiato.js +19 -0
- package/dist/themes/data/catppuccin-mocha.js +19 -0
- package/dist/themes/data/cyberpunk.js +2 -0
- package/dist/themes/data/default.js +2 -0
- package/dist/themes/data/dracula.js +19 -0
- package/dist/themes/data/flexoki.js +19 -0
- package/dist/themes/data/gruvbox.js +19 -0
- package/dist/themes/data/monokai.js +19 -0
- package/dist/themes/data/nord.js +19 -0
- package/dist/themes/data/rose-pine-dawn.js +19 -0
- package/dist/themes/data/rose-pine-moon.js +19 -0
- package/dist/themes/data/rose-pine.js +19 -0
- package/dist/themes/data/solarized-dark.js +19 -0
- package/dist/themes/data/solarized-light.js +19 -0
- package/dist/themes/data/svg-export.js +19 -0
- package/dist/themes/data/textual-ansi.js +2 -0
- package/dist/themes/data/textual-dark.js +2 -0
- package/dist/themes/data/textual-light.js +2 -0
- package/dist/themes/data/tokyo-night.js +19 -0
- package/dist/themes/data/types.d.ts +16 -0
- package/dist/themes/data/types.js +10 -1
- package/dist/themes/registry.d.ts +3 -2
- package/dist/themes/registry.js +13 -1
- package/dist/themes/terminalThemes.d.ts +2 -7
- package/dist/themes/terminalThemes.js +4 -9
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +4 -2
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +5 -3
- package/dist/widgets/dropdown.d.ts +2 -2
- package/dist/widgets/dropdown.js +13 -16
- package/dist/widgets/event-router.d.ts +21 -28
- package/dist/widgets/event-router.js +80 -73
- package/dist/widgets/focus-manager.d.ts +25 -6
- package/dist/widgets/focus-manager.js +63 -65
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +5 -7
- package/dist/widgets/index.js +3 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/slider.js +7 -7
- package/dist/widgets/static-item.d.ts +9 -15
- package/dist/widgets/static-item.js +9 -15
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +17 -14
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +3 -2
- package/dist/widgets/types.d.ts +14 -35
- package/dist/widgets/types.js +0 -1
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +4 -6
- package/dist/widgets/widget-base.js +22 -30
- package/package.json +12 -4
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
|
@@ -95,6 +95,7 @@ import { Style } from "../core/style.js";
|
|
|
95
95
|
import { ColorSpec } from "../core/color.js";
|
|
96
96
|
import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, nextCodePoint, prevCodePoint, } from "../core/cells.js";
|
|
97
97
|
import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
|
|
98
|
+
import { drawable } from "../core/protocol.js";
|
|
98
99
|
import { WidgetBase } from "./widget-base.js";
|
|
99
100
|
/**
|
|
100
101
|
* Character-greedy soft wrap. Breaks at any character once `firstWidth`
|
|
@@ -124,7 +125,12 @@ export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
|
|
|
124
125
|
return rows;
|
|
125
126
|
};
|
|
126
127
|
const MIN_CONTENT_WIDTH = 8;
|
|
128
|
+
// Each glyph and its ASCII stand-in are one code unit and one cell, so the
|
|
129
|
+
// display string maps 1:1 onto `value` and hit-testing needs no render options.
|
|
127
130
|
const NEWLINE_GLYPH = "↵";
|
|
131
|
+
const ASCII_NEWLINE_GLYPH = "~";
|
|
132
|
+
const MASK_GLYPH = "•";
|
|
133
|
+
const ASCII_MASK_GLYPH = "*";
|
|
128
134
|
// Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
|
|
129
135
|
// readline default (alphanumeric + underscore), which is what users typing
|
|
130
136
|
// shell commands or code identifiers expect.
|
|
@@ -510,17 +516,13 @@ let TextInput = (() => {
|
|
|
510
516
|
return;
|
|
511
517
|
if (event.type !== "mouse_down")
|
|
512
518
|
return;
|
|
513
|
-
const b = this.bounds;
|
|
514
|
-
if (!b)
|
|
515
|
-
return;
|
|
516
519
|
if (this._multiline) {
|
|
517
520
|
// Multiline has no brackets; rows may have a leading continuation marker.
|
|
518
521
|
// Use event.y to pick the visual row, then project event.x into its content.
|
|
519
522
|
const rows = this._visualRows;
|
|
520
523
|
if (!rows || rows.length === 0)
|
|
521
524
|
return;
|
|
522
|
-
const
|
|
523
|
-
const rawRowIdx = this._scrollStart + relY;
|
|
525
|
+
const rawRowIdx = this._scrollStart + event.y;
|
|
524
526
|
// Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
|
|
525
527
|
if (rawRowIdx >= rows.length) {
|
|
526
528
|
this.cursorPosition = asCodePoint(this.value.length);
|
|
@@ -531,7 +533,7 @@ let TextInput = (() => {
|
|
|
531
533
|
const row = rows[rowIdx];
|
|
532
534
|
// Continuation rows start after the marker; non-continuation rows start at column 0.
|
|
533
535
|
const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
|
|
534
|
-
const relX = asCellCol(Math.max(0, event.x -
|
|
536
|
+
const relX = asCellCol(Math.max(0, event.x - contentXOff));
|
|
535
537
|
const pos = cellColToCodeUnitOffset(row.content, relX);
|
|
536
538
|
const absPos = row.valueStart + pos;
|
|
537
539
|
const chu = absPos < this.value.length ? this.value.charCodeAt(absPos) : 0;
|
|
@@ -545,9 +547,9 @@ let TextInput = (() => {
|
|
|
545
547
|
// display string. Add _singleLineViewportStart (the scroll offset captured
|
|
546
548
|
// by _renderSingleLine) so a click on a scrolled viewport maps to the
|
|
547
549
|
// correct code-unit in the underlying value.
|
|
548
|
-
const relX = asCellCol(Math.max(0, event.x -
|
|
550
|
+
const relX = asCellCol(Math.max(0, event.x - 1) + this._singleLineViewportStart);
|
|
549
551
|
const displayForHitTest = this._password
|
|
550
|
-
?
|
|
552
|
+
? MASK_GLYPH.repeat(this.value.length)
|
|
551
553
|
: this.value.indexOf("\n") >= 0
|
|
552
554
|
? this.value.replace(/\n/g, NEWLINE_GLYPH)
|
|
553
555
|
: this.value;
|
|
@@ -872,7 +874,7 @@ let TextInput = (() => {
|
|
|
872
874
|
this.emitChange();
|
|
873
875
|
}
|
|
874
876
|
// --- Rendering ---
|
|
875
|
-
|
|
877
|
+
draw(options) {
|
|
876
878
|
if (this._multiline)
|
|
877
879
|
return this._renderMultiline(options);
|
|
878
880
|
return this._renderSingleLine(options);
|
|
@@ -886,9 +888,9 @@ let TextInput = (() => {
|
|
|
886
888
|
const rawDisplay = showPlaceholder
|
|
887
889
|
? this.placeholder
|
|
888
890
|
: this._password
|
|
889
|
-
?
|
|
891
|
+
? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(this.value.length)
|
|
890
892
|
: this.value.indexOf("\n") >= 0
|
|
891
|
-
? this.value.replace(/\n/g, NEWLINE_GLYPH)
|
|
893
|
+
? this.value.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH))
|
|
892
894
|
: this.value;
|
|
893
895
|
// Measure in cell space — cursorPosition is a code-unit index; convert to
|
|
894
896
|
// cell column for all viewport arithmetic.
|
|
@@ -1006,14 +1008,15 @@ let TextInput = (() => {
|
|
|
1006
1008
|
if (i > 0)
|
|
1007
1009
|
segments.push(new Segment("\n"));
|
|
1008
1010
|
if (row.isContinuation) {
|
|
1009
|
-
|
|
1011
|
+
// An ASCII stand-in as wide as the marker, so the rows keep their geometry.
|
|
1012
|
+
segments.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
|
|
1010
1013
|
}
|
|
1011
1014
|
let indicator;
|
|
1012
1015
|
if (i === 0 && canScrollUp) {
|
|
1013
|
-
indicator = { ch: "▲", style: indicatorStyle };
|
|
1016
|
+
indicator = { ch: drawable(options, "▲", "^"), style: indicatorStyle };
|
|
1014
1017
|
}
|
|
1015
1018
|
else if (i === visibleCount - 1 && canScrollDown) {
|
|
1016
|
-
indicator = { ch: "▼", style: indicatorStyle };
|
|
1019
|
+
indicator = { ch: drawable(options, "▼", "v"), style: indicatorStyle };
|
|
1017
1020
|
}
|
|
1018
1021
|
// When a continuation marker was emitted, the row's printable width is
|
|
1019
1022
|
// reduced by the marker width — the indicator column is still at
|
package/dist/widgets/toggle.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare class Toggle extends WidgetBase {
|
|
|
35
35
|
setTheme(theme: TerminalTheme): void;
|
|
36
36
|
handleKey(event: KeyEvent): void;
|
|
37
37
|
handleMouse(event: WidgetMouseEvent): void;
|
|
38
|
-
|
|
38
|
+
protected draw(_options: RenderOptions): Iterable<Segment>;
|
|
39
39
|
measure(_options: RenderOptions): {
|
|
40
40
|
minimum: number;
|
|
41
41
|
maximum: number;
|
package/dist/widgets/toggle.js
CHANGED
|
@@ -145,13 +145,14 @@ let Toggle = (() => {
|
|
|
145
145
|
handleMouse(event) {
|
|
146
146
|
if (this.disabled)
|
|
147
147
|
return;
|
|
148
|
-
|
|
148
|
+
// A release that dragged off the toggle cancels the press.
|
|
149
|
+
if (event.type === "mouse_up" && event.over) {
|
|
149
150
|
this.on = !this.on;
|
|
150
151
|
this.emitChange();
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
// --- Rendering ---
|
|
154
|
-
|
|
155
|
+
draw(_options) {
|
|
155
156
|
// Both indicators are exactly 5 cells: "[ON] " and "[OFF]".
|
|
156
157
|
const indicator = this.on ? "[ON] " : "[OFF]";
|
|
157
158
|
const text = `${indicator} ${this.label}`;
|
package/dist/widgets/types.d.ts
CHANGED
|
@@ -30,23 +30,24 @@ export type KeyHandlerPriority = "high" | "normal";
|
|
|
30
30
|
export interface KeyHandlerOptions {
|
|
31
31
|
priority?: KeyHandlerPriority;
|
|
32
32
|
}
|
|
33
|
-
export
|
|
34
|
-
|
|
33
|
+
export type ScreenMouseEvent = {
|
|
34
|
+
[T in MouseEventType]: MouseEventOf<T>;
|
|
35
|
+
}[MouseEventType];
|
|
36
|
+
type MouseEventType = "mouse_down" | "mouse_up" | "mouse_move" | "scroll_up" | "scroll_down";
|
|
37
|
+
interface MouseEventOf<Type extends string> {
|
|
38
|
+
type: Type;
|
|
35
39
|
x: number;
|
|
36
40
|
y: number;
|
|
37
41
|
button: number;
|
|
38
42
|
shift: boolean;
|
|
39
43
|
ctrl: boolean;
|
|
40
44
|
}
|
|
45
|
+
export interface WidgetMouseEvent extends MouseEventOf<"mouse_down" | "mouse_up" | "mouse_move"> {
|
|
46
|
+
over: boolean;
|
|
47
|
+
}
|
|
41
48
|
export interface WidgetFocusEvent {
|
|
42
49
|
type: "focus" | "blur";
|
|
43
50
|
}
|
|
44
|
-
export interface WidgetBounds {
|
|
45
|
-
x: number;
|
|
46
|
-
y: number;
|
|
47
|
-
width: number;
|
|
48
|
-
height: number;
|
|
49
|
-
}
|
|
50
51
|
export interface InteractiveWidget extends Renderable, Measurable {
|
|
51
52
|
readonly id: string;
|
|
52
53
|
readonly focusable: boolean;
|
|
@@ -54,8 +55,6 @@ export interface InteractiveWidget extends Renderable, Measurable {
|
|
|
54
55
|
hovered: boolean;
|
|
55
56
|
active: boolean;
|
|
56
57
|
disabled: boolean;
|
|
57
|
-
visible: boolean;
|
|
58
|
-
bounds: WidgetBounds | null;
|
|
59
58
|
handleKey(event: KeyEvent): void;
|
|
60
59
|
handleMouse(event: WidgetMouseEvent): void;
|
|
61
60
|
handleFocus(event: WidgetFocusEvent): void;
|
|
@@ -63,46 +62,26 @@ export interface InteractiveWidget extends Renderable, Measurable {
|
|
|
63
62
|
blur(): void;
|
|
64
63
|
setDisabled(value: boolean): void;
|
|
65
64
|
setHovered(value: boolean): void;
|
|
66
|
-
containsPoint(x: number, y: number): boolean;
|
|
67
65
|
onChange(handler: (widget: InteractiveWidget) => void): Unsubscribe;
|
|
68
66
|
onSubmit(handler: (widget: InteractiveWidget) => void): Unsubscribe;
|
|
69
67
|
}
|
|
70
|
-
export type Placement = {
|
|
71
|
-
kind: "flow";
|
|
72
|
-
} | {
|
|
73
|
-
kind: "inline";
|
|
74
|
-
} | {
|
|
75
|
-
kind: "fixed";
|
|
76
|
-
x: number;
|
|
77
|
-
y: number;
|
|
78
|
-
};
|
|
79
|
-
export declare const FLOW: Placement;
|
|
80
68
|
export interface OverlayRenderable {
|
|
81
69
|
renderOverlay(options: RenderOptions): Iterable<Segment> | null;
|
|
82
70
|
}
|
|
83
71
|
export declare function hasOverlay(value: object): value is OverlayRenderable;
|
|
84
72
|
export interface FocusManager {
|
|
85
73
|
readonly current: InteractiveWidget | null;
|
|
74
|
+
/** The widgets Tab moves between, in the order it moves. */
|
|
86
75
|
readonly widgets: readonly InteractiveWidget[];
|
|
87
|
-
register(widget: InteractiveWidget): void;
|
|
88
|
-
unregister(widget: InteractiveWidget): void;
|
|
89
76
|
next(): void;
|
|
90
77
|
prev(): void;
|
|
91
78
|
focus(widget: InteractiveWidget): void;
|
|
92
|
-
blur(): void;
|
|
93
79
|
handleKey(event: KeyEvent): void;
|
|
94
80
|
onChange(handler: (current: InteractiveWidget | null) => void): Unsubscribe;
|
|
95
81
|
}
|
|
96
|
-
export
|
|
97
|
-
widget: InteractiveWidget;
|
|
98
|
-
placement: Placement;
|
|
99
|
-
};
|
|
100
|
-
export interface Screen {
|
|
101
|
-
mount(...entries: MountEntry[]): void;
|
|
102
|
-
unmount(widget: InteractiveWidget): void;
|
|
103
|
-
start(): void;
|
|
104
|
-
stop(): void;
|
|
82
|
+
export interface FrameSource {
|
|
105
83
|
readonly focusManager: FocusManager;
|
|
106
|
-
readonly
|
|
107
|
-
|
|
84
|
+
readonly frame: readonly (readonly Segment[])[];
|
|
85
|
+
refresh(): void;
|
|
108
86
|
}
|
|
87
|
+
export {};
|
package/dist/widgets/types.js
CHANGED
|
@@ -24,7 +24,6 @@ export class KeyEvent {
|
|
|
24
24
|
// and no focused-widget handler downstream of the caller will see it.
|
|
25
25
|
stop() { this._stopped = true; }
|
|
26
26
|
}
|
|
27
|
-
export const FLOW = { kind: "flow" };
|
|
28
27
|
export function hasOverlay(value) {
|
|
29
28
|
return ("renderOverlay" in value &&
|
|
30
29
|
typeof value.renderOverlay === "function");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WidgetApp — an `App` whose view has widgets in it: keys reach the focused
|
|
3
|
+
* widget, the pointer reaches the widget that drew the cell under it, and a
|
|
4
|
+
* change to anything a frame read paints the next one.
|
|
5
|
+
*
|
|
6
|
+
* A widget goes wherever a renderable goes — in a `Panel`, a `Layout` pane, a
|
|
7
|
+
* `Table` cell — and nothing lays it out a second time. Everything added here
|
|
8
|
+
* reads the frame the view drew:
|
|
9
|
+
*
|
|
10
|
+
* - Focus moves in document order: the order widgets start to render in
|
|
11
|
+
* (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
|
|
12
|
+
* Not reading order — in a `Layout` split into columns, reading order
|
|
13
|
+
* zigzags between the columns row by row.
|
|
14
|
+
* - Overlays are painted over the view's frame, in document order, directly
|
|
15
|
+
* below their owner's footprint (`OverlayRenderable`). Paint order is the
|
|
16
|
+
* z-order: a later overlay covers an earlier one.
|
|
17
|
+
* - A pointer event goes to the innermost widget that drew the cell under it
|
|
18
|
+
* (`widgetAt`), in that widget's own coordinates; the wheel goes to the
|
|
19
|
+
* innermost `Viewport` that can move (`scrollTargetAt`).
|
|
20
|
+
*
|
|
21
|
+
* [LAW:locality-or-seam] `App` knows nothing of widgets — `host/` depends on
|
|
22
|
+
* `core/` alone — and this reaches it only through the seams it already has:
|
|
23
|
+
* the view is a renderable, and `onPaint` hears the frame.
|
|
24
|
+
*/
|
|
25
|
+
import { Segment } from "../core/segment.js";
|
|
26
|
+
import type { Unsubscribe } from "../core/subscription.js";
|
|
27
|
+
import { type AppOptions, type AppPhase } from "../host/app.js";
|
|
28
|
+
import type { FocusManager, FrameSource, KeyEvent, KeyHandlerOptions, ScreenMouseEvent } from "./types.js";
|
|
29
|
+
export declare class WidgetApp implements FrameSource {
|
|
30
|
+
readonly focusManager: FocusManager;
|
|
31
|
+
private readonly focus;
|
|
32
|
+
private readonly app;
|
|
33
|
+
private readonly router;
|
|
34
|
+
private readonly reaction;
|
|
35
|
+
private drawn;
|
|
36
|
+
constructor(options: AppOptions);
|
|
37
|
+
get phase(): AppPhase;
|
|
38
|
+
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
39
|
+
get frame(): readonly (readonly Segment[])[];
|
|
40
|
+
/**
|
|
41
|
+
* Take the terminal, paint the first frame, and route input to its widgets
|
|
42
|
+
* until the app stops — `App.run`, with the input.
|
|
43
|
+
*/
|
|
44
|
+
run(): Promise<void>;
|
|
45
|
+
stop(): void;
|
|
46
|
+
fail(error: unknown): void;
|
|
47
|
+
/**
|
|
48
|
+
* Paint the next frame, as `App.refresh` does. A change to an observable
|
|
49
|
+
* the view read needs none; one no observable carries — a `Viewport`
|
|
50
|
+
* scrolled from a key handler — does.
|
|
51
|
+
*/
|
|
52
|
+
refresh(): void;
|
|
53
|
+
suspend(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Hear every key. A `high` handler hears it before the focused widget, a
|
|
56
|
+
* `normal` one after it — and after Tab has moved focus — and a handler
|
|
57
|
+
* that calls `event.stop()` ends the key there.
|
|
58
|
+
*/
|
|
59
|
+
onKey(handler: (event: KeyEvent) => void, options?: KeyHandlerOptions): Unsubscribe;
|
|
60
|
+
/** Hear every pointer event, in terminal cells, before any widget does. */
|
|
61
|
+
onMouse(handler: (event: ScreenMouseEvent) => void): Unsubscribe;
|
|
62
|
+
private compose;
|
|
63
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WidgetApp — an `App` whose view has widgets in it: keys reach the focused
|
|
3
|
+
* widget, the pointer reaches the widget that drew the cell under it, and a
|
|
4
|
+
* change to anything a frame read paints the next one.
|
|
5
|
+
*
|
|
6
|
+
* A widget goes wherever a renderable goes — in a `Panel`, a `Layout` pane, a
|
|
7
|
+
* `Table` cell — and nothing lays it out a second time. Everything added here
|
|
8
|
+
* reads the frame the view drew:
|
|
9
|
+
*
|
|
10
|
+
* - Focus moves in document order: the order widgets start to render in
|
|
11
|
+
* (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
|
|
12
|
+
* Not reading order — in a `Layout` split into columns, reading order
|
|
13
|
+
* zigzags between the columns row by row.
|
|
14
|
+
* - Overlays are painted over the view's frame, in document order, directly
|
|
15
|
+
* below their owner's footprint (`OverlayRenderable`). Paint order is the
|
|
16
|
+
* z-order: a later overlay covers an earlier one.
|
|
17
|
+
* - A pointer event goes to the innermost widget that drew the cell under it
|
|
18
|
+
* (`widgetAt`), in that widget's own coordinates; the wheel goes to the
|
|
19
|
+
* innermost `Viewport` that can move (`scrollTargetAt`).
|
|
20
|
+
*
|
|
21
|
+
* [LAW:locality-or-seam] `App` knows nothing of widgets — `host/` depends on
|
|
22
|
+
* `core/` alone — and this reaches it only through the seams it already has:
|
|
23
|
+
* the view is a renderable, and `onPaint` hears the frame.
|
|
24
|
+
*/
|
|
25
|
+
import { Reaction } from "mobx";
|
|
26
|
+
import { Segment } from "../core/segment.js";
|
|
27
|
+
import { App } from "../host/app.js";
|
|
28
|
+
import { DefaultFocusManager } from "./focus-manager.js";
|
|
29
|
+
import { EventRouter } from "./event-router.js";
|
|
30
|
+
import { footprintOf, ownersOn } from "./hit.js";
|
|
31
|
+
import { hasOverlay } from "./types.js";
|
|
32
|
+
import { WidgetBase } from "./widget-base.js";
|
|
33
|
+
export class WidgetApp {
|
|
34
|
+
focusManager;
|
|
35
|
+
focus;
|
|
36
|
+
app;
|
|
37
|
+
router;
|
|
38
|
+
// Paints the next frame when anything the last one read changes.
|
|
39
|
+
reaction;
|
|
40
|
+
// The widgets the view drew for the frame on screen, in document order.
|
|
41
|
+
drawn = [];
|
|
42
|
+
constructor(options) {
|
|
43
|
+
const { view } = options;
|
|
44
|
+
this.reaction = new Reaction("WidgetApp", () => this.app.refresh());
|
|
45
|
+
const root = { render: (renderOptions) => this.compose(view, renderOptions) };
|
|
46
|
+
this.app = new App({ ...options, view: () => root });
|
|
47
|
+
this.focus = new DefaultFocusManager(() => {
|
|
48
|
+
const owners = ownersOn(this.app.frame);
|
|
49
|
+
return this.drawn.filter((widget) => owners.has(widget));
|
|
50
|
+
});
|
|
51
|
+
this.focusManager = this.focus;
|
|
52
|
+
this.router = new EventRouter({ source: this, host: options.host });
|
|
53
|
+
this.app.onPaint(() => {
|
|
54
|
+
this.focus.settle(this.drawn);
|
|
55
|
+
this.router.framePainted();
|
|
56
|
+
});
|
|
57
|
+
// [LAW:single-enforcer] `settle` judges every focus move, so every move
|
|
58
|
+
// paints — including one onto a widget no frame has read yet.
|
|
59
|
+
this.focus.onChange(() => this.app.refresh());
|
|
60
|
+
}
|
|
61
|
+
get phase() {
|
|
62
|
+
return this.app.phase;
|
|
63
|
+
}
|
|
64
|
+
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
65
|
+
get frame() {
|
|
66
|
+
return this.app.frame;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Take the terminal, paint the first frame, and route input to its widgets
|
|
70
|
+
* until the app stops — `App.run`, with the input.
|
|
71
|
+
*/
|
|
72
|
+
async run() {
|
|
73
|
+
// App refuses a second run; this one must not end the input of the first.
|
|
74
|
+
if (this.app.phase !== "idle")
|
|
75
|
+
return this.app.run();
|
|
76
|
+
// Listening before the app starts, so a first frame that throws — and
|
|
77
|
+
// stops the host — is not followed by a subscription that restarts it.
|
|
78
|
+
this.router.start();
|
|
79
|
+
const running = this.app.run();
|
|
80
|
+
try {
|
|
81
|
+
await running;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.router.stop();
|
|
85
|
+
this.reaction.dispose();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
stop() {
|
|
89
|
+
this.app.stop();
|
|
90
|
+
}
|
|
91
|
+
fail(error) {
|
|
92
|
+
this.app.fail(error);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Paint the next frame, as `App.refresh` does. A change to an observable
|
|
96
|
+
* the view read needs none; one no observable carries — a `Viewport`
|
|
97
|
+
* scrolled from a key handler — does.
|
|
98
|
+
*/
|
|
99
|
+
refresh() {
|
|
100
|
+
this.app.refresh();
|
|
101
|
+
}
|
|
102
|
+
suspend() {
|
|
103
|
+
return this.app.suspend();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Hear every key. A `high` handler hears it before the focused widget, a
|
|
107
|
+
* `normal` one after it — and after Tab has moved focus — and a handler
|
|
108
|
+
* that calls `event.stop()` ends the key there.
|
|
109
|
+
*/
|
|
110
|
+
onKey(handler, options) {
|
|
111
|
+
return this.router.onKey(handler, options);
|
|
112
|
+
}
|
|
113
|
+
/** Hear every pointer event, in terminal cells, before any widget does. */
|
|
114
|
+
onMouse(handler) {
|
|
115
|
+
return this.router.onMouse(handler);
|
|
116
|
+
}
|
|
117
|
+
compose(view, options) {
|
|
118
|
+
const drawn = new Set();
|
|
119
|
+
// [LAW:parse-dont-validate] An owner is any object; only a `WidgetBase`
|
|
120
|
+
// reports itself as a widget, so this is where `object` becomes one.
|
|
121
|
+
const onDraw = (owner) => {
|
|
122
|
+
if (owner instanceof WidgetBase)
|
|
123
|
+
drawn.add(owner);
|
|
124
|
+
};
|
|
125
|
+
// The view, its render and the overlays are all tracked, so an observable
|
|
126
|
+
// any of them reads — a label, `focused`, the state that picks what the
|
|
127
|
+
// view shows — paints the next frame when it changes.
|
|
128
|
+
// An overlay reports what it draws too: a widget in it is on the frame,
|
|
129
|
+
// and visiting `drawn` in insertion order paints its overlay in turn.
|
|
130
|
+
const render = { ...options, onDraw };
|
|
131
|
+
const lines = tracked(this.reaction, () => {
|
|
132
|
+
const base = Segment.splitLines(view().render(render));
|
|
133
|
+
for (const widget of drawn)
|
|
134
|
+
paintOverlay(base, widget, render);
|
|
135
|
+
return base;
|
|
136
|
+
});
|
|
137
|
+
this.drawn = [...drawn];
|
|
138
|
+
return lines.flatMap((line) => [...line, Segment.line()]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `fn`, with every observable it reads subscribed to by `reaction`.
|
|
143
|
+
*
|
|
144
|
+
* [LAW:no-silent-failure] mobx reports an error thrown inside `track` and
|
|
145
|
+
* carries on, which would paint an empty frame for a view that threw. The
|
|
146
|
+
* error is carried out instead, and fails the app as `App` fails on any frame
|
|
147
|
+
* that throws.
|
|
148
|
+
*/
|
|
149
|
+
function tracked(reaction, fn) {
|
|
150
|
+
// Written inside `track`, which the compiler cannot see run.
|
|
151
|
+
let outcome = {
|
|
152
|
+
ok: false,
|
|
153
|
+
error: new Error("WidgetApp: a frame drawn after the app stopped"),
|
|
154
|
+
};
|
|
155
|
+
reaction.track(() => {
|
|
156
|
+
try {
|
|
157
|
+
outcome = { ok: true, value: fn() };
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
outcome = { ok: false, error };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (!outcome.ok)
|
|
164
|
+
throw outcome.error;
|
|
165
|
+
return outcome.value;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Paint `widget`'s overlay over `lines`, below its footprint and at the column
|
|
169
|
+
* of its row 0, each overlay row stamped as the widget's row below its last —
|
|
170
|
+
* so a click there reaches the widget, in its own coordinates. A widget whose
|
|
171
|
+
* row 0 is not on the frame has nowhere to hang an overlay, and paints none.
|
|
172
|
+
*/
|
|
173
|
+
function paintOverlay(lines, widget, options) {
|
|
174
|
+
if (!hasOverlay(widget))
|
|
175
|
+
return;
|
|
176
|
+
const at = footprintOf(lines, widget);
|
|
177
|
+
if (!at)
|
|
178
|
+
return;
|
|
179
|
+
// An overlay floats over the frame; no region holds it.
|
|
180
|
+
const overlay = widget.renderOverlay({ ...options, height: undefined });
|
|
181
|
+
if (overlay === null)
|
|
182
|
+
return;
|
|
183
|
+
const width = Math.max(0, options.maxWidth - at.x);
|
|
184
|
+
const rows = Segment.anchorLines(Segment.splitLines(overlay), widget, at.rows).map((line) => Segment.adjustLineLength(line, width, undefined, false));
|
|
185
|
+
paintLines(lines, rows, at.x, at.y + at.rows);
|
|
186
|
+
}
|
|
187
|
+
// Paint `rows` into `lines` with its top-left cell at (x, y), growing `lines`
|
|
188
|
+
// as needed; each painted cell replaces the one beneath it.
|
|
189
|
+
function paintLines(lines, rows, x, y) {
|
|
190
|
+
while (lines.length < y + rows.length)
|
|
191
|
+
lines.push([]);
|
|
192
|
+
rows.forEach((source, i) => {
|
|
193
|
+
const row = lines[y + i];
|
|
194
|
+
// Padded to `x`, so a row that ends short of the overlay still reaches it.
|
|
195
|
+
const gap = Math.max(0, x - Segment.getLineLength(row));
|
|
196
|
+
const [before = [], , after = []] = Segment.divide([...row, new Segment(" ".repeat(gap))], [x, x + Segment.getLineLength(source)]);
|
|
197
|
+
lines[y + i] = [...before, ...source, ...after];
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* `accessor` members. Each decorator registers its own member, so there is no
|
|
8
8
|
* makeObservable call and subclasses add observables the same way.
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
10
|
+
import { Segment } from "../core/segment.js";
|
|
11
11
|
import type { RenderOptions } from "../core/protocol.js";
|
|
12
|
-
import type { InteractiveWidget, KeyEvent, WidgetMouseEvent, WidgetFocusEvent
|
|
12
|
+
import type { InteractiveWidget, KeyEvent, WidgetMouseEvent, WidgetFocusEvent } from "./types.js";
|
|
13
13
|
import type { Unsubscribe } from "../core/subscription.js";
|
|
14
14
|
export declare abstract class WidgetBase implements InteractiveWidget {
|
|
15
15
|
abstract readonly id: string;
|
|
@@ -18,8 +18,6 @@ export declare abstract class WidgetBase implements InteractiveWidget {
|
|
|
18
18
|
accessor hovered: boolean;
|
|
19
19
|
accessor active: boolean;
|
|
20
20
|
accessor disabled: boolean;
|
|
21
|
-
accessor visible: boolean;
|
|
22
|
-
accessor bounds: WidgetBounds | null;
|
|
23
21
|
private readonly changeHandlers;
|
|
24
22
|
private readonly submitHandlers;
|
|
25
23
|
abstract handleKey(event: KeyEvent): void;
|
|
@@ -29,12 +27,12 @@ export declare abstract class WidgetBase implements InteractiveWidget {
|
|
|
29
27
|
blur(): void;
|
|
30
28
|
setDisabled(value: boolean): void;
|
|
31
29
|
setHovered(value: boolean): void;
|
|
32
|
-
containsPoint(x: number, y: number): boolean;
|
|
33
30
|
onChange(handler: (widget: InteractiveWidget) => void): Unsubscribe;
|
|
34
31
|
onSubmit(handler: (widget: InteractiveWidget) => void): Unsubscribe;
|
|
35
32
|
protected emitChange(): void;
|
|
36
33
|
protected emitSubmit(): void;
|
|
37
|
-
|
|
34
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
35
|
+
protected abstract draw(options: RenderOptions): Iterable<Segment>;
|
|
38
36
|
abstract measure(options: RenderOptions): {
|
|
39
37
|
minimum: number;
|
|
40
38
|
maximum: number;
|
|
@@ -42,6 +42,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
42
42
|
done = true;
|
|
43
43
|
};
|
|
44
44
|
import { observable, action } from "mobx";
|
|
45
|
+
import { Segment } from "../core/segment.js";
|
|
45
46
|
let WidgetBase = (() => {
|
|
46
47
|
let _instanceExtraInitializers = [];
|
|
47
48
|
let _focused_decorators;
|
|
@@ -56,9 +57,6 @@ let WidgetBase = (() => {
|
|
|
56
57
|
let _disabled_decorators;
|
|
57
58
|
let _disabled_initializers = [];
|
|
58
59
|
let _disabled_extraInitializers = [];
|
|
59
|
-
let _visible_decorators;
|
|
60
|
-
let _visible_initializers = [];
|
|
61
|
-
let _visible_extraInitializers = [];
|
|
62
60
|
let _handleFocus_decorators;
|
|
63
61
|
let _focus_decorators;
|
|
64
62
|
let _blur_decorators;
|
|
@@ -71,7 +69,6 @@ let WidgetBase = (() => {
|
|
|
71
69
|
_hovered_decorators = [observable];
|
|
72
70
|
_active_decorators = [observable];
|
|
73
71
|
_disabled_decorators = [observable];
|
|
74
|
-
_visible_decorators = [observable];
|
|
75
72
|
_handleFocus_decorators = [action];
|
|
76
73
|
_focus_decorators = [action];
|
|
77
74
|
_blur_decorators = [action];
|
|
@@ -81,7 +78,6 @@ let WidgetBase = (() => {
|
|
|
81
78
|
__esDecorate(this, null, _hovered_decorators, { kind: "accessor", name: "hovered", static: false, private: false, access: { has: obj => "hovered" in obj, get: obj => obj.hovered, set: (obj, value) => { obj.hovered = value; } }, metadata: _metadata }, _hovered_initializers, _hovered_extraInitializers);
|
|
82
79
|
__esDecorate(this, null, _active_decorators, { kind: "accessor", name: "active", static: false, private: false, access: { has: obj => "active" in obj, get: obj => obj.active, set: (obj, value) => { obj.active = value; } }, metadata: _metadata }, _active_initializers, _active_extraInitializers);
|
|
83
80
|
__esDecorate(this, null, _disabled_decorators, { kind: "accessor", name: "disabled", static: false, private: false, access: { has: obj => "disabled" in obj, get: obj => obj.disabled, set: (obj, value) => { obj.disabled = value; } }, metadata: _metadata }, _disabled_initializers, _disabled_extraInitializers);
|
|
84
|
-
__esDecorate(this, null, _visible_decorators, { kind: "accessor", name: "visible", static: false, private: false, access: { has: obj => "visible" in obj, get: obj => obj.visible, set: (obj, value) => { obj.visible = value; } }, metadata: _metadata }, _visible_initializers, _visible_extraInitializers);
|
|
85
81
|
__esDecorate(this, null, _handleFocus_decorators, { kind: "method", name: "handleFocus", static: false, private: false, access: { has: obj => "handleFocus" in obj, get: obj => obj.handleFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
86
82
|
__esDecorate(this, null, _focus_decorators, { kind: "method", name: "focus", static: false, private: false, access: { has: obj => "focus" in obj, get: obj => obj.focus }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
87
83
|
__esDecorate(this, null, _blur_decorators, { kind: "method", name: "blur", static: false, private: false, access: { has: obj => "blur" in obj, get: obj => obj.blur }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -101,20 +97,7 @@ let WidgetBase = (() => {
|
|
|
101
97
|
#disabled_accessor_storage = (__runInitializers(this, _active_extraInitializers), __runInitializers(this, _disabled_initializers, false));
|
|
102
98
|
get disabled() { return this.#disabled_accessor_storage; }
|
|
103
99
|
set disabled(value) { this.#disabled_accessor_storage = value; }
|
|
104
|
-
|
|
105
|
-
get visible() { return this.#visible_accessor_storage; }
|
|
106
|
-
set visible(value) { this.#visible_accessor_storage = value; }
|
|
107
|
-
#bounds_accessor_storage = (__runInitializers(this, _visible_extraInitializers), null);
|
|
108
|
-
// [LAW:types-are-the-program] Layout output, not reactive state. In
|
|
109
|
-
// DefaultScreen usage the sole writer is Screen.draw(); custom hosts
|
|
110
|
-
// (test harnesses, alternative layout loops) also write directly. The
|
|
111
|
-
// only readers are imperative input handlers (containsPoint,
|
|
112
|
-
// handleMouse). Modeling as observable adds reactive surface no
|
|
113
|
-
// consumer uses and creates a strict-mode hazard when draw() runs from
|
|
114
|
-
// a microtask outside its origin autorun.
|
|
115
|
-
get bounds() { return this.#bounds_accessor_storage; }
|
|
116
|
-
set bounds(value) { this.#bounds_accessor_storage = value; }
|
|
117
|
-
changeHandlers = new Set();
|
|
100
|
+
changeHandlers = (__runInitializers(this, _disabled_extraInitializers), new Set());
|
|
118
101
|
submitHandlers = new Set();
|
|
119
102
|
handleMouse(_event) { }
|
|
120
103
|
handleFocus(event) {
|
|
@@ -135,20 +118,12 @@ let WidgetBase = (() => {
|
|
|
135
118
|
this.disabled = value;
|
|
136
119
|
}
|
|
137
120
|
// [LAW:single-enforcer] One canonical setter for hover state lives on
|
|
138
|
-
// the base. EventRouter calls it
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
// no longer maintains a "does this widget have setHovered" fallback path.
|
|
121
|
+
// the base. EventRouter calls it when the pointer moves onto or off this
|
|
122
|
+
// widget; widgets that need to react to hover transitions override
|
|
123
|
+
// handleMouse and read the synthesized `mouse_move`.
|
|
142
124
|
setHovered(value) {
|
|
143
125
|
this.hovered = value;
|
|
144
126
|
}
|
|
145
|
-
// --- Hit-testing ---
|
|
146
|
-
containsPoint(x, y) {
|
|
147
|
-
const b = this.bounds;
|
|
148
|
-
if (!b)
|
|
149
|
-
return false;
|
|
150
|
-
return x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height;
|
|
151
|
-
}
|
|
152
127
|
// --- Subscriptions ---
|
|
153
128
|
onChange(handler) {
|
|
154
129
|
this.changeHandlers.add(handler);
|
|
@@ -168,6 +143,23 @@ let WidgetBase = (() => {
|
|
|
168
143
|
handler(this);
|
|
169
144
|
}
|
|
170
145
|
}
|
|
146
|
+
// --- Renderable + Measurable ---
|
|
147
|
+
// [LAW:single-enforcer] The one place a widget's cells get their anchor, so
|
|
148
|
+
// a hit on any cell names this widget and the cell's place in its output
|
|
149
|
+
// (`widgetAt`). Subclasses draw; none stamps itself. A `RichText` admits no
|
|
150
|
+
// anchor, so stamping has to follow every text layout `draw` does.
|
|
151
|
+
// Every row is padded to the widest, so the widget owns its whole rectangle
|
|
152
|
+
// and a click past the end of a short row still reaches it; every row ends
|
|
153
|
+
// in a newline, so a blank last row survives the next `splitLines`.
|
|
154
|
+
// Reporting itself before it draws is what puts a widget ahead of the
|
|
155
|
+
// widgets nested in it in document order — the order focus traverses.
|
|
156
|
+
render(options) {
|
|
157
|
+
options.onDraw?.(this);
|
|
158
|
+
const drawn = Segment.splitLines(this.draw(options));
|
|
159
|
+
const [width] = Segment.getShape(drawn);
|
|
160
|
+
const lines = Segment.anchorLines(drawn.map((line) => Segment.adjustLineLength(line, width)), this);
|
|
161
|
+
return lines.flatMap((line) => [...line, Segment.line()]);
|
|
162
|
+
}
|
|
171
163
|
};
|
|
172
164
|
})();
|
|
173
165
|
export { WidgetBase };
|