claude-resume-hub 1.4.2 → 1.5.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 +71 -86
- package/app/icon.js +171 -0
- package/app/main.js +322 -0
- package/app/preload.js +31 -0
- package/app/renderer/index.html +681 -0
- package/lib/account.js +56 -0
- package/lib/detect.js +17 -1
- package/lib/engine.js +70 -9
- package/lib/notify.js +57 -0
- package/lib/sessions.js +40 -1
- package/lib/update.js +45 -0
- package/package.json +28 -3
package/README.md
CHANGED
|
@@ -1,137 +1,122 @@
|
|
|
1
|
-
<
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<img src="https://raw.githubusercontent.com/IbrahimKalemci/claude-resume-hub/main/docs/icon.png" width="84" alt="claude-resume-hub logo">
|
|
4
|
+
|
|
5
|
+
# claude-resume-hub
|
|
6
|
+
|
|
7
|
+
### Hit a Claude Code limit at 2am? **Wake up to finished work.**
|
|
8
|
+
|
|
9
|
+
Auto-continues your Claude Code session the moment a usage/session limit resets — as a **tray desktop app** or a one-line **CLI**. Cross-platform, zero-dependency, and it **never touches your tokens**.
|
|
7
10
|
|
|
8
|
-
<p
|
|
9
|
-
<a href="https://www.npmjs.com/package/claude-resume-hub"><img src="https://img.shields.io/npm/v/claude-resume-hub?color=c96442" alt="npm"></a>
|
|
11
|
+
<p>
|
|
12
|
+
<a href="https://www.npmjs.com/package/claude-resume-hub"><img src="https://img.shields.io/npm/v/claude-resume-hub?color=c96442&label=npm" alt="npm"></a>
|
|
10
13
|
<a href="https://github.com/IbrahimKalemci/claude-resume-hub/actions/workflows/ci.yml"><img src="https://github.com/IbrahimKalemci/claude-resume-hub/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
11
|
-
<img src="https://img.shields.io/badge/
|
|
14
|
+
<a href="https://github.com/IbrahimKalemci/claude-resume-hub/releases/latest"><img src="https://img.shields.io/badge/download-.exe-3fb950" alt="download"></a>
|
|
12
15
|
<img src="https://img.shields.io/badge/Windows%20%7C%20macOS%20%7C%20Linux-8b93a7" alt="platforms">
|
|
13
16
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT"></a>
|
|
14
17
|
</p>
|
|
15
18
|
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<img src="https://raw.githubusercontent.com/IbrahimKalemci/claude-resume-hub/main/docs/app-demo.svg" width="380" alt="claude-resume-hub desktop app: finds your session, waits out the limit, counts down, resumes automatically">
|
|
20
|
+
|
|
21
|
+
**🇬🇧 [English](#-english) · 🇹🇷 [Türkçe](#-türkçe)**
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
</div>
|
|
21
24
|
|
|
22
25
|
---
|
|
23
26
|
|
|
24
27
|
## 🇬🇧 English
|
|
25
28
|
|
|
26
|
-
Claude Code hits your limit and
|
|
27
|
-
**claude-resume-hub** waits for the exact reset time and continues **for you**, then pings your desktop when it's rolling again. One command, zero setup, every OS.
|
|
29
|
+
When Claude Code hits your limit it prints `Claude AI usage limit reached|…` and **stops** — you have to come back and type `continue`. **claude-resume-hub** finds your session, waits for the exact reset time, and continues for you. Then it pings you.
|
|
28
30
|
|
|
29
|
-
###
|
|
31
|
+
### 🖥️ Desktop app — download & run
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
The tray app finds your last session automatically, shows a live countdown, and resumes on its own. No terminal.
|
|
32
34
|
|
|
33
|
-
**
|
|
35
|
+
**[⬇ Download claude-resume-hub.exe](https://github.com/IbrahimKalemci/claude-resume-hub/releases/latest)** (Windows · macOS · Linux)
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**3.** A dashboard opens in your browser — click **“Enable alerts”**, then walk away. 🎉
|
|
40
|
-
|
|
41
|
-
That's it. No install, no config. It works on **Windows, macOS and Linux**.
|
|
37
|
+
1. Download and run it — it lives in your **system tray**.
|
|
38
|
+
2. It **auto-finds** the session you were last working in.
|
|
39
|
+
3. Pick **Continue** (or type a task), press **Start**, and walk away.
|
|
40
|
+
4. Limit hit → it counts down → resumes the moment the window reopens → you get a notification. 🎉
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
> It wraps Claude Code, so the `claude` CLI must be on your PATH.
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
- 🔔 **Desktop alerts** — close the terminal, go live your life
|
|
47
|
-
- 🖥️ **Beautiful live dashboard** — countdown, status, and a streaming log
|
|
48
|
-
- 🪶 **Zero dependencies**, 100% local — nothing leaves your machine
|
|
49
|
-
|
|
50
|
-
### Handy commands
|
|
44
|
+
### ⌨️ Prefer the terminal? One line, no install
|
|
51
45
|
|
|
52
46
|
```bash
|
|
53
|
-
npx claude-resume-hub
|
|
54
|
-
npx claude-resume-hub
|
|
55
|
-
npx claude-resume-hub --tray # background system-tray icon (Windows)
|
|
56
|
-
npx claude-resume-hub -t "run all tests and fix failures" # start a NEW session (not resume)
|
|
57
|
-
npx claude-resume-hub --list # list this project's sessions (+ their ids)
|
|
58
|
-
npx claude-resume-hub --web --smart # context-aware resume (picks up your last step)
|
|
59
|
-
npx claude-resume-hub -s <id> # resume a specific session
|
|
60
|
-
npx claude-resume-hub --help # all options
|
|
47
|
+
npx claude-resume-hub --web # auto-resume + a live dashboard
|
|
48
|
+
npx claude-resume-hub # ...or headless, no dashboard
|
|
61
49
|
```
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
**Always up to date:** `npx claude-resume-hub@latest` pulls the newest version automatically.
|
|
65
|
-
**No Node?** [**⬇ Download a standalone binary**](https://github.com/IbrahimKalemci/claude-resume-hub/releases/latest) (Windows `.exe` / macOS / Linux) — runs without Node installed. It still needs the `claude` CLI on your PATH.
|
|
51
|
+
Handy flags: `--list` (see your sessions) · `--session <id>` (resume a specific one) · `--smart` (context-aware resume) · `--tray` (Windows tray) · `--help`.
|
|
66
52
|
|
|
67
|
-
|
|
53
|
+
### ✨ Features
|
|
68
54
|
|
|
69
|
-
|
|
55
|
+
- ⏱️ **Exact countdown** — reads the real reset timestamp, not a guess
|
|
56
|
+
- 🎯 **Finds the right session** and pins it (no more resuming the wrong one)
|
|
57
|
+
- 📚 **Multi-project queue** — several projects resume in order on one reset clock
|
|
58
|
+
- 🔔 **Phone/chat alerts** — optional webhook or Telegram ping on reset / done / error
|
|
59
|
+
- 📺 **Live output** — watch what Claude did, even while you were away
|
|
60
|
+
- ⬆️ **Update banner** — tells you when a newer version ships
|
|
61
|
+
- 🪶 **Zero dependencies**, ~one small package · 🔒 **never reads your tokens**
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
**claude-resume-hub** limitin tam açılma saatini bekleyip **senin yerine** devam eder, iş yeniden başlayınca da masaüstüne bildirim atar. Tek komut, sıfır ayar, her işletim sistemi.
|
|
63
|
+
### 🔒 Security & privacy
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
It **never** reads or stores your Claude credentials (`~/.claude/.credentials.json` / keychain). It only reads your own conversation transcripts under `~/.claude/projects`, locally. No telemetry; the dashboard is localhost-only; release binaries are built by CI from source on clean machines, so a downloaded `.exe` carries no one's data.
|
|
75
66
|
|
|
76
|
-
|
|
67
|
+
---
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
## 🇹🇷 Türkçe
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
Claude Code limitine takılınca `Claude AI usage limit reached|…` yazıp **durur** — gelip `continue` yazman gerekir. **claude-resume-hub** oturumunu bulur, tam açılma saatini bekler ve senin yerine devam eder. Sonra da sana haber verir.
|
|
72
|
+
|
|
73
|
+
### 🖥️ Masaüstü uygulaması — indir çalıştır
|
|
83
74
|
|
|
84
|
-
|
|
75
|
+
Tepside duran uygulama son oturumunu otomatik bulur, canlı geri sayım gösterir, kendi devam eder. Terminal yok.
|
|
85
76
|
|
|
86
|
-
|
|
77
|
+
**[⬇ claude-resume-hub.exe indir](https://github.com/IbrahimKalemci/claude-resume-hub/releases/latest)** (Windows · macOS · Linux)
|
|
87
78
|
|
|
88
|
-
|
|
79
|
+
1. İndir ve çalıştır — **sistem tepsisinde** yaşar.
|
|
80
|
+
2. Son çalıştığın oturumu **kendi bulur**.
|
|
81
|
+
3. **Continue** seç (ya da task yaz), **Start**'a bas, işine bak.
|
|
82
|
+
4. Limit dolunca → geri sayar → açılır açılmaz devam eder → bildirim alırsın. 🎉
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
- 🔔 **Masaüstü bildirimi** — terminali kapat, hayatına dön
|
|
92
|
-
- 🖥️ **Şık canlı panel** — geri sayım, durum ve canlı log
|
|
93
|
-
- 🪶 **Sıfır bağımlılık**, %100 yerel — hiçbir veri makineden çıkmaz
|
|
84
|
+
> Claude Code'u sarmalar; `claude` CLI'ın PATH'te olması gerekir.
|
|
94
85
|
|
|
95
|
-
###
|
|
86
|
+
### ⌨️ Terminal mi? Tek satır, kurulum yok
|
|
96
87
|
|
|
97
88
|
```bash
|
|
98
|
-
npx claude-resume-hub
|
|
99
|
-
npx claude-resume-hub
|
|
100
|
-
npx claude-resume-hub --tray # arka planda sistem tepsisi ikonu (Windows)
|
|
101
|
-
npx claude-resume-hub -t "tüm testleri çalıştır ve hataları düzelt" # YENİ session başlat (devam DEĞİL)
|
|
102
|
-
npx claude-resume-hub --list # bu projenin session'larını listele (+ id'leri)
|
|
103
|
-
npx claude-resume-hub --web --smart # bağlam-farkında devam (kaldığın adımdan sürer)
|
|
104
|
-
npx claude-resume-hub -s <id> # belirli bir session'ı devam ettir
|
|
105
|
-
npx claude-resume-hub --help # tüm seçenekler
|
|
89
|
+
npx claude-resume-hub --web # otomatik devam + canlı panel
|
|
90
|
+
npx claude-resume-hub # ...ya da panelsiz
|
|
106
91
|
```
|
|
107
92
|
|
|
108
|
-
|
|
109
|
-
**Hep güncel:** `npx claude-resume-hub@latest` otomatik en yeni sürümü çeker.
|
|
110
|
-
**Node yok mu?** [**⬇ Standalone binary indir**](https://github.com/IbrahimKalemci/claude-resume-hub/releases/latest) (Windows `.exe` / macOS / Linux) — Node kurulu olmadan çalışır. Yine de `claude` CLI'ın PATH'te olması gerekir.
|
|
93
|
+
İşe yarayan bayraklar: `--list` (oturumları gör) · `--session <id>` (belirli oturum) · `--smart` (bağlam-farkında devam) · `--tray` (Windows tepsi) · `--help`.
|
|
111
94
|
|
|
112
|
-
|
|
95
|
+
### ✨ Özellikler
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
- ⏱️ **Net geri sayım** — tahmin değil, gerçek reset zamanını okur
|
|
98
|
+
- 🎯 **Doğru oturumu bulur** ve sabitler (yanlış oturumu devam ettirme derdi yok)
|
|
99
|
+
- 📚 **Çoklu proje kuyruğu** — birden fazla proje tek reset saatinde sırayla devam eder
|
|
100
|
+
- 🔔 **Telefon/sohbet bildirimi** — webhook ya da Telegram ile reset/bitiş/hata bildirimi
|
|
101
|
+
- 📺 **Canlı çıktı** — Claude ne yaptı, sen yokken bile gör
|
|
102
|
+
- ⬆️ **Güncelleme banner'ı** — yeni sürüm çıkınca söyler
|
|
103
|
+
- 🪶 **Sıfır bağımlılık** · 🔒 **token'ına asla dokunmaz**
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
- **Nothing leaves your machine.** No telemetry, no network calls of its own. The `--web` dashboard is localhost-only.
|
|
118
|
-
- **The binary contains only code — no sessions, no tokens.** Release executables are built by GitHub Actions from source on clean runners, so a downloaded `.exe` can't expose anyone's data (there's none embedded). Each user's run reads only *their own* local `~/.claude`.
|
|
105
|
+
### 🔒 Güvenlik & gizlilik
|
|
119
106
|
|
|
120
|
-
|
|
107
|
+
Claude kimlik bilgilerini (`~/.claude/.credentials.json` / keychain) **asla** okumaz/saklamaz. Sadece kendi makinendeki `~/.claude/projects` konuşma metinlerini okur. Telemetri yok; panel yalnızca localhost; release binary'leri CI'da temiz sunucularda kaynaktan derlenir — indirilen `.exe` kimsenin verisini taşımaz.
|
|
121
108
|
|
|
122
|
-
|
|
109
|
+
---
|
|
123
110
|
|
|
124
|
-
|
|
111
|
+
## 🛠️ From source · Kaynaktan
|
|
125
112
|
|
|
126
113
|
```bash
|
|
127
114
|
git clone https://github.com/IbrahimKalemci/claude-resume-hub.git
|
|
128
115
|
cd claude-resume-hub
|
|
129
|
-
|
|
116
|
+
npm start # CLI
|
|
117
|
+
npm run app:start # desktop app (needs: npm install)
|
|
130
118
|
```
|
|
131
119
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Made for everyone who's tired of waiting on the reset clock.<br>
|
|
136
|
-
<b>MIT</b> © Ibrahim Kalemci · <a href="CHANGELOG.md">Changelog</a>
|
|
137
|
-
</p>
|
|
120
|
+
<div align="center">
|
|
121
|
+
<sub>Made for everyone tired of waiting on the reset clock · <b>MIT</b> © Ibrahim Kalemci · <a href="CHANGELOG.md">Changelog</a></sub>
|
|
122
|
+
</div>
|
package/app/icon.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tiny dependency-free PNG generator, used to draw the tray icon at runtime so
|
|
5
|
+
* the repo needs no binary image assets. Produces an anti-aliased filled circle
|
|
6
|
+
* (the app's "dot" mark) in a given colour.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const zlib = require("zlib");
|
|
10
|
+
|
|
11
|
+
let CRC_TABLE = null;
|
|
12
|
+
function crcTable() {
|
|
13
|
+
if (CRC_TABLE) return CRC_TABLE;
|
|
14
|
+
CRC_TABLE = new Int32Array(256);
|
|
15
|
+
for (let n = 0; n < 256; n++) {
|
|
16
|
+
let c = n;
|
|
17
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
18
|
+
CRC_TABLE[n] = c;
|
|
19
|
+
}
|
|
20
|
+
return CRC_TABLE;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function crc32(buf) {
|
|
24
|
+
const t = crcTable();
|
|
25
|
+
let c = 0xffffffff;
|
|
26
|
+
for (let i = 0; i < buf.length; i++) c = t[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
27
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function chunk(type, data) {
|
|
31
|
+
const len = Buffer.alloc(4);
|
|
32
|
+
len.writeUInt32BE(data.length, 0);
|
|
33
|
+
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
34
|
+
const crc = Buffer.alloc(4);
|
|
35
|
+
crc.writeUInt32BE(crc32(body), 0);
|
|
36
|
+
return Buffer.concat([len, body, crc]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build an RGBA PNG of `size`x`size` using pixel(x, y) -> [r,g,b,a]. */
|
|
40
|
+
function makePNG(size, pixel) {
|
|
41
|
+
const raw = Buffer.alloc(size * (size * 4 + 1));
|
|
42
|
+
let p = 0;
|
|
43
|
+
for (let y = 0; y < size; y++) {
|
|
44
|
+
raw[p++] = 0; // filter: none
|
|
45
|
+
for (let x = 0; x < size; x++) {
|
|
46
|
+
const [r, g, b, a] = pixel(x, y);
|
|
47
|
+
raw[p++] = r; raw[p++] = g; raw[p++] = b; raw[p++] = a;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const ihdr = Buffer.alloc(13);
|
|
51
|
+
ihdr.writeUInt32BE(size, 0);
|
|
52
|
+
ihdr.writeUInt32BE(size, 4);
|
|
53
|
+
ihdr[8] = 8; // bit depth
|
|
54
|
+
ihdr[9] = 6; // colour type: RGBA
|
|
55
|
+
return Buffer.concat([
|
|
56
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
57
|
+
chunk("IHDR", ihdr),
|
|
58
|
+
chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
|
|
59
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hexToRgb(hex) {
|
|
64
|
+
const h = hex.replace("#", "");
|
|
65
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A filled, softly anti-aliased circle — the tray mark. */
|
|
69
|
+
function dotIcon(hex, size = 16) {
|
|
70
|
+
const [r, g, b] = hexToRgb(hex);
|
|
71
|
+
const c = (size - 1) / 2;
|
|
72
|
+
const radius = size * 0.42;
|
|
73
|
+
return makePNG(size, (x, y) => {
|
|
74
|
+
const d = Math.hypot(x - c, y - c);
|
|
75
|
+
// 1px feathered edge
|
|
76
|
+
const a = Math.max(0, Math.min(1, radius - d + 0.5));
|
|
77
|
+
return [r, g, b, Math.round(a * 255)];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
|
|
82
|
+
function segDist(px, py, ax, ay, bx, by) {
|
|
83
|
+
const dx = bx - ax, dy = by - ay;
|
|
84
|
+
const t = clamp(((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1), 0, 1);
|
|
85
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The app / window / exe icon: a terracotta disc with a cream clock glyph
|
|
90
|
+
* (ring + two hands) — matches the header logo's identity. Fully generated,
|
|
91
|
+
* no binary asset.
|
|
92
|
+
*/
|
|
93
|
+
function appIcon(size = 256, discHex = "#c96442") {
|
|
94
|
+
const [br, bg, bb] = hexToRgb(discHex); // disc colour (phase-tinted in the tray)
|
|
95
|
+
const [fr, fg, fb] = hexToRgb("#f6efe8"); // cream glyph
|
|
96
|
+
const c = (size - 1) / 2;
|
|
97
|
+
const discR = size * 0.47;
|
|
98
|
+
const ringR = size * 0.27;
|
|
99
|
+
const ringT = size * 0.052;
|
|
100
|
+
const handT = size * 0.05;
|
|
101
|
+
|
|
102
|
+
// hand endpoints (12 o'clock hour hand, ~2 o'clock minute hand)
|
|
103
|
+
const hx = c, hy = c - ringR * 0.60;
|
|
104
|
+
const mx = c + ringR * 0.52, my = c - ringR * 0.34;
|
|
105
|
+
|
|
106
|
+
return makePNG(size, (x, y) => {
|
|
107
|
+
const px = x + 0.5, py = y + 0.5;
|
|
108
|
+
const dc = Math.hypot(px - c, py - c);
|
|
109
|
+
const discA = clamp(discR - dc + 0.5, 0, 1); // disc coverage
|
|
110
|
+
if (discA <= 0) return [0, 0, 0, 0];
|
|
111
|
+
|
|
112
|
+
// glyph coverage: ring OR either hand
|
|
113
|
+
const ringA = clamp(ringT / 2 - Math.abs(dc - ringR) + 0.5, 0, 1);
|
|
114
|
+
const hourA = clamp(handT / 2 - segDist(px, py, c, c, hx, hy) + 0.5, 0, 1);
|
|
115
|
+
const minA = clamp(handT / 2 - segDist(px, py, c, c, mx, my) + 0.5, 0, 1);
|
|
116
|
+
const g = Math.max(ringA, hourA, minA);
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
Math.round(br + (fr - br) * g),
|
|
120
|
+
Math.round(bg + (fg - bg) * g),
|
|
121
|
+
Math.round(bb + (fb - bb) * g),
|
|
122
|
+
Math.round(discA * 255),
|
|
123
|
+
];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Tray mark: the same clock, drawn as a solid glyph in the phase colour on a
|
|
129
|
+
* transparent background. A stroked ring + hands stays legible at 16px, where a
|
|
130
|
+
* filled disc with a cut-out glyph turns to mush.
|
|
131
|
+
*/
|
|
132
|
+
function clockIcon(hex, size = 16) {
|
|
133
|
+
const [r, g, b] = hexToRgb(hex);
|
|
134
|
+
const c = (size - 1) / 2;
|
|
135
|
+
const ringR = size * 0.38;
|
|
136
|
+
const ringT = size * 0.13;
|
|
137
|
+
const handT = size * 0.12;
|
|
138
|
+
|
|
139
|
+
const hx = c, hy = c - ringR * 0.58; // hour hand -> 12
|
|
140
|
+
const mx = c + ringR * 0.50, my = c - ringR * 0.32; // minute hand -> ~2
|
|
141
|
+
|
|
142
|
+
return makePNG(size, (x, y) => {
|
|
143
|
+
const px = x + 0.5, py = y + 0.5;
|
|
144
|
+
const dc = Math.hypot(px - c, py - c);
|
|
145
|
+
const ringA = clamp(ringT / 2 - Math.abs(dc - ringR) + 0.5, 0, 1);
|
|
146
|
+
const hourA = clamp(handT / 2 - segDist(px, py, c, c, hx, hy) + 0.5, 0, 1);
|
|
147
|
+
const minA = clamp(handT / 2 - segDist(px, py, c, c, mx, my) + 0.5, 0, 1);
|
|
148
|
+
const a = Math.max(ringA, hourA, minA);
|
|
149
|
+
return [r, g, b, Math.round(a * 255)];
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Wrap a (square) PNG in a single-image .ico container (Vista+ PNG-in-ICO). */
|
|
154
|
+
function makeICO(png) {
|
|
155
|
+
const header = Buffer.alloc(6);
|
|
156
|
+
header.writeUInt16LE(0, 0); // reserved
|
|
157
|
+
header.writeUInt16LE(1, 2); // type: icon
|
|
158
|
+
header.writeUInt16LE(1, 4); // image count
|
|
159
|
+
const entry = Buffer.alloc(16);
|
|
160
|
+
entry[0] = 0; // width (0 => 256)
|
|
161
|
+
entry[1] = 0; // height (0 => 256)
|
|
162
|
+
entry[2] = 0; // palette
|
|
163
|
+
entry[3] = 0; // reserved
|
|
164
|
+
entry.writeUInt16LE(1, 4); // colour planes
|
|
165
|
+
entry.writeUInt16LE(32, 6); // bits per pixel
|
|
166
|
+
entry.writeUInt32LE(png.length, 8); // image size
|
|
167
|
+
entry.writeUInt32LE(22, 12); // offset (6 + 16)
|
|
168
|
+
return Buffer.concat([header, entry, png]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { dotIcon, appIcon, clockIcon, makePNG, makeICO };
|