flying-lobster 1.6.2 → 1.6.3
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/assets/icon.icns +0 -0
- package/package.json +8 -3
- package/src/main/index.js +61 -3
- package/src/renderer/index.html +28 -1
- package/src/renderer/settings.html +102 -55
- package/src/renderer/settings.js +167 -12
package/assets/icon.icns
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flying-lobster",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "Always-on-top chat window for OpenClaw gateways 🦞",
|
|
5
5
|
"author": "Rootlab.ai",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,15 +31,20 @@
|
|
|
31
31
|
"assets/**/*"
|
|
32
32
|
],
|
|
33
33
|
"mac": {
|
|
34
|
+
"icon": "assets/icon.icns",
|
|
34
35
|
"category": "public.app-category.productivity",
|
|
35
36
|
"target": [
|
|
36
37
|
{
|
|
37
38
|
"target": "dmg",
|
|
38
|
-
"arch": [
|
|
39
|
+
"arch": [
|
|
40
|
+
"universal"
|
|
41
|
+
]
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
"target": "dir",
|
|
42
|
-
"arch": [
|
|
45
|
+
"arch": [
|
|
46
|
+
"universal"
|
|
47
|
+
]
|
|
43
48
|
}
|
|
44
49
|
]
|
|
45
50
|
},
|
package/src/main/index.js
CHANGED
|
@@ -36,6 +36,8 @@ const OPENCLAW_CSS = `
|
|
|
36
36
|
.chat-compose__actions .btn { padding: 0 10px !important; }
|
|
37
37
|
.chat-group-messages { max-width: 100% !important; }
|
|
38
38
|
.chat-group { margin-right: 4px !important; margin-left: 4px !important; }
|
|
39
|
+
/* Hide tool call/result cards */
|
|
40
|
+
.chat-tool-card { display: none !important; }
|
|
39
41
|
`;
|
|
40
42
|
|
|
41
43
|
const OPENCLAW_JS = `
|
|
@@ -107,9 +109,17 @@ ipcMain.handle('delete-gateway', (_e, id) => {
|
|
|
107
109
|
return gateways;
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
ipcMain.handle('get-active-gateway', () =>
|
|
112
|
+
ipcMain.handle('get-active-gateway', () => {
|
|
113
|
+
const id = store.get('activeGateway');
|
|
114
|
+
console.log('[Main] get-active-gateway:', id);
|
|
115
|
+
return id;
|
|
116
|
+
});
|
|
111
117
|
|
|
112
118
|
ipcMain.handle('set-active-gateway', (_e, id) => {
|
|
119
|
+
console.log('[Main] set-active-gateway:', id);
|
|
120
|
+
const gateways = store.get('gateways');
|
|
121
|
+
const gw = gateways.find(g => g.id === id);
|
|
122
|
+
console.log('[Main] Gateway details:', gw?.name, gw?.url);
|
|
113
123
|
store.set('activeGateway', id);
|
|
114
124
|
return id;
|
|
115
125
|
});
|
|
@@ -148,6 +158,19 @@ ipcMain.handle('get-theme', () => {
|
|
|
148
158
|
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
|
149
159
|
});
|
|
150
160
|
|
|
161
|
+
// Toggle between dark and light theme
|
|
162
|
+
function toggleTheme() {
|
|
163
|
+
// Cycle: system -> dark -> light -> system
|
|
164
|
+
// Or simply toggle: dark <-> light
|
|
165
|
+
if (nativeTheme.themeSource === 'system') {
|
|
166
|
+
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';
|
|
167
|
+
} else if (nativeTheme.themeSource === 'dark') {
|
|
168
|
+
nativeTheme.themeSource = 'light';
|
|
169
|
+
} else {
|
|
170
|
+
nativeTheme.themeSource = 'dark';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
151
174
|
// Notify all windows when theme changes
|
|
152
175
|
nativeTheme.on('updated', () => {
|
|
153
176
|
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
|
@@ -193,11 +216,29 @@ function createWindow() {
|
|
|
193
216
|
|
|
194
217
|
mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
|
|
195
218
|
|
|
219
|
+
// Open DevTools with Cmd+Option+I
|
|
220
|
+
mainWindow.webContents.on('before-input-event', (event, input) => {
|
|
221
|
+
if (input.meta && input.alt && input.key === 'i') {
|
|
222
|
+
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
196
226
|
// Inject CSS/JS into any webview that loads inside this window
|
|
197
227
|
mainWindow.webContents.on('did-attach-webview', (event, wvWebContents) => {
|
|
228
|
+
console.log('[Main] Webview attached, URL:', wvWebContents.getURL());
|
|
229
|
+
|
|
230
|
+
wvWebContents.on('did-start-loading', () => console.log('[Main] Webview did-start-loading'));
|
|
231
|
+
wvWebContents.on('did-stop-loading', () => console.log('[Main] Webview did-stop-loading'));
|
|
232
|
+
wvWebContents.on('did-fail-load', (e, code, desc) => console.log('[Main] Webview did-fail-load:', code, desc));
|
|
233
|
+
|
|
198
234
|
wvWebContents.on('dom-ready', () => {
|
|
199
|
-
|
|
200
|
-
|
|
235
|
+
const url = wvWebContents.getURL();
|
|
236
|
+
console.log('[Main] Webview dom-ready, URL:', url);
|
|
237
|
+
|
|
238
|
+
// TEMPORARILY DISABLE INJECTION TO DEBUG
|
|
239
|
+
// wvWebContents.insertCSS(OPENCLAW_CSS).catch(e => console.error('Main insertCSS failed:', e));
|
|
240
|
+
// wvWebContents.executeJavaScript(OPENCLAW_JS).catch(e => console.error('Main executeJS failed:', e));
|
|
241
|
+
console.log('[Main] CSS/JS injection DISABLED for debugging');
|
|
201
242
|
});
|
|
202
243
|
});
|
|
203
244
|
|
|
@@ -223,6 +264,19 @@ function createWindow() {
|
|
|
223
264
|
if (input.key === 'Escape') {
|
|
224
265
|
mainWindow.hide();
|
|
225
266
|
}
|
|
267
|
+
// Catch agent switching shortcuts even when webview has focus
|
|
268
|
+
if (input.type === 'keyDown' && input.shift && (input.meta || input.control)) {
|
|
269
|
+
if (input.key === 'ArrowRight') {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
cycleGateway('next');
|
|
272
|
+
} else if (input.key === 'ArrowLeft') {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
cycleGateway('prev');
|
|
275
|
+
} else if (input.key === 'k' || input.key === 'K') {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
toggleTheme();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
226
280
|
});
|
|
227
281
|
|
|
228
282
|
mainWindow.on('closed', () => {
|
|
@@ -337,6 +391,10 @@ function registerHotkey() {
|
|
|
337
391
|
|
|
338
392
|
if (!nextRegistered) console.error('Failed to register Cmd+Shift+Right');
|
|
339
393
|
if (!prevRegistered) console.error('Failed to register Cmd+Shift+Left');
|
|
394
|
+
|
|
395
|
+
// Theme toggle shortcut (Cmd+Shift+K)
|
|
396
|
+
const themeRegistered = globalShortcut.register('CommandOrControl+Shift+K', toggleTheme);
|
|
397
|
+
if (!themeRegistered) console.error('Failed to register Cmd+Shift+K for theme toggle');
|
|
340
398
|
}
|
|
341
399
|
|
|
342
400
|
if (process.platform === 'darwin') {
|
package/src/renderer/index.html
CHANGED
|
@@ -353,6 +353,9 @@
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
function loadGatewayUrl(url, token) {
|
|
356
|
+
console.log('[FL Debug] loadGatewayUrl called:', url, 'token:', token ? 'yes' : 'no');
|
|
357
|
+
console.log('[FL Debug] currentGatewayUrl:', currentGatewayUrl, 'currentWebview:', !!currentWebview);
|
|
358
|
+
|
|
356
359
|
if (!url) {
|
|
357
360
|
destroyWebview();
|
|
358
361
|
showOverlay('empty');
|
|
@@ -361,9 +364,13 @@
|
|
|
361
364
|
}
|
|
362
365
|
|
|
363
366
|
// Don't reload if same URL
|
|
364
|
-
if (currentGatewayUrl === url && currentWebview)
|
|
367
|
+
if (currentGatewayUrl === url && currentWebview) {
|
|
368
|
+
console.log('[FL Debug] Skipping reload - same URL');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
365
371
|
|
|
366
372
|
destroyWebview();
|
|
373
|
+
console.log('[FL Debug] Showing loading overlay');
|
|
367
374
|
showOverlay('loading');
|
|
368
375
|
dot.className = 'status-dot loading';
|
|
369
376
|
|
|
@@ -375,11 +382,19 @@
|
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
const wv = document.createElement('webview');
|
|
385
|
+
console.log('[FL Debug] Loading URL:', loadUrl);
|
|
378
386
|
wv.src = loadUrl;
|
|
379
387
|
wv.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none;';
|
|
380
388
|
wv.setAttribute('allowpopups', '');
|
|
381
389
|
wv.setAttribute('partition', 'persist:gateway');
|
|
390
|
+
|
|
391
|
+
wv.addEventListener('did-start-loading', () => console.log('[FL Debug] did-start-loading'));
|
|
392
|
+
wv.addEventListener('did-stop-loading', () => console.log('[FL Debug] did-stop-loading'));
|
|
393
|
+
wv.addEventListener('did-finish-load', () => console.log('[FL Debug] did-finish-load'));
|
|
394
|
+
wv.addEventListener('console-message', (e) => console.log('[FL Webview]', e.message));
|
|
395
|
+
|
|
382
396
|
wv.addEventListener('dom-ready', () => {
|
|
397
|
+
console.log('[FL Debug] dom-ready');
|
|
383
398
|
showOverlay(null); // hide all
|
|
384
399
|
dot.className = 'status-dot online';
|
|
385
400
|
// Inject CSS at document level — CSS custom properties inherit into shadow DOM
|
|
@@ -505,6 +520,18 @@
|
|
|
505
520
|
window.api.onGatewaysUpdated(loadGateways);
|
|
506
521
|
}
|
|
507
522
|
|
|
523
|
+
// Listen for agent switch from main process (Cmd+Shift+Arrow shortcuts)
|
|
524
|
+
if (window.api && window.api.onSwitchGateway) {
|
|
525
|
+
window.api.onSwitchGateway(async (gatewayId) => {
|
|
526
|
+
const gw = gatewaysList.find(g => g.id === gatewayId);
|
|
527
|
+
if (gw) {
|
|
528
|
+
select.value = gw.id;
|
|
529
|
+
showAgentIndicator(gw.name);
|
|
530
|
+
loadGatewayUrl(gw.url, gw.token);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
508
535
|
// Add Gateway button (empty state)
|
|
509
536
|
document.getElementById('add-gw-btn').addEventListener('click', () => {
|
|
510
537
|
window.api.openSettings();
|
|
@@ -130,50 +130,92 @@
|
|
|
130
130
|
:root.light .btn-secondary { background: #c7c7cc; color: #1d1d1f; }
|
|
131
131
|
:root.light .btn-secondary:hover { background: #aeaeb2; }
|
|
132
132
|
|
|
133
|
-
/*
|
|
134
|
-
.
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.toast {
|
|
139
|
-
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
|
|
140
|
-
background: var(--success); color: #fff; padding: 8px 20px; border-radius: 6px;
|
|
141
|
-
font-size: 13px; opacity: 0; transition: opacity 0.3s; pointer-events: none;
|
|
142
|
-
}
|
|
143
|
-
.toast.show { opacity: 1; }
|
|
144
|
-
.toast.error { background: var(--danger); }
|
|
145
|
-
|
|
146
|
-
/* Keyboard shortcuts hint */
|
|
147
|
-
.shortcuts-hint {
|
|
148
|
-
margin-top: 24px;
|
|
149
|
-
padding: 12px;
|
|
133
|
+
/* Keyboard Shortcuts section */
|
|
134
|
+
.shortcuts-section { margin-top: 8px; }
|
|
135
|
+
.shortcuts-list {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
150
138
|
background: var(--bg-secondary);
|
|
151
139
|
border-radius: 8px;
|
|
152
|
-
|
|
153
|
-
color: var(--text-secondary);
|
|
154
|
-
transition: background 0.2s, color 0.2s;
|
|
155
|
-
}
|
|
156
|
-
.shortcuts-hint h3 {
|
|
157
|
-
font-size: 12px;
|
|
158
|
-
text-transform: uppercase;
|
|
159
|
-
letter-spacing: 0.5px;
|
|
160
|
-
margin-bottom: 8px;
|
|
161
|
-
color: var(--text-muted);
|
|
140
|
+
overflow: hidden;
|
|
162
141
|
}
|
|
163
|
-
.
|
|
142
|
+
.shortcut-item {
|
|
164
143
|
display: flex;
|
|
144
|
+
align-items: center;
|
|
165
145
|
justify-content: space-between;
|
|
166
|
-
padding:
|
|
146
|
+
padding: 12px 14px;
|
|
147
|
+
border-bottom: 1px solid var(--border-color);
|
|
148
|
+
transition: background 0.2s;
|
|
167
149
|
}
|
|
168
|
-
.
|
|
150
|
+
.shortcut-item:last-child { border-bottom: none; }
|
|
151
|
+
.shortcut-item .label {
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
color: var(--text-primary);
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
.shortcut-item .keys {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 4px;
|
|
163
|
+
}
|
|
164
|
+
.shortcut-item kbd {
|
|
169
165
|
background: var(--bg-input);
|
|
170
166
|
border: 1px solid var(--border-color);
|
|
171
167
|
border-radius: 4px;
|
|
172
|
-
padding:
|
|
168
|
+
padding: 4px 8px;
|
|
173
169
|
font-family: -apple-system, BlinkMacSystemFont, monospace;
|
|
174
|
-
font-size:
|
|
170
|
+
font-size: 12px;
|
|
175
171
|
color: var(--text-primary);
|
|
172
|
+
min-width: 24px;
|
|
173
|
+
text-align: center;
|
|
174
|
+
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
|
175
|
+
}
|
|
176
|
+
.shortcut-item.editable {
|
|
177
|
+
background: rgba(93,173,226,0.05);
|
|
178
|
+
}
|
|
179
|
+
:root.light .shortcut-item.editable {
|
|
180
|
+
background: rgba(0,113,227,0.05);
|
|
181
|
+
}
|
|
182
|
+
.shortcut-item .hotkey-input-wrapper {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 8px;
|
|
186
|
+
}
|
|
187
|
+
.shortcut-item #hotkey-input {
|
|
188
|
+
width: 180px;
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
padding: 6px 10px;
|
|
191
|
+
text-align: center;
|
|
192
|
+
font-family: -apple-system, BlinkMacSystemFont, monospace;
|
|
193
|
+
}
|
|
194
|
+
.shortcut-item .btn {
|
|
195
|
+
padding: 6px 12px;
|
|
196
|
+
font-size: 12px;
|
|
176
197
|
}
|
|
198
|
+
.editable-badge {
|
|
199
|
+
font-size: 9px;
|
|
200
|
+
text-transform: uppercase;
|
|
201
|
+
letter-spacing: 0.5px;
|
|
202
|
+
color: var(--accent);
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
background: rgba(93,173,226,0.15);
|
|
205
|
+
padding: 2px 6px;
|
|
206
|
+
border-radius: 4px;
|
|
207
|
+
}
|
|
208
|
+
:root.light .editable-badge {
|
|
209
|
+
background: rgba(0,113,227,0.1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.toast {
|
|
213
|
+
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
|
|
214
|
+
background: var(--success); color: #fff; padding: 8px 20px; border-radius: 6px;
|
|
215
|
+
font-size: 13px; opacity: 0; transition: opacity 0.3s; pointer-events: none;
|
|
216
|
+
}
|
|
217
|
+
.toast.show { opacity: 1; }
|
|
218
|
+
.toast.error { background: var(--danger); }
|
|
177
219
|
</style>
|
|
178
220
|
</head>
|
|
179
221
|
<body>
|
|
@@ -186,7 +228,8 @@
|
|
|
186
228
|
<h2>Gateways</h2>
|
|
187
229
|
<div class="gw-list" id="gw-list"></div>
|
|
188
230
|
|
|
189
|
-
<
|
|
231
|
+
<button class="btn btn-secondary" id="gw-add-btn" style="margin-bottom: 16px;">+ Add Gateway</button>
|
|
232
|
+
<div class="form" id="gw-form" style="display: none;">
|
|
190
233
|
<div class="form-row">
|
|
191
234
|
<input id="gw-name" placeholder="Name (e.g. Rooty)" />
|
|
192
235
|
<input id="gw-url" placeholder="URL (e.g. http://localhost:19002)" />
|
|
@@ -196,31 +239,35 @@
|
|
|
196
239
|
</div>
|
|
197
240
|
<div class="form-row">
|
|
198
241
|
<button class="btn" id="gw-save">Add Gateway</button>
|
|
199
|
-
<button class="btn btn-secondary" id="gw-cancel"
|
|
242
|
+
<button class="btn btn-secondary" id="gw-cancel">Cancel</button>
|
|
200
243
|
</div>
|
|
201
244
|
</div>
|
|
202
245
|
|
|
203
|
-
<h2>
|
|
204
|
-
<div class="
|
|
205
|
-
<div class="
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
<h2>Keyboard Shortcuts</h2>
|
|
247
|
+
<div class="shortcuts-section">
|
|
248
|
+
<div class="shortcuts-list">
|
|
249
|
+
<div class="shortcut-item editable">
|
|
250
|
+
<span class="label">
|
|
251
|
+
Show/Hide Window
|
|
252
|
+
<span class="editable-badge">Editable</span>
|
|
253
|
+
</span>
|
|
254
|
+
<div class="hotkey-input-wrapper">
|
|
255
|
+
<input id="hotkey-input" placeholder="⌘⇧L" />
|
|
256
|
+
<button class="btn" id="hotkey-save">Save</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="shortcut-item">
|
|
260
|
+
<span class="label">Next Agent</span>
|
|
261
|
+
<span class="keys"><kbd>⌘</kbd><kbd>⇧</kbd><kbd>→</kbd></span>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="shortcut-item">
|
|
264
|
+
<span class="label">Previous Agent</span>
|
|
265
|
+
<span class="keys"><kbd>⌘</kbd><kbd>⇧</kbd><kbd>←</kbd></span>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="shortcut-item">
|
|
268
|
+
<span class="label">Toggle Theme</span>
|
|
269
|
+
<span class="keys"><kbd>⌘</kbd><kbd>⇧</kbd><kbd>K</kbd></span>
|
|
270
|
+
</div>
|
|
224
271
|
</div>
|
|
225
272
|
</div>
|
|
226
273
|
</div>
|
package/src/renderer/settings.js
CHANGED
|
@@ -58,6 +58,25 @@ function esc(s) {
|
|
|
58
58
|
return d.innerHTML;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// ── Form visibility ───────────────────────────────────────────
|
|
62
|
+
function showForm() {
|
|
63
|
+
$('#gw-form').style.display = '';
|
|
64
|
+
$('#gw-add-btn').style.display = 'none';
|
|
65
|
+
$('#gw-name').focus();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hideForm() {
|
|
69
|
+
$('#gw-form').style.display = 'none';
|
|
70
|
+
$('#gw-add-btn').style.display = '';
|
|
71
|
+
editingId = null;
|
|
72
|
+
$('#gw-name').value = '';
|
|
73
|
+
$('#gw-url').value = '';
|
|
74
|
+
$('#gw-token').value = '';
|
|
75
|
+
$('#gw-save').textContent = 'Add Gateway';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$('#gw-add-btn').addEventListener('click', showForm);
|
|
79
|
+
|
|
61
80
|
// ── Add / Edit ────────────────────────────────────────────────
|
|
62
81
|
function startEdit(gw) {
|
|
63
82
|
editingId = gw.id;
|
|
@@ -65,16 +84,11 @@ function startEdit(gw) {
|
|
|
65
84
|
$('#gw-url').value = gw.url;
|
|
66
85
|
$('#gw-token').value = gw.token || '';
|
|
67
86
|
$('#gw-save').textContent = 'Update Gateway';
|
|
68
|
-
|
|
87
|
+
showForm();
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
function cancelEdit() {
|
|
72
|
-
|
|
73
|
-
$('#gw-name').value = '';
|
|
74
|
-
$('#gw-url').value = '';
|
|
75
|
-
$('#gw-token').value = '';
|
|
76
|
-
$('#gw-save').textContent = 'Add Gateway';
|
|
77
|
-
$('#gw-cancel').style.display = 'none';
|
|
91
|
+
hideForm();
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
$('#gw-save').addEventListener('click', async () => {
|
|
@@ -90,7 +104,7 @@ $('#gw-save').addEventListener('click', async () => {
|
|
|
90
104
|
await window.api.addGateway({ name, url, token });
|
|
91
105
|
toast('Gateway added');
|
|
92
106
|
}
|
|
93
|
-
|
|
107
|
+
hideForm();
|
|
94
108
|
renderGateways();
|
|
95
109
|
});
|
|
96
110
|
|
|
@@ -103,11 +117,148 @@ async function deleteGateway(id) {
|
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
// ── Hotkey ────────────────────────────────────────────────────
|
|
120
|
+
// Stores the Electron-format hotkey string
|
|
121
|
+
let currentHotkeyElectron = '';
|
|
122
|
+
|
|
123
|
+
// Map keys to display symbols (macOS style)
|
|
124
|
+
const MODIFIER_SYMBOLS = {
|
|
125
|
+
meta: '⌘', // Cmd
|
|
126
|
+
ctrl: '⌃', // Control
|
|
127
|
+
alt: '⌥', // Option
|
|
128
|
+
shift: '⇧', // Shift
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Special key display names
|
|
132
|
+
const KEY_DISPLAY = {
|
|
133
|
+
'ArrowUp': '↑',
|
|
134
|
+
'ArrowDown': '↓',
|
|
135
|
+
'ArrowLeft': '←',
|
|
136
|
+
'ArrowRight': '→',
|
|
137
|
+
'Escape': 'Esc',
|
|
138
|
+
'Backspace': '⌫',
|
|
139
|
+
'Delete': '⌦',
|
|
140
|
+
'Enter': '↵',
|
|
141
|
+
'Tab': '⇥',
|
|
142
|
+
'Space': '␣',
|
|
143
|
+
' ': '␣',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Convert keyboard event to display string and Electron format
|
|
147
|
+
function parseHotkey(e) {
|
|
148
|
+
const modifiers = [];
|
|
149
|
+
const electronParts = [];
|
|
150
|
+
|
|
151
|
+
// Order: Ctrl, Alt, Shift, Meta (standard order for display)
|
|
152
|
+
// But for macOS feel, we show: ⌃⌥⇧⌘
|
|
153
|
+
if (e.ctrlKey && !e.metaKey) {
|
|
154
|
+
modifiers.push(MODIFIER_SYMBOLS.ctrl);
|
|
155
|
+
electronParts.push('Control');
|
|
156
|
+
}
|
|
157
|
+
if (e.altKey) {
|
|
158
|
+
modifiers.push(MODIFIER_SYMBOLS.alt);
|
|
159
|
+
electronParts.push('Alt');
|
|
160
|
+
}
|
|
161
|
+
if (e.shiftKey) {
|
|
162
|
+
modifiers.push(MODIFIER_SYMBOLS.shift);
|
|
163
|
+
electronParts.push('Shift');
|
|
164
|
+
}
|
|
165
|
+
if (e.metaKey) {
|
|
166
|
+
modifiers.push(MODIFIER_SYMBOLS.meta);
|
|
167
|
+
electronParts.push('CommandOrControl');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Get the main key (ignore standalone modifier keys)
|
|
171
|
+
const key = e.key;
|
|
172
|
+
const code = e.code;
|
|
173
|
+
|
|
174
|
+
// Skip if only modifier pressed
|
|
175
|
+
if (['Meta', 'Control', 'Alt', 'Shift'].includes(key)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Determine display and Electron key
|
|
180
|
+
let displayKey = KEY_DISPLAY[key] || KEY_DISPLAY[code] || key.toUpperCase();
|
|
181
|
+
let electronKey = key.length === 1 ? key.toUpperCase() : key;
|
|
182
|
+
|
|
183
|
+
// Handle special cases for Electron
|
|
184
|
+
if (code.startsWith('Arrow')) {
|
|
185
|
+
electronKey = code.replace('Arrow', '');
|
|
186
|
+
} else if (key === ' ') {
|
|
187
|
+
electronKey = 'Space';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Must have at least one modifier for a global hotkey
|
|
191
|
+
if (modifiers.length === 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
display: modifiers.join('') + displayKey,
|
|
197
|
+
electron: [...electronParts, electronKey].join('+'),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Convert Electron format back to display symbols
|
|
202
|
+
function electronToDisplay(electronStr) {
|
|
203
|
+
if (!electronStr) return '';
|
|
204
|
+
|
|
205
|
+
const parts = electronStr.split('+');
|
|
206
|
+
const display = [];
|
|
207
|
+
let mainKey = '';
|
|
208
|
+
|
|
209
|
+
for (const part of parts) {
|
|
210
|
+
switch (part) {
|
|
211
|
+
case 'CommandOrControl':
|
|
212
|
+
case 'Command':
|
|
213
|
+
case 'Cmd':
|
|
214
|
+
display.push('⌘');
|
|
215
|
+
break;
|
|
216
|
+
case 'Control':
|
|
217
|
+
case 'Ctrl':
|
|
218
|
+
display.push('⌃');
|
|
219
|
+
break;
|
|
220
|
+
case 'Alt':
|
|
221
|
+
case 'Option':
|
|
222
|
+
display.push('⌥');
|
|
223
|
+
break;
|
|
224
|
+
case 'Shift':
|
|
225
|
+
display.push('⇧');
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
// Main key
|
|
229
|
+
mainKey = KEY_DISPLAY[part] || KEY_DISPLAY['Arrow' + part] || part;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return display.join('') + mainKey;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Hotkey input capture
|
|
237
|
+
const hotkeyInput = $('#hotkey-input');
|
|
238
|
+
|
|
239
|
+
hotkeyInput.addEventListener('focus', () => {
|
|
240
|
+
hotkeyInput.classList.add('capturing');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
hotkeyInput.addEventListener('blur', () => {
|
|
244
|
+
hotkeyInput.classList.remove('capturing');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
hotkeyInput.addEventListener('keydown', (e) => {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
e.stopPropagation();
|
|
250
|
+
|
|
251
|
+
const result = parseHotkey(e);
|
|
252
|
+
if (result) {
|
|
253
|
+
hotkeyInput.value = result.display;
|
|
254
|
+
currentHotkeyElectron = result.electron;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
106
258
|
$('#hotkey-save').addEventListener('click', async () => {
|
|
107
|
-
|
|
108
|
-
if (!hotkey) return toast('Enter a hotkey', true);
|
|
259
|
+
if (!currentHotkeyElectron) return toast('Press a hotkey combination first', true);
|
|
109
260
|
try {
|
|
110
|
-
await window.api.updateSettings({ hotkey });
|
|
261
|
+
await window.api.updateSettings({ hotkey: currentHotkeyElectron });
|
|
111
262
|
toast('Hotkey saved');
|
|
112
263
|
} catch (e) {
|
|
113
264
|
toast('Failed to save hotkey', true);
|
|
@@ -121,5 +272,9 @@ $('#close-btn').addEventListener('click', () => window.close());
|
|
|
121
272
|
(async () => {
|
|
122
273
|
renderGateways();
|
|
123
274
|
const settings = await window.api.getSettings();
|
|
124
|
-
|
|
275
|
+
// Load existing hotkey and display as symbols
|
|
276
|
+
if (settings.hotkey) {
|
|
277
|
+
currentHotkeyElectron = settings.hotkey;
|
|
278
|
+
$('#hotkey-input').value = electronToDisplay(settings.hotkey);
|
|
279
|
+
}
|
|
125
280
|
})();
|