@@ -60,13 +60,148 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
60
60
61
61
}
62
62
63
+ // Register the main popup shortcut
63
64
KeyboardShortcuts . onKeyUp ( for: . showPopup) { [ weak self] in
64
65
if !AppSettings. shared. hotkeysPaused {
65
66
self ? . showPopup ( )
66
67
} else {
67
68
NSLog ( " Hotkeys are paused " )
68
69
}
69
70
}
71
+
72
+ // Set up command-specific shortcuts
73
+ setupCommandShortcuts ( )
74
+
75
+ // Register for command changes to update shortcuts
76
+ NotificationCenter . default. addObserver (
77
+ self ,
78
+ selector: #selector( setupCommandShortcuts) ,
79
+ name: NSNotification . Name ( " CommandsChanged " ) ,
80
+ object: nil
81
+ )
82
+ }
83
+
84
+ // Setup and register all command shortcuts
85
+ @objc private func setupCommandShortcuts( ) {
86
+ // Only reset shortcuts for commands that should not have shortcuts
87
+ for command in appState. commandManager. commands. filter ( { !$0. hasShortcut } ) {
88
+ KeyboardShortcuts . reset ( . commandShortcut( for: command. id) )
89
+ }
90
+
91
+ // Register handlers for commands with shortcuts enabled
92
+ for command in appState. commandManager. commands. filter ( { $0. hasShortcut } ) {
93
+ KeyboardShortcuts . onKeyUp ( for: . commandShortcut( for: command. id) ) { [ weak self] in
94
+ guard let self = self , !AppSettings. shared. hotkeysPaused else { return }
95
+
96
+ // Execute the command directly
97
+ self . executeCommandDirectly ( command)
98
+ }
99
+ }
100
+ }
101
+
102
+ // Executes a command without showing the popup
103
+ private func executeCommandDirectly( _ command: CommandModel ) {
104
+ // Cancel any ongoing processing first
105
+ appState. activeProvider. cancel ( )
106
+
107
+ DispatchQueue . main. async { [ weak self] in
108
+ guard let self = self else { return }
109
+
110
+ // Save the current app so we can return to it
111
+ if let currentFrontmostApp = NSWorkspace . shared. frontmostApplication {
112
+ self . appState. previousApplication = currentFrontmostApp
113
+ }
114
+
115
+ let generalPasteboard = NSPasteboard . general
116
+
117
+ // Get initial pasteboard content to restore later
118
+ let oldContents = generalPasteboard. string ( forType: . string)
119
+
120
+ // Clear and perform copy command to get selected text
121
+ generalPasteboard. clearContents ( )
122
+ let source = CGEventSource ( stateID: . hidSystemState)
123
+ let keyDown = CGEvent ( keyboardEventSource: source, virtualKey: 0x08 , keyDown: true )
124
+ let keyUp = CGEvent ( keyboardEventSource: source, virtualKey: 0x08 , keyDown: false )
125
+ keyDown? . flags = . maskCommand
126
+ keyUp? . flags = . maskCommand
127
+ keyDown? . post ( tap: . cghidEventTap)
128
+ keyUp? . post ( tap: . cghidEventTap)
129
+
130
+ // Wait for copy operation to complete
131
+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.1 ) { [ weak self] in
132
+ guard let self = self else { return }
133
+
134
+ // Get the selected text
135
+ let selectedText = generalPasteboard. string ( forType: . string) ?? " "
136
+
137
+ // Restore original clipboard contents
138
+ generalPasteboard. clearContents ( )
139
+ if let oldContents = oldContents {
140
+ generalPasteboard. setString ( oldContents, forType: . string)
141
+ }
142
+
143
+ // Skip if no text is selected
144
+ guard !selectedText. isEmpty else {
145
+ NSLog ( " No text selected for command: \( command. name) " )
146
+ return
147
+ }
148
+
149
+ // Store the selected text in app state
150
+ self . appState. selectedText = selectedText
151
+
152
+ // Process the command
153
+ Task {
154
+ await self . processCommandWithUI ( command)
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ // Process a command with appropriate UI feedback
161
+ private func processCommandWithUI( _ command: CommandModel ) async {
162
+ // Set processing flag to prevent duplicate operations
163
+ if appState. isProcessing {
164
+ return
165
+ }
166
+
167
+ appState. isProcessing = true
168
+
169
+ do {
170
+ // Process the text with the AI provider
171
+ let result = try await appState. activeProvider. processText (
172
+ systemPrompt: command. prompt,
173
+ userPrompt: appState. selectedText,
174
+ images: [ ] ,
175
+ streaming: false
176
+ )
177
+
178
+ // Handle the result
179
+ await MainActor . run {
180
+ if command. useResponseWindow {
181
+ // Show in response window
182
+ let window = ResponseWindow (
183
+ title: command. name,
184
+ content: result,
185
+ selectedText: appState. selectedText,
186
+ option: nil
187
+ )
188
+
189
+ NSApp . activate ( ignoringOtherApps: true )
190
+ WindowManager . shared. addResponseWindow ( window)
191
+ window. makeKeyAndOrderFront ( nil )
192
+ window. orderFrontRegardless ( )
193
+ } else {
194
+ // Replace text directly
195
+ appState. replaceSelectedText ( with: result)
196
+ }
197
+ }
198
+ } catch {
199
+ print ( " Error processing command \( command. name) : \( error. localizedDescription) " )
200
+ }
201
+
202
+ await MainActor . run {
203
+ appState. isProcessing = false
204
+ }
70
205
}
71
206
72
207
// Called when app is about to close - performs cleanup
@@ -227,85 +362,112 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
227
362
}
228
363
}
229
364
230
- // Shows the main popup window when shortcut is triggered
231
365
@MainActor private func showPopup( ) {
232
- appState. activeProvider. cancel ( )
233
-
234
- DispatchQueue . main. async { [ weak self] in
235
- guard let self = self else { return }
236
-
237
- if let currentFrontmostApp = NSWorkspace . shared. frontmostApplication {
238
- self . appState. previousApplication = currentFrontmostApp
239
- }
240
-
241
- self . closePopupWindow ( )
242
-
243
- let generalPasteboard = NSPasteboard . general
244
-
245
- // Get initial pasteboard content
246
- let oldContents = generalPasteboard. string ( forType: . string)
247
-
248
- // Prioritized image types (in order of preference)
249
- let supportedImageTypes = [
250
- NSPasteboard . PasteboardType ( " public.png " ) ,
251
- NSPasteboard . PasteboardType ( " public.jpeg " ) ,
252
- NSPasteboard . PasteboardType ( " public.tiff " ) ,
253
- NSPasteboard . PasteboardType ( " com.compuserve.gif " ) ,
254
- NSPasteboard . PasteboardType ( " public.image " )
255
- ]
256
- var foundImage : Data ? = nil
366
+ appState. activeProvider. cancel ( )
257
367
258
- // Try to find the first available image in order of preference
259
- for type in supportedImageTypes {
260
- if let data = generalPasteboard. data ( forType: type) {
261
- foundImage = data
262
- NSLog ( " Selected image type: \( type) " )
263
- break // Take only the first matching format
264
- }
265
- }
266
-
267
- // Clear and perform copy command
268
- generalPasteboard. clearContents ( )
269
- let source = CGEventSource ( stateID: . hidSystemState)
270
- let keyDown = CGEvent ( keyboardEventSource: source, virtualKey: 0x08 , keyDown: true )
271
- let keyUp = CGEvent ( keyboardEventSource: source, virtualKey: 0x08 , keyDown: false )
272
- keyDown? . flags = . maskCommand
273
- keyUp? . flags = . maskCommand
274
- keyDown? . post ( tap: . cghidEventTap)
275
- keyUp? . post ( tap: . cghidEventTap)
276
-
277
- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.1 ) { [ weak self] in
368
+ DispatchQueue . main. async { [ weak self] in
278
369
guard let self = self else { return }
279
- let selectedText = generalPasteboard. string ( forType: . string) ?? " "
280
-
281
- // Update app state with found image if any
282
- self . appState. selectedImages = foundImage. map { [ $0] } ?? [ ]
283
370
284
- generalPasteboard. clearContents ( )
285
- if let oldContents = oldContents {
286
- generalPasteboard. setString ( oldContents, forType: . string)
371
+ if let currentFrontmostApp = NSWorkspace . shared. frontmostApplication {
372
+ self . appState. previousApplication = currentFrontmostApp
287
373
}
288
374
289
- let window = PopupWindow ( appState: self . appState)
290
- window. delegate = self
375
+ self . closePopupWindow ( )
291
376
292
- self . appState . selectedText = selectedText
293
- self . popupWindow = window
377
+ let generalPasteboard = NSPasteboard . general
378
+ let oldContents = generalPasteboard . string ( forType : . string )
294
379
295
- // Set appropriate window size based on content
296
- if !selectedText. isEmpty || !self . appState. selectedImages. isEmpty {
297
- window. setContentSize ( NSSize ( width: 400 , height: 400 ) )
298
- } else {
299
- window. setContentSize ( NSSize ( width: 400 , height: 100 ) )
300
- }
380
+ // Clear and perform copy command to get current selection
381
+ generalPasteboard. clearContents ( )
382
+ let source = CGEventSource ( stateID: . hidSystemState)
383
+ let keyDown = CGEvent ( keyboardEventSource: source, virtualKey: 0x08 , keyDown: true )
384
+ let keyUp = CGEvent ( keyboardEventSource: source, virtualKey: 0x08 , keyDown: false )
385
+ keyDown? . flags = . maskCommand
386
+ keyUp? . flags = . maskCommand
387
+ keyDown? . post ( tap: . cghidEventTap)
388
+ keyUp? . post ( tap: . cghidEventTap)
301
389
302
- window. positionNearMouse ( )
303
- NSApp . activate ( ignoringOtherApps: true )
304
- window. makeKeyAndOrderFront ( nil )
305
- window. orderFrontRegardless ( )
390
+ // Wait for the copy operation to complete, then process the pasteboard
391
+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.2 ) { [ weak self] in
392
+ guard let self = self else { return }
393
+
394
+ var foundImages : [ Data ] = [ ]
395
+ var selectedText = " "
396
+
397
+ // First check for file URLs (for Finder selections)
398
+ let classes = [ NSURL . self]
399
+ let options : [ NSPasteboard . ReadingOptionKey : Any ] = [
400
+ . urlReadingFileURLsOnly: true ,
401
+ . urlReadingContentsConformToTypes: [
402
+ " public.image " ,
403
+ " public.png " ,
404
+ " public.jpeg " ,
405
+ " public.tiff " ,
406
+ " com.compuserve.gif "
407
+ ]
408
+ ]
409
+
410
+ if let urls = generalPasteboard. readObjects ( forClasses: classes, options: options) as? [ URL ] {
411
+ for url in urls {
412
+ if let imageData = try ? Data ( contentsOf: url) {
413
+ if NSImage ( data: imageData) != nil {
414
+ foundImages. append ( imageData)
415
+ NSLog ( " Loaded image data from file: \( url. lastPathComponent) " )
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ // If no file URLs found, check for direct image data
422
+ if foundImages. isEmpty {
423
+ let supportedImageTypes = [
424
+ NSPasteboard . PasteboardType ( " public.png " ) ,
425
+ NSPasteboard . PasteboardType ( " public.jpeg " ) ,
426
+ NSPasteboard . PasteboardType ( " public.tiff " ) ,
427
+ NSPasteboard . PasteboardType ( " com.compuserve.gif " ) ,
428
+ NSPasteboard . PasteboardType ( " public.image " )
429
+ ]
430
+
431
+ for type in supportedImageTypes {
432
+ if let data = generalPasteboard. data ( forType: type) {
433
+ foundImages. append ( data)
434
+ NSLog ( " Found direct image data of type: \( type) " )
435
+ break
436
+ }
437
+ }
438
+ }
439
+
440
+ // Get any text content
441
+ selectedText = generalPasteboard. string ( forType: . string) ?? " "
442
+
443
+ // Restore original pasteboard contents
444
+ generalPasteboard. clearContents ( )
445
+ if let oldContents = oldContents {
446
+ generalPasteboard. setString ( oldContents, forType: . string)
447
+ }
448
+
449
+ // Update app state and show popup
450
+ self . appState. selectedImages = foundImages
451
+ self . appState. selectedText = selectedText
452
+
453
+ let window = PopupWindow ( appState: self . appState)
454
+ window. delegate = self
455
+ self . popupWindow = window
456
+
457
+ // Set window size based on content
458
+ if !selectedText. isEmpty || !foundImages. isEmpty {
459
+ window. setContentSize ( NSSize ( width: 400 , height: 400 ) )
460
+ } else {
461
+ window. setContentSize ( NSSize ( width: 400 , height: 100 ) )
462
+ }
463
+
464
+ window. positionNearMouse ( )
465
+ NSApp . activate ( ignoringOtherApps: true )
466
+ window. makeKeyAndOrderFront ( nil )
467
+ window. orderFrontRegardless ( )
468
+ }
306
469
}
307
470
}
308
- }
309
471
310
472
// Closes and cleans up the popup window
311
473
private func closePopupWindow( ) {
0 commit comments