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.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. 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';