copyhub-cli 1.0.8 → 1.1.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 CHANGED
@@ -1,54 +1,53 @@
1
1
  # CopyHub
2
2
 
3
- CopyHub theo dõi **clipboard**, lưu **lịch sử cục bộ** (`~/.copyhub/history.jsonl`), tùy chọn đồng bộ lên **Google Sheets** (một tab mỗi ngày), mở **overlay Electron** để xem chọn bản copy gần đây.
4
- English summary: watches clipboard, local JSONL history, optional daily Google Sheets tabs, floating history overlay.
3
+ CopyHub watches your **clipboard**, stores **local history** (`~/.copyhub/history.jsonl`), optionally syncs to **Google Sheets** (one tab per day), and opens an **Electron overlay** to browse and pick recent copies.
5
4
 
6
- Chạy trên **Windows**, **macOS**, **Linux**.
5
+ Runs on **Windows**, **macOS**, and **Linux**.
7
6
 
8
7
  ---
9
8
 
10
- ## Mục lục / Table of contents
9
+ ## Table of contents
11
10
 
12
- Các mục `##` bên dưới theo thứ tự: **Tính năng** · **Yêu cầu** · **Cài đặt** · **File `.env`** · **Google Cloud & OAuth** · **OAuth config vs env** · **Chạy lần đầu** · **Lệnh CLI** · **Biến môi trường** · **Thư mục dữ liệu** · **Google Sheets** · **Overlay** · **Clipboard & lịch sử** · **Nâng cấp** · **Xử lý sự cố** · **Bảo mật** · **Giấy phép**.
11
+ Sections below: **Features** · **Requirements** · **Installation** · **Environment files** · **Google Cloud & OAuth** · **OAuth config vs env** · **First run** · **CLI commands** · **Environment variables** · **Data directory** · **Google Sheets** · **Overlay** · **Clipboard & history** · **Updating** · **Troubleshooting** · **Security** · **License**.
13
12
 
14
13
  ---
15
14
 
16
- ## Tính năng / Features
15
+ ## Features
17
16
 
18
- - Theo dõi clipboard theo chu kỳ ( thể chỉnh `COPYHUB_POLL_MS`).
19
- - Không lưu hai lần liên tiếp **cùng một nội dung** vào `history.jsonl` / Sheet.
20
- - Ghi Sheet theo tab **`COPYHUB-YYYY-MM-DD`** (timezone máy).
21
- - Overlay: phân trang lịch sử, đồng bộ Sheet theo từng bước (không tải hết một lúc), gợi ý khi đang load Sheet.
17
+ - Clipboard polling (tunable via `COPYHUB_POLL_MS`).
18
+ - Skips saving the **same content twice in a row** to `history.jsonl` / Sheets.
19
+ - Writes Sheets to tabs named **`COPYHUB-YYYY-MM-DD`** (machine timezone).
20
+ - Overlay: paginated history, incremental Sheet sync (not all tabs at once), hints while Sheet data loads.
22
21
 
23
22
  ---
24
23
 
25
- ## Yêu cầu / Requirements
24
+ ## Requirements
26
25
 
27
26
  - **Node.js** ≥ 18
28
- - Một **Google Cloud project** có:
29
- - **Google Sheets API** bật **trên cùng project** với OAuth client
30
- - OAuth client kiểu **Web application** redirect URI đúng (xem dưới)
27
+ - A **Google Cloud project** with:
28
+ - **Google Sheets API** enabled **on the same project** as the OAuth client
29
+ - OAuth client type **Web application** and redirect URI configured correctly (see below)
31
30
 
32
31
  ---
33
32
 
34
- ## Cài đặt / Installation
33
+ ## Installation
35
34
 
36
- ### Cài global (npm)
35
+ ### Global install (npm)
37
36
 
38
37
  ```bash
39
38
  npm install -g copyhub-cli
40
39
  ```
41
40
 
