clauddy 1.13.0 → 1.14.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 +13 -1
- package/config.json +1 -0
- package/main.js +3 -0
- package/package.json +1 -1
- package/renderer/index.html +16 -0
- package/renderer/pet.js +28 -7
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A cute pixel-art desktop pet for macOS that tracks your Claude Code usage — mi
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
|
|
7
|
-
https://github.com/user-attachments/assets/
|
|
7
|
+
https://github.com/user-attachments/assets/dbe00d9a-b49c-48ea-941c-76c517dec358
|
|
8
8
|
|
|
9
9
|
<em>A little terracotta creature that lives in the corner of your screen, eats your tokens, and naps when you're idle.</em>
|
|
10
10
|
</p>
|
|
@@ -142,6 +142,18 @@ chmod +x Clauddy-*.AppImage
|
|
|
142
142
|
./Clauddy-*.AppImage
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
+
Want the pet icon to show up in your app menu / taskbar too, instead of a generic icon? Running the AppImage directly doesn't register it anywhere — GNOME (and most Wayland desktops) only pick up an app's icon from an installed `.desktop` entry. One-time setup, right next to the AppImage:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
./Clauddy-*.AppImage --appimage-extract clauddy.desktop >/dev/null
|
|
149
|
+
./Clauddy-*.AppImage --appimage-extract usr/share/icons/hicolor/512x512/apps/clauddy.png >/dev/null
|
|
150
|
+
mkdir -p ~/.local/share/applications ~/.local/share/icons/hicolor/512x512/apps
|
|
151
|
+
cp squashfs-root/usr/share/icons/hicolor/512x512/apps/clauddy.png ~/.local/share/icons/hicolor/512x512/apps/clauddy.png
|
|
152
|
+
sed "s|^Exec=.*|Exec=$(readlink -f Clauddy-*.AppImage) --no-sandbox %U|" squashfs-root/clauddy.desktop > ~/.local/share/applications/clauddy.desktop
|
|
153
|
+
rm -rf squashfs-root
|
|
154
|
+
update-desktop-database ~/.local/share/applications 2>/dev/null
|
|
155
|
+
```
|
|
156
|
+
|
|
145
157
|
> The system tray icon needs an indicator extension on vanilla GNOME (e.g. "AppIndicator and KStatusNotifier Item Support") — it works out of the box on Cinnamon, KDE, and XFCE. Autostart-at-login is wired up via an XDG `.desktop` entry in `~/.config/autostart/`.
|
|
146
158
|
|
|
147
159
|
> The app keeps its data in `~/.claude-usage-monitor`, regardless of platform or how you run it.
|
package/config.json
CHANGED
package/main.js
CHANGED
|
@@ -65,6 +65,7 @@ function publicConfig(c) {
|
|
|
65
65
|
alerts: c.alerts,
|
|
66
66
|
alertThresholds: c.alertThresholds,
|
|
67
67
|
fireThreshold: c.fireThreshold,
|
|
68
|
+
zoom: c.zoom,
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -78,6 +79,7 @@ function loadConfig() {
|
|
|
78
79
|
alerts: true,
|
|
79
80
|
alertThresholds: [80, 95],
|
|
80
81
|
fireThreshold: 90, // session % at which the pet catches fire (tired still fixed at 100)
|
|
82
|
+
zoom: 100, // widget scale %, 100-200
|
|
81
83
|
pollIntervalMs: 4000,
|
|
82
84
|
activeThresholdMs: 20000,
|
|
83
85
|
sleepThresholdMs: 300000,
|
|
@@ -137,6 +139,7 @@ function createWindow() {
|
|
|
137
139
|
skipTaskbar: true,
|
|
138
140
|
hasShadow: false,
|
|
139
141
|
fullscreenable: false,
|
|
142
|
+
icon: path.join(__dirname, 'build', 'icon.png'),
|
|
140
143
|
webPreferences: {
|
|
141
144
|
preload: path.join(__dirname, 'preload.js'),
|
|
142
145
|
contextIsolation: true,
|
package/package.json
CHANGED
package/renderer/index.html
CHANGED
|
@@ -328,6 +328,22 @@
|
|
|
328
328
|
</span>
|
|
329
329
|
</div>
|
|
330
330
|
|
|
331
|
+
<div class="set-section">
|
|
332
|
+
<div class="set-sec-title">Zoom</div>
|
|
333
|
+
<div class="set-thresholds">
|
|
334
|
+
<span class="thr">
|
|
335
|
+
<span class="thr-cap">size</span>
|
|
336
|
+
<span class="num">
|
|
337
|
+
<input id="set-zoom" type="number" min="100" max="200" step="25" />
|
|
338
|
+
<span class="num-spin">
|
|
339
|
+
<button type="button" class="num-btn" data-for="set-zoom" data-step="25">▲</button>
|
|
340
|
+
<button type="button" class="num-btn" data-for="set-zoom" data-step="-25">▼</button>
|
|
341
|
+
</span>
|
|
342
|
+
</span>
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
331
347
|
<div class="set-section">
|
|
332
348
|
<div class="set-sec-title">
|
|
333
349
|
Alerts
|
package/renderer/pet.js
CHANGED
|
@@ -392,12 +392,15 @@ function renderBars(boxId, list, limit) {
|
|
|
392
392
|
function fitNames(box) {
|
|
393
393
|
const names = [...box.querySelectorAll('.mname')]
|
|
394
394
|
// a collapsed section measures zero — it re-fits when it opens
|
|
395
|
-
if (!names.length || !box.
|
|
395
|
+
if (!names.length || !box.offsetWidth) return
|
|
396
396
|
box.style.setProperty('--name-w', 'auto')
|
|
397
|
+
// offsetWidth, not getBoundingClientRect: the width we hand back is a layout px,
|
|
398
|
+
// so it has to be measured in layout px too. A CSS transform on an ancestor
|
|
399
|
+
// (the video stage scales the card) inflates the rect and squeezes the bars away.
|
|
397
400
|
let widest = 0
|
|
398
|
-
for (const n of names) widest = Math.max(widest, n.
|
|
399
|
-
const room = box.
|
|
400
|
-
const w = Math.max(NAME_MIN, Math.min(
|
|
401
|
+
for (const n of names) widest = Math.max(widest, n.offsetWidth)
|
|
402
|
+
const room = box.offsetWidth - BAR_MIN
|
|
403
|
+
const w = Math.max(NAME_MIN, Math.min(widest + 2, room))
|
|
401
404
|
box.style.setProperty('--name-w', `${w}px`)
|
|
402
405
|
}
|
|
403
406
|
|
|
@@ -593,8 +596,12 @@ let lastW = 0
|
|
|
593
596
|
function fitSize() {
|
|
594
597
|
requestAnimationFrame(() => {
|
|
595
598
|
const collapsed = document.body.classList.contains('collapsed')
|
|
596
|
-
const
|
|
597
|
-
const
|
|
599
|
+
const zoom = Number.parseFloat(document.body.style.zoom) || 1
|
|
600
|
+
const w = (collapsed ? 140 : 276) * zoom
|
|
601
|
+
// offsetHeight never reflects a CSS zoom applied to an ancestor (confirmed
|
|
602
|
+
// empirically against this Electron build) — so measure the unzoomed content
|
|
603
|
+
// height, then scale the whole thing (content + margin) ourselves
|
|
604
|
+
const h = (el('card').offsetHeight + 24) * zoom // 12px margin top + bottom
|
|
598
605
|
if (Math.abs(h - lastH) > 2 || w !== lastW) {
|
|
599
606
|
lastH = h
|
|
600
607
|
lastW = w
|
|
@@ -603,6 +610,12 @@ function fitSize() {
|
|
|
603
610
|
})
|
|
604
611
|
}
|
|
605
612
|
|
|
613
|
+
// widget scale, applied as a CSS zoom (not transform) so offsetWidth/Height
|
|
614
|
+
// keep reflecting it — see fitNames()'s comment on why transform can't be used here
|
|
615
|
+
function applyZoom(z) {
|
|
616
|
+
document.body.style.zoom = (z || 100) / 100
|
|
617
|
+
}
|
|
618
|
+
|
|
606
619
|
let currentConfig = {}
|
|
607
620
|
let realUsage = null
|
|
608
621
|
let debugState = null
|
|
@@ -621,6 +634,8 @@ window.api.onError((msg) => {
|
|
|
621
634
|
window.api.onConfig((cfg) => {
|
|
622
635
|
currentConfig = cfg || {}
|
|
623
636
|
document.body.classList.toggle('is-menubar', currentConfig.mode === 'menubar')
|
|
637
|
+
applyZoom(currentConfig.zoom)
|
|
638
|
+
fitSize()
|
|
624
639
|
})
|
|
625
640
|
window.api.onRealUsage((u) => {
|
|
626
641
|
realUsage = u || null
|
|
@@ -806,6 +821,7 @@ function snapshotSettings() {
|
|
|
806
821
|
t1: el('set-t1').value,
|
|
807
822
|
t2: el('set-t2').value,
|
|
808
823
|
fire: el('set-fire').value,
|
|
824
|
+
zoom: el('set-zoom').value,
|
|
809
825
|
})
|
|
810
826
|
}
|
|
811
827
|
function refreshSaveDirty() {
|
|
@@ -824,6 +840,7 @@ function populateSettings() {
|
|
|
824
840
|
el('set-t1').value = th[0] != null ? th[0] : 80
|
|
825
841
|
el('set-t2').value = th[1] != null ? th[1] : 95
|
|
826
842
|
el('set-fire').value = c.fireThreshold != null ? c.fireThreshold : 90
|
|
843
|
+
el('set-zoom').value = c.zoom != null ? c.zoom : 100
|
|
827
844
|
clearSaveDirty() // fields now match the saved config
|
|
828
845
|
}
|
|
829
846
|
function openSettings() {
|
|
@@ -847,7 +864,7 @@ for (const b of document.querySelectorAll('.num-btn')) {
|
|
|
847
864
|
})
|
|
848
865
|
}
|
|
849
866
|
// light up Save whenever an editable field changes
|
|
850
|
-
for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire']) {
|
|
867
|
+
for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire', 'set-zoom']) {
|
|
851
868
|
el(id).addEventListener('input', refreshSaveDirty)
|
|
852
869
|
el(id).addEventListener('change', refreshSaveDirty)
|
|
853
870
|
}
|
|
@@ -866,6 +883,9 @@ el('set-cancel').addEventListener('click', () => {
|
|
|
866
883
|
el('set-save').addEventListener('click', () => {
|
|
867
884
|
const num = (id) => parseFloat(el(id).value)
|
|
868
885
|
const fire = num('set-fire')
|
|
886
|
+
const zoomRaw = num('set-zoom')
|
|
887
|
+
const zoom = zoomRaw >= 100 && zoomRaw <= 200 ? Math.round(zoomRaw) : 100
|
|
888
|
+
applyZoom(zoom) // apply immediately so the fitSize() below measures the new size
|
|
869
889
|
window.api.saveConfig({
|
|
870
890
|
mode: selectedMode,
|
|
871
891
|
alerts: el('set-alerts').checked,
|
|
@@ -873,6 +893,7 @@ el('set-save').addEventListener('click', () => {
|
|
|
873
893
|
.filter((n) => n >= 1 && n <= 100)
|
|
874
894
|
.sort((a, b) => a - b),
|
|
875
895
|
fireThreshold: fire >= 1 && fire <= 99 ? fire : 90,
|
|
896
|
+
zoom,
|
|
876
897
|
})
|
|
877
898
|
clearSaveDirty()
|
|
878
899
|
document.body.classList.remove('settings-open')
|