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 +135 -125
- package/package.json +1 -1
- package/src/cli.js +108 -70
- package/src/oauth.js +4 -2
- package/ui/main.mjs +218 -24
package/README.md
CHANGED
|
@@ -1,54 +1,53 @@
|
|
|
1
1
|
# CopyHub
|
|
2
2
|
|
|
3
|
-
CopyHub
|
|
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
|
-
|
|
5
|
+
Runs on **Windows**, **macOS**, and **Linux**.
|
|
7
6
|
|
|
8
7
|
---
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## Table of contents
|
|
11
10
|
|
|
12
|
-
|
|
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
|
-
##
|
|
15
|
+
## Features
|
|
17
16
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- Overlay:
|
|
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
|
-
##
|
|
24
|
+
## Requirements
|
|
26
25
|
|
|
27
26
|
- **Node.js** ≥ 18
|
|
28
|
-
-
|
|
29
|
-
- **Google Sheets API**
|
|
30
|
-
- OAuth client
|
|
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
|
-
##
|
|
33
|
+
## Installation
|
|
35
34
|
|
|
36
|
-
###
|
|
35
|
+
### Global install (npm)
|
|
37
36
|
|
|
38
37
|
```bash
|
|
39
38
|
npm install -g copyhub-cli
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
|
|
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
|
-
###
|
|
43
|
+
### From source (this repo)
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
46
|
npm install
|
|
48
47
|
npm link
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
|
|
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
|
-
##
|
|
58
|
+
## Environment files
|
|
60
59
|
|
|
61
|
-
CLI
|
|
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
|
-
|
|
62
|
+
File order:
|
|
64
63
|
|
|
65
|
-
1. `<package>/.env` (
|
|
64
|
+
1. `<package>/.env` (installed package directory / repo when developing)
|
|
66
65
|
2. `~/.copyhub/.env`
|
|
67
|
-
3. `./.env`
|
|
66
|
+
3. `./.env` in the **current working directory** (`cwd`)
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
So after `npm install -g`, variables in `~/.copyhub/.env` still load regardless of `cwd`.
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
See the template: `.env.example`.
|
|
72
71
|
|
|
73
72
|
---
|
|
74
73
|
|
|
75
74
|
## Google Cloud & OAuth
|
|
76
75
|
|
|
77
|
-
1.
|
|
78
|
-
2. **Credentials** → **Create credentials** → **OAuth client ID** →
|
|
79
|
-
3. **Authorized redirect URIs** —
|
|
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
|
-
|
|
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. **
|
|
86
|
+
4. **Do not** mix Client ID from env with Secret from file (CopyHub refuses mixed half-pairs). Credential precedence — see the next section.
|
|
88
87
|
|
|
89
|
-
###
|
|
88
|
+
### How to supply Client ID / Secret
|
|
90
89
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
| **`copyhub login`** |
|
|
94
|
-
| **`copyhub config --client-id … --client-secret …`** |
|
|
95
|
-
| **`.env`**
|
|
90
|
+
| Method | Notes |
|
|
91
|
+
|--------|--------|
|
|
92
|
+
| **`copyhub login`** | Recommended first time: localhost wizard for ID/Secret → saves `config.json` → Google sign-in → spreadsheet 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
|
-
|
|
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 (
|
|
100
|
+
## OAuth: config vs env (important)
|
|
102
101
|
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
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 both → use **`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
|
-
|
|
106
|
+
ID/Secret values are **sanitized** on read/write (BOM, CRLF, NBSP, zero-width characters, stray brackets around strings).
|
|
108
107
|
|
|
109
|
-
|
|
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
|
-
##
|
|
116
|
+
## First run
|
|
118
117
|
|
|
119
118
|
```bash
|
|
120
119
|
copyhub login
|
|
121
120
|
```
|
|
122
121
|
|
|
123
|
-
1.
|
|
124
|
-
2.
|
|
125
|
-
3.
|
|
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
|
-
|
|
126
|
+
Start the daemon (clipboard + Sheet + overlay by default):
|
|
128
127
|
|
|
129
128
|
```bash
|
|
130
129
|
copyhub start
|
|
131
130
|
```
|
|
132
131
|
|
|
133
|
-
|
|
132
|
+
You can close the terminal; the process runs in the background. Use `copyhub list`, stop with `copyhub stop`.
|
|
134
133
|
|
|
135
|
-
|
|
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
|
-
##
|
|
150
|
+
## CLI commands
|
|
144
151
|
|
|
145
|
-
|
|
|
146
|
-
|
|
147
|
-
| `copyhub config --client-id ID --client-secret SEC [--redirect-port P] [--sheet-id ID]` |
|
|
148
|
-
| `copyhub login` |
|
|
149
|
-
| `copyhub logout` |
|
|
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]` |
|
|
152
|
-
| `copyhub
|
|
153
|
-
| `copyhub
|
|
154
|
-
| `copyhub
|
|
155
|
-
| `copyhub
|
|
156
|
-
| `copyhub
|
|
157
|
-
| `copyhub
|
|
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
|
-
##
|
|
169
|
+
## Environment variables
|
|
162
170
|
|
|
163
|
-
|
|
|
164
|
-
|
|
165
|
-
| `COPYHUB_GOOGLE_CLIENT_ID` | OAuth Client ID (
|
|
166
|
-
| `COPYHUB_GOOGLE_CLIENT_SECRET` | OAuth Client Secret (
|
|
167
|
-
| `COPYHUB_OAUTH_REDIRECT_PORT` |
|
|
168
|
-
| `COPYHUB_OVERLAY_ACCELERATOR` |
|
|
169
|
-
| `COPYHUB_START_NO_OVERLAY` | `=1` → `copyhub start`
|
|
170
|
-
| `COPYHUB_OVERLAY_STICKY` | `=1` → overlay
|
|
171
|
-
| `COPYHUB_OVERLAY_HIDE_ON_START` | `=1` →
|
|
172
|
-
| `COPYHUB_OVERLAY_SKIP_TASKBAR` | `=1` →
|
|
173
|
-
| `COPYHUB_POLL_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
|
|
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
|
-
##
|
|
187
|
+
## Data directory
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
Everything lives under **`~/.copyhub/`** (Windows: **`%USERPROFILE%\.copyhub`**):
|
|
182
190
|
|
|
183
|
-
| File |
|
|
184
|
-
|
|
191
|
+
| File | Contents |
|
|
192
|
+
|------|----------|
|
|
185
193
|
| `config.json` | OAuth (`clientId`, `clientSecret`, `redirectPort`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform`, … |
|
|
186
|
-
| `tokens.json` |
|
|
187
|
-
| `history.jsonl` |
|
|
188
|
-
| `run.json` | PID
|
|
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
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
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 logs — some errors include Enable API links from formatted messages in code.
|
|
198
206
|
|
|
199
207
|
---
|
|
200
208
|
|
|
201
209
|
## Overlay (Electron)
|
|
202
210
|
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
- **macOS**:
|
|
206
|
-
- Overlay
|
|
207
|
-
- Click
|
|
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 &
|
|
219
|
+
## Clipboard & history
|
|
212
220
|
|
|
213
|
-
- Watcher
|
|
214
|
-
-
|
|
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
|
-
##
|
|
226
|
+
## Updating
|
|
219
227
|
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
242
|
+
## Troubleshooting
|
|
233
243
|
|
|
234
|
-
### `invalid_client`
|
|
244
|
+
### `invalid_client` or “client secret is invalid” after Google sign-in
|
|
235
245
|
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
- `copyhub status` —
|
|
239
|
-
-
|
|
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
|
-
###
|
|
251
|
+
### OAuth port already in use (`EADDRINUSE`)
|
|
242
252
|
|
|
243
|
-
|
|
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`
|
|
255
|
+
### `copyhub start` says already running
|
|
246
256
|
|
|
247
|
-
|
|
257
|
+
Single background instance: `copyhub list` / `copyhub stop`, then start again.
|
|
248
258
|
|
|
249
|
-
### Sheet
|
|
259
|
+
### Sheet not writing / API errors
|
|
250
260
|
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
- Share spreadsheet
|
|
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
|
|
265
|
+
### Overlay won’t open / shortcut doesn’t work
|
|
256
266
|
|
|
257
|
-
- **macOS**:
|
|
258
|
-
-
|
|
259
|
-
-
|
|
260
|
-
-
|
|
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
|
-
###
|
|
272
|
+
### Reset and start clean
|
|
263
273
|
|
|
264
274
|
```bash
|
|
265
275
|
copyhub stop
|
|
266
276
|
copyhub reset --yes
|
|
267
277
|
```
|
|
268
278
|
|
|
269
|
-
|
|
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
|
-
##
|
|
283
|
+
## Security notes
|
|
274
284
|
|
|
275
|
-
- `config.json`
|
|
276
|
-
-
|
|
285
|
+
- `config.json` and `tokens.json` contain OAuth secrets — standard user-only permissions under `~/.copyhub`.
|
|
286
|
+
- Do not commit `.env` or CopyHub data to public git.
|
|
277
287
|
|
|
278
288
|
---
|
|
279
289
|
|
|
280
|
-
##
|
|
290
|
+
## License
|
|
281
291
|
|
|
282
|
-
MIT —
|
|
292
|
+
MIT — see `package.json`.
|
package/package.json
CHANGED
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
395
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
209
|
-
win.
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', () => {
|