clauddy 1.11.0 → 1.11.1
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/main.js +84 -6
- package/package.json +1 -1
package/main.js
CHANGED
|
@@ -36,6 +36,9 @@ if (process.env.CLAUDE_CONFIG_DIR) {
|
|
|
36
36
|
const EXTERNAL_CONFIG = path.join(DATA_DIR, 'config.json')
|
|
37
37
|
// debug channel: `./pet <state>` writes here to force a state (dev only)
|
|
38
38
|
const DEBUG_FILE = path.join(DATA_DIR, 'debug.json')
|
|
39
|
+
// remembered floating-widget position: survives restarts and lets the widget
|
|
40
|
+
// return to the monitor the user parked it on after a display is unplugged/replugged
|
|
41
|
+
const WINDOW_STATE = path.join(DATA_DIR, 'window.json')
|
|
39
42
|
|
|
40
43
|
let win
|
|
41
44
|
let pollTimer
|
|
@@ -49,6 +52,8 @@ let currentMode = 'floating' // 'floating' widget | 'menubar' popover
|
|
|
49
52
|
let trayBounds = null // last known tray icon rect, to anchor the popover
|
|
50
53
|
let lastBlurHide = 0 // debounce: ignore the tray click that dismissed the popover
|
|
51
54
|
let sessionPct = null // authoritative session % shown in the tray title
|
|
55
|
+
let lastProgrammaticMove = 0 // ignore the 'moved' event our own setPosition triggers
|
|
56
|
+
let displayChanging = 0 // ignore OS window-shuffles while a display (dis)connects
|
|
52
57
|
|
|
53
58
|
function publicConfig(c) {
|
|
54
59
|
return {
|
|
@@ -151,6 +156,27 @@ function createWindow() {
|
|
|
151
156
|
}
|
|
152
157
|
})
|
|
153
158
|
|
|
159
|
+
// remember where the user parks the widget — but not the moves we make
|
|
160
|
+
// ourselves (resize re-anchoring) nor the ones the OS forces when a display
|
|
161
|
+
// (dis)connects, so a monitor going dark never overwrites the saved spot
|
|
162
|
+
win.on('moved', () => {
|
|
163
|
+
if (Date.now() - lastProgrammaticMove < 500) return
|
|
164
|
+
if (Date.now() - displayChanging < 2000) return
|
|
165
|
+
saveWindowState()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// when a monitor is unplugged/replugged (or its layout changes), put the
|
|
169
|
+
// floating widget back on the display the user parked it on — the OS dumps
|
|
170
|
+
// it on the primary display otherwise, and never moves it back on its own
|
|
171
|
+
const onDisplayChange = () => {
|
|
172
|
+
displayChanging = Date.now()
|
|
173
|
+
if (currentMode === 'floating' && win && !win.isDestroyed() && win.isVisible())
|
|
174
|
+
positionFloating()
|
|
175
|
+
}
|
|
176
|
+
screen.on('display-added', onDisplayChange)
|
|
177
|
+
screen.on('display-removed', onDisplayChange)
|
|
178
|
+
screen.on('display-metrics-changed', onDisplayChange)
|
|
179
|
+
|
|
154
180
|
const tick = () => {
|
|
155
181
|
if (!win || win.isDestroyed()) return
|
|
156
182
|
try {
|
|
@@ -250,6 +276,43 @@ function showPopover() {
|
|
|
250
276
|
win.focus()
|
|
251
277
|
}
|
|
252
278
|
|
|
279
|
+
// route every programmatic move through here so the 'moved' handler can tell
|
|
280
|
+
// our own repositioning apart from a genuine user drag (and skip saving it)
|
|
281
|
+
function moveWindow(x, y) {
|
|
282
|
+
lastProgrammaticMove = Date.now()
|
|
283
|
+
win.setPosition(Math.round(x), Math.round(y))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// persist the widget's anchor — its bottom-right corner, since the window's
|
|
287
|
+
// size changes as the pet animates — so it can be restored later
|
|
288
|
+
function saveWindowState() {
|
|
289
|
+
if (currentMode !== 'floating' || !win || win.isDestroyed()) return
|
|
290
|
+
const b = win.getBounds()
|
|
291
|
+
try {
|
|
292
|
+
fs.mkdirSync(DATA_DIR, { recursive: true })
|
|
293
|
+
fs.writeFileSync(WINDOW_STATE, JSON.stringify({ right: b.x + b.width, bottom: b.y + b.height }))
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function loadWindowState() {
|
|
298
|
+
try {
|
|
299
|
+
const s = JSON.parse(fs.readFileSync(WINDOW_STATE, 'utf8'))
|
|
300
|
+
return Number.isFinite(s?.right) && Number.isFinite(s?.bottom) ? s : null
|
|
301
|
+
} catch {
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// is the saved bottom-right corner on a display that's currently connected?
|
|
307
|
+
function cornerVisible(right, bottom) {
|
|
308
|
+
const x = Math.round(right - 1)
|
|
309
|
+
const y = Math.round(bottom - 1)
|
|
310
|
+
return screen.getAllDisplays().some((d) => {
|
|
311
|
+
const b = d.bounds
|
|
312
|
+
return x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
253
316
|
// center the popover under the tray icon, kept on-screen
|
|
254
317
|
function positionUnderTray() {
|
|
255
318
|
const b = win.getBounds()
|
|
@@ -261,13 +324,20 @@ function positionUnderTray() {
|
|
|
261
324
|
y = Math.round(trayBounds.y + trayBounds.height)
|
|
262
325
|
}
|
|
263
326
|
x = Math.max(8, Math.min(x, workAreaSize.width - b.width - 8))
|
|
264
|
-
|
|
327
|
+
moveWindow(x, y)
|
|
265
328
|
}
|
|
266
329
|
|
|
267
330
|
function positionFloating() {
|
|
268
|
-
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
269
331
|
const b = win.getBounds()
|
|
270
|
-
|
|
332
|
+
// reuse the saved spot when that monitor is still connected; otherwise fall
|
|
333
|
+
// back to the primary display's bottom-right corner
|
|
334
|
+
const saved = loadWindowState()
|
|
335
|
+
if (saved && cornerVisible(saved.right, saved.bottom)) {
|
|
336
|
+
moveWindow(saved.right - b.width, saved.bottom - b.height)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
340
|
+
moveWindow(workAreaSize.width - b.width - 24, workAreaSize.height - b.height - 24)
|
|
271
341
|
}
|
|
272
342
|
|
|
273
343
|
// the tray shows the live session % (macOS title), turning 🔥 near the limit
|
|
@@ -296,12 +366,20 @@ ipcMain.on('resize', (_e, w, h) => {
|
|
|
296
366
|
if (!win || win.isDestroyed()) return
|
|
297
367
|
const width = Math.max(100, Math.round(w))
|
|
298
368
|
const height = Math.max(110, Math.round(h))
|
|
299
|
-
win.setContentSize(width, height)
|
|
300
369
|
if (currentMode === 'menubar') {
|
|
370
|
+
win.setContentSize(width, height)
|
|
301
371
|
if (win.isVisible()) positionUnderTray() // keep it anchored under the tray
|
|
302
372
|
} else {
|
|
303
|
-
|
|
304
|
-
|
|
373
|
+
// keep the widget pinned to its current bottom-right corner on whatever
|
|
374
|
+
// display the user dragged it to. setContentSize grows from the top-left
|
|
375
|
+
// origin, so re-anchor by the old corner instead of snapping to the primary
|
|
376
|
+
// display's bottom-right (which yanked the widget back on every update).
|
|
377
|
+
const before = win.getBounds()
|
|
378
|
+
const right = before.x + before.width
|
|
379
|
+
const bottom = before.y + before.height
|
|
380
|
+
win.setContentSize(width, height)
|
|
381
|
+
const after = win.getBounds()
|
|
382
|
+
moveWindow(right - after.width, bottom - after.height)
|
|
305
383
|
}
|
|
306
384
|
})
|
|
307
385
|
|