@medeo-plugin/plugin-screenshot 0.0.0-stage → 1.0.1
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 +38 -2
- package/package.json +41 -4
- package/plugin.medeoplugin +0 -0
- package/src/README.md +28 -0
- package/src/README.zh-CN.md +20 -0
- package/src/about.json +7 -0
- package/src/icon.svg +1 -0
- package/src/locales/en.json +141 -0
- package/src/locales/zh-CN.json +141 -0
- package/src/locales/zh-TW.json +141 -0
- package/src/manifest.json +125 -0
- package/src/media/en/editor.png +0 -0
- package/src/media/en/settings.png +0 -0
- package/src/media/zh-CN/editor.png +0 -0
- package/src/media/zh-CN/settings.png +0 -0
- package/src/resources/helper.js +99 -0
- package/src/resources/trayTemplate.png +0 -0
- package/src/resources/trayTemplate.svg +1 -0
- package/src/src/annotations.ts +398 -0
- package/src/src/beautify.ts +41 -0
- package/src/src/geometry.ts +79 -0
- package/src/src/naming.ts +19 -0
- package/src/src/protocol.ts +53 -0
- package/src/src/settings.ts +48 -0
- package/src/src/shortcuts.ts +25 -0
- package/src/src/ui/overlay.ts +874 -0
- package/src/src/ui/settings.ts +140 -0
- package/src/tests/logic.test.mjs +101 -0
- package/src/ui/entry.ts +4 -0
- package/src/ui/icons/align-center.svg +1 -0
- package/src/ui/icons/align-left.svg +1 -0
- package/src/ui/icons/align-right.svg +1 -0
- package/src/ui/icons/arrow-double.svg +1 -0
- package/src/ui/icons/arrow-open.svg +1 -0
- package/src/ui/icons/arrow-single.svg +1 -0
- package/src/ui/icons/arrow-tail.svg +1 -0
- package/src/ui/icons/arrow-thick.svg +1 -0
- package/src/ui/icons/arrow.svg +1 -0
- package/src/ui/icons/beautify.svg +1 -0
- package/src/ui/icons/bold.svg +1 -0
- package/src/ui/icons/censor.svg +1 -0
- package/src/ui/icons/close.svg +1 -0
- package/src/ui/icons/copy-doc.svg +1 -0
- package/src/ui/icons/copy.svg +1 -0
- package/src/ui/icons/ellipse.svg +1 -0
- package/src/ui/icons/fill-fill.svg +1 -0
- package/src/ui/icons/fill-stroke.svg +1 -0
- package/src/ui/icons/fill-strokeFill.svg +1 -0
- package/src/ui/icons/highlight.svg +1 -0
- package/src/ui/icons/invert.svg +1 -0
- package/src/ui/icons/italic.svg +1 -0
- package/src/ui/icons/line-dashed.svg +1 -0
- package/src/ui/icons/line-dotted.svg +1 -0
- package/src/ui/icons/line-solid.svg +1 -0
- package/src/ui/icons/line.svg +1 -0
- package/src/ui/icons/marker.svg +1 -0
- package/src/ui/icons/measure.svg +1 -0
- package/src/ui/icons/number.svg +1 -0
- package/src/ui/icons/ocr.svg +1 -0
- package/src/ui/icons/open.svg +1 -0
- package/src/ui/icons/pencil.svg +1 -0
- package/src/ui/icons/rect.svg +1 -0
- package/src/ui/icons/redo.svg +1 -0
- package/src/ui/icons/sampler.svg +1 -0
- package/src/ui/icons/save.svg +1 -0
- package/src/ui/icons/select.svg +1 -0
- package/src/ui/icons/stamp.svg +1 -0
- package/src/ui/icons/text-bg.svg +1 -0
- package/src/ui/icons/text.svg +1 -0
- package/src/ui/icons/trash.svg +1 -0
- package/src/ui/icons/underline.svg +1 -0
- package/src/ui/icons/undo.svg +1 -0
- package/src/ui/index.html +12 -0
- package/src/ui/settings.html +5 -0
- package/src/ui/style.css +105 -0
- package/src/worker.ts +285 -0
package/src/worker.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Screenshot: a shortcut or the menu-bar
|
|
2
|
+
// icon freezes the display under the pointer with macOS's own `screencapture`, a transparent
|
|
3
|
+
// overlay lets the user select and annotate, and the result comes back here to be copied, saved
|
|
4
|
+
// or read with Apple Vision (OCR + QR).
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { appWindows, capture, channel, definePlugin, events, i18n, launcher, notifications, shortcuts, tray } from '@medeo-plugin/sdk';
|
|
11
|
+
import { expandHome, fileName } from './src/naming.ts';
|
|
12
|
+
import { joinLines, split, type Action, type FromOverlay, type FromSettings, type Mode, type SettingsState, type Shot, type ToOverlay, type ToSettings } from './src/protocol.ts';
|
|
13
|
+
import { DEFAULTS, normalize, type Settings, type ShortcutKey } from './src/settings.ts';
|
|
14
|
+
|
|
15
|
+
const OVERLAY = 'overlay', SETTINGS = 'settings';
|
|
16
|
+
const OSASCRIPT = '/usr/bin/osascript', SCREENCAPTURE = '/usr/sbin/screencapture', OPEN = '/usr/bin/open';
|
|
17
|
+
const PRIVACY = 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture';
|
|
18
|
+
|
|
19
|
+
let root = '', dir = '', settingsFile = '';
|
|
20
|
+
let settings: Settings = DEFAULTS;
|
|
21
|
+
let errors: SettingsState['errors'] = {};
|
|
22
|
+
let registered: Record<string, string> = {};
|
|
23
|
+
let overlayReady = false;
|
|
24
|
+
let pending: { shot: Shot; chunks: string[] } | null = null;
|
|
25
|
+
let current: { seq: number; at: number } | null = null;
|
|
26
|
+
let results: { seq: number; action: Action; mime: string; parts: string[]; total: number } | null = null;
|
|
27
|
+
let seq = 0;
|
|
28
|
+
let posts: Promise<unknown> = Promise.resolve();
|
|
29
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
30
|
+
|
|
31
|
+
const run = (cmd: string, args: string[], timeout = 15_000) => new Promise<string>((ok, no) =>
|
|
32
|
+
execFile(cmd, args, { timeout, maxBuffer: 4 << 20 }, (error, stdout, stderr) => error ? no(new Error(String(stderr || error.message).trim())) : ok(String(stdout))));
|
|
33
|
+
const helper = (...args: string[]) => run(OSASCRIPT, ['-l', 'JavaScript', path.join(root, 'resources/helper.js'), ...args], 120_000);
|
|
34
|
+
/** Messages go out one at a time so chunks arrive in order. */
|
|
35
|
+
const post = (m: ToOverlay | ToSettings) => { const next = posts.then(() => channel.post(m)).catch(error => console.warn('post failed:', String(error))); posts = next; return next; };
|
|
36
|
+
const serial = <T>(task: () => Promise<T>) => { const next = queue.then(task); queue = next.catch(error => console.error('screenshot failed:', error)); return next; };
|
|
37
|
+
const label = (key: string, params: Record<string, string | number> = {}) => i18n.t(key, params).catch(() => key);
|
|
38
|
+
const saveDir = () => expandHome(settings.saveDir, os.homedir(), expandHome(DEFAULTS.saveDir, os.homedir(), os.homedir()));
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------- capturing
|
|
41
|
+
async function permissionMissing(): Promise<boolean> {
|
|
42
|
+
const status = await capture.status(false).catch(() => null);
|
|
43
|
+
const value = status?.os?.screen_capture;
|
|
44
|
+
return value === 'denied' || value === 'not_granted' || value === 'restricted';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function shoot(mode: Mode, delay = 0) {
|
|
48
|
+
// A capture already on screen: a second press is ignored (Esc in the overlay cancels).
|
|
49
|
+
if (current && Date.now() - current.at < 10 * 60_000) {
|
|
50
|
+
const open = await appWindows.list().catch(() => []);
|
|
51
|
+
if (open.some(w => w.id === OVERLAY && w.visible)) return;
|
|
52
|
+
}
|
|
53
|
+
const t0 = Date.now();
|
|
54
|
+
if (await permissionMissing()) { console.info('screen recording not granted: opening settings'); await openSettings(); return; }
|
|
55
|
+
if (delay) await new Promise(r => setTimeout(r, delay));
|
|
56
|
+
const info = JSON.parse(await helper('screen')) as { display: { x: number; y: number; width: number; height: number; scale: number }; windows: Shot['windows'] };
|
|
57
|
+
const d = info.display, file = path.join(dir, `capture-${process.pid}.png`);
|
|
58
|
+
try {
|
|
59
|
+
await run(SCREENCAPTURE, ['-x', '-t', 'png', '-R', `${d.x},${d.y},${d.width},${d.height}`, file]);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('screencapture failed:', error);
|
|
62
|
+
// Without the screen-recording grant macOS refuses the rectangle.
|
|
63
|
+
if (/could not create image/i.test(String(error))) { await openSettings(); return; }
|
|
64
|
+
await notify('error.capture');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const png = await readFile(file); await rm(file, { force: true });
|
|
68
|
+
const t1 = Date.now();
|
|
69
|
+
const shot: Shot = { seq: ++seq, mode, width: d.width, height: d.height, scale: d.scale, windows: info.windows, output: { format: settings.format, downscale: settings.downscale, enter: settings.enterAction } };
|
|
70
|
+
const chunks = split(png.toString('base64'));
|
|
71
|
+
current = { seq: shot.seq, at: Date.now() };
|
|
72
|
+
results = null;
|
|
73
|
+
// A window that is gone (closed, or the plugin restarted) loads again and says when it is ready.
|
|
74
|
+
const open = await appWindows.list().catch(() => []);
|
|
75
|
+
if (!open.some(w => w.id === OVERLAY)) overlayReady = false;
|
|
76
|
+
await appWindows.open({ id: OVERLAY, entry: 'ui/index.html', title: await label('plugin.name'), width: d.width, height: d.height, overlay: true });
|
|
77
|
+
if (overlayReady) await send(shot, chunks); else pending = { shot, chunks };
|
|
78
|
+
console.info(`capture #${shot.seq} ${mode}: ${d.width}x${d.height}@${d.scale} ${Math.round(png.length / 1024)} KiB, captured in ${t1 - t0 - delay} ms, overlay ${overlayReady ? 'reused' : 'loading'} after ${Date.now() - t1} ms`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function send(shot: Shot, chunks: string[]) {
|
|
82
|
+
await post({ type: 'shot', shot, parts: chunks.length });
|
|
83
|
+
for (let i = 0; i < chunks.length; i++) await post({ type: 'chunk', seq: shot.seq, i, data: chunks[i] });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function finished(r: { seq: number; action: Action; mime: string; parts: string[] }) {
|
|
87
|
+
const png = Buffer.from(r.parts.join(''), 'base64');
|
|
88
|
+
console.info(`capture #${r.seq} ${r.action}: ${Math.round(png.length / 1024)} KiB ${r.mime}`);
|
|
89
|
+
if (r.action === 'ocr') { await recognize(r.seq, png); return; }
|
|
90
|
+
current = null;
|
|
91
|
+
if (r.action === 'copy') {
|
|
92
|
+
const file = path.join(dir, `copy-${process.pid}.png`);
|
|
93
|
+
await writeFile(file, png);
|
|
94
|
+
try {
|
|
95
|
+
const out = (await helper('copy', file)).trim();
|
|
96
|
+
if (out !== 'ok') throw new Error(out);
|
|
97
|
+
} catch (error) { console.error('copy failed:', error); await notify('error.copy'); }
|
|
98
|
+
finally { await rm(file, { force: true }); }
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const target = saveDir();
|
|
102
|
+
try {
|
|
103
|
+
await mkdir(target, { recursive: true });
|
|
104
|
+
const name = fileName('Medeo', new Date(), n => existsSync(path.join(target, n)), r.mime === 'image/jpeg' ? 'jpg' : 'png');
|
|
105
|
+
const file = path.join(target, name);
|
|
106
|
+
await writeFile(file, png, { flag: 'wx' });
|
|
107
|
+
if (settings.openAfterSave) await run(OPEN, ['-R', file]).catch(() => {});
|
|
108
|
+
await notifications.show('saved', await label('notify.saved.title'), await label('notify.saved.body', { name, folder: target })).catch(() => {});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('save failed:', error);
|
|
111
|
+
await notify('error.save', { folder: target });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reads the selection with Vision and sends the text and any QR payloads back to the overlay. */
|
|
116
|
+
async function recognize(seqNo: number, png: Buffer) {
|
|
117
|
+
const file = path.join(dir, `ocr-${process.pid}.png`);
|
|
118
|
+
let result: { text: string; codes: string[] } | null = null;
|
|
119
|
+
try {
|
|
120
|
+
await writeFile(file, png);
|
|
121
|
+
const raw = JSON.parse(await helper('ocr', file)) as { lines: { t: string; x: number; y: number; h: number }[]; codes: string[] };
|
|
122
|
+
result = { text: joinLines(raw.lines), codes: raw.codes };
|
|
123
|
+
} catch (error) { console.error('ocr failed:', error); }
|
|
124
|
+
finally { await rm(file, { force: true }); }
|
|
125
|
+
await post({ type: 'ocr', seq: seqNo, result });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function copyText(text: string) {
|
|
129
|
+
const file = path.join(dir, `text-${process.pid}.txt`);
|
|
130
|
+
try { await writeFile(file, text.slice(0, 1 << 20)); if ((await helper('copy-text', file)).trim() !== 'ok') throw new Error('copy failed'); }
|
|
131
|
+
catch (error) { console.error('copy text failed:', error); await notify('error.copy'); }
|
|
132
|
+
finally { await rm(file, { force: true }); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function notify(key: string, params: Record<string, string | number> = {}) {
|
|
136
|
+
await notifications.show('error', await label('notify.error.title'), await label(key, params)).catch(() => {});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------- settings, shortcuts, menu bar
|
|
140
|
+
async function loadSettings() {
|
|
141
|
+
try { settings = normalize(JSON.parse(await readFile(settingsFile, 'utf8'))); } catch { settings = DEFAULTS; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function setSettings(patch: Partial<Settings>) {
|
|
145
|
+
settings = normalize({ ...settings, ...patch });
|
|
146
|
+
await writeFile(settingsFile, JSON.stringify(settings));
|
|
147
|
+
await registerShortcuts();
|
|
148
|
+
await pushSettings();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const pushSettings = () => post({ type: 'settings', state: { settings, saveDir: saveDir(), errors } });
|
|
152
|
+
|
|
153
|
+
/** Shortcut id (as registered) ↔ settings key ↔ capture mode. */
|
|
154
|
+
const MODES: Record<string, { key: ShortcutKey; mode: Mode }> = { area: { key: 'shortcutArea', mode: 'area' }, full: { key: 'shortcutFull', mode: 'full' }, quick: { key: 'shortcutQuick', mode: 'quick' }, ocr: { key: 'shortcutOcr', mode: 'ocr' } };
|
|
155
|
+
|
|
156
|
+
async function registerShortcuts() {
|
|
157
|
+
const wanted: Record<string, string | null> = Object.fromEntries(Object.entries(MODES).map(([id, m]) => [id, settings[m.key]]));
|
|
158
|
+
errors = {};
|
|
159
|
+
// Old ids from earlier builds.
|
|
160
|
+
for (const old of ['capture', 'save']) if (registered[old]) { await shortcuts.unregister(old).catch(() => {}); delete registered[old]; }
|
|
161
|
+
for (const [id, accelerator] of Object.entries(wanted)) {
|
|
162
|
+
if (registered[id] && registered[id] !== accelerator) { await shortcuts.unregister(id).catch(() => {}); delete registered[id]; }
|
|
163
|
+
if (!accelerator || registered[id] === accelerator) continue;
|
|
164
|
+
try { await shortcuts.register(id, accelerator, false); registered[id] = accelerator; }
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(`shortcut ${accelerator} failed:`, error);
|
|
167
|
+
errors[MODES[id].key] = /permission/.test(String(error)) ? 'permission' : 'taken';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function showTray() {
|
|
173
|
+
await tray.set('', [
|
|
174
|
+
{ id: 'tray.area', title: await label('tray.area') },
|
|
175
|
+
{ id: 'tray.full', title: await label('tray.full') },
|
|
176
|
+
{ id: 'tray.quick', title: await label('tray.quick') },
|
|
177
|
+
{ id: 'tray.ocr', title: await label('tray.ocr') },
|
|
178
|
+
{ separator: true },
|
|
179
|
+
{ id: 'tray.folder', title: await label('tray.folder') },
|
|
180
|
+
{ id: 'tray.settings', title: await label('tray.settings') },
|
|
181
|
+
], { icon: 'resources/trayTemplate.png', click: 'tray.area' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function openSettings() {
|
|
185
|
+
await appWindows.open({ id: SETTINGS, entry: 'ui/settings.html', title: await label('settings.title'), width: 560, height: 600 });
|
|
186
|
+
await appWindows.focus(SETTINGS).catch(() => {});
|
|
187
|
+
await pushSettings();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function revealFolder() {
|
|
191
|
+
const target = saveDir();
|
|
192
|
+
await mkdir(target, { recursive: true }).catch(() => {});
|
|
193
|
+
await run(OPEN, [target]).catch(error => console.error('open folder failed:', error));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function chooseFolder() {
|
|
197
|
+
const picked = (await helper('choose-folder', await label('save.choose'), saveDir()).catch(() => '')).trim();
|
|
198
|
+
if (!picked) return;
|
|
199
|
+
const home = os.homedir();
|
|
200
|
+
await setSettings({ saveDir: picked === home || picked.startsWith(home + '/') ? '~' + picked.slice(home.length).replace(/\/$/, '') : picked.replace(/\/$/, '') });
|
|
201
|
+
await post({ type: 'toast', tone: 'success', key: 'save.changed' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------- messages from windows
|
|
205
|
+
function onMessage(m: FromOverlay | FromSettings) {
|
|
206
|
+
switch (m?.type) {
|
|
207
|
+
case 'overlay-ready':
|
|
208
|
+
console.info('overlay ready');
|
|
209
|
+
overlayReady = true;
|
|
210
|
+
if (pending) { const p = pending; pending = null; void send(p.shot, p.chunks); }
|
|
211
|
+
break;
|
|
212
|
+
case 'result': {
|
|
213
|
+
if (!current || m.seq !== current.seq) return;
|
|
214
|
+
if (!results || results.seq !== m.seq || results.action !== m.action) results = { seq: m.seq, action: m.action, mime: m.mime, parts: [], total: m.parts };
|
|
215
|
+
results.parts[m.i] = m.data;
|
|
216
|
+
if (results.parts.filter(p => p !== undefined).length === results.total) { const r = results; results = null; void serial(() => finished(r)); }
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case 'cancel': console.info(`capture #${m.seq} cancelled`); if (current && m.seq === current.seq) { current = null; pending = null; } break;
|
|
220
|
+
case 'copy-text': if (typeof m.text === 'string') void serial(() => copyText(m.text)); break;
|
|
221
|
+
case 'open-url': if (typeof m.url === 'string' && /^https?:\/\//i.test(m.url)) void run(OPEN, [m.url]).catch(() => {}); break;
|
|
222
|
+
case 'settings-ready': void pushSettings(); break;
|
|
223
|
+
case 'set': void serial(() => setSettings(m.patch)); break;
|
|
224
|
+
case 'choose-folder': void serial(chooseFolder); break;
|
|
225
|
+
case 'reveal-folder': void revealFolder().catch(() => {}); break;
|
|
226
|
+
case 'open-privacy': void run(OPEN, [PRIVACY]).catch(() => {}); break;
|
|
227
|
+
case 'capture': void serial(async () => { await appWindows.hide(SETTINGS).catch(() => {}); await shoot('area', 250); }); break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const { activate, deactivate } = definePlugin({
|
|
232
|
+
activate(context) {
|
|
233
|
+
root = context.extensionPath!; dir = context.storagePath!; settingsFile = path.join(dir, 'settings.json');
|
|
234
|
+
let started: Promise<void> | null = null;
|
|
235
|
+
// The platform is not this instance's until activate returns: the first call that needs it
|
|
236
|
+
// (here `shortcuts.list`) is retried by the loop below, and a failed start is tried again.
|
|
237
|
+
const ready = () => started ??= (async () => {
|
|
238
|
+
await mkdir(dir, { recursive: true });
|
|
239
|
+
await loadSettings();
|
|
240
|
+
await shortcuts.list();
|
|
241
|
+
await registerShortcuts();
|
|
242
|
+
await showTray().catch(error => console.error('tray failed:', error));
|
|
243
|
+
})().catch(error => { started = null; throw error; });
|
|
244
|
+
// Every callback runs through here: a call made before this instance took over (stale_instance)
|
|
245
|
+
// or after it was replaced must never become an unhandled rejection that kills the worker.
|
|
246
|
+
const later = (task: () => unknown) => { void ready().then(task).catch(error => console.warn('screenshot:', String(error))); };
|
|
247
|
+
context.subscriptions.push({ dispose: channel.onMessage<FromOverlay | FromSettings>(m => later(() => onMessage(m))) });
|
|
248
|
+
context.subscriptions.push({ dispose: events.onSignal('shortcuts', () => later(async () => {
|
|
249
|
+
const pressed = await shortcuts.events().catch(() => []) as { id: string }[];
|
|
250
|
+
for (const p of pressed) if (MODES[p.id]) void serial(() => shoot(MODES[p.id].mode));
|
|
251
|
+
})) });
|
|
252
|
+
context.subscriptions.push({ dispose: events.onSignal('tray', () => later(async () => {
|
|
253
|
+
const pressed = await tray.events().catch(() => []) as { id: string }[];
|
|
254
|
+
for (const { id } of pressed) {
|
|
255
|
+
const mode = ({ 'tray.area': 'area', 'tray.full': 'full', 'tray.quick': 'quick', 'tray.ocr': 'ocr' } as Record<string, Mode>)[id];
|
|
256
|
+
if (mode) void serial(() => shoot(mode, 200));
|
|
257
|
+
else if (id === 'tray.folder') void revealFolder().catch(() => {});
|
|
258
|
+
else if (id === 'tray.settings') void openSettings().catch(() => {});
|
|
259
|
+
}
|
|
260
|
+
})) });
|
|
261
|
+
context.subscriptions.push({ dispose: events.onSignal('permissions', () => later(async () => { await registerShortcuts(); await showTray().catch(() => {}); await pushSettings(); })) });
|
|
262
|
+
context.subscriptions.push({ dispose: i18n.onDidChangeLocale(() => later(() => showTray().catch(() => {}))) });
|
|
263
|
+
// Launcher and plugin-center commands. The launcher shows "done" and keeps its panel up for
|
|
264
|
+
// 400 ms after a command returns, so a capture started from it waits until the panel is gone
|
|
265
|
+
// (otherwise the frozen screen shows the launcher); other callers only need a short pause.
|
|
266
|
+
const command = (mode: Mode) => async (args?: unknown) => {
|
|
267
|
+
await ready();
|
|
268
|
+
void serial(() => shoot(mode, launcher.session(args) ? 750 : 250));
|
|
269
|
+
return { started: true };
|
|
270
|
+
};
|
|
271
|
+
context.commands.register('screenshot.capture', command('area'));
|
|
272
|
+
context.commands.register('screenshot.capture-full', command('full'));
|
|
273
|
+
context.commands.register('screenshot.capture-quick', command('quick'));
|
|
274
|
+
context.commands.register('screenshot.capture-ocr', command('ocr'));
|
|
275
|
+
context.commands.register('screenshot.settings', async () => { await ready(); await openSettings(); return { opened: true }; });
|
|
276
|
+
context.commands.register('screenshot.folder', async () => { await ready(); await revealFolder(); return { opened: true }; });
|
|
277
|
+
// Resident: register the shortcuts and the menu-bar icon as soon as this instance has taken over.
|
|
278
|
+
void (async () => {
|
|
279
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
280
|
+
await new Promise(r => setTimeout(r, attempt ? 1000 : 50));
|
|
281
|
+
try { await ready(); return; } catch (error) { if (/denied|invalid|unsupported/.test(String(error))) { console.error('screenshot start failed:', error); return; } }
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
284
|
+
},
|
|
285
|
+
});
|