@ryan_nookpi/pi-extension-idle-screensaver 0.1.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 (3) hide show
  1. package/README.md +15 -0
  2. package/index.ts +209 -0
  3. package/package.json +25 -0
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @ryan_nookpi/pi-extension-idle-screensaver
2
+
3
+ Standalone pi extension package for the idle screensaver overlay.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install /Users/creatrip/Documents/pi-extension/packages/idle-screensaver
9
+ pi install npm:@ryan_nookpi/pi-extension-idle-screensaver
10
+ ```
11
+
12
+ ## What it provides
13
+
14
+ - idle screensaver overlay
15
+ - `./index.ts` entry
package/index.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { execSync } from "node:child_process";
2
+ import type { ExtensionAPI, ExtensionContext, ThemeColor } from "@mariozechner/pi-coding-agent";
3
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
4
+ import { visibleWidth } from "@mariozechner/pi-tui";
5
+
6
+ /**
7
+ * Idle screensaver extension
8
+ * Shows a full-screen overlay after 15 min of inactivity.
9
+ * Dismissed by any keypress.
10
+ */
11
+
12
+ const IDLE_MS = 60 * 60 * 1000; // 60 minutes
13
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
14
+ let agentRunning = false;
15
+ type ScreensaverTui = { terminal?: { rows?: number } };
16
+ type ScreensaverTheme = { fg: (color: ThemeColor, text: string) => string; bold: (text: string) => string };
17
+
18
+ let overlayActive = false;
19
+ let askUserQuestionActive = false;
20
+ let latestCtx: ExtensionContext | null = null;
21
+ let piRef: ExtensionAPI | null = null;
22
+
23
+ // ── Timer helpers ─────────────────────────────────────────────────────────────
24
+
25
+ function clearIdleTimer(): void {
26
+ if (idleTimer) {
27
+ clearTimeout(idleTimer);
28
+ idleTimer = null;
29
+ }
30
+ }
31
+
32
+ function scheduleIdleTimer(): void {
33
+ clearIdleTimer();
34
+ if (agentRunning || overlayActive || askUserQuestionActive) return;
35
+ idleTimer = setTimeout(() => {
36
+ void showScreensaver();
37
+ }, IDLE_MS);
38
+ }
39
+
40
+ // ── Screensaver logic ─────────────────────────────────────────────────────────
41
+
42
+ async function showScreensaver(): Promise<void> {
43
+ if (!latestCtx?.hasUI) return;
44
+
45
+ overlayActive = true;
46
+ clearIdleTimer();
47
+
48
+ // Resolve title: prefer session name, fallback to folder/branch
49
+ const sessionName = piRef?.getSessionName() ?? "";
50
+
51
+ let title: string;
52
+ if (sessionName) {
53
+ title = sessionName;
54
+ } else {
55
+ const folder = latestCtx.sessionManager.getCwd();
56
+ const fallbackName = latestCtx.sessionManager.getSessionName() ?? "Pi";
57
+ let branch = "";
58
+ try {
59
+ branch = execSync("git branch --show-current", {
60
+ cwd: folder,
61
+ encoding: "utf8",
62
+ stdio: ["pipe", "pipe", "pipe"],
63
+ }).trim();
64
+ } catch {}
65
+ title = branch ? `${folder.split("/").pop()}/${branch}` : fallbackName;
66
+ }
67
+
68
+ await latestCtx.ui.custom(
69
+ (tui: ScreensaverTui, theme: ScreensaverTheme, _kb: unknown, done: (v: undefined) => void) => ({
70
+ render: (w: number) => renderScreensaver(w, (tui.terminal?.rows as number | undefined) ?? 40, title, theme),
71
+ handleInput: (_data: string) => {
72
+ done(undefined);
73
+ },
74
+ invalidate: () => {},
75
+ }),
76
+ { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "center" } },
77
+ );
78
+
79
+ overlayActive = false;
80
+ scheduleIdleTimer();
81
+ }
82
+
83
+ // ── Screensaver renderer ──────────────────────────────────────────────────────
84
+
85
+ function renderScreensaver(width: number, height: number, title: string, theme: ScreensaverTheme): string[] {
86
+ const lines: string[] = [];
87
+
88
+ // Border color helper
89
+ const bc = (s: string): string => theme.fg("accent", s);
90
+
91
+ // Top/bottom horizontal rules via DynamicBorder
92
+ const hRule = new DynamicBorder(bc).render(width)[0] ?? bc("─".repeat(width));
93
+
94
+ // Side border chars
95
+ const L = bc("│");
96
+ const R = bc("│");
97
+ const innerWidth = width - 2;
98
+
99
+ const emptyLine = (): string => L + " ".repeat(innerWidth) + R;
100
+
101
+ const placeLine = (chars: string): string => {
102
+ const vw = visibleWidth(chars);
103
+ return L + chars + " ".repeat(Math.max(0, innerWidth - vw)) + R;
104
+ };
105
+
106
+ const centerLine = (text: string): string => {
107
+ const tw = visibleWidth(text);
108
+ const pad = Math.max(0, Math.floor((innerWidth - tw) / 2));
109
+ return placeLine(" ".repeat(pad) + text);
110
+ };
111
+
112
+ // ── Title separators (no box) ───────────────────────────────
113
+ const compact = title.trim();
114
+ const spread = compact.length <= 24 ? compact.split("").join(" ") : compact;
115
+ const titleText = spread || "Pi";
116
+
117
+ const separatorWidth = Math.min(innerWidth - 4, Math.max(visibleWidth(titleText) + 8, 24));
118
+ const separator = bc("─".repeat(Math.max(1, separatorWidth)));
119
+ const topSeparatorLine = centerLine(separator);
120
+ const titleLine = centerLine(theme.fg("accent", titleText) as string);
121
+ const bottomSeparatorLine = centerLine(separator);
122
+
123
+ // ── Layout ───────────────────────────────────────────────────
124
+ const TITLE_BLOCK_H = 3;
125
+ const FOOTER_H = 1;
126
+ const innerHeight = height - 2;
127
+
128
+ const contentH = TITLE_BLOCK_H + FOOTER_H;
129
+ const topPad = Math.max(0, Math.floor((innerHeight - contentH) / 2) - 1);
130
+
131
+ // ── Render ───────────────────────────────────────────────────
132
+ // 1. Top border
133
+ lines.push(hRule);
134
+
135
+ // 2. Top padding
136
+ for (let i = 0; i < topPad; i++) lines.push(emptyLine());
137
+
138
+ // 3. Title with top/bottom separators (3 lines)
139
+ lines.push(topSeparatorLine);
140
+ lines.push(titleLine);
141
+ lines.push(bottomSeparatorLine);
142
+
143
+ // 4. Fill until footer
144
+ while (lines.length < height - 2) lines.push(emptyLine());
145
+
146
+ // 5. Footer hint
147
+ if (lines.length === height - 2) {
148
+ lines.push(centerLine(theme.fg("dim", "Press any key to dismiss") as string));
149
+ }
150
+
151
+ // 6. Bottom border
152
+ while (lines.length < height - 1) lines.push(emptyLine());
153
+ lines.push(hRule);
154
+
155
+ return lines;
156
+ }
157
+
158
+ // ── Extension entry point ─────────────────────────────────────────────────────
159
+
160
+ export default function idleScreensaver(pi: ExtensionAPI): void {
161
+ piRef = pi;
162
+ pi.on("input", (event, ctx) => {
163
+ latestCtx = ctx;
164
+ if (event.source !== "extension") {
165
+ scheduleIdleTimer();
166
+ }
167
+ });
168
+
169
+ pi.on("agent_start", (_event, ctx) => {
170
+ latestCtx = ctx;
171
+ agentRunning = true;
172
+ clearIdleTimer();
173
+ });
174
+
175
+ pi.on("agent_end", (_event, ctx) => {
176
+ latestCtx = ctx;
177
+ agentRunning = false;
178
+ scheduleIdleTimer();
179
+ });
180
+
181
+ pi.on("tool_execution_start", (event, ctx) => {
182
+ latestCtx = ctx;
183
+ if (event.toolName === "AskUserQuestion") {
184
+ askUserQuestionActive = true;
185
+ clearIdleTimer();
186
+ }
187
+ });
188
+
189
+ pi.on("tool_execution_end", (event, ctx) => {
190
+ latestCtx = ctx;
191
+ if (event.toolName === "AskUserQuestion") {
192
+ askUserQuestionActive = false;
193
+ scheduleIdleTimer();
194
+ }
195
+ });
196
+
197
+ pi.on("session_start", (_event, ctx) => {
198
+ latestCtx = ctx;
199
+ clearIdleTimer();
200
+ overlayActive = false;
201
+ scheduleIdleTimer();
202
+ });
203
+
204
+ pi.on("session_shutdown", () => {
205
+ clearIdleTimer();
206
+ });
207
+
208
+ scheduleIdleTimer();
209
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@ryan_nookpi/pi-extension-idle-screensaver",
3
+ "version": "0.1.0",
4
+ "description": "Standalone Pi extension package for the idle screensaver overlay.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "files": [
10
+ "index.ts",
11
+ "README.md"
12
+ ],
13
+ "pi": {
14
+ "extensions": [
15
+ "./index.ts"
16
+ ]
17
+ },
18
+ "peerDependencies": {
19
+ "@mariozechner/pi-coding-agent": "*",
20
+ "@mariozechner/pi-tui": "*"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }