@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.
- package/README.md +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
package/dist/host/app.js
CHANGED
|
@@ -23,68 +23,104 @@
|
|
|
23
23
|
* frame's encoding. What differs per host — whether the program can end
|
|
24
24
|
* under us, how it is suspended — is the `TerminalHost`'s, so the same app
|
|
25
25
|
* runs in node and in a browser with no path of its own for either.
|
|
26
|
+
*
|
|
27
|
+
* [LAW:single-enforcer] Input has one reader: the app subscribes to its
|
|
28
|
+
* host's bytes once and parses them (`host/input`), so a program hears keys
|
|
29
|
+
* and the pointer as events and never decodes a byte. The quit keys are
|
|
30
|
+
* checked here, before any handler hears the key, so nothing a program binds
|
|
31
|
+
* — a focused field in a `WidgetApp` included — can swallow them.
|
|
26
32
|
*/
|
|
33
|
+
import { frameRate, systemClock } from "../core/clock.js";
|
|
27
34
|
import { Console } from "../core/console.js";
|
|
28
35
|
import { Segment } from "../core/segment.js";
|
|
29
|
-
import {
|
|
36
|
+
import { SurfacePainter } from "../core/paint.js";
|
|
30
37
|
import { fitHeight } from "../core/protocol.js";
|
|
31
38
|
import { hostEnvironment } from "./host-environment.js";
|
|
39
|
+
import { InputParser, keyName, parseKeyName } from "./input.js";
|
|
40
|
+
// [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
|
|
41
|
+
// it knows, what happens to a style it cannot resolve, markup and
|
|
42
|
+
// highlighting — is the console's to say, so App takes those options as the
|
|
43
|
+
// console declares them. This list is both the type and what is copied: the
|
|
44
|
+
// host alone decides the console's size, colours and sink, so no other
|
|
45
|
+
// property of the options object reaches it, whatever the object carries.
|
|
46
|
+
const DRAW_OPTIONS = ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
|
|
47
|
+
function drawOptions(options) {
|
|
48
|
+
const picked = {};
|
|
49
|
+
const copy = (key) => {
|
|
50
|
+
picked[key] = options[key];
|
|
51
|
+
};
|
|
52
|
+
DRAW_OPTIONS.forEach(copy);
|
|
53
|
+
return picked;
|
|
54
|
+
}
|
|
55
|
+
const DEFAULT_RATE = frameRate(30);
|
|
56
|
+
const DEFAULT_QUIT_KEYS = ["ctrl+c"];
|
|
32
57
|
// Button presses, motion and the wheel, in the SGR encoding: coordinates as
|
|
33
58
|
// decimal numbers, so a terminal wider than 223 columns still reports them.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
leave: () => POINTER_OFF + "\x1b[?1049l",
|
|
41
|
-
home: () => "\x1b[H",
|
|
42
|
-
// The frame is every row of the screen, so there is nothing left under it
|
|
43
|
-
// — and after the terminal shrinks, painting the old count would scroll.
|
|
44
|
-
painted: (frameRows) => frameRows,
|
|
45
|
-
},
|
|
46
|
-
inline: {
|
|
47
|
-
exact: false,
|
|
48
|
-
enter: "",
|
|
49
|
-
// Below the frame, where the program's next line belongs.
|
|
50
|
-
leave: (rows) => (rows > 0 ? "\n" : ""),
|
|
51
|
-
// After N rows written with N-1 newlines between them, the cursor is on
|
|
52
|
-
// the last. `ESC[0A` still moves a row on some terminals, so a one-row
|
|
53
|
-
// frame is returned to with the carriage return alone.
|
|
54
|
-
home: (rows) => (rows > 1 ? `\x1b[${rows - 1}A\r` : "\r"),
|
|
55
|
-
// A shorter frame blanks the rows the last one left below it.
|
|
56
|
-
painted: (frameRows, lastRows) => Math.max(frameRows, lastRows),
|
|
57
|
-
},
|
|
59
|
+
// The terminal reports a pointer by its row on the screen, and an inline
|
|
60
|
+
// frame does not know which row of the screen it starts on, so only the
|
|
61
|
+
// alternate surface gets them.
|
|
62
|
+
const POINTER = {
|
|
63
|
+
alternate: { on: "\x1b[?1006h\x1b[?1000h\x1b[?1003h", off: "\x1b[?1003l\x1b[?1000l\x1b[?1006l" },
|
|
64
|
+
inline: { on: "", off: "" },
|
|
58
65
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
66
|
+
// The sideways wheel is not here: nothing scrolls by columns, inside an app or
|
|
67
|
+
// around it.
|
|
68
|
+
const WHEEL_LINES = { scroll_up: -1, scroll_down: 1 };
|
|
69
|
+
const isWheelTurn = (event) => event.type in WHEEL_LINES;
|
|
63
70
|
export class App {
|
|
64
71
|
host;
|
|
65
72
|
surface;
|
|
66
73
|
view;
|
|
74
|
+
clock;
|
|
75
|
+
_rate;
|
|
76
|
+
// What is animating now: one token per `animate()` not yet released.
|
|
77
|
+
animations = new Set();
|
|
78
|
+
// The timer painting frames, and the rate it was started at.
|
|
79
|
+
ticking = null;
|
|
67
80
|
// [LAW:one-source-of-truth] The host is the console's whole environment:
|
|
68
81
|
// its size, its colours, where bytes go. Every frame renders with this
|
|
69
82
|
// console's options and is encoded for its destination.
|
|
70
83
|
console;
|
|
71
84
|
_phase = "idle";
|
|
72
85
|
_frame = [];
|
|
73
|
-
|
|
74
|
-
rows = 0;
|
|
86
|
+
painter;
|
|
75
87
|
refreshQueued = false;
|
|
76
88
|
subscriptions = [];
|
|
77
89
|
paintHandlers = new Set();
|
|
90
|
+
keyHandlers = new Set();
|
|
91
|
+
mouseHandlers = new Set();
|
|
92
|
+
wheelHandlers = new Set();
|
|
93
|
+
quitKeys;
|
|
94
|
+
input = new InputParser({
|
|
95
|
+
key: (press) => this.key(press),
|
|
96
|
+
mouse: (event) => this.mouse(event),
|
|
97
|
+
});
|
|
78
98
|
settle = () => { };
|
|
79
99
|
constructor(options) {
|
|
80
100
|
this.host = options.host;
|
|
81
|
-
this.surface =
|
|
101
|
+
this.surface = options.surface;
|
|
102
|
+
this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
|
|
82
103
|
this.view = options.view;
|
|
83
|
-
this.
|
|
104
|
+
this.clock = options.clock ?? systemClock();
|
|
105
|
+
this._rate = options.rate ?? DEFAULT_RATE;
|
|
106
|
+
this.quitKeys = new Set((options.quitKeys ?? DEFAULT_QUIT_KEYS).map(parseKeyName));
|
|
107
|
+
this.console = new Console({ ...drawOptions(options), environment: hostEnvironment(options.host) });
|
|
84
108
|
}
|
|
85
109
|
get phase() {
|
|
86
110
|
return this._phase;
|
|
87
111
|
}
|
|
112
|
+
/** How often the app paints while something is animating. */
|
|
113
|
+
get rate() {
|
|
114
|
+
return this._rate;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Set how often the app paints: an animation under way goes on at the new
|
|
118
|
+
* rate from its next frame, without a frame lost or one painted twice.
|
|
119
|
+
*/
|
|
120
|
+
set rate(rate) {
|
|
121
|
+
this._rate = rate;
|
|
122
|
+
this.tick();
|
|
123
|
+
}
|
|
88
124
|
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
89
125
|
get frame() {
|
|
90
126
|
return this._frame;
|
|
@@ -116,6 +152,7 @@ export class App {
|
|
|
116
152
|
this._phase = "running";
|
|
117
153
|
this.host.start();
|
|
118
154
|
this.subscriptions = [
|
|
155
|
+
this.host.onData((chunk) => this.input.feed(chunk)),
|
|
119
156
|
this.host.onResize(() => this.refresh()),
|
|
120
157
|
// The program is ending under the app — by a signal, a crash, or an
|
|
121
158
|
// exit the app's own code never saw. Ending hands the terminal back,
|
|
@@ -124,6 +161,7 @@ export class App {
|
|
|
124
161
|
];
|
|
125
162
|
this.enter();
|
|
126
163
|
this.paint();
|
|
164
|
+
this.tick();
|
|
127
165
|
});
|
|
128
166
|
}
|
|
129
167
|
/**
|
|
@@ -140,6 +178,10 @@ export class App {
|
|
|
140
178
|
return;
|
|
141
179
|
this.refreshQueued = true;
|
|
142
180
|
setTimeout(() => {
|
|
181
|
+
// A frame painted since it was asked for — a tick — drew every change
|
|
182
|
+
// made before it, so there is nothing left to paint.
|
|
183
|
+
if (!this.refreshQueued)
|
|
184
|
+
return;
|
|
143
185
|
this.refreshQueued = false;
|
|
144
186
|
// [LAW:types-are-the-program] Only a running app holds the terminal; a
|
|
145
187
|
// suspended one repaints when it resumes, and a stopped one never does.
|
|
@@ -147,6 +189,24 @@ export class App {
|
|
|
147
189
|
this.paint();
|
|
148
190
|
});
|
|
149
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Paint at the app's `rate` until the returned function is called: what an
|
|
194
|
+
* effect sampled at the frame's time asks for while it moves. The app ticks
|
|
195
|
+
* while anything is animating and it holds the terminal, and stops when
|
|
196
|
+
* nothing is, so a still app paints only when asked. The animation's first
|
|
197
|
+
* frame does not wait out an interval: at one frame every two seconds, that
|
|
198
|
+
* would hold its start off screen for two.
|
|
199
|
+
*/
|
|
200
|
+
animate() {
|
|
201
|
+
const animation = {};
|
|
202
|
+
this.animations.add(animation);
|
|
203
|
+
this.tick();
|
|
204
|
+
this.refresh();
|
|
205
|
+
return () => {
|
|
206
|
+
this.animations.delete(animation);
|
|
207
|
+
this.tick();
|
|
208
|
+
};
|
|
209
|
+
}
|
|
150
210
|
/**
|
|
151
211
|
* Hand the terminal to whatever launched the program, and take it back and
|
|
152
212
|
* repaint when that resumes it. What sends the app here is the app's own —
|
|
@@ -156,6 +216,7 @@ export class App {
|
|
|
156
216
|
if (this._phase !== "running")
|
|
157
217
|
return;
|
|
158
218
|
this._phase = "suspended";
|
|
219
|
+
this.tick();
|
|
159
220
|
this.leave();
|
|
160
221
|
await this.host.suspend();
|
|
161
222
|
// Stopped while suspended — by a signal, say: the terminal is already
|
|
@@ -163,11 +224,9 @@ export class App {
|
|
|
163
224
|
if (this._phase !== "suspended")
|
|
164
225
|
return;
|
|
165
226
|
this._phase = "running";
|
|
166
|
-
// What the last frame occupied is gone under whatever ran meanwhile, so
|
|
167
|
-
// the next one starts where the cursor is.
|
|
168
|
-
this.rows = 0;
|
|
169
227
|
this.enter();
|
|
170
228
|
this.paint();
|
|
229
|
+
this.tick();
|
|
171
230
|
}
|
|
172
231
|
/**
|
|
173
232
|
* Hear each frame once it is on screen — what `frame` now returns. A
|
|
@@ -177,6 +236,35 @@ export class App {
|
|
|
177
236
|
this.paintHandlers.add(handler);
|
|
178
237
|
return () => this.paintHandlers.delete(handler);
|
|
179
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Hear every key the app does not quit on, by the `KeyPress` the terminal
|
|
241
|
+
* reported; `keyName` names it. A handler that throws fails the app.
|
|
242
|
+
*/
|
|
243
|
+
onKey(handler) {
|
|
244
|
+
this.keyHandlers.add(handler);
|
|
245
|
+
return () => this.keyHandlers.delete(handler);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Hear every pointer event but the vertical wheel, which is `onWheel`'s,
|
|
249
|
+
* in terminal cells. Only the alternate surface reports the pointer. A
|
|
250
|
+
* handler that throws fails the app.
|
|
251
|
+
*/
|
|
252
|
+
onMouse(handler) {
|
|
253
|
+
this.mouseHandlers.add(handler);
|
|
254
|
+
return () => this.mouseHandlers.delete(handler);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Offer every wheel turn to `handler`, which returns whether it moved
|
|
258
|
+
* something. A turn no handler moves anything with is passed outward to the
|
|
259
|
+
* host (`TerminalHost.passWheel`), so the page or terminal around the app
|
|
260
|
+
* scrolls instead. Handlers are offered a turn in the order they subscribed,
|
|
261
|
+
* and the first that moves something takes it. A handler that throws fails
|
|
262
|
+
* the app.
|
|
263
|
+
*/
|
|
264
|
+
onWheel(handler) {
|
|
265
|
+
this.wheelHandlers.add(handler);
|
|
266
|
+
return () => this.wheelHandlers.delete(handler);
|
|
267
|
+
}
|
|
180
268
|
/** Hand the terminal back for good; `run` resolves. */
|
|
181
269
|
stop() {
|
|
182
270
|
this.end({ kind: "stopped" });
|
|
@@ -193,26 +281,91 @@ export class App {
|
|
|
193
281
|
// --- the terminal ---
|
|
194
282
|
enter() {
|
|
195
283
|
this.host.setRawMode(true);
|
|
196
|
-
this.
|
|
284
|
+
this.painter.take();
|
|
285
|
+
this.host.write(POINTER[this.surface].on);
|
|
197
286
|
}
|
|
198
287
|
leave() {
|
|
199
|
-
this.host.write(
|
|
288
|
+
this.host.write(POINTER[this.surface].off);
|
|
289
|
+
this.painter.handBack();
|
|
200
290
|
this.host.setRawMode(false);
|
|
201
291
|
}
|
|
202
292
|
end(outcome) {
|
|
203
293
|
const phase = this._phase;
|
|
204
|
-
|
|
294
|
+
// [LAW:no-silent-failure] An error after the app ended has no `run` left
|
|
295
|
+
// to reject, so it is thrown to whoever raised it instead of dropped.
|
|
296
|
+
if (phase === "idle" || phase === "stopped") {
|
|
297
|
+
if (outcome.kind === "failed")
|
|
298
|
+
throw outcome.error;
|
|
205
299
|
return;
|
|
300
|
+
}
|
|
206
301
|
this._phase = "stopped";
|
|
302
|
+
this.tick();
|
|
207
303
|
for (const unsubscribe of this.subscriptions)
|
|
208
304
|
unsubscribe();
|
|
209
305
|
this.subscriptions = [];
|
|
306
|
+
this.input.reset();
|
|
210
307
|
// A suspended app handed the terminal back when it suspended.
|
|
211
308
|
if (phase === "running")
|
|
212
309
|
this.leave();
|
|
213
310
|
this.host.stop();
|
|
214
311
|
this.settle(outcome);
|
|
215
312
|
}
|
|
313
|
+
// --- input ---
|
|
314
|
+
key(press) {
|
|
315
|
+
if (this.quitKeys.has(keyName(press)))
|
|
316
|
+
return this.stop();
|
|
317
|
+
this.hear(this.keyHandlers, press);
|
|
318
|
+
}
|
|
319
|
+
// [LAW:dataflow-not-control-flow] One rule for every app: a wheel turn
|
|
320
|
+
// nothing in the app moved with goes outward. The vertical wheel reaches the
|
|
321
|
+
// program through `onWheel` alone, so moving something with it and claiming
|
|
322
|
+
// the turn are one act, and an app that reads no wheel passes every turn.
|
|
323
|
+
// [LAW:single-enforcer] Only a running app holds the terminal, so only a
|
|
324
|
+
// running app is offered a turn or passes one on.
|
|
325
|
+
mouse(event) {
|
|
326
|
+
if (!isWheelTurn(event))
|
|
327
|
+
return this.hear(this.mouseHandlers, event);
|
|
328
|
+
if (this._phase !== "running")
|
|
329
|
+
return;
|
|
330
|
+
const lines = WHEEL_LINES[event.type];
|
|
331
|
+
const wheel = { x: event.x, y: event.y, lines };
|
|
332
|
+
try {
|
|
333
|
+
const moved = [...this.wheelHandlers].some((handler) => this._phase === "running" && handler(wheel));
|
|
334
|
+
if (!moved && this._phase === "running")
|
|
335
|
+
this.host.passWheel(lines);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
this.fail(error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// [LAW:no-silent-failure] The host calls these handlers, not the program,
|
|
342
|
+
// so one that throws ends the app with its error, as a frame that throws
|
|
343
|
+
// does. A handler that stops the app is the last to hear the event.
|
|
344
|
+
hear(handlers, event) {
|
|
345
|
+
try {
|
|
346
|
+
for (const handler of [...handlers])
|
|
347
|
+
if (this._phase !== "stopped")
|
|
348
|
+
handler(event);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
this.fail(error);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// [LAW:single-enforcer] Whether the clock is ticking, and at what rate,
|
|
355
|
+
// follows from the phase, what is animating and the rate, and is brought
|
|
356
|
+
// in line with them here alone, after every change to any — so a timer
|
|
357
|
+
// runs exactly while a running app has something animating, at the rate
|
|
358
|
+
// the app has now, and none outlives the app.
|
|
359
|
+
tick() {
|
|
360
|
+
const wanted = this._phase === "running" && this.animations.size > 0;
|
|
361
|
+
if (this.ticking !== null && (!wanted || this.ticking.rate.perSecond !== this._rate.perSecond)) {
|
|
362
|
+
this.ticking.stop();
|
|
363
|
+
this.ticking = null;
|
|
364
|
+
}
|
|
365
|
+
if (wanted && this.ticking === null) {
|
|
366
|
+
this.ticking = { rate: this._rate, stop: this.clock.every(this._rate, () => this.paint()) };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
216
369
|
// [LAW:no-silent-failure] A frame that throws ends the app with that
|
|
217
370
|
// error: the terminal is handed back, and `run` rejects with it.
|
|
218
371
|
paint() {
|
|
@@ -224,31 +377,18 @@ export class App {
|
|
|
224
377
|
}
|
|
225
378
|
}
|
|
226
379
|
draw() {
|
|
380
|
+
// This frame draws every change made before it, a queued refresh's included.
|
|
381
|
+
this.refreshQueued = false;
|
|
227
382
|
const options = this.console.options;
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
383
|
+
const height = this.painter.height(options.height.rows);
|
|
384
|
+
// [LAW:no-ambient-temporal-coupling] The frame owner reads the clock,
|
|
385
|
+
// once a frame; the view is handed the time as data.
|
|
386
|
+
const lines = Segment.splitLines(this.view(this.clock.now()).render({ ...options, height }));
|
|
231
387
|
// The app set the budget, so the app shapes what comes back: no deeper
|
|
232
|
-
// than the terminal — a taller frame would scroll the rows
|
|
233
|
-
// back over — and, as a region, exactly that deep.
|
|
234
|
-
|
|
235
|
-
// down a row the frame does not know about.
|
|
236
|
-
const frame = fitHeight(lines.slice(0, height.rows), height).map((line) => Segment.adjustLineLength(line, cols, undefined, false));
|
|
237
|
-
const painted = Math.min(this.surface.painted(frame.length, this.rows), height.rows);
|
|
238
|
-
// Each row is erased as it is reached rather than the frame cleared
|
|
239
|
-
// first, so no blank screen shows between two frames. The erase leads
|
|
240
|
-
// its row: after a row that fills the width, the cursor sits on its last
|
|
241
|
-
// cell, and an erase there would take it.
|
|
242
|
-
const destination = this.console.destination;
|
|
243
|
-
const body = Array.from({ length: painted }, (_, row) => ERASE_LINE + segmentsToString(frame[row] ?? [], destination)).join("\n");
|
|
244
|
-
// Rows blanked below the frame are not the frame's: the cursor goes back
|
|
245
|
-
// up to its last row, so the next frame and the program's next line start
|
|
246
|
-
// from the frame's own height.
|
|
247
|
-
const blanked = painted - Math.max(frame.length, 1);
|
|
248
|
-
const back = blanked > 0 ? `\x1b[${blanked}A` : "";
|
|
249
|
-
this.host.write(this.surface.home(this.rows) + body + back);
|
|
388
|
+
// than the terminal — a taller frame would scroll the rows the painter
|
|
389
|
+
// goes back over — and, as a region, exactly that deep.
|
|
390
|
+
const frame = this.painter.paint(() => fitHeight(lines.slice(0, height.rows), height), { rows: height.rows, cols: options.maxWidth }, this.console.destination);
|
|
250
391
|
this._frame = frame;
|
|
251
|
-
this.rows = frame.length;
|
|
252
392
|
for (const handler of [...this.paintHandlers])
|
|
253
393
|
handler(frame);
|
|
254
394
|
}
|
package/dist/host/index.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ export { BrowserTerminalHost } from "./terminal-host.js";
|
|
|
2
2
|
export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
3
3
|
export { hostEnvironment } from "./host-environment.js";
|
|
4
4
|
export { App } from "./app.js";
|
|
5
|
-
export type { AppOptions, AppPhase,
|
|
5
|
+
export type { AppMouseEvent, AppOptions, AppPhase, Wheel } from "./app.js";
|
|
6
|
+
export type { Surface } from "../core/paint.js";
|
|
7
|
+
export { keyName } from "./input.js";
|
|
8
|
+
export type { KeyPress, ScreenMouseEvent } from "./input.js";
|
package/dist/host/index.js
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal input: the bytes a terminal sends for keys and the pointer,
|
|
3
|
+
* parsed into `KeyPress` and `ScreenMouseEvent` values, and the one name
|
|
4
|
+
* each key goes by.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:single-enforcer] Escape-sequence parsing lives only here. `App` owns
|
|
7
|
+
* the one parser over its host's input; a `WidgetApp` routes the events `App`
|
|
8
|
+
* hears, and no program reads its terminal's bytes itself.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:dataflow-not-control-flow] The same parse pipeline runs for every
|
|
11
|
+
* input chunk: append → consume one event at a time until the buffer can't
|
|
12
|
+
* yield more. The byte's value, not a side-mode, decides which event is
|
|
13
|
+
* emitted. There is no "are we mid-sequence" mode flag — the buffer head
|
|
14
|
+
* carries all the state we need, so a UTF-8 character or an escape sequence
|
|
15
|
+
* split across two chunks is completed by the second.
|
|
16
|
+
*
|
|
17
|
+
* Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
|
|
18
|
+
* CSI sequence). When the buffer is drained but ends with a lone ESC, the
|
|
19
|
+
* parser defers emission via setTimeout(0). The next chunk cancels the timer
|
|
20
|
+
* if it extends the sequence. `flush()` drains it synchronously.
|
|
21
|
+
*/
|
|
22
|
+
/** A key as the terminal reported it: plain data, with no say over who hears it. */
|
|
23
|
+
export interface KeyPress {
|
|
24
|
+
readonly key: string;
|
|
25
|
+
/** The printable character the key types, or "" for a key that types none. */
|
|
26
|
+
readonly character: string;
|
|
27
|
+
readonly shift: boolean;
|
|
28
|
+
readonly ctrl: boolean;
|
|
29
|
+
readonly meta: boolean;
|
|
30
|
+
}
|
|
31
|
+
export type ScreenMouseEvent = {
|
|
32
|
+
[T in MouseEventType]: MouseEventOf<T>;
|
|
33
|
+
}[MouseEventType];
|
|
34
|
+
type MouseEventType = "mouse_down" | "mouse_up" | "mouse_move" | "scroll_up" | "scroll_down" | "scroll_left" | "scroll_right";
|
|
35
|
+
interface MouseEventOf<Type extends string> {
|
|
36
|
+
readonly type: Type;
|
|
37
|
+
readonly x: number;
|
|
38
|
+
readonly y: number;
|
|
39
|
+
readonly button: number;
|
|
40
|
+
readonly shift: boolean;
|
|
41
|
+
readonly ctrl: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The one name a key goes by — what a keymap is keyed by and what
|
|
45
|
+
* `quitKeys` lists. A key that types a character is named by it (`q`, `G`,
|
|
46
|
+
* `>`); any other is named by its key (`up`, `enter`, `space`, `f5`). Held
|
|
47
|
+
* modifiers come first, always in the order `ctrl+`, `meta+`, `shift+`:
|
|
48
|
+
* `ctrl+c`, `shift+tab`, `meta+left`. Shift is not named on a character,
|
|
49
|
+
* which already carries it: Shift+G is `G`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function keyName(press: KeyPress): string;
|
|
52
|
+
/** Where a parser delivers what it parses. */
|
|
53
|
+
export interface InputSink {
|
|
54
|
+
key(press: KeyPress): void;
|
|
55
|
+
mouse(event: ScreenMouseEvent): void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `name`, if `keyName` can produce it: modifiers in its order, then a named
|
|
59
|
+
* key or one glyph, with no `shift+` on a glyph and, after `ctrl+`, only a
|
|
60
|
+
* glyph a control byte stands for.
|
|
61
|
+
* [LAW:parse-dont-validate] A name no key can produce throws here, at the
|
|
62
|
+
* one place a program hands names in, rather than binding a key that never fires.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseKeyName(name: string): string;
|
|
65
|
+
export declare class InputParser {
|
|
66
|
+
private readonly sink;
|
|
67
|
+
private buffer;
|
|
68
|
+
private escTimer;
|
|
69
|
+
constructor(sink: InputSink);
|
|
70
|
+
/** Feed a chunk of bytes (or a string of them) into the parser. */
|
|
71
|
+
feed(chunk: Uint8Array | string): void;
|
|
72
|
+
/** Force any pending lone ESC out of the buffer. */
|
|
73
|
+
flush(): void;
|
|
74
|
+
/** Drop what is buffered and any pending lone ESC: the parser as constructed. */
|
|
75
|
+
reset(): void;
|
|
76
|
+
private cancelEscape;
|
|
77
|
+
private drain;
|
|
78
|
+
private consumeOne;
|
|
79
|
+
private consumePrintable;
|
|
80
|
+
private consumeEscape;
|
|
81
|
+
private consumeSS3;
|
|
82
|
+
private consumeCSI;
|
|
83
|
+
}
|
|
84
|
+
export {};
|