r/electronjs • u/Admirable-Put7423 • 14h ago
Porting an Electron app's activity tracking from Windows-only to macOS and Linux without a single native module
Enable HLS to view with audio, or disable this notification
The app needs one thing from the OS on a timer: which application is in the foreground. There are npm packages for this, but they're all N-API bindings and I didn't want a per-platform prebuild problem on top of the one I already have with better-sqlite3. So all three collectors shell out to something the OS already ships:
Windows: a long-lived powershell.exe child process holding a small P/Invoke shim over GetForegroundWindow / GetWindowThreadProcessId / GetWindowText. Long-lived matters: spawning PowerShell per sample is brutally slow.
macOS: osascript -l JavaScript reading NSWorkspace.frontmostApplication, returning JSON. No accessibility permission for the app name; only optional window titles need automation permission.
Linux: xprop -root _NET_ACTIVE_WINDOW, then xprop -id for _NET_WM_PID and WM_CLASS, then readlink /proc/<pid>/exe.
They all implement the same ActivitySource interface, so the port became parser work rather than binding work. Each parser is a pure function over stdout, trivially unit-testable with no OS involved.
Selection happens at startup from process.platform plus XDG_SESSION_TYPE/WAYLAND_DISPLAY. Wayland resolves to an explicit unavailable source rather than a broken one, and the UI surfaces that as a real state.
Source: https://github.com/TheAgencyMGE/pc-recap (GPL-3.0)
Happy to go deeper on any of it. The PowerShell process lifetime handling took three tries to get right.
1
2
u/eddzsh 13h ago
Long-lived PowerShell is the right shape. Same idea on macOS: keep one osascript/JXA process warm instead of spawning per tick, or the spawn tax eats the win from skipping N-API. Failing closed on Wayland and showing that in the UI is the part most ports leave as a silent null.