@tryinget/pi-activity-strip 0.4.0 → 0.6.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 +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +499 -0
- package/src/native/panel-projection.mjs +161 -0
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -500
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Trigger focused-workspace reconciliation immediately from Niri's event
|
|
9
|
-
* stream while retaining the slower poll as a fail-closed fallback.
|
|
9
|
+
* stream while retaining the slower poll as a fail-closed fallback. Window
|
|
10
|
+
* open/change/close and focus events are forwarded so title changes that reveal
|
|
11
|
+
* a Ghostty tab, or windows moving between workspaces, are seen between polls.
|
|
10
12
|
* @param {{
|
|
11
|
-
* spawn:
|
|
13
|
+
* spawn: typeof import("node:child_process").spawn;
|
|
12
14
|
* env: NodeJS.ProcessEnv;
|
|
13
15
|
* onFocusedWorkspace: (workspaceId: number) => void;
|
|
14
16
|
* onFallback: () => void;
|
|
17
|
+
* onWindowChanged?: (window: Record<string, unknown>) => void;
|
|
18
|
+
* onWindowClosed?: (windowId: number) => void;
|
|
19
|
+
* onWindowFocusChanged?: (windowId: number | null) => void;
|
|
15
20
|
* fallbackMs: number;
|
|
16
21
|
* setIntervalFn?: typeof setInterval;
|
|
17
22
|
* clearIntervalFn?: typeof clearInterval;
|
|
@@ -22,7 +27,9 @@ export function createNiriWorkspaceEventWatcher(options) {
|
|
|
22
27
|
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
23
28
|
let buffer = "";
|
|
24
29
|
let stopped = false;
|
|
30
|
+
/** @type {ReturnType<typeof import("node:child_process").spawn> | null} */
|
|
25
31
|
let child = null;
|
|
32
|
+
/** @type {number | null} */
|
|
26
33
|
let lastFocusedWorkspaceId = null;
|
|
27
34
|
|
|
28
35
|
const fallbackTimer = setIntervalFn(() => {
|
|
@@ -30,6 +37,7 @@ export function createNiriWorkspaceEventWatcher(options) {
|
|
|
30
37
|
}, options.fallbackMs);
|
|
31
38
|
fallbackTimer.unref?.();
|
|
32
39
|
|
|
40
|
+
/** @param {unknown} chunk */
|
|
33
41
|
function consume(chunk) {
|
|
34
42
|
if (stopped) return;
|
|
35
43
|
buffer += String(chunk ?? "");
|
|
@@ -49,6 +57,16 @@ export function createNiriWorkspaceEventWatcher(options) {
|
|
|
49
57
|
lastFocusedWorkspaceId = activation.id;
|
|
50
58
|
options.onFocusedWorkspace(activation.id);
|
|
51
59
|
}
|
|
60
|
+
const changedWindow = event?.WindowOpenedOrChanged?.window;
|
|
61
|
+
if (changedWindow && typeof changedWindow === "object") {
|
|
62
|
+
options.onWindowChanged?.(changedWindow);
|
|
63
|
+
}
|
|
64
|
+
const closedId = event?.WindowClosed?.id;
|
|
65
|
+
if (Number.isInteger(closedId)) options.onWindowClosed?.(closedId);
|
|
66
|
+
if (event && typeof event === "object" && "WindowFocusChanged" in event) {
|
|
67
|
+
const focusedId = event.WindowFocusChanged?.id;
|
|
68
|
+
options.onWindowFocusChanged?.(Number.isInteger(focusedId) ? focusedId : null);
|
|
69
|
+
}
|
|
52
70
|
} catch {
|
|
53
71
|
// A malformed event is ignored; the fallback poll remains authoritative.
|
|
54
72
|
}
|
package/src/common/electron.mjs
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// ---
|
|
2
|
-
// summary: "resolves an executable Electron binary from explicit settings, local dependencies, or PATH candidates"
|
|
3
|
-
// read_when:
|
|
4
|
-
// - "changing Electron discovery order, environment overrides, or executable checks"
|
|
5
|
-
// ---
|
|
6
|
-
|
|
7
|
-
import { constants } from "node:fs";
|
|
8
|
-
import { access } from "node:fs/promises";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { DEFAULT_ELECTRON_CANDIDATES } from "./constants.mjs";
|
|
11
|
-
|
|
12
|
-
/** @param {string} targetPath */
|
|
13
|
-
async function canExecute(targetPath) {
|
|
14
|
-
try {
|
|
15
|
-
await access(targetPath, constants.X_OK);
|
|
16
|
-
return true;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** @param {string} binaryName */
|
|
23
|
-
async function findOnPath(binaryName) {
|
|
24
|
-
for (const entry of String(process.env.PATH ?? "")
|
|
25
|
-
.split(":")
|
|
26
|
-
.filter(Boolean)) {
|
|
27
|
-
const candidate = path.join(entry, binaryName);
|
|
28
|
-
if (await canExecute(candidate)) return candidate;
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** @param {string | null} [explicitPath] */
|
|
34
|
-
export async function locateElectron(explicitPath = null) {
|
|
35
|
-
if (explicitPath) {
|
|
36
|
-
if (await canExecute(explicitPath)) return explicitPath;
|
|
37
|
-
throw new Error(`Configured Electron binary is not executable: ${explicitPath}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const envBinary = process.env.GLIMPSE_ELECTRON_BIN || process.env.PI_ACTIVITY_STRIP_ELECTRON_BIN;
|
|
41
|
-
if (envBinary) {
|
|
42
|
-
if (await canExecute(envBinary)) return envBinary;
|
|
43
|
-
throw new Error(`Configured Electron binary is not executable: ${envBinary}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const localBinary = path.resolve(process.cwd(), "node_modules", ".bin", "electron");
|
|
47
|
-
if (await canExecute(localBinary)) return localBinary;
|
|
48
|
-
|
|
49
|
-
for (const candidate of DEFAULT_ELECTRON_CANDIDATES) {
|
|
50
|
-
const found = await findOnPath(candidate);
|
|
51
|
-
if (found) return found;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
throw new Error(
|
|
55
|
-
"Could not find an Electron binary. Install electron (or electron39 on Arch) or set GLIMPSE_ELECTRON_BIN / PI_ACTIVITY_STRIP_ELECTRON_BIN.",
|
|
56
|
-
);
|
|
57
|
-
}
|
package/src/electron/main.mjs
DELETED
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
// ---
|
|
2
|
-
// summary: "runs the Electron overlay, broker, primary-display window placement, and Niri top-edge alignment"
|
|
3
|
-
// read_when:
|
|
4
|
-
// - "changing overlay lifecycle, window behavior, runtime readiness, or desktop alignment"
|
|
5
|
-
// ---
|
|
6
|
-
|
|
7
|
-
import { execFile, spawn } from "node:child_process";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { promisify } from "node:util";
|
|
11
|
-
import { app, BrowserWindow, ipcMain, screen } from "electron";
|
|
12
|
-
import { createActivityStripBroker } from "../broker/server.mjs";
|
|
13
|
-
import { createLatestOnlyRunner } from "../common/alignment-controller.mjs";
|
|
14
|
-
import { detectDisplayServer, detectWindowManager } from "../common/compatibility.mjs";
|
|
15
|
-
import {
|
|
16
|
-
ACTIVITY_STRIP_EXPANDED_HEIGHT,
|
|
17
|
-
ACTIVITY_STRIP_HEIGHT,
|
|
18
|
-
ACTIVITY_STRIP_NIRI_ANIMATION_SETTLE_MS,
|
|
19
|
-
ACTIVITY_STRIP_WIDTH_PADDING,
|
|
20
|
-
ACTIVITY_STRIP_WORKSPACE_SYNC_MS,
|
|
21
|
-
} from "../common/constants.mjs";
|
|
22
|
-
import {
|
|
23
|
-
focusNiriSession,
|
|
24
|
-
readNiriWindows,
|
|
25
|
-
readNiriWorkspaces,
|
|
26
|
-
resolveSnapshotSession,
|
|
27
|
-
} from "../common/niri-focus.mjs";
|
|
28
|
-
import { createStripHtml } from "../ui/strip-html.mjs";
|
|
29
|
-
import { createNiriNativeWindowRuntime } from "./niri-native-window-runtime.mjs";
|
|
30
|
-
import { createNiriWorkspaceEventWatcher } from "./niri-workspace-events.mjs";
|
|
31
|
-
import { createBrowserWindowVisibilityRuntime } from "./renderer-visibility-runtime.mjs";
|
|
32
|
-
import { createNiriWorkspaceViewRuntime, haveSameSessionIds } from "./workspace-view-runtime.mjs";
|
|
33
|
-
|
|
34
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
-
const __dirname = path.dirname(__filename);
|
|
36
|
-
const preloadPath = path.join(__dirname, "preload.cjs");
|
|
37
|
-
const interactive = process.env.PI_ACTIVITY_STRIP_CLICK_THROUGH !== "1";
|
|
38
|
-
const execFileAsync = promisify(execFile);
|
|
39
|
-
|
|
40
|
-
let browserWindow = null;
|
|
41
|
-
let broker = null;
|
|
42
|
-
let workspaceEventWatcher = null;
|
|
43
|
-
let windowCreation = null;
|
|
44
|
-
let appQuitting = false;
|
|
45
|
-
let rendererVisible = !isNiriSession();
|
|
46
|
-
let focusedWorkspaceViewVisible = !isNiriSession();
|
|
47
|
-
let expanded = false;
|
|
48
|
-
let latestSnapshot = { generatedAt: Date.now(), sessions: [] };
|
|
49
|
-
let focusedSessionId = null;
|
|
50
|
-
let workspaceSessionIds = new Set();
|
|
51
|
-
const runtimeStatus = {
|
|
52
|
-
state: "starting",
|
|
53
|
-
startedAt: Date.now(),
|
|
54
|
-
readyAt: null,
|
|
55
|
-
windowVisible: false,
|
|
56
|
-
displayServer: detectDisplayServer(process.env),
|
|
57
|
-
windowManager: detectWindowManager(process.env),
|
|
58
|
-
displayCount: null,
|
|
59
|
-
alignmentMode: isNiriSession() ? "niri" : "generic",
|
|
60
|
-
warnings: [],
|
|
61
|
-
error: null,
|
|
62
|
-
};
|
|
63
|
-
const alignmentController = createLatestOnlyRunner(({ isCurrent }) =>
|
|
64
|
-
niriNativeWindowRuntime.alignWindowToTop(isCurrent),
|
|
65
|
-
);
|
|
66
|
-
const visibilityRuntime = createBrowserWindowVisibilityRuntime(() => browserWindow, interactive);
|
|
67
|
-
|
|
68
|
-
async function applyRendererVisibility(visible, isCurrent = () => true) {
|
|
69
|
-
if (appQuitting) return false;
|
|
70
|
-
const applied = await visibilityRuntime.apply(visible, () => !appQuitting && isCurrent());
|
|
71
|
-
if (applied) rendererVisible = Boolean(visible);
|
|
72
|
-
refreshRuntimeStatus();
|
|
73
|
-
return applied;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function isNiriSession() {
|
|
77
|
-
return Boolean(process.env.NIRI_SOCKET);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function isUsableBrowserWindow(window) {
|
|
81
|
-
return Boolean(
|
|
82
|
-
window && !window.isDestroyed() && window.webContents && !window.webContents.isDestroyed(),
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function ensureBrowserWindow() {
|
|
87
|
-
if (appQuitting) return null;
|
|
88
|
-
if (windowCreation) {
|
|
89
|
-
const pendingWindow = await windowCreation;
|
|
90
|
-
return isUsableBrowserWindow(pendingWindow) ? pendingWindow : null;
|
|
91
|
-
}
|
|
92
|
-
if (isUsableBrowserWindow(browserWindow)) return browserWindow;
|
|
93
|
-
|
|
94
|
-
const staleWindow = browserWindow;
|
|
95
|
-
browserWindow = null;
|
|
96
|
-
rendererVisible = false;
|
|
97
|
-
focusedWorkspaceViewVisible = false;
|
|
98
|
-
if (staleWindow && !staleWindow.isDestroyed()) staleWindow.destroy();
|
|
99
|
-
|
|
100
|
-
runtimeStatus.state = "starting";
|
|
101
|
-
runtimeStatus.error = null;
|
|
102
|
-
refreshRuntimeStatus();
|
|
103
|
-
windowCreation = createWindow()
|
|
104
|
-
.catch((error) => {
|
|
105
|
-
runtimeStatus.state = "error";
|
|
106
|
-
runtimeStatus.error = error instanceof Error ? error.message : String(error);
|
|
107
|
-
refreshRuntimeStatus();
|
|
108
|
-
throw error;
|
|
109
|
-
})
|
|
110
|
-
.finally(() => {
|
|
111
|
-
windowCreation = null;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const createdWindow = await windowCreation;
|
|
115
|
-
return isUsableBrowserWindow(createdWindow) ? createdWindow : null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const getNiriWindows = () =>
|
|
119
|
-
readNiriWindows(execFileAsync, process.env, ACTIVITY_STRIP_WORKSPACE_SYNC_MS);
|
|
120
|
-
const getNiriWorkspaces = () =>
|
|
121
|
-
readNiriWorkspaces(execFileAsync, process.env, ACTIVITY_STRIP_WORKSPACE_SYNC_MS);
|
|
122
|
-
|
|
123
|
-
function sendRendererSnapshot() {
|
|
124
|
-
if (appQuitting || !isUsableBrowserWindow(browserWindow)) return;
|
|
125
|
-
const sessions = isNiriSession()
|
|
126
|
-
? latestSnapshot.sessions.filter((session) => workspaceSessionIds.has(session.sessionId))
|
|
127
|
-
: latestSnapshot.sessions;
|
|
128
|
-
browserWindow.webContents.send("pi-activity-strip:snapshot", {
|
|
129
|
-
...latestSnapshot,
|
|
130
|
-
sessions,
|
|
131
|
-
focusedSessionId,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function publishWorkspaceView(view) {
|
|
136
|
-
workspaceSessionIds = new Set(view.sessions.map((session) => session.sessionId));
|
|
137
|
-
focusedSessionId = view.focusedSessionId;
|
|
138
|
-
focusedWorkspaceViewVisible = Boolean(view.workspace?.is_focused && view.sessions.length > 0);
|
|
139
|
-
sendRendererSnapshot();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const niriNativeWindowRuntime = createNiriNativeWindowRuntime({
|
|
143
|
-
execFileAsync,
|
|
144
|
-
env: process.env,
|
|
145
|
-
timeoutMs: ACTIVITY_STRIP_WORKSPACE_SYNC_MS,
|
|
146
|
-
processId: process.pid,
|
|
147
|
-
readWindows: getNiriWindows,
|
|
148
|
-
getBrowserWindow: () => browserWindow,
|
|
149
|
-
getBounds: currentBounds,
|
|
150
|
-
isNiriSession,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const workspaceViewRuntime = createNiriWorkspaceViewRuntime({
|
|
154
|
-
readWindows: getNiriWindows,
|
|
155
|
-
readWorkspaces: getNiriWorkspaces,
|
|
156
|
-
getSessions: () => latestSnapshot.sessions,
|
|
157
|
-
getStripWindow: niriNativeWindowRuntime.resolveProcessWindow,
|
|
158
|
-
isWindowVisible: () =>
|
|
159
|
-
Boolean(browserWindow && !browserWindow.isDestroyed() && browserWindow.isVisible()),
|
|
160
|
-
isWindowExpanded: () => expanded,
|
|
161
|
-
showWindow: async () => {
|
|
162
|
-
if (appQuitting) return;
|
|
163
|
-
const window = await ensureBrowserWindow();
|
|
164
|
-
if (!window || appQuitting) return;
|
|
165
|
-
window.showInactive?.();
|
|
166
|
-
},
|
|
167
|
-
hideWindow: () => {
|
|
168
|
-
rendererVisible = false;
|
|
169
|
-
focusedWorkspaceViewVisible = false;
|
|
170
|
-
browserWindow?.hide?.();
|
|
171
|
-
refreshRuntimeStatus();
|
|
172
|
-
},
|
|
173
|
-
concealWindow: async () => {
|
|
174
|
-
const window = await ensureBrowserWindow();
|
|
175
|
-
if (!window) return false;
|
|
176
|
-
return applyRendererVisibility(false);
|
|
177
|
-
},
|
|
178
|
-
revealWindow: (isCurrent) => applyRendererVisibility(true, isCurrent),
|
|
179
|
-
cancelReveal: () => {
|
|
180
|
-
void applyRendererVisibility(false);
|
|
181
|
-
},
|
|
182
|
-
collapseWindow: async () => {
|
|
183
|
-
browserWindow?.webContents.send("pi-activity-strip:collapse");
|
|
184
|
-
await setExpanded(false, { reconcile: false });
|
|
185
|
-
},
|
|
186
|
-
publishView: publishWorkspaceView,
|
|
187
|
-
moveWindowToWorkspace: niriNativeWindowRuntime.moveWindowToWorkspace,
|
|
188
|
-
alignWindow: niriNativeWindowRuntime.alignWindowToTop,
|
|
189
|
-
settleWindow: async (isCurrent) => {
|
|
190
|
-
await new Promise((resolve) => setTimeout(resolve, ACTIVITY_STRIP_NIRI_ANIMATION_SETTLE_MS));
|
|
191
|
-
return isCurrent();
|
|
192
|
-
},
|
|
193
|
-
isWindowAligned: niriNativeWindowRuntime.isWindowAligned,
|
|
194
|
-
identityOptions: { env: process.env },
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
function requestWindowRecovery() {
|
|
198
|
-
if (appQuitting) return;
|
|
199
|
-
if (isNiriSession()) {
|
|
200
|
-
workspaceViewRuntime.request();
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
void ensureBrowserWindow()
|
|
204
|
-
.then((window) => {
|
|
205
|
-
if (!window || appQuitting) return;
|
|
206
|
-
window.showInactive?.();
|
|
207
|
-
sendRendererSnapshot();
|
|
208
|
-
scheduleTopAlignment();
|
|
209
|
-
})
|
|
210
|
-
.catch(() => {});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function beginShutdown() {
|
|
214
|
-
if (appQuitting) return;
|
|
215
|
-
appQuitting = true;
|
|
216
|
-
workspaceEventWatcher?.stop();
|
|
217
|
-
visibilityRuntime.dispose();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function requestTopAlignment() {
|
|
221
|
-
if (isNiriSession()) workspaceViewRuntime.request();
|
|
222
|
-
else alignmentController.request();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function scheduleTopAlignment() {
|
|
226
|
-
if (isNiriSession()) {
|
|
227
|
-
workspaceViewRuntime.request();
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
requestTopAlignment();
|
|
231
|
-
|
|
232
|
-
for (const delayMs of [150, 450, 900, 1500, 2400]) {
|
|
233
|
-
setTimeout(requestTopAlignment, delayMs);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function refreshRuntimeStatus() {
|
|
238
|
-
runtimeStatus.alignmentMode = isNiriSession() ? "niri" : "generic";
|
|
239
|
-
runtimeStatus.windowVisible = Boolean(
|
|
240
|
-
rendererVisible &&
|
|
241
|
-
focusedWorkspaceViewVisible &&
|
|
242
|
-
browserWindow &&
|
|
243
|
-
!browserWindow.isDestroyed() &&
|
|
244
|
-
browserWindow.isVisible(),
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
if (!app.isReady()) {
|
|
248
|
-
runtimeStatus.warnings = isNiriSession()
|
|
249
|
-
? []
|
|
250
|
-
: [
|
|
251
|
-
"Top-edge repair is optimized for Niri. Other window managers fall back to generic Electron bounds and may need manual adjustment.",
|
|
252
|
-
];
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const displays = screen.getAllDisplays();
|
|
257
|
-
runtimeStatus.displayCount = displays.length;
|
|
258
|
-
const warnings = [];
|
|
259
|
-
if (displays.length > 1) {
|
|
260
|
-
warnings.push(
|
|
261
|
-
`Detected ${displays.length} displays; the strip currently renders on the primary display only.`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
if (!isNiriSession()) {
|
|
265
|
-
warnings.push(
|
|
266
|
-
"Top-edge repair is optimized for Niri. Other window managers fall back to generic Electron bounds and may need manual adjustment.",
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
runtimeStatus.warnings = warnings;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function currentDisplay() {
|
|
273
|
-
return screen.getPrimaryDisplay();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function currentBounds() {
|
|
277
|
-
const display = currentDisplay();
|
|
278
|
-
const bounds = display.bounds;
|
|
279
|
-
return {
|
|
280
|
-
x: bounds.x + ACTIVITY_STRIP_WIDTH_PADDING / 2,
|
|
281
|
-
y: bounds.y,
|
|
282
|
-
width: Math.max(420, bounds.width - ACTIVITY_STRIP_WIDTH_PADDING),
|
|
283
|
-
height: expanded ? ACTIVITY_STRIP_EXPANDED_HEIGHT : ACTIVITY_STRIP_HEIGHT,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function setExpanded(nextExpanded, { reconcile = true } = {}) {
|
|
288
|
-
if (!interactive || !browserWindow || browserWindow.isDestroyed()) return { ok: false };
|
|
289
|
-
const next = Boolean(nextExpanded);
|
|
290
|
-
expanded = next;
|
|
291
|
-
const bounds = currentBounds();
|
|
292
|
-
// Always reconcile the native surface, even when the logical state already matches. BrowserWindow
|
|
293
|
-
// blur can collapse main-process state before the renderer has removed its expanded layout.
|
|
294
|
-
browserWindow.setSize(bounds.width, bounds.height, false);
|
|
295
|
-
if (reconcile) {
|
|
296
|
-
if (isNiriSession()) workspaceViewRuntime.request();
|
|
297
|
-
else scheduleTopAlignment();
|
|
298
|
-
}
|
|
299
|
-
return { ok: true, expanded };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function updateWindowBounds() {
|
|
303
|
-
if (!browserWindow || browserWindow.isDestroyed()) return;
|
|
304
|
-
refreshRuntimeStatus();
|
|
305
|
-
if (isNiriSession()) workspaceViewRuntime.request();
|
|
306
|
-
else scheduleTopAlignment();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function createWindow() {
|
|
310
|
-
const createdWindow = new BrowserWindow({
|
|
311
|
-
...currentBounds(),
|
|
312
|
-
title: "Pi Activity Strip",
|
|
313
|
-
frame: false,
|
|
314
|
-
transparent: true,
|
|
315
|
-
hasShadow: false,
|
|
316
|
-
// Keep the Wayland surface resizable so Electron can honor both grow and shrink configure
|
|
317
|
-
// requests. The application still owns and continuously supplies the exact two legal heights.
|
|
318
|
-
resizable: true,
|
|
319
|
-
movable: false,
|
|
320
|
-
minimizable: false,
|
|
321
|
-
maximizable: false,
|
|
322
|
-
closable: true,
|
|
323
|
-
skipTaskbar: true,
|
|
324
|
-
show: false,
|
|
325
|
-
focusable: !isNiriSession() && interactive,
|
|
326
|
-
alwaysOnTop: true,
|
|
327
|
-
webPreferences: {
|
|
328
|
-
preload: preloadPath,
|
|
329
|
-
contextIsolation: true,
|
|
330
|
-
sandbox: true,
|
|
331
|
-
nodeIntegration: false,
|
|
332
|
-
backgroundThrottling: false,
|
|
333
|
-
},
|
|
334
|
-
backgroundColor: "#00000000",
|
|
335
|
-
});
|
|
336
|
-
browserWindow = createdWindow;
|
|
337
|
-
|
|
338
|
-
createdWindow.setAlwaysOnTop(true, "screen-saver");
|
|
339
|
-
createdWindow.setIgnoreMouseEvents(isNiriSession() || !interactive, { forward: true });
|
|
340
|
-
|
|
341
|
-
createdWindow.once("ready-to-show", () => {
|
|
342
|
-
if (appQuitting || browserWindow !== createdWindow || createdWindow.isDestroyed()) return;
|
|
343
|
-
runtimeStatus.state = "ready";
|
|
344
|
-
runtimeStatus.readyAt = Date.now();
|
|
345
|
-
runtimeStatus.error = null;
|
|
346
|
-
if (!isNiriSession()) {
|
|
347
|
-
createdWindow.showInactive?.();
|
|
348
|
-
sendRendererSnapshot();
|
|
349
|
-
scheduleTopAlignment();
|
|
350
|
-
}
|
|
351
|
-
refreshRuntimeStatus();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
createdWindow.on("show", () => refreshRuntimeStatus());
|
|
355
|
-
createdWindow.on("hide", () => refreshRuntimeStatus());
|
|
356
|
-
createdWindow.on("blur", () => {
|
|
357
|
-
if (browserWindow !== createdWindow) return;
|
|
358
|
-
createdWindow.webContents.send("pi-activity-strip:collapse");
|
|
359
|
-
setExpanded(false).catch(() => {});
|
|
360
|
-
});
|
|
361
|
-
createdWindow.webContents.on("render-process-gone", () => {
|
|
362
|
-
if (appQuitting || browserWindow !== createdWindow) return;
|
|
363
|
-
rendererVisible = false;
|
|
364
|
-
focusedWorkspaceViewVisible = false;
|
|
365
|
-
runtimeStatus.state = "starting";
|
|
366
|
-
refreshRuntimeStatus();
|
|
367
|
-
if (!createdWindow.isDestroyed()) createdWindow.destroy();
|
|
368
|
-
});
|
|
369
|
-
createdWindow.on("closed", () => {
|
|
370
|
-
if (browserWindow !== createdWindow) return;
|
|
371
|
-
rendererVisible = false;
|
|
372
|
-
focusedWorkspaceViewVisible = false;
|
|
373
|
-
browserWindow = null;
|
|
374
|
-
if (!appQuitting) runtimeStatus.state = "starting";
|
|
375
|
-
refreshRuntimeStatus();
|
|
376
|
-
if (!appQuitting) {
|
|
377
|
-
const recoveryTimer = setTimeout(requestWindowRecovery, 0);
|
|
378
|
-
recoveryTimer.unref?.();
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const html = createStripHtml({ interactive, initiallyVisible: !isNiriSession() });
|
|
383
|
-
try {
|
|
384
|
-
await createdWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
if (browserWindow === createdWindow) browserWindow = null;
|
|
387
|
-
if (!createdWindow.isDestroyed()) createdWindow.destroy();
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (!isNiriSession() && !createdWindow.isVisible()) {
|
|
392
|
-
createdWindow.showInactive?.();
|
|
393
|
-
sendRendererSnapshot();
|
|
394
|
-
refreshRuntimeStatus();
|
|
395
|
-
scheduleTopAlignment();
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return createdWindow;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async function main() {
|
|
402
|
-
if (!app.requestSingleInstanceLock()) {
|
|
403
|
-
app.quit();
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const focusSession = async (sessionId) => {
|
|
408
|
-
const session = resolveSnapshotSession(latestSnapshot.sessions, sessionId);
|
|
409
|
-
if (!session) {
|
|
410
|
-
return {
|
|
411
|
-
ok: false,
|
|
412
|
-
error: "Session is no longer present or is ambiguous; focus did nothing.",
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
const result = await focusNiriSession(session, execFileAsync, process.env);
|
|
416
|
-
if (result.ok) {
|
|
417
|
-
focusedSessionId = session.sessionId;
|
|
418
|
-
sendRendererSnapshot();
|
|
419
|
-
}
|
|
420
|
-
return result;
|
|
421
|
-
};
|
|
422
|
-
broker = await createActivityStripBroker({
|
|
423
|
-
focusSession,
|
|
424
|
-
getRuntimeStatus: () => ({
|
|
425
|
-
...runtimeStatus,
|
|
426
|
-
warnings: [...runtimeStatus.warnings],
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
429
|
-
ipcMain.handle("pi-activity-strip:focus", (_event, sessionId) => focusSession(String(sessionId)));
|
|
430
|
-
ipcMain.handle("pi-activity-strip:set-expanded", (_event, nextExpanded) =>
|
|
431
|
-
setExpanded(Boolean(nextExpanded)),
|
|
432
|
-
);
|
|
433
|
-
ipcMain.on(visibilityRuntime.appliedChannel, visibilityRuntime.acknowledge);
|
|
434
|
-
broker.on("snapshot", (snapshot) => {
|
|
435
|
-
const sessionMembershipChanged = !haveSameSessionIds(
|
|
436
|
-
latestSnapshot.sessions,
|
|
437
|
-
snapshot.sessions,
|
|
438
|
-
);
|
|
439
|
-
latestSnapshot = snapshot;
|
|
440
|
-
if (!resolveSnapshotSession(latestSnapshot.sessions, focusedSessionId)) focusedSessionId = null;
|
|
441
|
-
sendRendererSnapshot();
|
|
442
|
-
if (isNiriSession() && sessionMembershipChanged) workspaceViewRuntime.request();
|
|
443
|
-
});
|
|
444
|
-
broker.on("shutdown-requested", async () => {
|
|
445
|
-
beginShutdown();
|
|
446
|
-
await broker?.stop();
|
|
447
|
-
app.quit();
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
app.setName("pi-activity-strip");
|
|
451
|
-
|
|
452
|
-
await app.whenReady();
|
|
453
|
-
refreshRuntimeStatus();
|
|
454
|
-
if (!(await ensureBrowserWindow())) {
|
|
455
|
-
throw new Error("Activity strip BrowserWindow did not become usable.");
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
screen.on("display-metrics-changed", () => updateWindowBounds());
|
|
459
|
-
screen.on("display-added", () => updateWindowBounds());
|
|
460
|
-
screen.on("display-removed", () => updateWindowBounds());
|
|
461
|
-
if (isNiriSession()) {
|
|
462
|
-
workspaceViewRuntime.request();
|
|
463
|
-
workspaceEventWatcher = createNiriWorkspaceEventWatcher({
|
|
464
|
-
spawn,
|
|
465
|
-
env: process.env,
|
|
466
|
-
onFocusedWorkspace: () => workspaceViewRuntime.request(),
|
|
467
|
-
onFallback: () => workspaceViewRuntime.requestPassive(),
|
|
468
|
-
fallbackMs: ACTIVITY_STRIP_WORKSPACE_SYNC_MS,
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
app.on("second-instance", () => requestWindowRecovery());
|
|
473
|
-
|
|
474
|
-
app.on("before-quit", async () => {
|
|
475
|
-
beginShutdown();
|
|
476
|
-
ipcMain.removeHandler("pi-activity-strip:focus");
|
|
477
|
-
ipcMain.removeHandler("pi-activity-strip:set-expanded");
|
|
478
|
-
ipcMain.removeListener(visibilityRuntime.appliedChannel, visibilityRuntime.acknowledge);
|
|
479
|
-
visibilityRuntime.dispose();
|
|
480
|
-
await broker?.stop();
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
app.on("window-all-closed", () => {
|
|
484
|
-
// Keep the broker alive until an explicit stop or process exit.
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
main().catch(async (error) => {
|
|
489
|
-
runtimeStatus.state = "error";
|
|
490
|
-
runtimeStatus.error = error instanceof Error ? error.message : String(error);
|
|
491
|
-
refreshRuntimeStatus();
|
|
492
|
-
console.error(error?.stack ?? error?.message ?? String(error));
|
|
493
|
-
try {
|
|
494
|
-
await broker?.stop();
|
|
495
|
-
} catch {
|
|
496
|
-
// ignore cleanup errors on fatal exit
|
|
497
|
-
}
|
|
498
|
-
process.exit(1);
|
|
499
|
-
});
|