markupr 2.1.8
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/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotkeyManager - Global Hotkey Registration for markupr
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Global hotkey registration that works when any app is focused
|
|
6
|
+
* - Cross-platform accelerator normalization
|
|
7
|
+
* - Conflict detection and fallback handling
|
|
8
|
+
* - Hotkey customization via settings
|
|
9
|
+
*
|
|
10
|
+
* Default hotkeys:
|
|
11
|
+
* - Cmd+Shift+F (Ctrl+Shift+F on Windows): Toggle recording
|
|
12
|
+
* - Cmd+Shift+S (Ctrl+Shift+S on Windows): Manual screenshot
|
|
13
|
+
* - Cmd+Shift+P (Ctrl+Shift+P on Windows): Pause/resume recording
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { globalShortcut, app } from 'electron';
|
|
17
|
+
import type { HotkeyConfig } from '../shared/types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Available hotkey actions
|
|
21
|
+
*/
|
|
22
|
+
export type HotkeyAction = 'toggleRecording' | 'manualScreenshot' | 'pauseResume';
|
|
23
|
+
|
|
24
|
+
// HotkeyConfig is imported from '../shared/types' (single source of truth)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of a hotkey registration attempt
|
|
28
|
+
*/
|
|
29
|
+
export interface HotkeyRegistrationResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
action: HotkeyAction;
|
|
32
|
+
accelerator: string;
|
|
33
|
+
fallbackUsed?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* HotkeyManager interface
|
|
39
|
+
*/
|
|
40
|
+
export interface IHotkeyManager {
|
|
41
|
+
initialize(): HotkeyRegistrationResult[];
|
|
42
|
+
register(action: HotkeyAction, accelerator: string): HotkeyRegistrationResult;
|
|
43
|
+
unregister(action: HotkeyAction): void;
|
|
44
|
+
unregisterAll(): void;
|
|
45
|
+
getAccelerator(action: HotkeyAction): string | undefined;
|
|
46
|
+
getConfig(): HotkeyConfig;
|
|
47
|
+
isRegistered(action: HotkeyAction): boolean;
|
|
48
|
+
onHotkey(callback: (action: HotkeyAction) => void): () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default hotkey configuration
|
|
53
|
+
* Using CommandOrControl for cross-platform compatibility
|
|
54
|
+
*/
|
|
55
|
+
export const DEFAULT_HOTKEY_CONFIG: HotkeyConfig = {
|
|
56
|
+
toggleRecording: 'CommandOrControl+Shift+F',
|
|
57
|
+
manualScreenshot: 'CommandOrControl+Shift+S',
|
|
58
|
+
pauseResume: 'CommandOrControl+Shift+P',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fallback hotkeys if the primary ones are unavailable
|
|
63
|
+
*/
|
|
64
|
+
const FALLBACK_HOTKEYS: Record<HotkeyAction, string[]> = {
|
|
65
|
+
toggleRecording: [
|
|
66
|
+
'CommandOrControl+Shift+R',
|
|
67
|
+
'CommandOrControl+Alt+F',
|
|
68
|
+
'CommandOrControl+Alt+R',
|
|
69
|
+
],
|
|
70
|
+
manualScreenshot: [
|
|
71
|
+
'CommandOrControl+Shift+P',
|
|
72
|
+
'CommandOrControl+Alt+S',
|
|
73
|
+
'CommandOrControl+Alt+P',
|
|
74
|
+
],
|
|
75
|
+
pauseResume: [
|
|
76
|
+
'CommandOrControl+Shift+Space',
|
|
77
|
+
'CommandOrControl+Alt+P',
|
|
78
|
+
'CommandOrControl+Alt+Space',
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* HotkeyManager implementation
|
|
84
|
+
*/
|
|
85
|
+
class HotkeyManagerImpl implements IHotkeyManager {
|
|
86
|
+
private callbacks: Set<(action: HotkeyAction) => void> = new Set();
|
|
87
|
+
private registeredKeys: Map<HotkeyAction, string> = new Map();
|
|
88
|
+
private config: HotkeyConfig;
|
|
89
|
+
private initialized = false;
|
|
90
|
+
|
|
91
|
+
constructor(config?: Partial<HotkeyConfig>) {
|
|
92
|
+
this.config = { ...DEFAULT_HOTKEY_CONFIG, ...config };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initialize the hotkey manager with default or custom config
|
|
97
|
+
* Returns results for each hotkey registration attempt
|
|
98
|
+
*/
|
|
99
|
+
initialize(): HotkeyRegistrationResult[] {
|
|
100
|
+
if (this.initialized) {
|
|
101
|
+
console.warn('[HotkeyManager] Already initialized, skipping...');
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const results: HotkeyRegistrationResult[] = [];
|
|
106
|
+
|
|
107
|
+
// Register toggle recording hotkey
|
|
108
|
+
results.push(this.register('toggleRecording', this.config.toggleRecording));
|
|
109
|
+
|
|
110
|
+
// Register manual screenshot hotkey
|
|
111
|
+
results.push(this.register('manualScreenshot', this.config.manualScreenshot));
|
|
112
|
+
|
|
113
|
+
// Register pause/resume hotkey
|
|
114
|
+
results.push(this.register('pauseResume', this.config.pauseResume));
|
|
115
|
+
|
|
116
|
+
// Setup cleanup on app quit
|
|
117
|
+
app.on('will-quit', () => {
|
|
118
|
+
this.unregisterAll();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this.initialized = true;
|
|
122
|
+
console.log('[HotkeyManager] Initialized with hotkeys:', this.getConfig());
|
|
123
|
+
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register a global hotkey for an action
|
|
129
|
+
* Will try fallback accelerators if the primary fails
|
|
130
|
+
*/
|
|
131
|
+
register(action: HotkeyAction, accelerator: string): HotkeyRegistrationResult {
|
|
132
|
+
// Unregister existing if any
|
|
133
|
+
this.unregister(action);
|
|
134
|
+
|
|
135
|
+
// Normalize the accelerator
|
|
136
|
+
const normalizedAccelerator = this.normalizeAccelerator(accelerator);
|
|
137
|
+
|
|
138
|
+
// Try to register the primary accelerator
|
|
139
|
+
const success = this.tryRegister(action, normalizedAccelerator);
|
|
140
|
+
|
|
141
|
+
if (success) {
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
action,
|
|
145
|
+
accelerator: normalizedAccelerator,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Primary failed, try fallbacks
|
|
150
|
+
console.warn(
|
|
151
|
+
`[HotkeyManager] Failed to register ${normalizedAccelerator} for ${action}, trying fallbacks...`
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const fallbacks = FALLBACK_HOTKEYS[action] || [];
|
|
155
|
+
for (const fallback of fallbacks) {
|
|
156
|
+
const normalizedFallback = this.normalizeAccelerator(fallback);
|
|
157
|
+
const fallbackSuccess = this.tryRegister(action, normalizedFallback);
|
|
158
|
+
|
|
159
|
+
if (fallbackSuccess) {
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
action,
|
|
163
|
+
accelerator: normalizedFallback,
|
|
164
|
+
fallbackUsed: normalizedFallback,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// All attempts failed
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
action,
|
|
173
|
+
accelerator: normalizedAccelerator,
|
|
174
|
+
error: `Failed to register hotkey. ${normalizedAccelerator} and all fallbacks are unavailable.`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Attempt to register an accelerator
|
|
180
|
+
*/
|
|
181
|
+
private tryRegister(action: HotkeyAction, accelerator: string): boolean {
|
|
182
|
+
try {
|
|
183
|
+
// Check if already registered globally
|
|
184
|
+
if (globalShortcut.isRegistered(accelerator)) {
|
|
185
|
+
console.warn(`[HotkeyManager] Accelerator ${accelerator} is already registered globally`);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const success = globalShortcut.register(accelerator, () => {
|
|
190
|
+
console.log(`[HotkeyManager] Hotkey triggered: ${action}`);
|
|
191
|
+
this.emitHotkey(action);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (success) {
|
|
195
|
+
this.registeredKeys.set(action, accelerator);
|
|
196
|
+
this.config[action] = accelerator;
|
|
197
|
+
console.log(`[HotkeyManager] Registered ${accelerator} for ${action}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return success;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(`[HotkeyManager] Error registering ${accelerator}:`, error);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Unregister a hotkey for an action
|
|
209
|
+
*/
|
|
210
|
+
unregister(action: HotkeyAction): void {
|
|
211
|
+
const accelerator = this.registeredKeys.get(action);
|
|
212
|
+
if (accelerator) {
|
|
213
|
+
try {
|
|
214
|
+
globalShortcut.unregister(accelerator);
|
|
215
|
+
this.registeredKeys.delete(action);
|
|
216
|
+
console.log(`[HotkeyManager] Unregistered ${accelerator} for ${action}`);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`[HotkeyManager] Error unregistering ${accelerator}:`, error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Unregister all hotkeys
|
|
225
|
+
*/
|
|
226
|
+
unregisterAll(): void {
|
|
227
|
+
const entries = Array.from(this.registeredKeys.entries());
|
|
228
|
+
for (const [action, accelerator] of entries) {
|
|
229
|
+
try {
|
|
230
|
+
globalShortcut.unregister(accelerator);
|
|
231
|
+
console.log(`[HotkeyManager] Unregistered ${accelerator} for ${action}`);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`[HotkeyManager] Error unregistering ${accelerator}:`, error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.registeredKeys.clear();
|
|
237
|
+
console.log('[HotkeyManager] All hotkeys unregistered');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get the registered accelerator for an action
|
|
242
|
+
*/
|
|
243
|
+
getAccelerator(action: HotkeyAction): string | undefined {
|
|
244
|
+
return this.registeredKeys.get(action);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get the current hotkey configuration
|
|
249
|
+
*/
|
|
250
|
+
getConfig(): HotkeyConfig {
|
|
251
|
+
return { ...this.config };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if a hotkey is registered for an action
|
|
256
|
+
*/
|
|
257
|
+
isRegistered(action: HotkeyAction): boolean {
|
|
258
|
+
return this.registeredKeys.has(action);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Subscribe to hotkey events
|
|
263
|
+
* Returns an unsubscribe function
|
|
264
|
+
*/
|
|
265
|
+
onHotkey(callback: (action: HotkeyAction) => void): () => void {
|
|
266
|
+
this.callbacks.add(callback);
|
|
267
|
+
return () => {
|
|
268
|
+
this.callbacks.delete(callback);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Emit a hotkey event to all subscribers
|
|
274
|
+
*/
|
|
275
|
+
private emitHotkey(action: HotkeyAction): void {
|
|
276
|
+
const callbacksArray = Array.from(this.callbacks);
|
|
277
|
+
for (const callback of callbacksArray) {
|
|
278
|
+
try {
|
|
279
|
+
callback(action);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(`[HotkeyManager] Error in hotkey callback:`, error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Normalize accelerator string for consistency
|
|
288
|
+
* Handles platform differences and common aliases
|
|
289
|
+
*/
|
|
290
|
+
private normalizeAccelerator(accelerator: string): string {
|
|
291
|
+
let normalized = accelerator.trim();
|
|
292
|
+
|
|
293
|
+
// Normalize common aliases
|
|
294
|
+
normalized = normalized
|
|
295
|
+
.replace(/Cmd/gi, 'Command')
|
|
296
|
+
.replace(/Ctrl/gi, 'Control')
|
|
297
|
+
.replace(/Opt/gi, 'Alt')
|
|
298
|
+
.replace(/Option/gi, 'Alt');
|
|
299
|
+
|
|
300
|
+
// Ensure proper casing for Electron accelerator keys
|
|
301
|
+
const parts = normalized.split('+').map((part) => {
|
|
302
|
+
const lower = part.toLowerCase().trim();
|
|
303
|
+
switch (lower) {
|
|
304
|
+
case 'command':
|
|
305
|
+
return 'Command';
|
|
306
|
+
case 'control':
|
|
307
|
+
return 'Control';
|
|
308
|
+
case 'commandorcontrol':
|
|
309
|
+
return 'CommandOrControl';
|
|
310
|
+
case 'alt':
|
|
311
|
+
return 'Alt';
|
|
312
|
+
case 'shift':
|
|
313
|
+
return 'Shift';
|
|
314
|
+
case 'super':
|
|
315
|
+
return 'Super';
|
|
316
|
+
case 'meta':
|
|
317
|
+
return 'Meta';
|
|
318
|
+
default:
|
|
319
|
+
// For letter keys, ensure uppercase
|
|
320
|
+
if (lower.length === 1 && /[a-z]/.test(lower)) {
|
|
321
|
+
return lower.toUpperCase();
|
|
322
|
+
}
|
|
323
|
+
// For special keys, capitalize first letter
|
|
324
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return parts.join('+');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Update configuration with new hotkeys
|
|
333
|
+
* Will re-register affected hotkeys
|
|
334
|
+
*/
|
|
335
|
+
updateConfig(newConfig: Partial<HotkeyConfig>): HotkeyRegistrationResult[] {
|
|
336
|
+
const results: HotkeyRegistrationResult[] = [];
|
|
337
|
+
|
|
338
|
+
if (newConfig.toggleRecording && newConfig.toggleRecording !== this.config.toggleRecording) {
|
|
339
|
+
results.push(this.register('toggleRecording', newConfig.toggleRecording));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (newConfig.manualScreenshot && newConfig.manualScreenshot !== this.config.manualScreenshot) {
|
|
343
|
+
results.push(this.register('manualScreenshot', newConfig.manualScreenshot));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (newConfig.pauseResume && newConfig.pauseResume !== this.config.pauseResume) {
|
|
347
|
+
results.push(this.register('pauseResume', newConfig.pauseResume));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return results;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get display string for an accelerator (user-friendly format)
|
|
355
|
+
*/
|
|
356
|
+
getDisplayString(action: HotkeyAction): string {
|
|
357
|
+
const accelerator = this.registeredKeys.get(action);
|
|
358
|
+
if (!accelerator) return 'Not set';
|
|
359
|
+
|
|
360
|
+
const isMac = process.platform === 'darwin';
|
|
361
|
+
|
|
362
|
+
return accelerator
|
|
363
|
+
.replace('CommandOrControl', isMac ? '\u2318' : 'Ctrl')
|
|
364
|
+
.replace('Command', '\u2318')
|
|
365
|
+
.replace('Control', 'Ctrl')
|
|
366
|
+
.replace('Shift', isMac ? '\u21E7' : 'Shift')
|
|
367
|
+
.replace('Alt', isMac ? '\u2325' : 'Alt')
|
|
368
|
+
.replace('Super', isMac ? '\u2318' : 'Win')
|
|
369
|
+
.replace(/\+/g, isMac ? '' : '+');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Singleton instance
|
|
374
|
+
let hotkeyManagerInstance: HotkeyManagerImpl | null = null;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get or create the HotkeyManager singleton
|
|
378
|
+
*/
|
|
379
|
+
export function getHotkeyManager(config?: Partial<HotkeyConfig>): HotkeyManagerImpl {
|
|
380
|
+
if (!hotkeyManagerInstance) {
|
|
381
|
+
hotkeyManagerInstance = new HotkeyManagerImpl(config);
|
|
382
|
+
}
|
|
383
|
+
return hotkeyManagerInstance;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Create a new HotkeyManager instance (for testing)
|
|
388
|
+
*/
|
|
389
|
+
export function createHotkeyManager(config?: Partial<HotkeyConfig>): HotkeyManagerImpl {
|
|
390
|
+
return new HotkeyManagerImpl(config);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Export the singleton for convenience
|
|
394
|
+
export const hotkeyManager = getHotkeyManager();
|
|
395
|
+
|
|
396
|
+
export default hotkeyManager;
|
|
397
|
+
|
|
398
|
+
// Re-export HotkeyConfig from shared/types for downstream consumers
|
|
399
|
+
export type { HotkeyConfig } from '../shared/types';
|