WinPin is a native macOS AppKit menu bar app for best-effort per-window pinning.
The app tracks a specific Accessibility window, repeatedly raises that window with kAXRaiseAction, and draws a WinPin-owned yellow border overlay around it. macOS does not provide a public API to make another app's existing window truly always-on-top, so this behavior is intentionally best effort.
Implemented:
- Menu bar app with a configurable Dock icon
- Accessibility permission check and menu diagnostics
- Pin/unpin current focused window from the menu
- Unpin pinned windows from the menu list
- Fixed global shortcut:
Control + Option + Command + T - Timer-based
kAXRaiseActionmaintenance loop - Yellow non-interactive
NSPanelborder overlay - Pinned window list with drag handles, app icons, app names, window titles, and unpin buttons
- Stale window cleanup
- Multiple pinned windows use menu order: higher rows are raised above lower rows
- Settings window for toggling the Dock icon
- Unit tests for pin overlay creation, transient stale handling, stale removal, and menu-order raise behavior
Not implemented yet:
- Shortcut recorder UI
- User-configurable shortcut persistence
WinPin.xcodeproj/ Xcode project
WinPin/
main.swift AppKit entry point
AppDelegate.swift App lifecycle and manager wiring
MenuBarController.swift Status item and menu
AccessibilityPermissionManager.swift
AXWindowProvider.swift Focused window lookup and AX raise
PinManager.swift Pin state and raise loop
BorderOverlayManager.swift Yellow overlay panels
HotKeyManager.swift Fixed Carbon global hotkey
Models.swift Shared window models
Info.plist Standard AppKit app metadata
plan.md Implementation brief and progress
From the repository root:
xcodebuild -project WinPin.xcodeproj -scheme WinPin -configuration Debug -derivedDataPath build/DerivedData buildThe explicit -derivedDataPath build/DerivedData keeps generated Xcode output inside the repo-local ignored build/ directory.
xcodebuild -project WinPin.xcodeproj -scheme WinPin -configuration Debug -derivedDataPath build/DerivedData -destination platform=macOS testThe test suite covers the core pin state logic without touching real Accessibility windows.
After building, launch:
open build/DerivedData/Build/Products/Debug/WinPin.appWinPin is a menu bar app and does not show a Dock icon by default. The app hides the Dock icon at runtime with .accessory activation policy, rather than using LSUIElement, so the Dock icon can be toggled later from Settings. Look for the WinPin text status item in the menu bar.
For recovery, launch with a temporary Dock icon:
scripts/restart.sh --show-dockYou can also hold Option while launching the app. In recovery mode, WinPin appears as a regular app so the Dock menu can expose Show Menu Bar Item and Quit WinPin.
Open Settings with Command + ,, the Dock menu, or the WinPin menu bar item.
Settings currently includes:
Show Dock icon: persists whether WinPin should launch as a regular Dock/Cmd+Tab app or as a menu bar accessory app.
WinPin needs Accessibility permission to read and raise other apps' windows.
If permission is missing, the menu disables pinning and exposes one action to request Accessibility permission. After granting permission in System Settings, relaunching the app is the most reliable way to confirm the new trust state during development.
To reset the development Accessibility grant for the WinPin bundle ID:
scripts/reset-accessibility.shTo build and then reset the grant in one step:
scripts/build-debug-reset-accessibility.sh- Do not use app-level activation as the primary pinning mechanism. Raising should stay focused on the target
AXUIElementwindow. - Pinning is not a true always-on-top flag. When the user opens or activates another window, macOS may keep that active window above the raised Accessibility window.
- Keep the overlay level centralized in
BorderOverlayManagerso it can be lowered if.screenSaverproves too aggressive. - Treat Spaces and fullscreen behavior as best effort.
- Shortcut conflict detection for the fixed shortcut is based on whether Carbon hotkey registration succeeds.
- The fixed shortcut installs both Carbon hotkey registration and an
NSEventkey monitor fallback because menu bar apps can be sensitive to Carbon hotkey delivery differences. Hotkey delivery is throttled to avoid double toggles when both paths fire. - If the fixed shortcut cannot be registered, the menu reports that another app or macOS may already be using it. WinPin must not claim to know the owning app unless there is reliable evidence.
- Future configurable shortcuts should validate candidates using the same registration path and keep the previous working shortcut on failure.
- Known conflict family: Hammerspoon ShiftIt defaults use
Control + Option + Commandwith arrows,1,2,3,4,M,F,Z,C,N,P,=, and-. Avoid these occupied combinations for WinPin defaults.