Platform abstraction¶
This page mirrors implementation notes maintained in CLAUDE.md.
Update both when changing this subsystem.
All platform-specific code lives under src/cue/platform/. The
main app imports from cue.platform (the facade) — never directly
from cue.platform.macos or cue.platform.windows (except for
platform-guarded blocks).
src/cue/platform/
├── __init__.py # facade — only this module is imported by callers
├── macos.py # rumps tray, CGEventTap, AppleScript, Carbon, AX
├── windows.py # pystray tray, pynput, UIA, SetWinEventHook
├── overlay_macos.py # NSPanel per NSScreen, NSFloatingWindowLevel
└── overlay_windows.py # Tk Toplevel per monitor, WS_EX_LAYERED|TRANSPARENT
Key platform differences¶
| Aspect | macOS | Windows |
|---|---|---|
| Menu bar | rumps.App (unicode title ◈) |
pystray.Icon (image from assets/icon.png) |
| Global hotkey | CGEventTap (Shift+Space) |
pynput.keyboard.GlobalHotKeys (Alt+`) |
| Screenshots | mss + Pillow (shared capture.py) |
same |
| Selected text | osascript Cmd+C + NSPasteboard |
win32clipboard Ctrl+C simulation |
| Popup | subprocess popup_window.py (customtkinter) |
same |
| Dialogs | rumps.alert() |
ctypes.windll.user32.MessageBoxW |
| File permissions | cue.fs.secure_dir() / secure_file() (POSIX chmod) |
no-op (Windows ACLs not set) |
| Build | PyInstaller / Nuitka → .app → DMG |
PyInstaller → .exe → Inno Setup |
| Frozen detection | getattr(sys, 'frozen', False) or __compiled__ (Nuitka) |
same |
| Subprocess console | not needed | _CREATE_NO_WINDOW = 0x08000000 |
| Privacy overlay | pyobjc NSWindow per NSScreen, NSFloatingWindowLevel, CanJoinAllSpaces \| Stationary, main-thread via AppHelper.callAfter |
Dedicated Tk thread, Toplevel per monitor, WS_EX_LAYERED \| WS_EX_TRANSPARENT \| WS_EX_NOACTIVATE \| WS_EX_TOOLWINDOW for click-through, per-monitor DPI awareness set at import |
| Secure-input detection | Carbon IsSecureEventInputEnabled (system-wide latch) |
UIA IsPasswordPropertyId on focused element + window-class fallback for "Credential Dialog Xaml Host". UIA work runs on a single ThreadPoolExecutor(max_workers=1) with 0.2 s timeout + cached last value. |
| Browser URL read | AppleScript via osascript per browser (Chrome / Safari / Arc / Brave / Vivaldi / Opera / Edge / Firefox / Orion / Zen) |
UIA address-bar traversal (Chrome / Edge / Brave / Vivaldi / Opera / Firefox / Librewolf) |
| Pause hotkey | Cmd+Shift+Space via CGEventTap (checked before bare Shift+Space so no double-fire) |
<cmd>+<shift>+<space> via pynput.GlobalHotKeys (Windows maps <cmd> to Win key) |
| Foreground-app watcher | NSWorkspaceDidActivateApplicationNotification |
SetWinEventHook(EVENT_SYSTEM_FOREGROUND, WINEVENT_OUTOFCONTEXT) with its own message-pump thread |
Discipline rules¶
- Every feature added to one platform module must have its counterpart in the other. The Cross-platform rule lists the full PR checklist.
- Platform-specific imports stay inside
if sys.platform == "darwin":(or"win32") guards — never at module top level unguarded. This is what lets the docs build on a Linux runner without[macos]/[windows]extras. - File-permissions code goes through
cue.fs.secure_dir()/secure_file(). Never call rawos.chmod(). - UI font sizing:
FONT_UI/FONT_MONOconstants in the platform modules abstract platform-default fonts. Popup UI changes must test on both platforms.
Streaming recorder special case¶
The streaming recorder runs the vendored ocap-{platform} CLI as
a subprocess. Any new CLI option must be supported by both
ocap repos. Submodules must stay in sync; run
git submodule update --init --recursive after pulling. See
Submodules & vendoring.
See also¶
cue.platform— facade module reference (deferred to a later commit; the implementation is insrc/cue/platform/__init__.py).- Cross-platform rule — PR checklist.
- Architecture overview.