@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.
- package/README.md +15 -0
- package/index.ts +209 -0
- 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
|
+
}
|