Skip to content

Commit 983fa83

Browse files
marafCopilot
andauthored
Faster frontmost app detection on macOS via NSWorkspace + CGEvent (#127)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 65245ea commit 983fa83

1 file changed

Lines changed: 182 additions & 72 deletions

File tree

Lines changed: 182 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using System.Diagnostics;
21
using System.Globalization;
2+
using System.Runtime.InteropServices;
33
using System.Text;
44

55
namespace 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

Comments
 (0)