@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +2219 -0
  2. package/README.md +705 -0
  3. package/dist/types/autocomplete.d.ts +116 -0
  4. package/dist/types/bracketed-paste.d.ts +51 -0
  5. package/dist/types/components/box.d.ts +31 -0
  6. package/dist/types/components/cancellable-loader.d.ts +21 -0
  7. package/dist/types/components/editor.d.ts +162 -0
  8. package/dist/types/components/image.d.ts +112 -0
  9. package/dist/types/components/input.d.ts +25 -0
  10. package/dist/types/components/loader.d.ts +25 -0
  11. package/dist/types/components/markdown.d.ts +88 -0
  12. package/dist/types/components/scroll-view.d.ts +62 -0
  13. package/dist/types/components/select-list.d.ts +69 -0
  14. package/dist/types/components/settings-list.d.ts +123 -0
  15. package/dist/types/components/spacer.d.ts +11 -0
  16. package/dist/types/components/tab-bar.d.ts +89 -0
  17. package/dist/types/components/text.d.ts +27 -0
  18. package/dist/types/components/truncated-text.d.ts +10 -0
  19. package/dist/types/deccara.d.ts +49 -0
  20. package/dist/types/desktop-notify.d.ts +52 -0
  21. package/dist/types/editor-component.d.ts +38 -0
  22. package/dist/types/fuzzy.d.ts +48 -0
  23. package/dist/types/index.d.ts +32 -0
  24. package/dist/types/keybindings.d.ts +197 -0
  25. package/dist/types/keys.d.ts +210 -0
  26. package/dist/types/kill-ring.d.ts +20 -0
  27. package/dist/types/kitty-graphics.d.ts +76 -0
  28. package/dist/types/latex-block.d.ts +8 -0
  29. package/dist/types/latex-to-unicode.d.ts +50 -0
  30. package/dist/types/loop-watchdog.d.ts +44 -0
  31. package/dist/types/mouse.d.ts +67 -0
  32. package/dist/types/stdin-buffer.d.ts +60 -0
  33. package/dist/types/symbols.d.ts +25 -0
  34. package/dist/types/terminal-capabilities.d.ts +285 -0
  35. package/dist/types/terminal.d.ts +175 -0
  36. package/dist/types/tmux.d.ts +6 -0
  37. package/dist/types/ttyid.d.ts +9 -0
  38. package/dist/types/tui.d.ts +457 -0
  39. package/dist/types/utils.d.ts +100 -0
  40. package/package.json +70 -0
  41. package/src/autocomplete.ts +1079 -0
  42. package/src/bracketed-paste.ts +123 -0
  43. package/src/components/box.ts +236 -0
  44. package/src/components/cancellable-loader.ts +40 -0
  45. package/src/components/editor.ts +3301 -0
  46. package/src/components/image.ts +460 -0
  47. package/src/components/input.ts +482 -0
  48. package/src/components/loader.ts +174 -0
  49. package/src/components/markdown.ts +3119 -0
  50. package/src/components/scroll-view.ts +227 -0
  51. package/src/components/select-list.ts +539 -0
  52. package/src/components/settings-list.ts +793 -0
  53. package/src/components/spacer.ts +32 -0
  54. package/src/components/tab-bar.ts +300 -0
  55. package/src/components/text.ts +173 -0
  56. package/src/components/truncated-text.ts +69 -0
  57. package/src/deccara.ts +314 -0
  58. package/src/desktop-notify.ts +192 -0
  59. package/src/editor-component.ts +74 -0
  60. package/src/fuzzy.ts +384 -0
  61. package/src/index.ts +51 -0
  62. package/src/keybindings.ts +346 -0
  63. package/src/keys.ts +566 -0
  64. package/src/kill-ring.ts +51 -0
  65. package/src/kitty-graphics.ts +171 -0
  66. package/src/latex-block.ts +1338 -0
  67. package/src/latex-to-unicode.ts +2017 -0
  68. package/src/loop-watchdog.ts +115 -0
  69. package/src/mouse.ts +105 -0
  70. package/src/stdin-buffer.ts +781 -0
  71. package/src/symbols.ts +26 -0
  72. package/src/terminal-capabilities.ts +1211 -0
  73. package/src/terminal.ts +1854 -0
  74. package/src/tmux.ts +14 -0
  75. package/src/ttyid.ts +84 -0
  76. package/src/tui.ts +4275 -0
  77. 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
+ }