@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
|
@@ -11,5 +11,21 @@
|
|
|
11
11
|
export interface ThemePaletteData {
|
|
12
12
|
readonly name: string;
|
|
13
13
|
readonly dark: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The sixteen colours a terminal showing this theme draws `red`, `blue`,
|
|
16
|
+
* `bright_black` and the rest in. A theme without its own table would draw
|
|
17
|
+
* them in the VGA defaults (`blue` as #000080), which belong to no theme.
|
|
18
|
+
*/
|
|
19
|
+
readonly ansi: AnsiColorsData;
|
|
14
20
|
readonly vars: Readonly<Record<string, string>>;
|
|
15
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* ANSI colour numbers 0–15 in order: the eight normal colours, then their
|
|
24
|
+
* bright forms. Named rather than positional so a data file cannot shift a
|
|
25
|
+
* colour into its neighbour's slot by miscounting.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ANSI_SLOTS: readonly ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "brightBlack", "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"];
|
|
28
|
+
/** One `#RRGGBB` per ANSI colour number, keyed by `ANSI_SLOTS`. */
|
|
29
|
+
export type AnsiColorsData = {
|
|
30
|
+
readonly [K in (typeof ANSI_SLOTS)[number]]: string;
|
|
31
|
+
};
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ANSI colour numbers 0–15 in order: the eight normal colours, then their
|
|
3
|
+
* bright forms. Named rather than positional so a data file cannot shift a
|
|
4
|
+
* colour into its neighbour's slot by miscounting.
|
|
5
|
+
*/
|
|
6
|
+
export const ANSI_SLOTS = [
|
|
7
|
+
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
|
|
8
|
+
"brightBlack", "brightRed", "brightGreen", "brightYellow",
|
|
9
|
+
"brightBlue", "brightMagenta", "brightCyan", "brightWhite",
|
|
10
|
+
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorRgba } from "../core/color.js";
|
|
1
|
+
import { ColorRgba, ColorTable } from "../core/color.js";
|
|
2
2
|
import { Palette } from "./palette.js";
|
|
3
3
|
import { type ThemeName } from "./data/index.js";
|
|
4
4
|
export type { ThemeName };
|
|
@@ -17,7 +17,7 @@ export declare function listThemePalettes(): readonly ThemeName[];
|
|
|
17
17
|
export declare function getThemePalette(name: ThemeName): Palette;
|
|
18
18
|
export declare function getThemePalette(name: string): Palette | null;
|
|
19
19
|
/**
|
|
20
|
-
* The eight base colors every theme must declare. Returned by
|
|
20
|
+
* The eight base colors every theme must declare, and its ANSI table. Returned by
|
|
21
21
|
* `getThemeBaseColors` for callers (notably `terminalThemes.ts`) that only
|
|
22
22
|
* need the substrate and would otherwise pay the cost of hydrating the
|
|
23
23
|
* full ~150-var palette and polluting the registry cache.
|
|
@@ -33,6 +33,7 @@ export interface ThemeBaseColors {
|
|
|
33
33
|
readonly success: ColorRgba;
|
|
34
34
|
readonly warning: ColorRgba;
|
|
35
35
|
readonly error: ColorRgba;
|
|
36
|
+
readonly ansi: ColorTable;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* Read just the eight base colors from a theme's data file. Cheap — parses
|
package/dist/themes/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { parseRgbHex, parseRgbaHex } from "../core/color.js";
|
|
1
|
+
import { ColorTable, parseRgbHex, parseRgbaHex } from "../core/color.js";
|
|
2
2
|
import { Palette } from "./palette.js";
|
|
3
3
|
import { THEMES } from "./data/index.js";
|
|
4
|
+
import { ANSI_SLOTS } from "./data/types.js";
|
|
4
5
|
const THEME_NAMES = Object.freeze(Object.keys(THEMES).sort());
|
|
5
6
|
const cache = new Map();
|
|
6
7
|
/**
|
|
@@ -41,6 +42,7 @@ export function getThemeBaseColors(name) {
|
|
|
41
42
|
success: requireBaseVar(data, "success"),
|
|
42
43
|
warning: requireBaseVar(data, "warning"),
|
|
43
44
|
error: requireBaseVar(data, "error"),
|
|
45
|
+
ansi: new ColorTable(ANSI_SLOTS.map((slot) => parseOpaqueHex(data.ansi[slot], data.name, `ansi.${slot}`))),
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
function requireBaseVar(data, key) {
|
|
@@ -72,6 +74,16 @@ function hydrate(data) {
|
|
|
72
74
|
// the untrusted side (authored data files) and the place that has to
|
|
73
75
|
// reject `"0G"`, `""`, `"#GGGGGG"`, etc.
|
|
74
76
|
const HEX_RE = /^[0-9a-fA-F]+$/;
|
|
77
|
+
/**
|
|
78
|
+
* An ANSI slot is a colour a terminal paints, never a blend, so it takes no
|
|
79
|
+
* alpha: `flattenAlpha` leaves a STANDARD spec alone and would pass one through.
|
|
80
|
+
*/
|
|
81
|
+
function parseOpaqueHex(value, theme, key) {
|
|
82
|
+
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
83
|
+
throw new Error(`Theme ${theme}: ${key} has invalid hex ${JSON.stringify(value)} (expected #RRGGBB)`);
|
|
84
|
+
}
|
|
85
|
+
return parseRgbHex(value.slice(1));
|
|
86
|
+
}
|
|
75
87
|
function parseHex(value, theme, key) {
|
|
76
88
|
const hex = value.startsWith("#") ? value.slice(1) : value;
|
|
77
89
|
if ((hex.length !== 6 && hex.length !== 8) || !HEX_RE.test(hex)) {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* [LAW:one-source-of-truth] Hex values live in `data/<name>.ts`. This
|
|
13
13
|
* module is a derived view that builds a `TerminalTheme` from each
|
|
14
|
-
* theme's eight base colors via `buildPalette` for the substrate palette
|
|
15
|
-
* and
|
|
14
|
+
* theme's eight base colors via `buildPalette` for the substrate palette,
|
|
15
|
+
* and from the theme's own ANSI table for the sixteen named colours.
|
|
16
16
|
*
|
|
17
17
|
* [LAW:one-type-per-behavior] Every theme has the same shape — name,
|
|
18
18
|
* `dark` flag, full Palette in the registry, `TerminalTheme` constant
|
|
@@ -22,11 +22,6 @@
|
|
|
22
22
|
* [LAW:one-way-deps] `core/color → themes/palette` remains the only edge
|
|
23
23
|
* into themes/. This file imports only sibling theme modules and core
|
|
24
24
|
* color primitives; nothing in `core/` imports back.
|
|
25
|
-
*
|
|
26
|
-
* Note on `ansiColors`: every preset theme uses `STANDARD_TABLE`. Theme
|
|
27
|
-
* identity lives in the semantic `palette`, not in the standard ANSI
|
|
28
|
-
* 16/256 LUT — `ColorSpec.parse("red")` is intentionally canonical across
|
|
29
|
-
* themes, matching Textual's model.
|
|
30
25
|
*/
|
|
31
26
|
import { TerminalTheme } from "../core/color.js";
|
|
32
27
|
export declare const DEFAULT_TERMINAL_THEME: TerminalTheme;
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* [LAW:one-source-of-truth] Hex values live in `data/<name>.ts`. This
|
|
13
13
|
* module is a derived view that builds a `TerminalTheme` from each
|
|
14
|
-
* theme's eight base colors via `buildPalette` for the substrate palette
|
|
15
|
-
* and
|
|
14
|
+
* theme's eight base colors via `buildPalette` for the substrate palette,
|
|
15
|
+
* and from the theme's own ANSI table for the sixteen named colours.
|
|
16
16
|
*
|
|
17
17
|
* [LAW:one-type-per-behavior] Every theme has the same shape — name,
|
|
18
18
|
* `dark` flag, full Palette in the registry, `TerminalTheme` constant
|
|
@@ -22,17 +22,12 @@
|
|
|
22
22
|
* [LAW:one-way-deps] `core/color → themes/palette` remains the only edge
|
|
23
23
|
* into themes/. This file imports only sibling theme modules and core
|
|
24
24
|
* color primitives; nothing in `core/` imports back.
|
|
25
|
-
*
|
|
26
|
-
* Note on `ansiColors`: every preset theme uses `STANDARD_TABLE`. Theme
|
|
27
|
-
* identity lives in the semantic `palette`, not in the standard ANSI
|
|
28
|
-
* 16/256 LUT — `ColorSpec.parse("red")` is intentionally canonical across
|
|
29
|
-
* themes, matching Textual's model.
|
|
30
25
|
*/
|
|
31
|
-
import {
|
|
26
|
+
import { TerminalTheme } from "../core/color.js";
|
|
32
27
|
import { buildPalette } from "./buildPalette.js";
|
|
33
28
|
import { getThemeBaseColors } from "./registry.js";
|
|
34
29
|
function defineTheme(d) {
|
|
35
|
-
return new TerminalTheme(d.bg, d.fg,
|
|
30
|
+
return new TerminalTheme(d.bg, d.fg, d.ansi, buildPalette(d.name, d.dark, {
|
|
36
31
|
primary: d.primary,
|
|
37
32
|
secondary: d.secondary,
|
|
38
33
|
accent: d.accent,
|
package/dist/widgets/button.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export declare class Button extends WidgetBase {
|
|
|
39
39
|
handleKey(event: KeyEvent): void;
|
|
40
40
|
handleMouse(event: WidgetMouseEvent): void;
|
|
41
41
|
setActive(value: boolean): void;
|
|
42
|
-
|
|
42
|
+
protected draw(_options: RenderOptions): Iterable<Segment>;
|
|
43
43
|
measure(_options: RenderOptions): {
|
|
44
44
|
minimum: number;
|
|
45
45
|
maximum: number;
|
package/dist/widgets/button.js
CHANGED
|
@@ -151,17 +151,19 @@ let Button = (() => {
|
|
|
151
151
|
if (event.type === "mouse_down") {
|
|
152
152
|
this.active = true;
|
|
153
153
|
}
|
|
154
|
+
// A release that dragged off the button cancels the press.
|
|
154
155
|
if (event.type === "mouse_up") {
|
|
155
156
|
if (this.active) {
|
|
156
157
|
this.active = false;
|
|
157
|
-
|
|
158
|
+
if (event.over)
|
|
159
|
+
this.emitSubmit();
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
// --- Active mutator (the keyboard-activation microtask uses this) ---
|
|
162
164
|
setActive(value) { this.active = value; }
|
|
163
165
|
// --- Rendering ---
|
|
164
|
-
|
|
166
|
+
draw(_options) {
|
|
165
167
|
// [LAW:dataflow-not-control-flow] same segment count and width every state
|
|
166
168
|
const focused = this.focused;
|
|
167
169
|
const left = focused ? "[" : " ";
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* disabled — dimmed
|
|
12
12
|
*/
|
|
13
13
|
import { Segment } from "../core/segment.js";
|
|
14
|
-
import type
|
|
14
|
+
import { type RenderOptions } from "../core/protocol.js";
|
|
15
15
|
import type { TerminalTheme } from "../core/color.js";
|
|
16
16
|
import { WidgetBase } from "./widget-base.js";
|
|
17
17
|
import type { KeyEvent, WidgetMouseEvent } from "./types.js";
|
|
@@ -32,7 +32,7 @@ export declare class Checkbox extends WidgetBase {
|
|
|
32
32
|
setTheme(theme: TerminalTheme): void;
|
|
33
33
|
handleKey(event: KeyEvent): void;
|
|
34
34
|
handleMouse(event: WidgetMouseEvent): void;
|
|
35
|
-
|
|
35
|
+
protected draw(options: RenderOptions): Iterable<Segment>;
|
|
36
36
|
measure(_options: RenderOptions): {
|
|
37
37
|
minimum: number;
|
|
38
38
|
maximum: number;
|
package/dist/widgets/checkbox.js
CHANGED
|
@@ -50,6 +50,7 @@ import { Style } from "../core/style.js";
|
|
|
50
50
|
import { ColorSpec } from "../core/color.js";
|
|
51
51
|
import { cellLen } from "../core/cells.js";
|
|
52
52
|
import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
|
|
53
|
+
import { drawable } from "../core/protocol.js";
|
|
53
54
|
import { WidgetBase } from "./widget-base.js";
|
|
54
55
|
let Checkbox = (() => {
|
|
55
56
|
let _classSuper = WidgetBase;
|
|
@@ -124,14 +125,15 @@ let Checkbox = (() => {
|
|
|
124
125
|
handleMouse(event) {
|
|
125
126
|
if (this.disabled)
|
|
126
127
|
return;
|
|
127
|
-
|
|
128
|
+
// A release that dragged off the checkbox cancels the press.
|
|
129
|
+
if (event.type === "mouse_up" && event.over) {
|
|
128
130
|
this.checked = !this.checked;
|
|
129
131
|
this.emitChange();
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
// --- Rendering ---
|
|
133
|
-
|
|
134
|
-
const indicator = this.checked ? (options
|
|
135
|
+
draw(options) {
|
|
136
|
+
const indicator = this.checked ? drawable(options, "✓", "x") : " ";
|
|
135
137
|
const text = `[${indicator}] ${this.label}`;
|
|
136
138
|
if (this.disabled) {
|
|
137
139
|
return [new Segment(text, new Style({ color: "#666666", bgcolor: "#333333", dim: true }))];
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* than the header. See docs/widgets.md → Dropdown.
|
|
24
24
|
*/
|
|
25
25
|
import { Segment } from "../core/segment.js";
|
|
26
|
-
import type
|
|
26
|
+
import { type RenderOptions } from "../core/protocol.js";
|
|
27
27
|
import type { TerminalTheme } from "../core/color.js";
|
|
28
28
|
import { WidgetBase } from "./widget-base.js";
|
|
29
29
|
import type { KeyEvent, WidgetMouseEvent, WidgetFocusEvent, OverlayRenderable } from "./types.js";
|
|
@@ -53,7 +53,7 @@ export declare class Dropdown extends WidgetBase implements OverlayRenderable {
|
|
|
53
53
|
handleKey(event: KeyEvent): void;
|
|
54
54
|
handleMouse(event: WidgetMouseEvent): void;
|
|
55
55
|
handleFocus(event: WidgetFocusEvent): void;
|
|
56
|
-
|
|
56
|
+
protected draw(options: RenderOptions): Iterable<Segment>;
|
|
57
57
|
private headerText;
|
|
58
58
|
renderOverlay(_options: RenderOptions): Iterable<Segment> | null;
|
|
59
59
|
private renderNoMatchRow;
|
package/dist/widgets/dropdown.js
CHANGED
|
@@ -62,6 +62,7 @@ import { Style } from "../core/style.js";
|
|
|
62
62
|
import { ColorSpec } from "../core/color.js";
|
|
63
63
|
import { cellLen, setCellSize, splitText, asCellCol } from "../core/cells.js";
|
|
64
64
|
import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
|
|
65
|
+
import { drawable } from "../core/protocol.js";
|
|
65
66
|
import { WidgetBase } from "./widget-base.js";
|
|
66
67
|
let Dropdown = (() => {
|
|
67
68
|
let _classSuper = WidgetBase;
|
|
@@ -263,13 +264,11 @@ let Dropdown = (() => {
|
|
|
263
264
|
return;
|
|
264
265
|
if (event.type !== "mouse_up")
|
|
265
266
|
return;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
event.y >= b.y &&
|
|
272
|
-
event.y < b.y + b.height;
|
|
267
|
+
// [LAW:one-source-of-truth] Whether the release is on this dropdown is
|
|
268
|
+
// read off the frame (`over`), not its own idea of its size; a press on
|
|
269
|
+
// it that dragged away releases elsewhere. Row 0 is the header, and the
|
|
270
|
+
// overlay rows continue below it.
|
|
271
|
+
const inside = event.over;
|
|
273
272
|
if (!this.expanded) {
|
|
274
273
|
if (inside) {
|
|
275
274
|
this.expanded = true;
|
|
@@ -284,15 +283,14 @@ let Dropdown = (() => {
|
|
|
284
283
|
this.expanded = false;
|
|
285
284
|
return;
|
|
286
285
|
}
|
|
287
|
-
|
|
288
|
-
if (rowOffset === 0) {
|
|
286
|
+
if (event.y === 0) {
|
|
289
287
|
// Click the header → collapse + clear filter, no selection change.
|
|
290
288
|
this.filter = "";
|
|
291
289
|
this.expanded = false;
|
|
292
290
|
return;
|
|
293
291
|
}
|
|
294
292
|
// [LAW:one-source-of-truth] Filtered position → canonical idx.
|
|
295
|
-
const picked = this.filteredOptions[
|
|
293
|
+
const picked = this.filteredOptions[event.y - 1];
|
|
296
294
|
if (picked === undefined)
|
|
297
295
|
return;
|
|
298
296
|
this.selectedIndex = picked.idx;
|
|
@@ -314,15 +312,14 @@ let Dropdown = (() => {
|
|
|
314
312
|
}
|
|
315
313
|
}
|
|
316
314
|
// --- Rendering ---
|
|
317
|
-
|
|
315
|
+
draw(options) {
|
|
318
316
|
// Header only — the inline footprint that flow layout sees. Always
|
|
319
317
|
// 1 row regardless of `expanded`/`filter`. Option rows live in
|
|
320
318
|
// renderOverlay. Width invariant: maxLabelLen + 4.
|
|
321
|
-
const arrowChar = options
|
|
322
|
-
// [LAW:one-source-of-truth]
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
const caret = this.focused ? (options.asciiOnly ? "|" : "│") : "";
|
|
319
|
+
const arrowChar = drawable(options, "▾", "v");
|
|
320
|
+
// [LAW:one-source-of-truth] Both glyphs are resolved here, once, and flow
|
|
321
|
+
// into headerText as data rather than headerText re-reading the options.
|
|
322
|
+
const caret = this.focused ? drawable(options, "│", "|") : "";
|
|
326
323
|
const maxLabelLen = this.maxLabelLen();
|
|
327
324
|
const baseStyle = this.disabled
|
|
328
325
|
? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EventRouter — parses raw stdin into KeyEvent /
|
|
3
|
-
* and dispatches them to widgets via
|
|
2
|
+
* EventRouter — parses raw stdin into KeyEvent / ScreenMouseEvent values
|
|
3
|
+
* and dispatches them to widgets via a FocusManager and the frame on screen.
|
|
4
|
+
*
|
|
5
|
+
* It reads the terminal and never sets it up: raw mode and mouse tracking are
|
|
6
|
+
* the runtime's (`App`), which undoes them on every path out.
|
|
4
7
|
*
|
|
5
8
|
* [LAW:single-enforcer] ANSI escape parsing lives only here — widgets never
|
|
6
9
|
* see raw bytes. One parser, one dispatch surface.
|
|
@@ -17,51 +20,40 @@
|
|
|
17
20
|
* if it extends the sequence. Tests can call `flush()` to drain synchronously.
|
|
18
21
|
*/
|
|
19
22
|
import { KeyEvent } from "./types.js";
|
|
20
|
-
import type {
|
|
23
|
+
import type { FrameSource, ScreenMouseEvent, KeyHandlerOptions } from "./types.js";
|
|
21
24
|
import type { Unsubscribe } from "../core/subscription.js";
|
|
22
25
|
import type { TerminalHost } from "../host/terminal-host.js";
|
|
23
26
|
type KeyHandler = (event: KeyEvent) => void;
|
|
24
|
-
type WidgetSource = {
|
|
25
|
-
focusManager: FocusManager;
|
|
26
|
-
getWidgets: () => readonly InteractiveWidget[];
|
|
27
|
-
};
|
|
28
27
|
export interface EventRouterOptions {
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The I/O capability the router reads input from and writes
|
|
32
|
-
* mouse-tracking sequences to. Construct a `NodeTerminalHost` for
|
|
33
|
-
* production node demos; tests pass a host that wraps mock streams.
|
|
34
|
-
*/
|
|
28
|
+
source: FrameSource;
|
|
29
|
+
/** The I/O capability the router reads input from. */
|
|
35
30
|
host: TerminalHost;
|
|
36
|
-
/**
|
|
37
|
-
* When true (default when the host reports `isTTY`), enable mouse
|
|
38
|
-
* tracking on `start()` and disable on `stop()`. Disable in tests /
|
|
39
|
-
* non-TTY environments.
|
|
40
|
-
*/
|
|
41
|
-
manageMouse?: boolean;
|
|
42
|
-
/**
|
|
43
|
-
* When true (default when the host reports `isTTY`), switch the host
|
|
44
|
-
* into raw mode on `start()` and restore on `stop()`.
|
|
45
|
-
*/
|
|
46
|
-
manageRawMode?: boolean;
|
|
47
31
|
}
|
|
48
32
|
export declare class EventRouter {
|
|
49
33
|
private readonly source;
|
|
50
34
|
private readonly host;
|
|
51
|
-
private readonly manageMouse;
|
|
52
|
-
private readonly manageRawMode;
|
|
53
35
|
private buffer;
|
|
54
36
|
private running;
|
|
55
37
|
private dataUnsubscribe;
|
|
56
38
|
private escTimer;
|
|
57
|
-
private
|
|
39
|
+
private captured;
|
|
40
|
+
private pointer;
|
|
41
|
+
private hoveredWidget;
|
|
58
42
|
private readonly keyChain;
|
|
59
43
|
private readonly mouseHandlers;
|
|
60
44
|
constructor(options: EventRouterOptions);
|
|
61
45
|
start(): void;
|
|
62
46
|
stop(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Read hover again off the frame just painted: under a pointer that has not
|
|
49
|
+
* moved, a widget may have gone, moved, or arrived.
|
|
50
|
+
*
|
|
51
|
+
* [LAW:one-source-of-truth] Hover is the widget under the pointer on the
|
|
52
|
+
* frame on screen, so it is re-read whenever either one changes.
|
|
53
|
+
*/
|
|
54
|
+
framePainted(): void;
|
|
63
55
|
onKey(handler: KeyHandler, options?: KeyHandlerOptions): Unsubscribe;
|
|
64
|
-
onMouse(handler: (event:
|
|
56
|
+
onMouse(handler: (event: ScreenMouseEvent) => void): Unsubscribe;
|
|
65
57
|
/** Feed a chunk of bytes (or a string of bytes) into the parser. */
|
|
66
58
|
feed(chunk: Uint8Array | string): void;
|
|
67
59
|
/** Force any pending lone ESC out of the buffer. */
|
|
@@ -74,5 +66,6 @@ export declare class EventRouter {
|
|
|
74
66
|
private consumeCSI;
|
|
75
67
|
private dispatchKey;
|
|
76
68
|
private dispatchMouse;
|
|
69
|
+
private hover;
|
|
77
70
|
}
|
|
78
71
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EventRouter — parses raw stdin into KeyEvent /
|
|
3
|
-
* and dispatches them to widgets via
|
|
2
|
+
* EventRouter — parses raw stdin into KeyEvent / ScreenMouseEvent values
|
|
3
|
+
* and dispatches them to widgets via a FocusManager and the frame on screen.
|
|
4
|
+
*
|
|
5
|
+
* It reads the terminal and never sets it up: raw mode and mouse tracking are
|
|
6
|
+
* the runtime's (`App`), which undoes them on every path out.
|
|
4
7
|
*
|
|
5
8
|
* [LAW:single-enforcer] ANSI escape parsing lives only here — widgets never
|
|
6
9
|
* see raw bytes. One parser, one dispatch surface.
|
|
@@ -17,6 +20,7 @@
|
|
|
17
20
|
* if it extends the sequence. Tests can call `flush()` to drain synchronously.
|
|
18
21
|
*/
|
|
19
22
|
import { KeyEvent } from "./types.js";
|
|
23
|
+
import { drew, originOf, scrollTargetAt, widgetAt } from "./hit.js";
|
|
20
24
|
// Single-byte → key name table for the trivial cases.
|
|
21
25
|
const SINGLE_BYTE_KEYS = {
|
|
22
26
|
0x09: "tab",
|
|
@@ -79,8 +83,6 @@ const LANGLE = 0x3c; // <
|
|
|
79
83
|
const O_BYTE = 0x4f; // O
|
|
80
84
|
const M_UPPER = 0x4d; // M
|
|
81
85
|
const M_LOWER = 0x6d; // m
|
|
82
|
-
const MOUSE_TRACK_ON = "\x1b[?1006h\x1b[?1000h\x1b[?1003h";
|
|
83
|
-
const MOUSE_TRACK_OFF = "\x1b[?1003l\x1b[?1000l\x1b[?1006l";
|
|
84
86
|
// [LAW:single-enforcer] Shared encode/decode singletons. The parser
|
|
85
87
|
// receives `Uint8Array | string` from the host and walks bytes through
|
|
86
88
|
// its state machine; conversion happens at the two boundaries (string
|
|
@@ -107,8 +109,6 @@ function concatBytes(a, b) {
|
|
|
107
109
|
export class EventRouter {
|
|
108
110
|
source;
|
|
109
111
|
host;
|
|
110
|
-
manageMouse;
|
|
111
|
-
manageRawMode;
|
|
112
112
|
// [LAW:types-are-the-program] The parse buffer is `Uint8Array`, not
|
|
113
113
|
// `Buffer` — node's Buffer extends Uint8Array, so Buffer chunks from
|
|
114
114
|
// process.stdin satisfy this type and the parser still works on node,
|
|
@@ -123,29 +123,25 @@ export class EventRouter {
|
|
|
123
123
|
// microtasks drain), without forcing a `setImmediate` polyfill on the
|
|
124
124
|
// browser host.
|
|
125
125
|
escTimer;
|
|
126
|
-
// [LAW:single-enforcer] Drag capture lives only here. A mouse_down's
|
|
127
|
-
//
|
|
128
|
-
// mouse_up route to
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
126
|
+
// [LAW:single-enforcer] Drag capture lives only here. A mouse_down's
|
|
127
|
+
// target is recorded; mouse_move/mouse_up between then and the next
|
|
128
|
+
// mouse_up route to it unconditionally and relative to where it is painted
|
|
129
|
+
// (`relocate`), so a drag past the widget's edge still drives its state
|
|
130
|
+
// (Slider's _dragging) and reports a column outside it. Cleared on the next
|
|
131
|
+
// mouse_up.
|
|
132
|
+
captured = null;
|
|
133
|
+
// Where the terminal last reported the pointer, and the widget under it on
|
|
134
|
+
// the frame on screen — the one widget `hovered` is true on.
|
|
135
|
+
pointer = null;
|
|
136
|
+
hoveredWidget = null;
|
|
132
137
|
// [LAW:dataflow-not-control-flow] Ordered chain; the dispatcher walks it
|
|
133
138
|
// in priority tiers and stops as soon as some participant calls
|
|
134
139
|
// `event.stop()`. Insertion order is preserved within each tier.
|
|
135
140
|
keyChain = [];
|
|
136
141
|
mouseHandlers = new Set();
|
|
137
142
|
constructor(options) {
|
|
138
|
-
|
|
139
|
-
this.source = isWidgetSource(screen)
|
|
140
|
-
? screen
|
|
141
|
-
: { focusManager: screen.focusManager, getWidgets: () => screen.widgets };
|
|
143
|
+
this.source = options.source;
|
|
142
144
|
this.host = options.host;
|
|
143
|
-
// [LAW:one-source-of-truth] The host owns the "is this a real
|
|
144
|
-
// terminal?" question. Both raw-mode and mouse-tracking defaults
|
|
145
|
-
// derive from the same isTTY — no separate input/output skew.
|
|
146
|
-
const isTTY = this.host.isTTY;
|
|
147
|
-
this.manageRawMode = options.manageRawMode ?? isTTY;
|
|
148
|
-
this.manageMouse = options.manageMouse ?? isTTY;
|
|
149
145
|
// [LAW:single-enforcer] FocusManager owns Tab/Shift+Tab traversal —
|
|
150
146
|
// register it as a normal-priority handler so it participates in the
|
|
151
147
|
// chain. This MUST be the first registration so it lands ahead of any
|
|
@@ -159,14 +155,8 @@ export class EventRouter {
|
|
|
159
155
|
if (this.running)
|
|
160
156
|
return;
|
|
161
157
|
this.running = true;
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// via host.onData triggers the underlying stream's flow on node, no-ops
|
|
165
|
-
// on transports that don't have a paused mode.
|
|
166
|
-
if (this.manageRawMode)
|
|
167
|
-
this.host.setRawMode(true);
|
|
168
|
-
if (this.manageMouse)
|
|
169
|
-
this.host.write(MOUSE_TRACK_ON);
|
|
158
|
+
// Subscribing starts the underlying stream flowing on node, and is all
|
|
159
|
+
// it takes on a transport with no paused mode.
|
|
170
160
|
this.dataUnsubscribe = this.host.onData((chunk) => this.feed(chunk));
|
|
171
161
|
}
|
|
172
162
|
stop() {
|
|
@@ -176,14 +166,11 @@ export class EventRouter {
|
|
|
176
166
|
this.dataUnsubscribe();
|
|
177
167
|
this.dataUnsubscribe = undefined;
|
|
178
168
|
}
|
|
179
|
-
if (this.manageMouse)
|
|
180
|
-
this.host.write(MOUSE_TRACK_OFF);
|
|
181
|
-
if (this.manageRawMode)
|
|
182
|
-
this.host.setRawMode(false);
|
|
183
169
|
}
|
|
184
170
|
// [LAW:one-source-of-truth] Per-session state belongs to one session.
|
|
185
171
|
// stop() leaves the router in the same shape a fresh construction
|
|
186
|
-
// would: empty parse buffer, no captured widget,
|
|
172
|
+
// would: empty parse buffer, no captured widget, nothing hovered, no
|
|
173
|
+
// pending ESC timer.
|
|
187
174
|
// All cleanup is outside the `running` guard because feed() can arm
|
|
188
175
|
// escTimer (and grow buffer) even when running is false — without
|
|
189
176
|
// those, a pre-start feed could leave a deferred escape firing into
|
|
@@ -194,7 +181,20 @@ export class EventRouter {
|
|
|
194
181
|
this.escTimer = undefined;
|
|
195
182
|
}
|
|
196
183
|
this.buffer = EMPTY_BYTES;
|
|
197
|
-
this.
|
|
184
|
+
this.captured = null;
|
|
185
|
+
this.pointer = null;
|
|
186
|
+
this.hover(null);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Read hover again off the frame just painted: under a pointer that has not
|
|
190
|
+
* moved, a widget may have gone, moved, or arrived.
|
|
191
|
+
*
|
|
192
|
+
* [LAW:one-source-of-truth] Hover is the widget under the pointer on the
|
|
193
|
+
* frame on screen, so it is re-read whenever either one changes.
|
|
194
|
+
*/
|
|
195
|
+
framePainted() {
|
|
196
|
+
const hit = this.pointer && widgetAt(this.source.frame, this.pointer.x, this.pointer.y);
|
|
197
|
+
this.hover(hit ? hit.widget : null);
|
|
198
198
|
}
|
|
199
199
|
// --- External hooks ---
|
|
200
200
|
onKey(handler, options) {
|
|
@@ -502,42 +502,59 @@ export class EventRouter {
|
|
|
502
502
|
dispatchMouse(event) {
|
|
503
503
|
for (const handler of this.mouseHandlers)
|
|
504
504
|
handler(event);
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
505
|
+
const frame = this.source.frame;
|
|
506
|
+
const hit = widgetAt(frame, event.x, event.y);
|
|
507
|
+
this.pointer = { x: event.x, y: event.y };
|
|
508
|
+
this.hover(hit?.widget ?? null);
|
|
509
|
+
// The wheel scrolls the innermost viewport under the pointer that can
|
|
510
|
+
// move, a line a notch, past any widget it shows there and whatever a
|
|
511
|
+
// drag captured. With none, nothing moved and there is nothing to paint.
|
|
512
|
+
if (event.type === "scroll_up" || event.type === "scroll_down") {
|
|
513
|
+
const lines = event.type === "scroll_down" ? 1 : -1;
|
|
514
|
+
const target = scrollTargetAt(frame, event.x, event.y, lines);
|
|
515
|
+
if (!target)
|
|
516
|
+
return;
|
|
517
|
+
target.scrollBy(lines);
|
|
518
|
+
this.source.refresh();
|
|
519
|
+
return;
|
|
516
520
|
}
|
|
517
521
|
// [LAW:dataflow-not-control-flow] Same pipeline every event; the
|
|
518
522
|
// capture value and event type pick the target. mouse_down opens a
|
|
519
523
|
// capture, mouse_up closes it, mouse_move in between routes to the
|
|
520
|
-
// captured widget regardless of pointer position.
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const target = useCapture
|
|
525
|
-
?
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
target.
|
|
524
|
+
// captured widget regardless of pointer position. A hit's origin puts the
|
|
525
|
+
// pressed cell at its own column and row, so every delivery is the same
|
|
526
|
+
// subtraction.
|
|
527
|
+
const useCapture = event.type === "mouse_move" || event.type === "mouse_up";
|
|
528
|
+
const target = (useCapture && this.captured ? relocate(frame, this.captured) : null) ??
|
|
529
|
+
(hit ? { widget: hit.widget, originX: event.x - hit.col, originY: event.y - hit.row } : null);
|
|
530
|
+
target?.widget.handleMouse({
|
|
531
|
+
...event,
|
|
532
|
+
x: event.x - target.originX,
|
|
533
|
+
y: event.y - target.originY,
|
|
534
|
+
over: drew(frame, target.widget, event.x, event.y),
|
|
535
|
+
});
|
|
529
536
|
if (event.type === "mouse_down")
|
|
530
|
-
this.
|
|
537
|
+
this.captured = target;
|
|
531
538
|
else if (event.type === "mouse_up")
|
|
532
|
-
this.
|
|
539
|
+
this.captured = null;
|
|
540
|
+
}
|
|
541
|
+
// [LAW:single-enforcer] The canonical setter on the widget is the only
|
|
542
|
+
// writer of `hovered`; this is the only caller, and only on a change.
|
|
543
|
+
hover(widget) {
|
|
544
|
+
if (widget === this.hoveredWidget)
|
|
545
|
+
return;
|
|
546
|
+
this.hoveredWidget?.setHovered(false);
|
|
547
|
+
widget?.setHovered(true);
|
|
548
|
+
this.hoveredWidget = widget;
|
|
533
549
|
}
|
|
534
550
|
}
|
|
535
551
|
// --- Helpers ---
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
552
|
+
// [LAW:one-source-of-truth] A captured widget's origin is read off the frame
|
|
553
|
+
// just painted, so a drag follows the widget if it moved since the press; one
|
|
554
|
+
// no longer painted is where the press found it.
|
|
555
|
+
function relocate(frame, target) {
|
|
556
|
+
const origin = originOf(frame, target.widget);
|
|
557
|
+
return origin ? { widget: target.widget, originX: origin.x, originY: origin.y } : target;
|
|
541
558
|
}
|
|
542
559
|
function decodeMouseFromCb(cb, x, y, isPress) {
|
|
543
560
|
const isMotion = (cb & 32) !== 0;
|
|
@@ -610,13 +627,3 @@ function decodeModifier(mod) {
|
|
|
610
627
|
ctrl: (bits & 4) !== 0,
|
|
611
628
|
};
|
|
612
629
|
}
|
|
613
|
-
function topmostHit(widgets, x, y) {
|
|
614
|
-
for (let i = widgets.length - 1; i >= 0; i--) {
|
|
615
|
-
const w = widgets[i];
|
|
616
|
-
if (!w.visible)
|
|
617
|
-
continue;
|
|
618
|
-
if (w.containsPoint(x, y))
|
|
619
|
-
return w;
|
|
620
|
-
}
|
|
621
|
-
return null;
|
|
622
|
-
}
|