42
- Đảm bảo `node` `copyhub` trong `PATH`. Linux/macOS thể cần cấu hình prefix npm cho user — xem [npm global installation](https://docs.npmjs.com/cli/v10/commands/npm-install#global-installation).
41
+ Ensure `node` and `copyhub` are on your `PATH`. On Linux/macOS you may need an npm global prefix for your user — see [npm global installation](https://docs.npmjs.com/cli/v10/commands/npm-install#global-installation).
43
42
 
44
- ### Từ nguồn (repo)
43
+ ### From source (this repo)
45
44
 
46
45
  ```bash
47
46
  npm install
48
47
  npm link
49
48
  ```
50
49
 
51
- Hoặc không link:
50
+ Without linking:
52
51
 
53
52
  ```bash
54
53
  node src/cli.js <command>
@@ -56,57 +55,57 @@ node src/cli.js <command>
56
55
 
57
56
  ---
58
57
 
59
- ## File `.env` và thứ tự đọc / Environment files
58
+ ## Environment files
60
59
 
61
- CLI Electron overlay gọi `loadCopyhubEnv()`: đọc lần lượt các file `.env`, parse rồi **gộp một object** — **file sau ghi đè key trùng** với file trước. Sau đó mỗi key chỉ được đưa vào `process.env` nếu biến đó **chưa tồn tại** trong môi trường tiến trình (shell đã `export` trước khi chạy Node thì luôn thắng).
60
+ The CLI and Electron overlay call `loadCopyhubEnv()`: each `.env` file is parsed and merged into **one object** — **later files override keys** from earlier ones. Then each key is applied to `process.env` **only if that variable is not already set** in the process environment (values you `export` in the shell before starting Node always win).
62
61
 
63
- Thứ tự file:
62
+ File order:
64
63
 
65
- 1. `<package>/.env` (thư mục cài package / repo khi dev)
64
+ 1. `<package>/.env` (installed package directory / repo when developing)
66
65
  2. `~/.copyhub/.env`
67
- 3. `./.env` của **thư mục làm việc hiện tại** (`cwd`)
66
+ 3. `./.env` in the **current working directory** (`cwd`)
68
67
 
69
- Nhờ đó `npm install -g` vẫn đọc được biến khi bạn đặt `~/.copyhub/.env`, không phụ thuộc cwd.
68
+ So after `npm install -g`, variables in `~/.copyhub/.env` still load regardless of `cwd`.
70
69
 
71
- Tham khảo mẫu: `.env.example`.
70
+ See the template: `.env.example`.
72
71
 
73
72
  ---
74
73
 
75
74
  ## Google Cloud & OAuth
76
75
 
77
- 1. Bật **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** cho project.
78
- 2. **Credentials** → **Create credentials** → **OAuth client ID** → kiểu **Web application**.
79
- 3. **Authorized redirect URIs** — thêm **chính xác** (CopyHub dùng `127.0.0.1`, không dùng `localhost` trong redirect mặc định):
76
+ 1. Enable **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** for your project.
77
+ 2. **Credentials** → **Create credentials** → **OAuth client ID** → **Web application**.
78
+ 3. **Authorized redirect URIs** — add **exactly** (CopyHub uses `127.0.0.1`, not `localhost`, for the default redirect):
80
79
 
81
80
  ```text
82
81
  http://127.0.0.1:19999/oauth2callback
83
82
  ```
84
83
 
85
- Nếu đổi port (`COPYHUB_OAUTH_REDIRECT_PORT` hoặc `redirectPort` trong config), URI trong Console phải khớp port đó.
84
+ If you change the port (`COPYHUB_OAUTH_REDIRECT_PORT` or `redirectPort` in config), the Console URI must use that port.
86
85
 
87
- 4. **Không** trộn Client ID trong env với Secret trong file (CopyHub đã chặn trộn nửa cặp). Ưu tiên credentials xem mục sau.
86
+ 4. **Do not** mix Client ID from env with Secret from file (CopyHub refuses mixed half-pairs). Credential precedencesee the next section.
88
87
 
89
- ### Cách đưa Client ID / Secret vào CopyHub
88
+ ### How to supply Client ID / Secret
90
89
 
91
- | Cách | Ghi chú |
92
- |------|---------|
93
- | **`copyhub login`** | Khuyến nghị lần đầu: wizard localhost nhập ID/Secret → lưu `config.json` → Google đăng nhập trang nhập Spreadsheet ID / phím tắt. |
94
- | **`copyhub config --client-id … --client-secret …`** | Ghi thẳng `config.json`. |
95
- | **`.env`** hoặc shell | Chỉ dùng khi **chưa** đủ cặp trong `config.json`, hoặc bạn cố ý chỉ dùng env (không ghi OAuth vào config). |
90
+ | Method | Notes |
91
+ |--------|--------|
92
+ | **`copyhub login`** | Recommended first time: localhost wizard for ID/Secret → saves `config.json` → Google sign-inspreadsheet ID / shortcut setup. |
93
+ | **`copyhub config --client-id … --client-secret …`** | Writes `config.json` directly. |
94
+ | **`.env`** or shell | Use only when **`config.json` does not** contain a full ID+Secret pair, or you intentionally rely on env only (no OAuth in config). |
96
95
 
97
- Trên wizard (Mac/Safari): nên **Download JSON** từ Console paste `client_id` / `client_secret`; xóa sạch ô trước khi dán để tránh Keychain điền nhầm secret cũ.
96
+ On the wizard (Mac/Safari): prefer **Download JSON** from the Console and paste `client_id` / `client_secret`; clear the fields before pasting to avoid Keychain filling an old secret.
98
97
 
99
98
  ---
100
99
 
101
- ## OAuth: config vs env (quan trọng)
100
+ ## OAuth: config vs env (important)
102
101
 
103
- - Nếu **`~/.copyhub/config.json` đủ** `clientId` **và** `clientSecret` → CopyHub **luôn dùng cặp trong file** cho OAuth; **`COPYHUB_GOOGLE_*` trong env/.env bị bỏ qua** cho hai field đó.
104
- - Nếu file **không** đủ cặp dùng **`COPYHUB_GOOGLE_CLIENT_ID`** + **`COPYHUB_GOOGLE_CLIENT_SECRET`** từ env (đã merge từ `.env`).
105
- - Không bao giờ ghép ID env với Secret file (hoặc ngược lại).
102
+ - If **`~/.copyhub/config.json` contains both** `clientId` **and** `clientSecret` → CopyHub **always uses the file pair** for OAuth; **`COPYHUB_GOOGLE_*` from env/.env are ignored** for those two fields.
103
+ - If the file **does not** have bothuse **`COPYHUB_GOOGLE_CLIENT_ID`** + **`COPYHUB_GOOGLE_CLIENT_SECRET`** from env (merged from `.env`).
104
+ - Never combine ID from env with Secret from file (or the reverse).
106
105
 
107
- Giá trị ID/Secret được **sanitize** khi đọc/ghi (BOM, CRLF, NBSP, ký tự zero-width, ngoặc thừa quanh chuỗi).
106
+ ID/Secret values are **sanitized** on read/write (BOM, CRLF, NBSP, zero-width characters, stray brackets around strings).
108
107
 
109
- Kiểm tra nguồn đang dùng:
108
+ Check which source is active:
110
109
 
111
110
  ```bash
112
111
  copyhub status
@@ -114,25 +113,33 @@ copyhub status
114
113
 
115
114
  ---
116
115
 
117
- ## Chạy lần đầu / First run
116
+ ## First run
118
117
 
119
118
  ```bash
120
119
  copyhub login
121
120
  ```
122
121
 
123
- 1. Nếu chưa OAuth trong config/env đủ cặp trình duyệt mở **`http://127.0.0.1:<port>/credentials`** để nhập Client ID / Secret.
124
- 2. Sau đó đăng nhập Google; callback **`/oauth2callback`**.
125
- 3. Trang setup: **Spreadsheet ID** (từ URL `…/d/<ID>/edit`), **platform**, **phím tắt overlay** (tuỳ chọn).
122
+ 1. If OAuth is not fully configured in config/env → the browser opens **`http://127.0.0.1:<port>/credentials`** to enter Client ID / Secret.
123
+ 2. Then Google sign-in; callback **`/oauth2callback`**.
124
+ 3. Setup page: **Spreadsheet ID** (from URL `…/d/<ID>/edit`), **platform**, **overlay shortcut** (optional).
126
125
 
127
- Chạy daemon (clipboard + Sheet + overlay mặc định):
126
+ Start the daemon (clipboard + Sheet + overlay by default):
128
127
 
129
128
  ```bash
130
129
  copyhub start
131
130
  ```
132
131
 
133
- Terminal thể đóng; tiến trình chạy nền. Xem `copyhub list`, dừng `copyhub stop`.
132
+ You can close the terminal; the process runs in the background. Use `copyhub list`, stop with `copyhub stop`.
134
133
 
135
- Chạy gắn terminal (Ctrl+C tắt hết):
134
+ After editing `config.json`, `~/.copyhub/.env`, or shell variables that affect the daemon/overlay, **reload** without manual stop/start:
135
+
136
+ ```bash
137
+ copyhub restart
138
+ ```
139
+
140
+ (Same flags as `start`: `--no-sheet`, `--no-overlay`, `--foreground`.)
141
+
142
+ Foreground (Ctrl+C stops everything):
136
143
 
137
144
  ```bash
138
145
  copyhub start --foreground
@@ -140,84 +147,85 @@ copyhub start --foreground
140
147
 
141
148
  ---
142
149
 
143
- ## Lệnh CLI / CLI commands
150
+ ## CLI commands
144
151
 
145
- | Lệnh | tả |
146
- |------|--------|
147
- | `copyhub config --client-id ID --client-secret SEC [--redirect-port P] [--sheet-id ID]` | Ghi OAuth ( tuỳ chọn Sheet ID, port) vào `config.json`. |
148
- | `copyhub login` | Luồng OAuth + setup trình duyệt. |
149
- | `copyhub logout` | Xóa `tokens.json` (chưa xóa config). |
152
+ | Command | Description |
153
+ |---------|-------------|
154
+ | `copyhub config --client-id ID --client-secret SEC [--redirect-port P] [--sheet-id ID]` | Writes OAuth (and optional Sheet ID, port) to `config.json`. |
155
+ | `copyhub login` | OAuth flow + browser setup. |
156
+ | `copyhub logout` | Deletes `tokens.json` (config unchanged). |
150
157
  | `copyhub status` | OAuth, Sheet, token, overlay, daemon. |
151
- | `copyhub start [--no-sheet] [--no-overlay] [--foreground]` | Mặc định **nền**; một instance. |
152
- | `copyhub list` / `copyhub ls` | PID daemon (nếu có). |
153
- | `copyhub stop` | Dừng daemon overlay con. |
154
- | `copyhub overlay` | Chỉ cửa sổ Electron (không clipboard daemon). |
155
- | `copyhub reset --yes` | **Xóa toàn bộ** `~/.copyhub` (config, token, history, run state). Dữ liệu `.env` ngoài thư mục không đụng. |
156
- | `copyhub commands` / `copyhub cmds` | Liệt lệnh nhanh. |
157
- | `copyhub --help` | Trợ giúp Commander. |
158
+ | `copyhub start [--no-sheet] [--no-overlay] [--foreground]` | Default **background**; single instance. |
159
+ | `copyhub restart [--no-sheet] [--no-overlay] [--foreground]` | Stops the daemon if running, then **`start`s again** — reloads config, `.env`, overlay shortcut. |
160
+ | `copyhub list` / `copyhub ls` | Daemon PID (if any). |
161
+ | `copyhub stop` | Stops daemon and child overlay. |
162
+ | `copyhub overlay` | Electron window only (no clipboard daemon). |
163
+ | `copyhub reset --yes` | **Deletes all** of `~/.copyhub` (config, tokens, history, run state). `.env` files outside that folder are untouched. |
164
+ | `copyhub commands` / `copyhub cmds` | Quick command list. |
165
+ | `copyhub --help` | Commander help. |
158
166
 
159
167
  ---
160
168
 
161
- ## Biến môi trường / Environment variables
169
+ ## Environment variables
162
170
 
163
- | Biến | Ý nghĩa |
164
- |------|---------|
165
- | `COPYHUB_GOOGLE_CLIENT_ID` | OAuth Client ID (chỉ hiệu lực khi config **không** đủ cặp ID+Secret). |
166
- | `COPYHUB_GOOGLE_CLIENT_SECRET` | OAuth Client Secret (cùng điều kiện trên). |
167
- | `COPYHUB_OAUTH_REDIRECT_PORT` | Port localhost cho OAuth (mặc định `19999`). Phải khớp redirect URI trong Google Console. |
168
- | `COPYHUB_OVERLAY_ACCELERATOR` | Phím tắt Electron ([Accelerator](https://www.electronjs.org/docs/latest/api/accelerator)); **ghi đè** giá trị trong config nếu set. |
169
- | `COPYHUB_START_NO_OVERLAY` | `=1` → `copyhub start` không spawn overlay. |
170
- | `COPYHUB_OVERLAY_STICKY` | `=1` → overlay không tự ẩn khi blur (chỉ Esc / chọn dòng). |
171
- | `COPYHUB_OVERLAY_HIDE_ON_START` | `=1` → không show cửa sổ lúc khởi động overlay (mở bằng shortcut/tray). |
172
- | `COPYHUB_OVERLAY_SKIP_TASKBAR` | `=1` → ẩn khỏi taskbar (Windows/Electron). |
173
- | `COPYHUB_POLL_MS` | Chu kỳ đọc clipboard (ms). |
171
+ | Variable | Meaning |
172
+ |----------|---------|
173
+ | `COPYHUB_GOOGLE_CLIENT_ID` | OAuth Client ID (only used when config **does not** contain a full ID+Secret pair). |
174
+ | `COPYHUB_GOOGLE_CLIENT_SECRET` | OAuth Client Secret (same rule). |
175
+ | `COPYHUB_OAUTH_REDIRECT_PORT` | Localhost port for OAuth (default `19999`). Must match redirect URI in Google Console. |
176
+ | `COPYHUB_OVERLAY_ACCELERATOR` | Electron shortcut ([Accelerator](https://www.electronjs.org/docs/latest/api/accelerator)); **overrides** config when set. |
177
+ | `COPYHUB_START_NO_OVERLAY` | `=1` → `copyhub start` does not spawn overlay. |
178
+ | `COPYHUB_OVERLAY_STICKY` | `=1` → overlay does not hide on blur (only Esc / picking a row). |
179
+ | `COPYHUB_OVERLAY_HIDE_ON_START` | `=1` → do not show window at overlay startup (open via shortcut/tray). |
180
+ | `COPYHUB_OVERLAY_SKIP_TASKBAR` | `=1` → hide from taskbar (Windows/Electron). |
181
+ | `COPYHUB_POLL_MS` | Clipboard poll interval (ms). |
174
182
 
175
- Electron được spawn kế thừa `process.env` của daemon/CLI nên các biến trên áp dụng sau khi có trong môi trường tiến trình cha.
183
+ Electron inherits `process.env` from the daemon/CLI parent, so these apply once present in that environment.
176
184
 
177
185
  ---
178
186
 
179
- ## Thư mục dữ liệu / Data directory
187
+ ## Data directory
180
188
 
181
- Mọi thứ dưới **`~/.copyhub/`** (Windows: **`%USERPROFILE%\.copyhub`**):
189
+ Everything lives under **`~/.copyhub/`** (Windows: **`%USERPROFILE%\.copyhub`**):
182
190
 
183
- | File | Nội dung |
184
- |------|-----------|
191
+ | File | Contents |
192
+ |------|----------|
185
193
  | `config.json` | OAuth (`clientId`, `clientSecret`, `redirectPort`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform`, … |
186
- | `tokens.json` | Refresh / access token OAuth |
187
- | `history.jsonl` | Lịch sử clipboard (JSON Lines) |
188
- | `run.json` | PID metadata khi `copyhub start` chạy nền |
194
+ | `tokens.json` | OAuth refresh / access tokens |
195
+ | `history.jsonl` | Clipboard history (JSON Lines) |
196
+ | `run.json` | PID and metadata when `copyhub start` runs in the background |
189
197
 
190
198
  ---
191
199
 
192
200
  ## Google Sheets
193
201
 
194
- - Append dòng khi bật Sheet đã login token hợp lệ.
195
- - Tab mới theo **ngày lịch máy**: `COPYHUB-YYYY-MM-DD`.
196
- - Sheet phải được share cho đúng Google account đã OAuth (hoặc spreadsheet của chính user đó).
197
- - Nếu API báo disabled / permission: xem logREADME link Enable API trong một số thông báo lỗi được format sẵn trong code.
202
+ - Appends rows when Sheets are enabled and tokens are valid.
203
+ - New tab per **calendar day**: `COPYHUB-YYYY-MM-DD`.
204
+ - The spreadsheet must be shared with the Google account used for OAuth (or owned by that account).
205
+ - If the API reports disabled / permission errors: check logssome errors include Enable API links from formatted messages in code.
198
206
 
199
207
  ---
200
208
 
201
209
  ## Overlay (Electron)
202
210
 
203
- - Phím mặc định (**mọi hệ**, kể cả macOS): **`Control+Shift+H`** — phím **⌃ Control** (góc dưới trái trên bàn phím Apple) hoặc **Ctrl** (bàn phím PC), **cùng một vị trí quen thuộc**, không phụ thuộc ⌘ / Win.
204
- - Muốn dùng **⌘ Command + Shift + H** trên Mac: chỉnh `overlayAccelerator` trong `~/.copyhub/config.json`, đặt `COPYHUB_OVERLAY_ACCELERATOR`, hoặc chọn preset trên trang setup (`CommandOrControl+Shift+H` / `Command+Shift+H`).
205
- - **macOS**: vẫn thể cần quyền **Accessibility** (*Privacy & Security*) cho **Terminal** / tiến trình chạy Electron nếu phím global không hoạt động.
206
- - Overlay phân trang ~10 mục; Sheet load dần khi cần (không gom hết tab một lần).
207
- - Click ra ngoài cửa sổ thường đóng overlay (trừ khi `COPYHUB_OVERLAY_STICKY=1`). **Esc** đóng.
211
+ - Default shortcut (**all platforms**, including macOS): **`Control+Shift+H`** — **⌃ Control** (bottom-left on Apple keyboards) or **Ctrl** on PC layouts **same physical position**, not ⌘ / Win.
212
+ - For **⌘ Command + Shift + H** on Mac: set `overlayAccelerator` to `Command+Shift+H` (avoid `CommandOrControl+…` on macOS Electron maps that to ⌘, so **⌃ Control** will not trigger the overlay). Legacy preset `CommandOrControl+Shift+H` is migrated to ⌃+Shift+H when the overlay starts.
213
+ - **macOS**: you may need **Accessibility** (*Privacy & Security*) for the app that launches Electron (Terminal, iTerm, …). If shortcuts still fail with non-US layouts, try **Input Source QWERTY** (Electron `globalShortcut` limitation).
214
+ - Overlay paginates ~10 items; Sheet data loads incrementally (not all tabs at once).
215
+ - Click outside the window usually closes the overlay (unless `COPYHUB_OVERLAY_STICKY=1`). **Esc** closes.
208
216
 
209
217
  ---
210
218
 
211
- ## Clipboard & lịch sử
219
+ ## Clipboard & history
212
220
 
213
- - Watcher bỏ qua clipboard trùng hash liên tiếp.
214
- - Trước khi ghi file/Sheet, nếu nội dung **trùng hệt bản mới nhất** trong `history.jsonl` thì **bỏ qua** (tránh copy lại cùng chuỗi sau khi đổi clipboard khác rồi copy lại).
221
+ - Watcher skips consecutive clipboard duplicates (same hash).
222
+ - Before writing file/Sheet, if content **exactly matches the newest row** in `history.jsonl`, it is **skipped** (avoids re-saving the same string after clipboard churn).
215
223
 
216
224
  ---
217
225
 
218
- ## Nâng cấp / Updating
226
+ ## Updating
219
227
 
220
- Dữ liệu `~/.copyhub` được giữ khi nâng cấp package.
228
+ `~/.copyhub` data is kept when upgrading the package.
221
229
 
222
230
  ```bash
223
231
  copyhub stop
@@ -225,58 +233,60 @@ npm install -g copyhub-cli@latest
225
233
  copyhub start
226
234
  ```
227
235
 
228
- Từ source: `git pull`, `npm install`, rồi `copyhub start` (hoặc `npm link` nếu dev).
236
+ (If you only changed config / `.env`: `copyhub restart`.)
237
+
238
+ From source: `git pull`, `npm install`, then `copyhub start` (or `npm link` while developing).
229
239
 
230
240
  ---
231
241
 
232
- ## Xử lý sự cố / Troubleshooting
242
+ ## Troubleshooting
233
243
 
234
- ### `invalid_client` hoặc “client secret is invalid” sau khi đăng nhập Google
244
+ ### `invalid_client` or “client secret is invalid” after Google sign-in
235
245
 
236
- - Dùng OAuth client **Web application**, redirect đúng `http://127.0.0.1:<port>/oauth2callback`.
237
- - Reset secret hoặc **Download JSON** client mới; nhập lại qua wizard; trên Mac **xóa ô** trước khi dán.
238
- - `copyhub status` — xem nguồn Client ID/Secret.
239
- - Chi tiết thêm: CopyHub trang lỗi HTML khi đổi `code` token thất bại.
246
+ - Use OAuth client **Web application**, redirect `http://127.0.0.1:<port>/oauth2callback`.
247
+ - Rotate secret or **Download JSON** for a fresh client; enter again via wizard; on Mac **clear fields** before paste.
248
+ - `copyhub status` — verify Client ID/Secret source.
249
+ - CopyHub may show an HTML error page when exchanging the `code` fails.
240
250
 
241
- ### Port OAuth đã được dùng (`EADDRINUSE`)
251
+ ### OAuth port already in use (`EADDRINUSE`)
242
252
 
243
- Đổi port: `COPYHUB_OAUTH_REDIRECT_PORT` hoặc `copyhub config … --redirect-port P`, cập nhật redirect URI trong Google Console.
253
+ Change port: `COPYHUB_OAUTH_REDIRECT_PORT` or `copyhub config … --redirect-port P`, and update the redirect URI in Google Console.
244
254
 
245
- ### `copyhub start` báo đã chạy
255
+ ### `copyhub start` says already running
246
256
 
247
- Một instance nền: `copyhub list` / `copyhub stop` rồi start lại.
257
+ Single background instance: `copyhub list` / `copyhub stop`, then start again.
248
258
 
249
- ### Sheet không ghi / lỗi API
259
+ ### Sheet not writing / API errors
250
260
 
251
- - Bật Google Sheets API đúng project.
252
- - Kiểm tra `copyhub status` (token, Sheet ID).
253
- - Share spreadsheet cho account đã login.
261
+ - Enable Google Sheets API on the correct project.
262
+ - Check `copyhub status` (token, Sheet ID).
263
+ - Share the spreadsheet with the signed-in account.
254
264
 
255
- ### Overlay không mở / phím không hoạt động
265
+ ### Overlay won’t open / shortcut doesn’t work
256
266
 
257
- - **macOS**: bật **Accessibility** cho Terminal / Node / Electron.
258
- - Mặc định **Control+Shift+H** (⌃ hoặc Ctrl + Shift + H), không dùng phím Win.
259
- - Đảm bảo daemon đang chạy (`copyhub list`) hoặc thử `copyhub overlay`.
260
- - Tránh trùng phím với app khác (Spotlight, Alfred, …).
267
+ - **macOS**: enable **Accessibility** for Terminal / Node / Electron.
268
+ - Default is **Control+Shift+H** (⌃ or Ctrl + Shift + H), not the Win key.
269
+ - Ensure the daemon is running (`copyhub list`) or try `copyhub overlay`.
270
+ - Avoid conflicts with other apps (Spotlight, Alfred, …).
261
271
 
262
- ### Xóa sạch cấu hình và làm lại
272
+ ### Reset and start clean
263
273
 
264
274
  ```bash
265
275
  copyhub stop
266
276
  copyhub reset --yes
267
277
  ```
268
278
 
269
- Sau đó xóa hoặc sửa `COPYHUB_GOOGLE_*` trong shell/`~/.copyhub/.env` nếu không muốn dùng env nữa. `.env` **không** bị `reset` xóa.
279
+ Then remove or edit `COPYHUB_GOOGLE_*` in your shell / `~/.copyhub/.env` if you no longer want env-based OAuth. `.env` files are **not** removed by `reset`.
270
280
 
271
281
  ---
272
282
 
273
- ## Bảo mật / Security notes
283
+ ## Security notes
274
284
 
275
- - `config.json` `tokens.json` chứa mật OAuth quyền file thư mục user (`~/.copyhub`).
276
- - Không commit `.env` hoặc copyhub data vào git công khai.
285
+ - `config.json` and `tokens.json` contain OAuth secretsstandard user-only permissions under `~/.copyhub`.
286
+ - Do not commit `.env` or CopyHub data to public git.
277
287
 
278
288
  ---
279
289
 
280
- ## Giấy phép / License
290
+ ## License
281
291
 
282
- MIT — xem `package.json`.
292
+ MIT — see `package.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copyhub-cli",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "CopyHub — clipboard, local history, Google Sheets sync (OAuth). Windows, macOS, Linux.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -37,6 +37,91 @@ loadCopyhubEnv();
37
37
 
38
38
  const CLI_JS = fileURLToPath(new URL('./cli.js', import.meta.url));
39
39
 
40
+ /**
41
+ * Stop background daemon if present. Same behavior as `copyhub stop`.
42
+ * @returns {'stopped' | 'cleared-stale' | 'none'}
43
+ */
44
+ function stopBackgroundDaemonSync() {
45
+ pruneStaleRunState();
46
+ const s = readRunState();
47
+ if (!s) return 'none';
48
+ if (!isPidAlive(s.pid)) {
49
+ console.log(`PID ${s.pid} is not running — cleared run.json.`);
50
+ clearRunState();
51
+ return 'cleared-stale';
52
+ }
53
+ killDaemonTree(s.pid);
54
+ clearRunState();
55
+ console.log(`Stopped process PID ${s.pid}.`);
56
+ return 'stopped';
57
+ }
58
+
59
+ /**
60
+ * Shared by `start` and `restart`.
61
+ * @param {{ sheet?: boolean, overlay?: boolean, foreground?: boolean }} opts
62
+ */
63
+ async function runCopyhubStart(opts) {
64
+ pruneStaleRunState();
65
+
66
+ const useSheet = opts.sheet !== false;
67
+ const skipOverlay =
68
+ opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
69
+
70
+ const existing = readRunState();
71
+ if (existing && isPidAlive(existing.pid)) {
72
+ console.error(
73
+ `CopyHub already running in background (PID ${existing.pid}). See: copyhub list — Stop: copyhub stop`,
74
+ );
75
+ process.exit(1);
76
+ }
77
+ if (existing && !isPidAlive(existing.pid)) {
78
+ clearRunState();
79
+ }
80
+
81
+ if (opts.foreground) {
82
+ console.log('CopyHub foreground mode. Press Ctrl+C to stop.');
83
+ await ensureDir();
84
+
85
+ const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
86
+
87
+ const onStop = () => {
88
+ ctrl.stopSync();
89
+ process.exit(0);
90
+ };
91
+ process.on('SIGINT', onStop);
92
+ process.on('SIGTERM', onStop);
93
+ return;
94
+ }
95
+
96
+ await ensureDir();
97
+ const daemonArgs = [CLI_JS, '_daemon'];
98
+ if (!useSheet) daemonArgs.push('--no-sheet');
99
+ if (skipOverlay) daemonArgs.push('--no-overlay');
100
+
101
+ const child = spawn(process.execPath, daemonArgs, {
102
+ detached: true,
103
+ stdio: 'ignore',
104
+ windowsHide: process.platform === 'win32',
105
+ env: { ...process.env },
106
+ });
107
+ child.unref();
108
+
109
+ if (!child.pid) {
110
+ console.error('Could not spawn background process.');
111
+ process.exit(1);
112
+ }
113
+
114
+ writeRunState({
115
+ pid: child.pid,
116
+ startedAt: new Date().toISOString(),
117
+ foreground: false,
118
+ });
119
+
120
+ console.log(`CopyHub running in background (PID ${child.pid}). You may close this terminal.`);
121
+ console.log('Check: copyhub list | Stop: copyhub stop');
122
+ process.exit(0);
123
+ }
124
+
40
125
  program.name('copyhub').description(
41
126
  'CopyHub — clipboard, overlay history, Google Sheets sync (COPYHUB-daily tabs). Windows, macOS, Linux.',
42
127
  );
@@ -166,20 +251,30 @@ program
166
251
  .command('stop')
167
252
  .description('Stop the background process started by copyhub start (and overlay child)')
168
253
  .action(() => {
169
- pruneStaleRunState();
170
- const s = readRunState();
171
- if (!s) {
254
+ const r = stopBackgroundDaemonSync();
255
+ if (r === 'none') {
172
256
  console.log('No background process to stop.');
173
- return;
174
257
  }
175
- if (!isPidAlive(s.pid)) {
176
- console.log(`PID ${s.pid} is not running — cleared run.json.`);
177
- clearRunState();
178
- return;
258
+ });
259
+
260
+ program
261
+ .command('restart')
262
+ .description(
263
+ 'Stop the background daemon if running, then start again (reloads ~/.copyhub/config.json, .env, shortcut). Same flags as start.',
264
+ )
265
+ .option('--no-sheet', 'Local history only, do not write to Sheets')
266
+ .option('--no-overlay', 'Do not launch Electron')
267
+ .option('--foreground', 'Run in foreground after restart (Ctrl+C stops)')
268
+ .action(async (opts) => {
269
+ const r = stopBackgroundDaemonSync();
270
+ if (r === 'stopped') {
271
+ console.log('Starting again — config, ~/.copyhub/.env, and shell env will be re-read.');
272
+ } else if (r === 'none') {
273
+ console.log('No background daemon — starting CopyHub.');
274
+ } else {
275
+ console.log('Stale run state cleared — starting CopyHub.');
179
276
  }
180
- killDaemonTree(s.pid);
181
- clearRunState();
182
- console.log(`Stopped process PID ${s.pid}.`);
277
+ await runCopyhubStart(opts);
183
278
  });
184
279
 
185
280
  program
@@ -256,65 +351,7 @@ program
256
351
  .option('--no-overlay', 'Do not launch Electron')
257
352
  .option('--foreground', 'Run in foreground (Ctrl+C stops; no background PID file)')
258
353
  .action(async (opts) => {
259
- pruneStaleRunState();
260
-
261
- const useSheet = opts.sheet !== false;
262
- const skipOverlay =
263
- opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
264
-
265
- const existing = readRunState();
266
- if (existing && isPidAlive(existing.pid)) {
267
- console.error(
268
- `CopyHub already running in background (PID ${existing.pid}). See: copyhub list — Stop: copyhub stop`,
269
- );
270
- process.exit(1);
271
- }
272
- if (existing && !isPidAlive(existing.pid)) {
273
- clearRunState();
274
- }
275
-
276
- if (opts.foreground) {
277
- console.log('CopyHub foreground mode. Press Ctrl+C to stop.');
278
- await ensureDir();
279
-
280
- const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
281
-
282
- const onStop = () => {
283
- ctrl.stopSync();
284
- process.exit(0);
285
- };
286
- process.on('SIGINT', onStop);
287
- process.on('SIGTERM', onStop);
288
- return;
289
- }
290
-
291
- await ensureDir();
292
- const daemonArgs = [CLI_JS, '_daemon'];
293
- if (!useSheet) daemonArgs.push('--no-sheet');
294
- if (skipOverlay) daemonArgs.push('--no-overlay');
295
-
296
- const child = spawn(process.execPath, daemonArgs, {
297
- detached: true,
298
- stdio: 'ignore',
299
- windowsHide: process.platform === 'win32',
300
- env: { ...process.env },
301
- });
302
- child.unref();
303
-
304
- if (!child.pid) {
305
- console.error('Could not spawn background process.');
306
- process.exit(1);
307
- }
308
-
309
- writeRunState({
310
- pid: child.pid,
311
- startedAt: new Date().toISOString(),
312
- foreground: false,
313
- });
314
-
315
- console.log(`CopyHub running in background (PID ${child.pid}). You may close this terminal.`);
316
- console.log('Check: copyhub list | Stop: copyhub stop');
317
- process.exit(0);
354
+ await runCopyhubStart(opts);
318
355
  });
319
356
 
320
357
  program
@@ -359,6 +396,7 @@ program
359
396
  copyhub login | copyhub logout | copyhub status
360
397
  copyhub reset --yes (delete ~/.copyhub — stop daemon first is recommended)
361
398
  copyhub start [--no-sheet] [--no-overlay] [--foreground]
399
+ copyhub restart [--no-sheet] [--no-overlay] [--foreground] (stop daemon if running, then start — reloads config/.env)
362
400
  Default runs in background (terminal can close). Single instance — second start is blocked.
363
401
  copyhub list (ls) | copyhub stop
364
402
  copyhub overlay | copyhub commands / copyhub --help`);
package/src/oauth.js CHANGED
@@ -391,8 +391,10 @@ const PLATFORM_PRESETS = {
391
391
  label: '⌃ Control + Shift + H · recommended (Apple & PC keyboards)',
392
392
  value: 'Control+Shift+H',
393
393
  },
394
- { label: '⌘ Command + Shift + H', value: 'CommandOrControl+Shift+H' },
395
- { label: 'Command + Shift + H (explicit ⌘)', value: 'Command+Shift+H' },
394
+ {
395
+ label: 'Command + Shift + H',
396
+ value: 'Command+Shift+H',
397
+ },
396
398
  { label: '⌘ + Shift + V', value: 'Command+Shift+V' },
397
399
  ],
