pi-cockpit 0.7.0 → 0.9.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.
@@ -0,0 +1,128 @@
1
+ import type { Component, TUI } from "@earendil-works/pi-tui";
2
+
3
+ export const EDITOR_BOTTOM_WIDGET_KEY = "cockpit-editor-bottom-anchor";
4
+ export const COCKPIT_EDITOR_BOTTOM_MARKER = Symbol.for("pi-cockpit.editor-bottom-render");
5
+
6
+ const EDITOR_BOTTOM_SENTINEL = "\u2063\u2064\u2063\u2064\u2063";
7
+
8
+ type RenderFunction = TUI["render"];
9
+
10
+ interface RenderMarker {
11
+ owner: object;
12
+ original: RenderFunction;
13
+ }
14
+
15
+ export interface EditorBottomController {
16
+ attach(tui: TUI): void;
17
+ show(): void;
18
+ hide(): void;
19
+ isEnabled(): boolean;
20
+ requestRender(): void;
21
+ dispose(): void;
22
+ }
23
+
24
+ export interface EditorBottomControllerOptions {
25
+ onError?(error: unknown): void;
26
+ }
27
+
28
+ export function createEditorBottomSentinel(): Component {
29
+ return {
30
+ render: () => [EDITOR_BOTTOM_SENTINEL],
31
+ invalidate(): void {},
32
+ };
33
+ }
34
+
35
+ export function createEditorBottomController(
36
+ options: EditorBottomControllerOptions = {},
37
+ ): EditorBottomController {
38
+ const owner = {};
39
+ let tui: TUI | undefined;
40
+ let originalRender: RenderFunction | undefined;
41
+ let wrappedRender: RenderFunction | undefined;
42
+ let enabled = false;
43
+ let disposed = false;
44
+
45
+ const reportError = (error: unknown): void => {
46
+ try {
47
+ options.onError?.(error);
48
+ } catch {
49
+ // Layout failure reporting must not break Pi's renderer.
50
+ }
51
+ };
52
+
53
+ const requestRender = (): void => {
54
+ try {
55
+ tui?.requestRender();
56
+ } catch {
57
+ // The TUI may already be shutting down.
58
+ }
59
+ };
60
+
61
+ const attach = (nextTui: TUI): void => {
62
+ if (disposed) return;
63
+ if (tui === nextTui) return;
64
+ if (tui) throw new Error("Cockpit editor-bottom layout is already attached to another TUI");
65
+ const existing = (nextTui.render as RenderFunction & Record<symbol, RenderMarker | undefined>)[COCKPIT_EDITOR_BOTTOM_MARKER];
66
+ if (existing?.owner === owner) return;
67
+ if (existing) throw new Error("Cockpit editor-bottom layout is already attached to this TUI");
68
+
69
+ tui = nextTui;
70
+ originalRender = nextTui.render;
71
+ const previousRender = nextTui.render;
72
+ wrappedRender = function (this: TUI, width: number): string[] {
73
+ const rendered = previousRender.call(nextTui, width);
74
+ const markerIndex = rendered.findIndex((line) => line.includes(EDITOR_BOTTOM_SENTINEL));
75
+ if (markerIndex < 0) return rendered;
76
+ const withoutMarker = [...rendered.slice(0, markerIndex), ...rendered.slice(markerIndex + 1)];
77
+ if (!enabled) return withoutMarker;
78
+ try {
79
+ const terminalRows = Math.max(1, Math.trunc(nextTui.terminal.rows));
80
+ const padding = Math.max(0, terminalRows - withoutMarker.length);
81
+ if (padding === 0) return withoutMarker;
82
+ return [
83
+ ...withoutMarker.slice(0, markerIndex),
84
+ ...Array.from({ length: padding }, () => ""),
85
+ ...withoutMarker.slice(markerIndex),
86
+ ];
87
+ } catch (error) {
88
+ enabled = false;
89
+ reportError(error);
90
+ return withoutMarker;
91
+ }
92
+ };
93
+ Object.defineProperty(wrappedRender, COCKPIT_EDITOR_BOTTOM_MARKER, {
94
+ value: { owner, original: previousRender } satisfies RenderMarker,
95
+ configurable: false,
96
+ enumerable: false,
97
+ writable: false,
98
+ });
99
+ nextTui.render = wrappedRender;
100
+ requestRender();
101
+ };
102
+
103
+ return {
104
+ attach,
105
+ show() {
106
+ if (disposed || enabled) return;
107
+ enabled = true;
108
+ requestRender();
109
+ },
110
+ hide() {
111
+ if (!enabled) return;
112
+ enabled = false;
113
+ requestRender();
114
+ },
115
+ isEnabled: () => enabled,
116
+ requestRender,
117
+ dispose() {
118
+ if (disposed) return;
119
+ disposed = true;
120
+ enabled = false;
121
+ if (tui && originalRender && tui.render === wrappedRender) tui.render = originalRender;
122
+ requestRender();
123
+ tui = undefined;
124
+ originalRender = undefined;
125
+ wrappedRender = undefined;
126
+ },
127
+ };
128
+ }