@tryinget/pi-activity-strip 0.2.0 → 0.3.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 +37 -6
- package/bin/pi-activity-strip.mjs +55 -5
- package/examples/simulate-sessions.mjs +5 -0
- package/extensions/activity-strip.js +6 -0
- package/extensions/activity-strip.ts +6 -0
- package/package.json +4 -4
- package/src/broker/server.mjs +23 -2
- package/src/broker/session-store.mjs +6 -0
- package/src/client/broker-client.mjs +6 -0
- package/src/client/launcher.mjs +6 -0
- package/src/client/session-telemetry.mjs +11 -2
- package/src/common/activity-order.mjs +68 -0
- package/src/common/alignment-controller.mjs +74 -0
- package/src/common/compatibility.mjs +14 -2
- package/src/common/constants.mjs +10 -0
- package/src/common/contracts.d.ts +10 -0
- package/src/common/contracts.ts +10 -0
- package/src/common/electron.mjs +6 -0
- package/src/common/format.mjs +6 -0
- package/src/common/niri-focus.mjs +322 -0
- package/src/common/protocol.mjs +6 -0
- package/src/common/status-report.mjs +6 -0
- package/src/common/telemetry.mjs +13 -3
- package/src/electron/main.mjs +307 -127
- package/src/electron/niri-native-window-runtime.mjs +178 -0
- package/src/electron/niri-workspace-events.mjs +81 -0
- package/src/electron/preload.cjs +40 -0
- package/src/electron/renderer-visibility-runtime.mjs +107 -0
- package/src/electron/workspace-view-runtime.mjs +360 -0
- package/src/ui/strip-html.mjs +351 -242
package/src/electron/main.mjs
CHANGED
|
@@ -1,22 +1,53 @@
|
|
|
1
|
-
|
|
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";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
import { fileURLToPath } from "node:url";
|
|
4
10
|
import { promisify } from "node:util";
|
|
5
|
-
import { app, BrowserWindow, screen } from "electron";
|
|
11
|
+
import { app, BrowserWindow, ipcMain, screen } from "electron";
|
|
6
12
|
import { createActivityStripBroker } from "../broker/server.mjs";
|
|
13
|
+
import { createLatestOnlyRunner } from "../common/alignment-controller.mjs";
|
|
7
14
|
import { detectDisplayServer, detectWindowManager } from "../common/compatibility.mjs";
|
|
8
|
-
import {
|
|
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";
|
|
9
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";
|
|
10
33
|
|
|
11
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
35
|
const __dirname = path.dirname(__filename);
|
|
13
36
|
const preloadPath = path.join(__dirname, "preload.cjs");
|
|
14
|
-
const interactive = process.env.PI_ACTIVITY_STRIP_CLICK_THROUGH
|
|
37
|
+
const interactive = process.env.PI_ACTIVITY_STRIP_CLICK_THROUGH !== "1";
|
|
15
38
|
const execFileAsync = promisify(execFile);
|
|
16
39
|
|
|
17
40
|
let browserWindow = null;
|
|
18
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;
|
|
19
48
|
let latestSnapshot = { generatedAt: Date.now(), sessions: [] };
|
|
49
|
+
let focusedSessionId = null;
|
|
50
|
+
let workspaceSessionIds = new Set();
|
|
20
51
|
const runtimeStatus = {
|
|
21
52
|
state: "starting",
|
|
22
53
|
startedAt: Date.now(),
|
|
@@ -29,132 +60,188 @@ const runtimeStatus = {
|
|
|
29
60
|
warnings: [],
|
|
30
61
|
error: null,
|
|
31
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
|
+
}
|
|
32
75
|
|
|
33
76
|
function isNiriSession() {
|
|
34
77
|
return Boolean(process.env.NIRI_SOCKET);
|
|
35
78
|
}
|
|
36
79
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
function isUsableBrowserWindow(window) {
|
|
81
|
+
return Boolean(
|
|
82
|
+
window && !window.isDestroyed() && window.webContents && !window.webContents.isDestroyed(),
|
|
83
|
+
);
|
|
40
84
|
}
|
|
41
85
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
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;
|
|
50
93
|
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
const staleWindow = browserWindow;
|
|
95
|
+
browserWindow = null;
|
|
96
|
+
rendererVisible = false;
|
|
97
|
+
focusedWorkspaceViewVisible = false;
|
|
98
|
+
if (staleWindow && !staleWindow.isDestroyed()) staleWindow.destroy();
|
|
53
99
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
57
112
|
});
|
|
58
|
-
const windows = JSON.parse(stdout);
|
|
59
|
-
if (!Array.isArray(windows)) return null;
|
|
60
113
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
windows.find((window) => window?.pid === pid) ??
|
|
64
|
-
null
|
|
65
|
-
);
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
114
|
+
const createdWindow = await windowCreation;
|
|
115
|
+
return isUsableBrowserWindow(createdWindow) ? createdWindow : null;
|
|
69
116
|
}
|
|
70
117
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await execFileAsync(
|
|
88
|
-
"niri",
|
|
89
|
-
[
|
|
90
|
-
"msg",
|
|
91
|
-
"action",
|
|
92
|
-
"move-floating-window",
|
|
93
|
-
"--id",
|
|
94
|
-
String(niriWindow.id),
|
|
95
|
-
"-x",
|
|
96
|
-
formatDelta(deltaX),
|
|
97
|
-
"-y",
|
|
98
|
-
formatDelta(deltaY),
|
|
99
|
-
],
|
|
100
|
-
{ env: process.env },
|
|
101
|
-
);
|
|
102
|
-
} catch {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
107
|
-
const refreshedWindow = await getNiriWindowByPid(process.pid);
|
|
108
|
-
const refreshedPosition = readNiriPosition(refreshedWindow);
|
|
109
|
-
if (!refreshedPosition) return false;
|
|
110
|
-
|
|
111
|
-
deltaX = target.x - refreshedPosition.x;
|
|
112
|
-
deltaY = target.y - refreshedPosition.y;
|
|
113
|
-
if (Math.abs(deltaX) < 1 && Math.abs(deltaY) < 1) return true;
|
|
114
|
-
}
|
|
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
|
+
}
|
|
115
134
|
|
|
116
|
-
|
|
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();
|
|
117
140
|
}
|
|
118
141
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
});
|
|
121
152
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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();
|
|
125
201
|
return;
|
|
126
202
|
}
|
|
203
|
+
void ensureBrowserWindow()
|
|
204
|
+
.then((window) => {
|
|
205
|
+
if (!window || appQuitting) return;
|
|
206
|
+
window.showInactive?.();
|
|
207
|
+
sendRendererSnapshot();
|
|
208
|
+
scheduleTopAlignment();
|
|
209
|
+
})
|
|
210
|
+
.catch(() => {});
|
|
211
|
+
}
|
|
127
212
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
browserWindow.setSize(target.width, target.height, false);
|
|
135
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
136
|
-
}
|
|
213
|
+
function beginShutdown() {
|
|
214
|
+
if (appQuitting) return;
|
|
215
|
+
appQuitting = true;
|
|
216
|
+
workspaceEventWatcher?.stop();
|
|
217
|
+
visibilityRuntime.dispose();
|
|
218
|
+
}
|
|
137
219
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
220
|
+
function requestTopAlignment() {
|
|
221
|
+
if (isNiriSession()) workspaceViewRuntime.request();
|
|
222
|
+
else alignmentController.request();
|
|
142
223
|
}
|
|
143
224
|
|
|
144
225
|
function scheduleTopAlignment() {
|
|
145
|
-
|
|
226
|
+
if (isNiriSession()) {
|
|
227
|
+
workspaceViewRuntime.request();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
requestTopAlignment();
|
|
146
231
|
|
|
147
232
|
for (const delayMs of [150, 450, 900, 1500, 2400]) {
|
|
148
|
-
setTimeout(
|
|
149
|
-
alignWindowToTop().catch(() => {});
|
|
150
|
-
}, delayMs);
|
|
233
|
+
setTimeout(requestTopAlignment, delayMs);
|
|
151
234
|
}
|
|
152
235
|
}
|
|
153
236
|
|
|
154
237
|
function refreshRuntimeStatus() {
|
|
155
238
|
runtimeStatus.alignmentMode = isNiriSession() ? "niri" : "generic";
|
|
156
239
|
runtimeStatus.windowVisible = Boolean(
|
|
157
|
-
|
|
240
|
+
rendererVisible &&
|
|
241
|
+
focusedWorkspaceViewVisible &&
|
|
242
|
+
browserWindow &&
|
|
243
|
+
!browserWindow.isDestroyed() &&
|
|
244
|
+
browserWindow.isVisible(),
|
|
158
245
|
);
|
|
159
246
|
|
|
160
247
|
if (!app.isReady()) {
|
|
@@ -193,30 +280,49 @@ function currentBounds() {
|
|
|
193
280
|
x: bounds.x + ACTIVITY_STRIP_WIDTH_PADDING / 2,
|
|
194
281
|
y: bounds.y,
|
|
195
282
|
width: Math.max(420, bounds.width - ACTIVITY_STRIP_WIDTH_PADDING),
|
|
196
|
-
height: ACTIVITY_STRIP_HEIGHT,
|
|
283
|
+
height: expanded ? ACTIVITY_STRIP_EXPANDED_HEIGHT : ACTIVITY_STRIP_HEIGHT,
|
|
197
284
|
};
|
|
198
285
|
}
|
|
199
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
|
+
|
|
200
302
|
function updateWindowBounds() {
|
|
201
303
|
if (!browserWindow || browserWindow.isDestroyed()) return;
|
|
202
304
|
refreshRuntimeStatus();
|
|
203
|
-
|
|
305
|
+
if (isNiriSession()) workspaceViewRuntime.request();
|
|
306
|
+
else scheduleTopAlignment();
|
|
204
307
|
}
|
|
205
308
|
|
|
206
309
|
async function createWindow() {
|
|
207
|
-
|
|
310
|
+
const createdWindow = new BrowserWindow({
|
|
208
311
|
...currentBounds(),
|
|
209
312
|
title: "Pi Activity Strip",
|
|
210
313
|
frame: false,
|
|
211
314
|
transparent: true,
|
|
212
|
-
|
|
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,
|
|
213
319
|
movable: false,
|
|
214
320
|
minimizable: false,
|
|
215
321
|
maximizable: false,
|
|
216
322
|
closable: true,
|
|
217
323
|
skipTaskbar: true,
|
|
218
324
|
show: false,
|
|
219
|
-
focusable: interactive,
|
|
325
|
+
focusable: !isNiriSession() && interactive,
|
|
220
326
|
alwaysOnTop: true,
|
|
221
327
|
webPreferences: {
|
|
222
328
|
preload: preloadPath,
|
|
@@ -227,37 +333,69 @@ async function createWindow() {
|
|
|
227
333
|
},
|
|
228
334
|
backgroundColor: "#00000000",
|
|
229
335
|
});
|
|
336
|
+
browserWindow = createdWindow;
|
|
230
337
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
browserWindow.setIgnoreMouseEvents(!interactive, { forward: true });
|
|
338
|
+
createdWindow.setAlwaysOnTop(true, "screen-saver");
|
|
339
|
+
createdWindow.setIgnoreMouseEvents(isNiriSession() || !interactive, { forward: true });
|
|
234
340
|
|
|
235
|
-
|
|
341
|
+
createdWindow.once("ready-to-show", () => {
|
|
342
|
+
if (appQuitting || browserWindow !== createdWindow || createdWindow.isDestroyed()) return;
|
|
236
343
|
runtimeStatus.state = "ready";
|
|
237
344
|
runtimeStatus.readyAt = Date.now();
|
|
238
345
|
runtimeStatus.error = null;
|
|
239
|
-
|
|
240
|
-
|
|
346
|
+
if (!isNiriSession()) {
|
|
347
|
+
createdWindow.showInactive?.();
|
|
348
|
+
sendRendererSnapshot();
|
|
349
|
+
scheduleTopAlignment();
|
|
350
|
+
}
|
|
241
351
|
refreshRuntimeStatus();
|
|
242
|
-
scheduleTopAlignment();
|
|
243
352
|
});
|
|
244
353
|
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
|
247
390
|
|
|
248
|
-
if (!
|
|
249
|
-
|
|
250
|
-
|
|
391
|
+
if (!isNiriSession() && !createdWindow.isVisible()) {
|
|
392
|
+
createdWindow.showInactive?.();
|
|
393
|
+
sendRendererSnapshot();
|
|
251
394
|
refreshRuntimeStatus();
|
|
252
395
|
scheduleTopAlignment();
|
|
253
396
|
}
|
|
254
397
|
|
|
255
|
-
|
|
256
|
-
browserWindow.on("hide", () => refreshRuntimeStatus());
|
|
257
|
-
browserWindow.on("closed", () => {
|
|
258
|
-
browserWindow = null;
|
|
259
|
-
refreshRuntimeStatus();
|
|
260
|
-
});
|
|
398
|
+
return createdWindow;
|
|
261
399
|
}
|
|
262
400
|
|
|
263
401
|
async function main() {
|
|
@@ -266,17 +404,45 @@ async function main() {
|
|
|
266
404
|
return;
|
|
267
405
|
}
|
|
268
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
|
+
};
|
|
269
422
|
broker = await createActivityStripBroker({
|
|
423
|
+
focusSession,
|
|
270
424
|
getRuntimeStatus: () => ({
|
|
271
425
|
...runtimeStatus,
|
|
272
426
|
warnings: [...runtimeStatus.warnings],
|
|
273
427
|
}),
|
|
274
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);
|
|
275
434
|
broker.on("snapshot", (snapshot) => {
|
|
435
|
+
const sessionMembershipChanged = !haveSameSessionIds(
|
|
436
|
+
latestSnapshot.sessions,
|
|
437
|
+
snapshot.sessions,
|
|
438
|
+
);
|
|
276
439
|
latestSnapshot = snapshot;
|
|
277
|
-
|
|
440
|
+
if (!resolveSnapshotSession(latestSnapshot.sessions, focusedSessionId)) focusedSessionId = null;
|
|
441
|
+
sendRendererSnapshot();
|
|
442
|
+
if (isNiriSession() && sessionMembershipChanged) workspaceViewRuntime.request();
|
|
278
443
|
});
|
|
279
444
|
broker.on("shutdown-requested", async () => {
|
|
445
|
+
beginShutdown();
|
|
280
446
|
await broker?.stop();
|
|
281
447
|
app.quit();
|
|
282
448
|
});
|
|
@@ -285,18 +451,32 @@ async function main() {
|
|
|
285
451
|
|
|
286
452
|
await app.whenReady();
|
|
287
453
|
refreshRuntimeStatus();
|
|
288
|
-
await
|
|
454
|
+
if (!(await ensureBrowserWindow())) {
|
|
455
|
+
throw new Error("Activity strip BrowserWindow did not become usable.");
|
|
456
|
+
}
|
|
289
457
|
|
|
290
458
|
screen.on("display-metrics-changed", () => updateWindowBounds());
|
|
291
459
|
screen.on("display-added", () => updateWindowBounds());
|
|
292
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
|
+
}
|
|
293
471
|
|
|
294
|
-
app.on("second-instance", () =>
|
|
295
|
-
browserWindow?.webContents.send("pi-activity-strip:snapshot", latestSnapshot);
|
|
296
|
-
alignWindowToTop().catch(() => {});
|
|
297
|
-
});
|
|
472
|
+
app.on("second-instance", () => requestWindowRecovery());
|
|
298
473
|
|
|
299
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();
|
|
300
480
|
await broker?.stop();
|
|
301
481
|
});
|
|
302
482
|
|