@promptctl/rich-js 0.19.0 → 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 +3 -2
- package/dist/core/color.d.ts +1 -0
- package/dist/core/color.js +3 -2
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/paint.js +13 -2
- package/dist/core/pretty.d.ts +23 -2
- package/dist/core/pretty.js +54 -18
- package/dist/core/protocol.d.ts +2 -0
- package/dist/core/protocol.js +3 -2
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +103 -72
- package/dist/core/strip.js +240 -212
- package/dist/core/style.js +9 -7
- package/dist/host/app.d.ts +68 -2
- package/dist/host/app.js +125 -15
- package/dist/host/index.d.ts +3 -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 +12 -0
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -3
- package/dist/node/terminal-host.d.ts +7 -0
- package/dist/node/terminal-host.js +7 -0
- package/dist/renderables/effect.d.ts +23 -8
- package/dist/renderables/effect.js +48 -26
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +11 -5
- package/dist/renderables/embed.js +13 -6
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/progress.d.ts +6 -0
- package/dist/renderables/progress.js +11 -4
- package/dist/renderables/progressBar.d.ts +22 -2
- package/dist/renderables/progressBar.js +51 -7
- package/dist/renderables/rule.d.ts +2 -2
- package/dist/renderables/rule.js +12 -10
- package/dist/renderables/table.js +5 -7
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +59 -3
- package/dist/widgets/dropdown.js +3 -3
- 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/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +22 -7
- package/dist/widgets/widget-app.js +22 -12
- package/package.json +22 -18
package/dist/host/app.d.ts
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
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
|
*/
|
|
27
33
|
import { type Clock, type FrameRate } from "../core/clock.js";
|
|
28
34
|
import { type ConsoleOptions } from "../core/console.js";
|
|
@@ -30,6 +36,7 @@ import { Segment } from "../core/segment.js";
|
|
|
30
36
|
import { type Surface } from "../core/paint.js";
|
|
31
37
|
import { type Renderable } from "../core/protocol.js";
|
|
32
38
|
import type { Unsubscribe } from "../core/subscription.js";
|
|
39
|
+
import { type KeyPress, type ScreenMouseEvent } from "./input.js";
|
|
33
40
|
import type { TerminalHost } from "./terminal-host.js";
|
|
34
41
|
/**
|
|
35
42
|
* Where an app is in its life. `idle` has not run; `running` holds the
|
|
@@ -55,15 +62,39 @@ export interface AppOptions extends DrawOptions {
|
|
|
55
62
|
readonly clock?: Clock;
|
|
56
63
|
/** How often the app paints while something is animating. 30 frames a second, unless given. */
|
|
57
64
|
readonly rate?: FrameRate;
|
|
65
|
+
/**
|
|
66
|
+
* The keys that stop the app, by `keyName`, heard before any `onKey`
|
|
67
|
+
* handler. `["ctrl+c"]` unless given; `[]` leaves every key to the program.
|
|
68
|
+
* A name no key goes by throws here.
|
|
69
|
+
*/
|
|
70
|
+
readonly quitKeys?: readonly string[];
|
|
58
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A turn of the wheel, where the pointer was when it turned: `lines` is
|
|
74
|
+
* positive towards the end of the content, a line a notch.
|
|
75
|
+
*/
|
|
76
|
+
export interface Wheel {
|
|
77
|
+
readonly x: number;
|
|
78
|
+
readonly y: number;
|
|
79
|
+
readonly lines: number;
|
|
80
|
+
}
|
|
81
|
+
declare const WHEEL_LINES: {
|
|
82
|
+
readonly scroll_up: -1;
|
|
83
|
+
readonly scroll_down: 1;
|
|
84
|
+
};
|
|
85
|
+
type WheelTurn = Extract<ScreenMouseEvent, {
|
|
86
|
+
type: keyof typeof WHEEL_LINES;
|
|
87
|
+
}>;
|
|
88
|
+
/** A pointer event `onMouse` hears: every one but the vertical wheel, which is `onWheel`'s. */
|
|
89
|
+
export type AppMouseEvent = Exclude<ScreenMouseEvent, WheelTurn>;
|
|
59
90
|
export declare class App {
|
|
60
91
|
private readonly host;
|
|
61
92
|
private readonly surface;
|
|
62
93
|
private readonly view;
|
|
63
94
|
private readonly clock;
|
|
64
|
-
private
|
|
95
|
+
private _rate;
|
|
65
96
|
private readonly animations;
|
|
66
|
-
private
|
|
97
|
+
private ticking;
|
|
67
98
|
private readonly console;
|
|
68
99
|
private _phase;
|
|
69
100
|
private _frame;
|
|
@@ -71,9 +102,21 @@ export declare class App {
|
|
|
71
102
|
private refreshQueued;
|
|
72
103
|
private subscriptions;
|
|
73
104
|
private readonly paintHandlers;
|
|
105
|
+
private readonly keyHandlers;
|
|
106
|
+
private readonly mouseHandlers;
|
|
107
|
+
private readonly wheelHandlers;
|
|
108
|
+
private readonly quitKeys;
|
|
109
|
+
private readonly input;
|
|
74
110
|
private settle;
|
|
75
111
|
constructor(options: AppOptions);
|
|
76
112
|
get phase(): AppPhase;
|
|
113
|
+
/** How often the app paints while something is animating. */
|
|
114
|
+
get rate(): FrameRate;
|
|
115
|
+
/**
|
|
116
|
+
* Set how often the app paints: an animation under way goes on at the new
|
|
117
|
+
* rate from its next frame, without a frame lost or one painted twice.
|
|
118
|
+
*/
|
|
119
|
+
set rate(rate: FrameRate);
|
|
77
120
|
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
78
121
|
get frame(): readonly (readonly Segment[])[];
|
|
79
122
|
/**
|
|
@@ -114,6 +157,26 @@ export declare class App {
|
|
|
114
157
|
* handler that throws fails the app as a frame that throws does.
|
|
115
158
|
*/
|
|
116
159
|
onPaint(handler: (frame: readonly (readonly Segment[])[]) => void): Unsubscribe;
|
|
160
|
+
/**
|
|
161
|
+
* Hear every key the app does not quit on, by the `KeyPress` the terminal
|
|
162
|
+
* reported; `keyName` names it. A handler that throws fails the app.
|
|
163
|
+
*/
|
|
164
|
+
onKey(handler: (press: KeyPress) => void): Unsubscribe;
|
|
165
|
+
/**
|
|
166
|
+
* Hear every pointer event but the vertical wheel, which is `onWheel`'s,
|
|
167
|
+
* in terminal cells. Only the alternate surface reports the pointer. A
|
|
168
|
+
* handler that throws fails the app.
|
|
169
|
+
*/
|
|
170
|
+
onMouse(handler: (event: AppMouseEvent) => void): Unsubscribe;
|
|
171
|
+
/**
|
|
172
|
+
* Offer every wheel turn to `handler`, which returns whether it moved
|
|
173
|
+
* something. A turn no handler moves anything with is passed outward to the
|
|
174
|
+
* host (`TerminalHost.passWheel`), so the page or terminal around the app
|
|
175
|
+
* scrolls instead. Handlers are offered a turn in the order they subscribed,
|
|
176
|
+
* and the first that moves something takes it. A handler that throws fails
|
|
177
|
+
* the app.
|
|
178
|
+
*/
|
|
179
|
+
onWheel(handler: (wheel: Wheel) => boolean): Unsubscribe;
|
|
117
180
|
/** Hand the terminal back for good; `run` resolves. */
|
|
118
181
|
stop(): void;
|
|
119
182
|
/**
|
|
@@ -126,6 +189,9 @@ export declare class App {
|
|
|
126
189
|
private enter;
|
|
127
190
|
private leave;
|
|
128
191
|
private end;
|
|
192
|
+
private key;
|
|
193
|
+
private mouse;
|
|
194
|
+
private hear;
|
|
129
195
|
private tick;
|
|
130
196
|
private paint;
|
|
131
197
|
private draw;
|
package/dist/host/app.js
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
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
|
*/
|
|
27
33
|
import { frameRate, systemClock } from "../core/clock.js";
|
|
28
34
|
import { Console } from "../core/console.js";
|
|
@@ -30,6 +36,7 @@ import { Segment } from "../core/segment.js";
|
|
|
30
36
|
import { SurfacePainter } from "../core/paint.js";
|
|
31
37
|
import { fitHeight } from "../core/protocol.js";
|
|
32
38
|
import { hostEnvironment } from "./host-environment.js";
|
|
39
|
+
import { InputParser, keyName, parseKeyName } from "./input.js";
|
|
33
40
|
// [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
|
|
34
41
|
// it knows, what happens to a style it cannot resolve, markup and
|
|
35
42
|
// highlighting — is the console's to say, so App takes those options as the
|
|
@@ -46,6 +53,7 @@ function drawOptions(options) {
|
|
|
46
53
|
return picked;
|
|
47
54
|
}
|
|
48
55
|
const DEFAULT_RATE = frameRate(30);
|
|
56
|
+
const DEFAULT_QUIT_KEYS = ["ctrl+c"];
|
|
49
57
|
// Button presses, motion and the wheel, in the SGR encoding: coordinates as
|
|
50
58
|
// decimal numbers, so a terminal wider than 223 columns still reports them.
|
|
51
59
|
// The terminal reports a pointer by its row on the screen, and an inline
|
|
@@ -55,15 +63,20 @@ const POINTER = {
|
|
|
55
63
|
alternate: { on: "\x1b[?1006h\x1b[?1000h\x1b[?1003h", off: "\x1b[?1003l\x1b[?1000l\x1b[?1006l" },
|
|
56
64
|
inline: { on: "", off: "" },
|
|
57
65
|
};
|
|
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;
|
|
58
70
|
export class App {
|
|
59
71
|
host;
|
|
60
72
|
surface;
|
|
61
73
|
view;
|
|
62
74
|
clock;
|
|
63
|
-
|
|
75
|
+
_rate;
|
|
64
76
|
// What is animating now: one token per `animate()` not yet released.
|
|
65
77
|
animations = new Set();
|
|
66
|
-
|
|
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.
|
|
@@ -74,6 +87,14 @@ export class App {
|
|
|
74
87
|
refreshQueued = false;
|
|
75
88
|
subscriptions = [];
|
|
76
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
|
+
});
|
|
77
98
|
settle = () => { };
|
|
78
99
|
constructor(options) {
|
|
79
100
|
this.host = options.host;
|
|
@@ -81,12 +102,25 @@ export class App {
|
|
|
81
102
|
this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
|
|
82
103
|
this.view = options.view;
|
|
83
104
|
this.clock = options.clock ?? systemClock();
|
|
84
|
-
this.
|
|
105
|
+
this._rate = options.rate ?? DEFAULT_RATE;
|
|
106
|
+
this.quitKeys = new Set((options.quitKeys ?? DEFAULT_QUIT_KEYS).map(parseKeyName));
|
|
85
107
|
this.console = new Console({ ...drawOptions(options), environment: hostEnvironment(options.host) });
|
|
86
108
|
}
|
|
87
109
|
get phase() {
|
|
88
110
|
return this._phase;
|
|
89
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
|
+
}
|
|
90
124
|
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
91
125
|
get frame() {
|
|
92
126
|
return this._frame;
|
|
@@ -118,6 +152,7 @@ export class App {
|
|
|
118
152
|
this._phase = "running";
|
|
119
153
|
this.host.start();
|
|
120
154
|
this.subscriptions = [
|
|
155
|
+
this.host.onData((chunk) => this.input.feed(chunk)),
|
|
121
156
|
this.host.onResize(() => this.refresh()),
|
|
122
157
|
// The program is ending under the app — by a signal, a crash, or an
|
|
123
158
|
// exit the app's own code never saw. Ending hands the terminal back,
|
|
@@ -201,6 +236,35 @@ export class App {
|
|
|
201
236
|
this.paintHandlers.add(handler);
|
|
202
237
|
return () => this.paintHandlers.delete(handler);
|
|
203
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
|
+
}
|
|
204
268
|
/** Hand the terminal back for good; `run` resolves. */
|
|
205
269
|
stop() {
|
|
206
270
|
this.end({ kind: "stopped" });
|
|
@@ -227,33 +291,79 @@ export class App {
|
|
|
227
291
|
}
|
|
228
292
|
end(outcome) {
|
|
229
293
|
const phase = this._phase;
|
|
230
|
-
|
|
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;
|
|
231
299
|
return;
|
|
300
|
+
}
|
|
232
301
|
this._phase = "stopped";
|
|
233
302
|
this.tick();
|
|
234
303
|
for (const unsubscribe of this.subscriptions)
|
|
235
304
|
unsubscribe();
|
|
236
305
|
this.subscriptions = [];
|
|
306
|
+
this.input.reset();
|
|
237
307
|
// A suspended app handed the terminal back when it suspended.
|
|
238
308
|
if (phase === "running")
|
|
239
309
|
this.leave();
|
|
240
310
|
this.host.stop();
|
|
241
311
|
this.settle(outcome);
|
|
242
312
|
}
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
247
359
|
tick() {
|
|
248
360
|
const wanted = this._phase === "running" && this.animations.size > 0;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.stopTicking = this.clock.every(this.rate, () => this.paint());
|
|
361
|
+
if (this.ticking !== null && (!wanted || this.ticking.rate.perSecond !== this._rate.perSecond)) {
|
|
362
|
+
this.ticking.stop();
|
|
363
|
+
this.ticking = null;
|
|
253
364
|
}
|
|
254
|
-
|
|
255
|
-
ticking();
|
|
256
|
-
this.stopTicking = null;
|
|
365
|
+
if (wanted && this.ticking === null) {
|
|
366
|
+
this.ticking = { rate: this._rate, stop: this.clock.every(this._rate, () => this.paint()) };
|
|
257
367
|
}
|
|
258
368
|
}
|
|
259
369
|
// [LAW:no-silent-failure] A frame that throws ends the app with that
|
package/dist/host/index.d.ts
CHANGED
|
@@ -2,5 +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 } from "./app.js";
|
|
5
|
+
export type { AppMouseEvent, AppOptions, AppPhase, Wheel } from "./app.js";
|
|
6
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 {};
|