Skip to content

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 raw os.chmod().
  • UI font sizing: FONT_UI / FONT_MONO constants 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