398
400
  linux: [
package/ui/main.mjs CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  Tray,
11
11
  Menu,
12
12
  nativeImage,
13
+ systemPreferences,
14
+ powerMonitor,
13
15
  } from 'electron';
14
16
  import { loadCopyhubEnv } from '../src/load-env.js';
15
17
  import { readRecentHistorySync } from '../src/storage.js';
@@ -32,16 +34,35 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
34
  /** Set to 1 so the window does not hide on blur (only Esc / pick row to copy). */
33
35
  const STICKY_NO_BLUR = process.env.COPYHUB_OVERLAY_STICKY === '1';
34
36
 
35
- /** Electron Accelerator: use `Control`, not `Ctrl`; avoid bare `Ctrl` in strings — Electron expects `Control`. */
37
+ /**
38
+ * Electron Accelerator: use `Control`, not `Ctrl`; Unicode ⌃/⌘ → words.
39
+ * See migrateDarwinOverlayAccelerator — `CommandOrControl` is ⌘ on macOS, not ⌃.
40
+ */
36
41
  function normalizeAccelerator(raw) {
37
42
  if (!raw || typeof raw !== 'string') return '';
38
- let s = raw.trim();
43
+ let s = raw.trim().normalize('NFKC');
44
+ s = s.replace(/\u2303/g, 'Control'); // ⌃
45
+ s = s.replace(/\u2318/g, 'Command'); // ⌘
46
+ s = s.replace(/\u2325/g, 'Alt'); // ⌥
39
47
  s = s.replace(/\bCtrl\b/gi, 'Control');
40
48
  s = s.replace(/\bCmd\b/gi, 'Command');
49
+ s = s.replace(/\bCmdOrCtrl\b/gi, 'CommandOrControl');
41
50
  s = s.replace(/\s*\+\s*/g, '+');
42
51
  return s;
43
52
  }
