1- using System . Diagnostics ;
21using System . Globalization ;
2+ using System . Runtime . InteropServices ;
33using System . Text ;
44
55namespace Neptuo . Productivity . SnippetManager ;
@@ -49,44 +49,51 @@ internal static class MacOSApplication
4949 if ( ! OperatingSystem . IsMacOS ( ) )
5050 return null ;
5151
52- string ? output = RunAppleScript ( """
53- tell application "System Events"
54- set p to first application process whose frontmost is true
55- set sep to (character id 31)
56- set n to ""
57- try
58- set n to name of p
59- end try
60- set b to ""
61- try
62- set b to bundle identifier of p
63- end try
64- return ((unix id of p) as string) & sep & n & sep & b
65- end tell
66- """ ) ;
67-
68- if ( string . IsNullOrEmpty ( output ) )
52+ try
6953 {
70- DiagnosticsLog . Error ( "Unable to resolve the frontmost macOS application (empty AppleScript output)." ) ;
71- return null ;
72- }
54+ IntPtr workspaceClass = ObjC . objc_getClass ( "NSWorkspace" ) ;
55+ if ( workspaceClass == IntPtr . Zero )
56+ {
57+ if ( ObjC . AppKitHandle == IntPtr . Zero )
58+ DiagnosticsLog . Error ( "Unable to resolve the NSWorkspace class: AppKit failed to load." ) ;
59+ else
60+ DiagnosticsLog . Error ( "Unable to resolve the NSWorkspace class via the Objective-C runtime." ) ;
61+ return null ;
62+ }
7363
74- string [ ] parts = output . Split ( new [ ] { ' \u001F ' } , 3 ) ;
75- if ( parts . Length == 0 || ! int . TryParse ( parts [ 0 ] , NumberStyles . Integer , CultureInfo . InvariantCulture , out int processId ) )
76- {
77- DiagnosticsLog . Error ( $ "Unable to parse the frontmost macOS application info from ' { output } ' .") ;
78- return null ;
79- }
64+ IntPtr sharedWorkspace = ObjC . IntPtr_objc_msgSend ( workspaceClass , ObjC . Sel_sharedWorkspace ) ;
65+ if ( sharedWorkspace == IntPtr . Zero )
66+ {
67+ DiagnosticsLog . Error ( "Unable to resolve [NSWorkspace sharedWorkspace] ." ) ;
68+ return null ;
69+ }
8070
81- if ( parts . Length < 3 )
82- DiagnosticsLog . Error ( $ "Frontmost macOS application info is missing fields (got { parts . Length } ): '{ output } '.") ;
71+ IntPtr frontApp = ObjC . IntPtr_objc_msgSend ( sharedWorkspace , ObjC . Sel_frontmostApplication ) ;
72+ if ( frontApp == IntPtr . Zero )
73+ {
74+ DiagnosticsLog . Error ( "Unable to resolve the frontmost macOS application (NSWorkspace returned nil)." ) ;
75+ return null ;
76+ }
8377
84- string ? name = parts . Length > 1 && ! string . IsNullOrEmpty ( parts [ 1 ] ) ? parts [ 1 ] : null ;
85- string ? bundle = parts . Length > 2 && ! string . IsNullOrEmpty ( parts [ 2 ] ) ? parts [ 2 ] : null ;
78+ int processId = ObjC . Int_objc_msgSend ( frontApp , ObjC . Sel_processIdentifier ) ;
79+ string ? name = ObjC . ReadNSString ( ObjC . IntPtr_objc_msgSend ( frontApp , ObjC . Sel_localizedName ) ) ;
80+ string ? bundle = ObjC . ReadNSString ( ObjC . IntPtr_objc_msgSend ( frontApp , ObjC . Sel_bundleIdentifier ) ) ;
81+
82+ if ( processId <= 0 )
83+ {
84+ DiagnosticsLog . Error ( $ "Frontmost macOS application returned a non-positive PID ({ processId } ).") ;
85+ return null ;
86+ }
8687
87- var app = new FrontmostApplication ( processId , name , bundle ) ;
88- DiagnosticsLog . Info ( $ "Resolved the frontmost macOS application: { app . DescribeForLog ( ) } .") ;
89- return app ;
88+ var app = new FrontmostApplication ( processId , name , bundle ) ;
89+ DiagnosticsLog . Info ( $ "Resolved the frontmost macOS application: { app . DescribeForLog ( ) } .") ;
90+ return app ;
91+ }
92+ catch ( Exception ex )
93+ {
94+ DiagnosticsLog . Error ( "Unable to resolve the frontmost macOS application via NSWorkspace." , ex ) ;
95+ return null ;
96+ }
9097 }
9198
9299 public static void ActivateCurrentProcess ( )
@@ -102,60 +109,163 @@ public static void ActivateProcess(int processId, string? name = null)
102109
103110 string suffix = string . IsNullOrEmpty ( name ) ? string . Empty : $ " (name='{ name } ')";
104111 DiagnosticsLog . Info ( $ "Requesting macOS activation for process { processId } { suffix } .") ;
105- RunAppleScript ( $ """
106- tell application "System Events"
107- set frontmost of first application process whose unix id is { processId } to true
108- end tell
109- """ ) ;
112+
113+ try
114+ {
115+ IntPtr runningAppClass = ObjC . objc_getClass ( "NSRunningApplication" ) ;
116+ if ( runningAppClass == IntPtr . Zero )
117+ {
118+ DiagnosticsLog . Error ( "Unable to resolve the NSRunningApplication class." ) ;
119+ return ;
120+ }
121+
122+ IntPtr app = ObjC . IntPtr_IntPtr_objc_msgSend ( runningAppClass , ObjC . Sel_runningApplicationWithProcessIdentifier , new IntPtr ( processId ) ) ;
123+ if ( app == IntPtr . Zero )
124+ {
125+ DiagnosticsLog . Error ( $ "No NSRunningApplication found for PID { processId } .") ;
126+ return ;
127+ }
128+
129+ // NSApplicationActivateIgnoringOtherApps = 1 << 1
130+ const ulong NSApplicationActivateIgnoringOtherApps = 1UL << 1 ;
131+ bool ok = ObjC . Bool_ULong_objc_msgSend ( app , ObjC . Sel_activateWithOptions , NSApplicationActivateIgnoringOtherApps ) ;
132+ if ( ! ok )
133+ DiagnosticsLog . Error ( $ "NSRunningApplication activateWithOptions: returned NO for PID { processId } .") ;
134+ }
135+ catch ( Exception ex )
136+ {
137+ DiagnosticsLog . Error ( $ "Unable to activate macOS process { processId } via NSRunningApplication.", ex ) ;
138+ }
110139 }
111140
112141 public static void SendPasteShortcut ( )
113142 {
114143 if ( ! OperatingSystem . IsMacOS ( ) )
115144 return ;
116145
117- DiagnosticsLog . Info ( "Sending macOS paste shortcut via AppleScript." ) ;
118- RunAppleScript ( """
119- tell application "System Events"
120- keystroke "v" using command down
121- end tell
122- """ ) ;
123- }
146+ DiagnosticsLog . Info ( "Sending macOS paste shortcut via CGEvent." ) ;
124147
125- private static string ? RunAppleScript ( string script )
126- {
127148 try
128149 {
129- using var process = new Process ( ) ;
130- process . StartInfo = new ProcessStartInfo
150+ const ushort kVK_ANSI_V = 0x09 ;
151+ const ulong kCGEventFlagMaskCommand = 0x00100000 ;
152+ const uint kCGHIDEventTap = 0 ;
153+
154+ IntPtr source = CoreGraphics . CGEventSourceCreate ( 1 /* kCGEventSourceStateHIDSystemState */ ) ;
155+ try
131156 {
132- FileName = "osascript" ,
133- RedirectStandardOutput = true ,
134- RedirectStandardError = true ,
135- UseShellExecute = false ,
136- CreateNoWindow = true
137- } ;
138- process . StartInfo . ArgumentList . Add ( "-e" ) ;
139- process . StartInfo . ArgumentList . Add ( script ) ;
140-
141- process . Start ( ) ;
142-
143- string output = process . StandardOutput . ReadToEnd ( ) ;
144- string error = process . StandardError . ReadToEnd ( ) ;
145- process . WaitForExit ( ) ;
146-
147- if ( process . ExitCode != 0 )
157+ IntPtr keyDown = CoreGraphics . CGEventCreateKeyboardEvent ( source , kVK_ANSI_V , true ) ;
158+ IntPtr keyUp = CoreGraphics . CGEventCreateKeyboardEvent ( source , kVK_ANSI_V , false ) ;
159+ try
160+ {
161+ if ( keyDown == IntPtr . Zero || keyUp == IntPtr . Zero )
162+ {
163+ DiagnosticsLog . Error ( "CGEventCreateKeyboardEvent returned null; cannot send paste shortcut." ) ;
164+ return ;
165+ }
166+
167+ CoreGraphics . CGEventSetFlags ( keyDown , kCGEventFlagMaskCommand ) ;
168+ CoreGraphics . CGEventSetFlags ( keyUp , kCGEventFlagMaskCommand ) ;
169+ CoreGraphics . CGEventPost ( kCGHIDEventTap , keyDown ) ;
170+ CoreGraphics . CGEventPost ( kCGHIDEventTap , keyUp ) ;
171+ }
172+ finally
173+ {
174+ if ( keyDown != IntPtr . Zero ) CoreGraphics . CFRelease ( keyDown ) ;
175+ if ( keyUp != IntPtr . Zero ) CoreGraphics . CFRelease ( keyUp ) ;
176+ }
177+ }
178+ finally
148179 {
149- DiagnosticsLog . Error ( $ "AppleScript exited with code { process . ExitCode } : { error } ") ;
150- return null ;
180+ if ( source != IntPtr . Zero ) CoreGraphics . CFRelease ( source ) ;
151181 }
152-
153- return output . Trim ( ) ;
154182 }
155183 catch ( Exception ex )
156184 {
157- DiagnosticsLog . Error ( "Unable to run AppleScript." , ex ) ;
158- return null ;
185+ DiagnosticsLog . Error ( "Unable to send the macOS paste shortcut via CGEvent." , ex ) ;
186+ }
187+ }
188+
189+ private static class CoreGraphics
190+ {
191+ private const string Framework = "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics" ;
192+ private const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ;
193+
194+ [ DllImport ( Framework ) ]
195+ public static extern IntPtr CGEventSourceCreate ( int stateID ) ;
196+
197+ [ DllImport ( Framework ) ]
198+ public static extern IntPtr CGEventCreateKeyboardEvent ( IntPtr source , ushort virtualKey , [ MarshalAs ( UnmanagedType . I1 ) ] bool keyDown ) ;
199+
200+ [ DllImport ( Framework ) ]
201+ public static extern void CGEventSetFlags ( IntPtr @event , ulong flags ) ;
202+
203+ [ DllImport ( Framework ) ]
204+ public static extern void CGEventPost ( uint tap , IntPtr @event ) ;
205+
206+ [ DllImport ( CoreFoundation ) ]
207+ public static extern void CFRelease ( IntPtr cf ) ;
208+ }
209+
210+ private static class ObjC
211+ {
212+ private const string LibObjC = "/usr/lib/libobjc.dylib" ;
213+ private const string LibSystem = "/usr/lib/libSystem.dylib" ;
214+ private const string AppKitPath = "/System/Library/Frameworks/AppKit.framework/AppKit" ;
215+
216+ // Ensure AppKit is loaded so NSWorkspace is available. Idempotent; a no-op when Avalonia has already linked it.
217+ public static readonly IntPtr AppKitHandle = LoadAppKit ( ) ;
218+
219+ public static readonly IntPtr Sel_sharedWorkspace = sel_registerName ( "sharedWorkspace" ) ;
220+ public static readonly IntPtr Sel_frontmostApplication = sel_registerName ( "frontmostApplication" ) ;
221+ public static readonly IntPtr Sel_processIdentifier = sel_registerName ( "processIdentifier" ) ;
222+ public static readonly IntPtr Sel_localizedName = sel_registerName ( "localizedName" ) ;
223+ public static readonly IntPtr Sel_bundleIdentifier = sel_registerName ( "bundleIdentifier" ) ;
224+ public static readonly IntPtr Sel_UTF8String = sel_registerName ( "UTF8String" ) ;
225+ public static readonly IntPtr Sel_runningApplicationWithProcessIdentifier = sel_registerName ( "runningApplicationWithProcessIdentifier:" ) ;
226+ public static readonly IntPtr Sel_activateWithOptions = sel_registerName ( "activateWithOptions:" ) ;
227+
228+ [ DllImport ( LibSystem , CharSet = CharSet . Ansi ) ]
229+ private static extern IntPtr dlopen ( string path , int mode ) ;
230+
231+ [ DllImport ( LibObjC , CharSet = CharSet . Ansi ) ]
232+ public static extern IntPtr objc_getClass ( string name ) ;
233+
234+ [ DllImport ( LibObjC , CharSet = CharSet . Ansi ) ]
235+ public static extern IntPtr sel_registerName ( string name ) ;
236+
237+ [ DllImport ( LibObjC , EntryPoint = "objc_msgSend" ) ]
238+ public static extern IntPtr IntPtr_objc_msgSend ( IntPtr receiver , IntPtr selector ) ;
239+
240+ [ DllImport ( LibObjC , EntryPoint = "objc_msgSend" ) ]
241+ public static extern IntPtr IntPtr_IntPtr_objc_msgSend ( IntPtr receiver , IntPtr selector , IntPtr arg ) ;
242+
243+ [ DllImport ( LibObjC , EntryPoint = "objc_msgSend" ) ]
244+ public static extern int Int_objc_msgSend ( IntPtr receiver , IntPtr selector ) ;
245+
246+ [ DllImport ( LibObjC , EntryPoint = "objc_msgSend" ) ]
247+ [ return : MarshalAs ( UnmanagedType . I1 ) ]
248+ public static extern bool Bool_ULong_objc_msgSend ( IntPtr receiver , IntPtr selector , ulong arg ) ;
249+
250+ public static string ? ReadNSString ( IntPtr nsString )
251+ {
252+ if ( nsString == IntPtr . Zero )
253+ return null ;
254+
255+ IntPtr utf8 = IntPtr_objc_msgSend ( nsString , Sel_UTF8String ) ;
256+ if ( utf8 == IntPtr . Zero )
257+ return null ;
258+
259+ string ? value = Marshal . PtrToStringUTF8 ( utf8 ) ;
260+ return string . IsNullOrEmpty ( value ) ? null : value ;
261+ }
262+
263+ private static IntPtr LoadAppKit ( )
264+ {
265+ IntPtr handle = dlopen ( AppKitPath , 2 /* RTLD_NOW */ ) ;
266+ if ( handle == IntPtr . Zero )
267+ DiagnosticsLog . Error ( $ "dlopen failed to load AppKit from '{ AppKitPath } '; NSWorkspace lookups will not work.") ;
268+ return handle ;
159269 }
160270 }
161271}
0 commit comments