@linxiraos/pi-tui 1.0.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/CHANGELOG.md +2219 -0
- package/README.md +705 -0
- package/dist/types/autocomplete.d.ts +116 -0
- package/dist/types/bracketed-paste.d.ts +51 -0
- package/dist/types/components/box.d.ts +31 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +162 -0
- package/dist/types/components/image.d.ts +112 -0
- package/dist/types/components/input.d.ts +25 -0
- package/dist/types/components/loader.d.ts +25 -0
- package/dist/types/components/markdown.d.ts +88 -0
- package/dist/types/components/scroll-view.d.ts +62 -0
- package/dist/types/components/select-list.d.ts +69 -0
- package/dist/types/components/settings-list.d.ts +123 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +89 -0
- package/dist/types/components/text.d.ts +27 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/deccara.d.ts +49 -0
- package/dist/types/desktop-notify.d.ts +52 -0
- package/dist/types/editor-component.d.ts +38 -0
- package/dist/types/fuzzy.d.ts +48 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/keybindings.d.ts +197 -0
- package/dist/types/keys.d.ts +210 -0
- package/dist/types/kill-ring.d.ts +20 -0
- package/dist/types/kitty-graphics.d.ts +76 -0
- package/dist/types/latex-block.d.ts +8 -0
- package/dist/types/latex-to-unicode.d.ts +50 -0
- package/dist/types/loop-watchdog.d.ts +44 -0
- package/dist/types/mouse.d.ts +67 -0
- package/dist/types/stdin-buffer.d.ts +60 -0
- package/dist/types/symbols.d.ts +25 -0
- package/dist/types/terminal-capabilities.d.ts +285 -0
- package/dist/types/terminal.d.ts +175 -0
- package/dist/types/tmux.d.ts +6 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +457 -0
- package/dist/types/utils.d.ts +100 -0
- package/package.json +70 -0
- package/src/autocomplete.ts +1079 -0
- package/src/bracketed-paste.ts +123 -0
- package/src/components/box.ts +236 -0
- package/src/components/cancellable-loader.ts +40 -0
- package/src/components/editor.ts +3301 -0
- package/src/components/image.ts +460 -0
- package/src/components/input.ts +482 -0
- package/src/components/loader.ts +174 -0
- package/src/components/markdown.ts +3119 -0
- package/src/components/scroll-view.ts +227 -0
- package/src/components/select-list.ts +539 -0
- package/src/components/settings-list.ts +793 -0
- package/src/components/spacer.ts +32 -0
- package/src/components/tab-bar.ts +300 -0
- package/src/components/text.ts +173 -0
- package/src/components/truncated-text.ts +69 -0
- package/src/deccara.ts +314 -0
- package/src/desktop-notify.ts +192 -0
- package/src/editor-component.ts +74 -0
- package/src/fuzzy.ts +384 -0
- package/src/index.ts +51 -0
- package/src/keybindings.ts +346 -0
- package/src/keys.ts +566 -0
- package/src/kill-ring.ts +51 -0
- package/src/kitty-graphics.ts +171 -0
- package/src/latex-block.ts +1338 -0
- package/src/latex-to-unicode.ts +2017 -0
- package/src/loop-watchdog.ts +115 -0
- package/src/mouse.ts +105 -0
- package/src/stdin-buffer.ts +781 -0
- package/src/symbols.ts +26 -0
- package/src/terminal-capabilities.ts +1211 -0
- package/src/terminal.ts +1854 -0
- package/src/tmux.ts +14 -0
- package/src/ttyid.ts +84 -0
- package/src/tui.ts +4275 -0
- package/src/utils.ts +619 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { logger, takeRecentLoopPhase } from "@linxiraos/pi-utils";
|
|
3
|
+
|
|
4
|
+
export interface LoopWatchdogOptions {
|
|
5
|
+
/** How far ahead each probe tick is scheduled, in ms. Default 250. */
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
/** A tick later than this past its deadline counts as a block. Default 250. */
|
|
8
|
+
thresholdMs?: number;
|
|
9
|
+
/** Overshoot beyond this likely includes system sleep, so it is suppressed. Default 60_000. */
|
|
10
|
+
sleepMs?: number;
|
|
11
|
+
/** Monotonic clock source; injectable for tests. Default `performance.now`. */
|
|
12
|
+
now?: () => number;
|
|
13
|
+
/** Timer source; injectable for tests. Default `setTimeout`. */
|
|
14
|
+
schedule?: (cb: () => void, ms: number) => LoopWatchdogTimer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Timer handle the watchdog arms. `cancel`, when present, is invoked on stop()
|
|
19
|
+
* so a stopped watchdog leaves no armed timer to wake the loop even once.
|
|
20
|
+
*/
|
|
21
|
+
interface LoopWatchdogTimer {
|
|
22
|
+
unref?(): void;
|
|
23
|
+
cancel?(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Always-on event-loop lag probe. Each tick is scheduled `intervalMs` ahead of
|
|
28
|
+
* a recorded deadline; a tick that fires `thresholdMs` past its deadline means
|
|
29
|
+
* the loop was blocked that long. The overshoot is logged once on the rising
|
|
30
|
+
* edge (one block ⇒ one line, deduped via `#wasBlocked`), tagged with the phase
|
|
31
|
+
* active during the elapsed interval via {@link takeRecentLoopPhase} — which
|
|
32
|
+
* survives the synchronous push/pop the instrumented hot paths do before this
|
|
33
|
+
* delayed tick can run — so the stall names its cause instead of "unknown".
|
|
34
|
+
*
|
|
35
|
+
* The handle is `unref`'d so the probe never keeps the process alive, and stop()
|
|
36
|
+
* cancels the armed timer when the handle exposes `cancel` (the default
|
|
37
|
+
* `setTimeout` handle does, via `clearTimeout`). The `#generation` guard remains
|
|
38
|
+
* as a fallback for injected handles that cannot cancel. An overshoot beyond
|
|
39
|
+
* `sleepMs` is treated as system sleep rather than a synchronous stall: the
|
|
40
|
+
* process could not have run JS during the missed interval, and one resume
|
|
41
|
+
* should not produce a multi-minute `ui.loop-blocked` record.
|
|
42
|
+
*/
|
|
43
|
+
export class LoopWatchdog {
|
|
44
|
+
#intervalMs: number;
|
|
45
|
+
#thresholdMs: number;
|
|
46
|
+
#sleepMs: number;
|
|
47
|
+
#now: () => number;
|
|
48
|
+
#schedule: (cb: () => void, ms: number) => LoopWatchdogTimer;
|
|
49
|
+
#expected = 0;
|
|
50
|
+
#wasBlocked = false;
|
|
51
|
+
#running = false;
|
|
52
|
+
// Bumped by stop(); each scheduled tick captures the generation it was armed
|
|
53
|
+
// under and no-ops if it no longer matches, so a start()→stop()→start() cycle
|
|
54
|
+
// cannot leave the pre-stop timer chain rescheduling itself in parallel.
|
|
55
|
+
#generation = 0;
|
|
56
|
+
#handle: LoopWatchdogTimer | undefined;
|
|
57
|
+
|
|
58
|
+
constructor(options: LoopWatchdogOptions = {}) {
|
|
59
|
+
this.#intervalMs = options.intervalMs ?? 250;
|
|
60
|
+
this.#thresholdMs = options.thresholdMs ?? 250;
|
|
61
|
+
this.#sleepMs = options.sleepMs ?? 60_000;
|
|
62
|
+
this.#now = options.now ?? (() => performance.now());
|
|
63
|
+
this.#schedule =
|
|
64
|
+
options.schedule ??
|
|
65
|
+
((cb, ms) => {
|
|
66
|
+
const timer = setTimeout(cb, ms);
|
|
67
|
+
return { unref: () => timer.unref?.(), cancel: () => clearTimeout(timer) };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
start(): void {
|
|
72
|
+
if (this.#running) return;
|
|
73
|
+
this.#running = true;
|
|
74
|
+
this.#wasBlocked = false;
|
|
75
|
+
this.#armTick();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop(): void {
|
|
79
|
+
this.#running = false;
|
|
80
|
+
this.#wasBlocked = false;
|
|
81
|
+
this.#generation++;
|
|
82
|
+
this.#handle?.cancel?.();
|
|
83
|
+
this.#handle = undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#armTick(): void {
|
|
87
|
+
const generation = this.#generation;
|
|
88
|
+
this.#expected = this.#now() + this.#intervalMs;
|
|
89
|
+
this.#handle = this.#schedule(() => this.#tick(generation), this.#intervalMs);
|
|
90
|
+
this.#handle.unref?.();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#tick(generation: number): void {
|
|
94
|
+
if (!this.#running || generation !== this.#generation) return;
|
|
95
|
+
const blockedMs = this.#now() - this.#expected;
|
|
96
|
+
// Consume the recent phase every tick (block or not) so attribution is
|
|
97
|
+
// scoped to the just-elapsed interval and never carries a stale phase
|
|
98
|
+
// forward to a later, phase-less block.
|
|
99
|
+
const phase = takeRecentLoopPhase();
|
|
100
|
+
if (blockedMs > this.#thresholdMs) {
|
|
101
|
+
if (blockedMs > this.#sleepMs) {
|
|
102
|
+
this.#wasBlocked = false;
|
|
103
|
+
} else if (!this.#wasBlocked) {
|
|
104
|
+
this.#wasBlocked = true;
|
|
105
|
+
logger.warn("ui.loop-blocked", {
|
|
106
|
+
blockedMs: Math.round(blockedMs),
|
|
107
|
+
phase: phase ?? "unknown",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
this.#wasBlocked = false;
|
|
112
|
+
}
|
|
113
|
+
this.#armTick();
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/mouse.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SGR mouse report parsing (`\x1b[<button;col;rowM` / `…m`).
|
|
3
|
+
*
|
|
4
|
+
* Mouse tracking is enabled only while a fullscreen overlay holds the
|
|
5
|
+
* alternate screen (see tui.ts MOUSE_TRACKING_ON), so consumers are
|
|
6
|
+
* fullscreen components hit-testing against their own rendered frame:
|
|
7
|
+
* the frame paints from screen row 0, hence `row`/`col` are exposed
|
|
8
|
+
* 0-based for direct indexing into rendered lines.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A decoded SGR mouse report. */
|
|
12
|
+
export interface SgrMouseEvent {
|
|
13
|
+
/** Raw button code (bit 32 = motion, bit 64 = wheel, low bits = button). */
|
|
14
|
+
button: number;
|
|
15
|
+
/** 0-based column of the event. */
|
|
16
|
+
col: number;
|
|
17
|
+
/** 0-based row of the event. */
|
|
18
|
+
row: number;
|
|
19
|
+
/** True for a release report (`m` suffix). */
|
|
20
|
+
release: boolean;
|
|
21
|
+
/** Wheel direction: -1 up, 1 down, null when not a wheel event. */
|
|
22
|
+
wheel: -1 | 1 | null;
|
|
23
|
+
/** True when the pointer moved (hover or drag) rather than clicked. */
|
|
24
|
+
motion: boolean;
|
|
25
|
+
/** True for a left-button press (not motion, not release, not wheel). */
|
|
26
|
+
leftClick: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decode an SGR mouse report, or return null when `data` is not one.
|
|
31
|
+
* Callers on hot keypress paths should pre-check `data.startsWith("\x1b[<")`
|
|
32
|
+
* before paying for the regex.
|
|
33
|
+
*/
|
|
34
|
+
export function parseSgrMouse(data: string): SgrMouseEvent | null {
|
|
35
|
+
const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
const button = Number(match[1]);
|
|
38
|
+
const col = Number(match[2]) - 1;
|
|
39
|
+
const row = Number(match[3]) - 1;
|
|
40
|
+
const release = match[4] === "m";
|
|
41
|
+
const wheel = button & 64 ? ((button & 1 ? 1 : -1) as 1 | -1) : null;
|
|
42
|
+
const motion = (button & 32) !== 0 && wheel === null;
|
|
43
|
+
const leftClick = !release && wheel === null && !motion && (button & 3) === 0;
|
|
44
|
+
return { button, col, row, release, wheel, motion, leftClick };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Handler invoked with a decoded SGR event; returning `false` reports unhandled. */
|
|
48
|
+
export type SgrMouseHandler = (event: SgrMouseEvent) => boolean | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decode an SGR mouse report and forward it to `handler`. Returns `false` when
|
|
52
|
+
* `data` is not an SGR mouse report (or fails to parse), so callers can fall
|
|
53
|
+
* through to other input handling. Centralizes the repeated
|
|
54
|
+
* `data.startsWith("\x1b[<")` + `parseSgrMouse()` pattern.
|
|
55
|
+
*/
|
|
56
|
+
export function routeSgrMouseInput(data: string, handler: SgrMouseHandler): boolean {
|
|
57
|
+
if (!data.startsWith("\x1b[<")) return false;
|
|
58
|
+
const event = parseSgrMouse(data);
|
|
59
|
+
if (!event) return false;
|
|
60
|
+
return handler(event) !== false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Structural view of a SelectList-like target for mouse routing. Declared here
|
|
65
|
+
* (rather than importing the component) to keep this core module free of any
|
|
66
|
+
* component-to-core import cycle.
|
|
67
|
+
*/
|
|
68
|
+
export interface SelectListMouseTarget {
|
|
69
|
+
handleWheel(delta: -1 | 1): void;
|
|
70
|
+
hitTest(line: number): number | undefined;
|
|
71
|
+
setHoverIndex(index: number | null): void;
|
|
72
|
+
clickItem(index: number): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Route a decoded mouse event against a SelectList-like target at the given
|
|
77
|
+
* 0-based frame-local `line`. Centralizes the repeated wheel/hit-test/hover/
|
|
78
|
+
* click pattern. Returns `true` when the event was consumed.
|
|
79
|
+
*/
|
|
80
|
+
export function routeSelectListMouse(target: SelectListMouseTarget, event: SgrMouseEvent, line: number): boolean {
|
|
81
|
+
if (event.wheel !== null) {
|
|
82
|
+
target.handleWheel(event.wheel);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const index = target.hitTest(line);
|
|
86
|
+
if (event.motion) {
|
|
87
|
+
target.setHoverIndex(index ?? null);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (event.leftClick && index !== undefined) {
|
|
91
|
+
target.clickItem(index);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Implemented by components that accept routed mouse events at frame-local
|
|
99
|
+
* coordinates. Hosts translate screen coordinates to the component's own
|
|
100
|
+
* rendered lines before forwarding.
|
|
101
|
+
*/
|
|
102
|
+
export interface MouseRoutable {
|
|
103
|
+
/** `line`/`col` are 0-based within the component's rendered output. */
|
|
104
|
+
routeMouse(event: SgrMouseEvent, line: number, col: number): void;
|
|
105
|
+
}
|