44
53
 
54
+ /**
55
+ * On macOS, Electron maps `CommandOrControl` to ⌘. CopyHub defaults want ⌃ Control + Shift + H.
56
+ * Migrate only this chord so ⌃+Shift+H works if config still has the cross-platform preset.
57
+ * @param {string} normalized output of normalizeAccelerator
58
+ */
59
+ function migrateDarwinOverlayAccelerator(normalized) {
60
+ if (process.platform !== 'darwin' || !normalized) return normalized;
61
+ const compact = normalized.replace(/\s+/g, '');
62
+ if (/^commandorcontrol\+shift\+h$/i.test(compact)) return 'Control+Shift+H';
63
+ return normalized;
64
+ }
65
+
45
66
  /** Same physical ⌃ / Ctrl key on Mac (Apple & Windows-layout keyboards) and on Win/Linux — one default everywhere. */
46
67
  const DEFAULT_ACCEL = 'Control+Shift+H';
47
68
  const HIDE_ON_START = process.env.COPYHUB_OVERLAY_HIDE_ON_START === '1';
@@ -112,6 +133,28 @@ function applyAlwaysOnTopStack(w) {
112
133
  }
113
134
  }
114
135
 
136
+ /** Bring app + overlay forward (macOS often needs app focus for always-on-top popups after idle). */
137
+ function bringOverlayToFront(w) {
138
+ if (!w || w.isDestroyed()) return;
139
+ applyAlwaysOnTopStack(w);
140
+ if (process.platform === 'darwin') {
141
+ try {
142
+ app.focus({ steal: true });
143
+ } catch {
144
+ try {
145
+ app.focus();
146
+ } catch {
147
+ /* ignore */
148
+ }
149
+ }
150
+ }
151
+ try {
152
+ w.focus();
153
+ } catch {
154
+ /* ignore */
155
+ }
156
+ }
157
+
115
158
  function createWindow() {
116
159
  win = new BrowserWindow({
117
160
  width: OVERLAY_WIDTH,
@@ -134,6 +177,19 @@ function createWindow() {
134
177
 
135
178
  win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
136
179
 
180
+ win.webContents.on('render-process-gone', (_event, details) => {
181
+ console.warn('[CopyHub overlay] Renderer process ended:', details.reason);
182
+ if (!win || win.isDestroyed()) return;
183
+ try {
184
+ win.webContents.reload();
185
+ } catch (e) {
186
+ console.warn(
187
+ '[CopyHub overlay] Reload after renderer exit failed:',
188
+ /** @type {Error} */ (e).message,
189
+ );
190
+ }
191
+ });
192
+
137
193
  win.on('show', () => {
138
194
  applyAlwaysOnTopStack(win);
139
195
  });
@@ -165,8 +221,7 @@ function createWindow() {
165
221
  }
166
222
  placeWindowAtCursor(win);
167
223
  win.show();
168
- applyAlwaysOnTopStack(win);
169
- win.focus();
224
+ bringOverlayToFront(win);
170
225
  win.webContents.send('overlay:open');
171
226
  setTimeout(() => applyAlwaysOnTopStack(win), 120);
172
227
  armBlurHideEnable(win);
@@ -198,18 +253,50 @@ function placeWindowAtCursor(w) {
198
253
  }
199
254
 
200
255
  function toggleOverlay() {
201
- if (!win) return;
202
- if (win.isVisible()) {
203
- win.hide();
204
- } else {
256
+ try {
257
+ if (!win || win.isDestroyed()) {
258
+ blurHideEnabled = false;
259
+ createWindow();
260
+ return;
261
+ }
262
+ if (win.isVisible()) {
263
+ win.hide();
264
+ return;
265
+ }
205
266
  blurHideEnabled = false;
206
267
  placeWindowAtCursor(win);
207
268
  win.show();
208
- applyAlwaysOnTopStack(win);
209
- win.focus();
210
- win.webContents.send('overlay:open');
269
+ bringOverlayToFront(win);
270
+ const wc = win.webContents;
271
+ if (!wc.isDestroyed()) {
272
+ wc.send('overlay:open');
273
+ }
211
274
  setTimeout(() => applyAlwaysOnTopStack(win), 120);
212
275
  armBlurHideEnable(win);
276
+ } catch (e) {
277
+ const msg = /** @type {Error} */ (e).message || String(e);
278
+ console.warn('[CopyHub overlay] toggle failed:', msg);
279
+ try {
280
+ if (win && !win.isDestroyed()) {
281
+ const wc = win.webContents;
282
+ if (!wc.isDestroyed()) {
283
+ wc.reload();
284
+ return;
285
+ }
286
+ }
287
+ } catch {
288
+ /* recreate below */
289
+ }
290
+ try {
291
+ if (win && !win.isDestroyed()) {
292
+ win.destroy();
293
+ }
294
+ } catch {
295
+ /* ignore */
296
+ }
297
+ win = null;
298
+ blurHideEnabled = false;
299
+ createWindow();
213
300
  }
214
301
  }
215
302
 
@@ -223,7 +310,7 @@ function registerHotkeys() {
223
310
  loadOverlayAcceleratorFromConfigSync();
224
311
  const candidates = [];
225
312
  if (raw) {
226
- const n = normalizeAccelerator(raw);
313
+ const n = migrateDarwinOverlayAccelerator(normalizeAccelerator(raw));
227
314
  if (n) candidates.push(n);
228
315
  }
229
316
  candidates.push(DEFAULT_ACCEL);
@@ -232,10 +319,14 @@ function registerHotkeys() {
232
319
  for (let i = 0; i < candidates.length; i++) {
233
320
  const acc = candidates[i];
234
321
  try {
235
- if (globalShortcut.register(acc, () => toggleOverlay())) {
322
+ const ok = globalShortcut.register(acc, () => toggleOverlay());
323
+ if (ok) {
236
324
  if (i > 0) usedFallback = true;
237
325
  return { accelerator: acc, usedFallback };
238
326
  }
327
+ console.warn(
328
+ `CopyHub overlay — globalShortcut could not register "${acc}" (in use by another app, or macOS Input Source / permissions).`,
329
+ );
239
330
  } catch (e) {
240
331
  console.warn('Invalid accelerator:', acc, /** @type {Error} */ (e).message);
241
332
  }
@@ -245,9 +336,90 @@ function registerHotkeys() {
245
336
  /* ignore */
246
337
  }
247
338
  }
339
+ if (process.platform === 'darwin') {
340
+ const trusted = systemPreferences.isTrustedAccessibilityClient(false);
341
+ if (!trusted) {
342
+ console.error(
343
+ 'CopyHub overlay — macOS: enable Accessibility for the app that launches Electron (e.g. Terminal, iTerm, or Node) in System Settings → Privacy & Security → Accessibility.',
344
+ );
345
+ try {
346
+ systemPreferences.isTrustedAccessibilityClient(true);
347
+ } catch {
348
+ /* ignore */
349
+ }
350
+ } else {
351
+ console.error(
352
+ 'CopyHub overlay — shortcut still failed: try Input Source US/QWERTY (Electron globalShortcut quirk on macOS), pick another chord in config, or open from the menu bar icon.',
353
+ );
354
+ }
355
+ }
248
356
  return { accelerator: '', usedFallback: false };
249
357
  }
250
358
 
359
+ /**
360
+ * macOS often drops Electron globalShortcut listeners (sleep/wake or while running); re-register.
361
+ * @param {{ silentSuccess?: boolean }} [opts] — omit success log for periodic refresh noise
362
+ */
363
+ function reregisterOverlayHotkeys(opts = {}) {
364
+ const silentSuccess = Boolean(opts.silentSuccess);
365
+ const prev = overlayHotkeyMeta.accelerator;
366
+ if (prev) {
367
+ try {
368
+ globalShortcut.unregister(prev);
369
+ } catch {
370
+ /* ignore */
371
+ }
372
+ }
373
+ const { accelerator, usedFallback } = registerHotkeys();
374
+ overlayHotkeyMeta = {
375
+ accelerator,
376
+ usedFallback,
377
+ requestedRaw:
378
+ process.env.COPYHUB_OVERLAY_ACCELERATOR?.trim() ||
379
+ loadOverlayAcceleratorFromConfigSync() ||
380
+ '',
381
+ };
382
+ if (accelerator) {
383
+ if (!silentSuccess) {
384
+ console.log('[CopyHub overlay] Shortcut active again:', accelerator);
385
+ }
386
+ } else {
387
+ console.warn(
388
+ '[CopyHub overlay] Shortcut re-registration failed — open from menu bar or restart CopyHub.',
389
+ );
390
+ }
391
+ refreshTrayContextMenu();
392
+ }
393
+
394
+ /** Detect shortcut unregistered while process still runs (common on macOS without sleep). */
395
+ function startGlobalShortcutHealthMonitor() {
396
+ const intervalMs = process.platform === 'darwin' ? 45_000 : 120_000;
397
+ setInterval(() => {
398
+ const acc = overlayHotkeyMeta.accelerator;
399
+ if (!acc || !gotLock) return;
400
+ try {
401
+ if (!globalShortcut.isRegistered(acc)) {
402
+ console.warn('[CopyHub overlay] Global shortcut registration lost — repairing.');
403
+ reregisterOverlayHotkeys({ silentSuccess: false });
404
+ }
405
+ } catch (e) {
406
+ console.warn(
407
+ '[CopyHub overlay] Shortcut health check failed:',
408
+ /** @type {Error} */ (e).message,
409
+ );
410
+ }
411
+ }, intervalMs);
412
+ }
413
+
414
+ /** Proactive refresh: Electron/macOS can leave shortcuts broken while isRegistered stays true. */
415
+ function startDarwinShortcutKeepalive() {
416
+ if (process.platform !== 'darwin') return;
417
+ const periodMs = 8 * 60 * 1000;
418
+ setInterval(() => {
419
+ reregisterOverlayHotkeys({ silentSuccess: true });
420
+ }, periodMs);
421
+ }
422
+
251
423
  function mergeHistoryForOverlay(localItems, sheetItems, cap) {
252
424
  const seen = new Set();
253
425
  /** @type {typeof localItems} */
@@ -506,23 +678,37 @@ function registerIpc() {
506
678
  });
507
679
  }
508
680
 
681
+ function buildTrayMenuTemplate() {
682
+ const accLabel = overlayHotkeyMeta.accelerator
683
+ ? `Shortcut: ${overlayHotkeyMeta.accelerator}`
684
+ : 'Shortcut: (see terminal)';
685
+ return [
686
+ { label: accLabel, enabled: false },
687
+ { label: 'Open history (always on top)', click: () => toggleOverlay() },
688
+ { type: 'separator' },
689
+ { label: 'Quit', click: () => app.quit() },
690
+ ];
691
+ }
692
+
693
+ function refreshTrayContextMenu() {
694
+ if (!tray) return;
695
+ try {
696
+ tray.setContextMenu(Menu.buildFromTemplate(buildTrayMenuTemplate()));
697
+ } catch (e) {
698
+ console.warn(
699
+ '[CopyHub overlay] Tray menu refresh failed:',
700
+ /** @type {Error} */ (e).message,
701
+ );
702
+ }
703
+ }
704
+
509
705
  function registerTray() {
510
706
  const icon = nativeImage.createFromDataURL(
511
707
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhn1IGOMJoAmBGOSMDEwMmABWDWHJjBCSpBKGBSDjBAAAeoRBIEs/x0AAAAASUVORK5CYII=',
512
708
  );
513
709
  tray = new Tray(icon);
514
710
  tray.setToolTip('CopyHub overlay');
515
- const accLabel = overlayHotkeyMeta.accelerator
516
- ? `Shortcut: ${overlayHotkeyMeta.accelerator}`
517
- : 'Shortcut: (see terminal)';
518
- tray.setContextMenu(
519
- Menu.buildFromTemplate([
520
- { label: accLabel, enabled: false },
521
- { label: 'Open history (always on top)', click: () => toggleOverlay() },
522
- { type: 'separator' },
523
- { label: 'Quit', click: () => app.quit() },
524
- ]),
525
- );
711
+ tray.setContextMenu(Menu.buildFromTemplate(buildTrayMenuTemplate()));
526
712
  tray.on('click', () => toggleOverlay());
527
713
  }
528
714
 
@@ -576,6 +762,14 @@ if (gotLock) {
576
762
  } catch (e) {
577
763
  console.warn('Could not create system tray icon:', /** @type {Error} */ (e).message);
578
764
  }
765
+
766
+ /** Delay slightly so macOS finishes restoring input / accessibility after wake. */
767
+ powerMonitor.on('resume', () => {
768
+ setTimeout(() => reregisterOverlayHotkeys({ silentSuccess: false }), 400);
769
+ });
770
+
771
+ startGlobalShortcutHealthMonitor();
772
+ startDarwinShortcutKeepalive();
579
773
  });
580
774
 
581
775
  app.on('will-quit', () => {