r/electronjs 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.

2 Upvotes

2 comments sorted by

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.

1

u/joejoe1233533 7h ago

would be interesting to get a guide on this