How the Radial Menu Works as an Input Overlay in Vorssaint-utils
The radial menu in Vorssaint-utils functions as a transparent input overlay that intercepts mouse, keyboard, and system events through a dedicated NSPanel, presenting a glass-styled wheel at the cursor or screen center while swallowing all input until the user selects an action or dismisses the interface.
Vorssaint-utils implements a self-contained radial menu input overlay that appears instantly above all applications without requiring focus changes. The system captures low-level input events through Carbon hotkeys and Core Graphics event taps, ensuring that underlying applications never receive the triggering clicks or keystrokes while the menu is active. This architecture allows users to trigger actions via extra mouse buttons, keyboard shortcuts, or pointer movement without disrupting their current workflow.
Core Architecture of the Input Overlay
The overlay consists of three primary Swift components that manage the entire session lifecycle from activation to dismissal.
RadialMenuService (Session Controller)
Located in Sources/Vorssaint/Services/RadialMenu/RadialMenuService.swift, this singleton ObservableObject owns the overlay state machine. It registers global hotkeys via QuickToolHotkey, creates a transparent NSPanel through ensurePanel(), and manages the event tap lifecycle through installMonitors() and removeMonitors(). The service publishes state changes—such as highlightedIndex, visible, and the menu stack—that drive the SwiftUI interface.
RadialMenuView (Visual Rendering)
Defined in Sources/Vorssaint/UI/RadialMenu/RadialMenuView.swift, this view renders the glass-styled wheel, the sweeping RadialWedgeShape highlight, and the central hub. It reacts to RadialMenuService published properties and forwards user gestures back to the service via activatePointer() and stepBack() methods.
RadialMenuSettings (Configuration Interface)
Found in Sources/Vorssaint/UI/Settings/RadialMenuSettings.swift, this component manages user preferences stored in UserDefaults under keys like DefaultsKey.radialMenuProfiles. Changes persist immediately and trigger syncWithPreferences() in the service to update hotkey registrations and mouse-button triggers.
Initializing the Input Overlay Session
The radial menu input overlay begins its lifecycle when the user invokes it through one of two primary mechanisms.
Global Hotkey Activation
When the application launches, RadialMenuService.shared.syncWithPreferences() reads the stored shortcut configuration and registers a QuickToolHotkey. Upon triggering, the hotkey callback invokes beginSession(), which constructs the transparent panel and positions the wheel either at the mouse pointer (if DefaultsKey.radialMenuAtPointer is true) or at the screen center. The wheel initially renders at a minimal size before animating open.
Extra Mouse Button Capture
For users who prefer mouse-driven activation, the service creates a low-level event tap using CGEvent.tapCreate that monitors otherMouseDown and otherMouseUp events. When the configured extra button presses, handleMouseTap immediately calls beginSession() and swallows the click by returning nil from the callback, ensuring the underlying application never receives the button press.
Input Interception and Event Handling
Once active, the overlay must capture all input types without passing them through to background applications. The system installs temporary, session-scoped monitors that exist only while the wheel is visible.
Swallowing Extra Mouse Buttons
The mouse event tap installed in RadialMenuService runs at the CGEvent.tapCreate level with kCGEventTapOptionCGSession scope. When handleMouseTap detects the configured button, it returns nil to the event stream, effectively consuming the input. The tap also supports isReportingMouseButtons mode for the Settings UI, allowing users to configure which button triggers the menu.
Tracking Pointer Movement
Local and global monitors for mouseMoved and leftMouseDragged events feed into pointerMoved(), which calls refreshHighlight(). This method uses RadialMenuGeometry.highlightedIndex to calculate which pie slice sits beneath the cursor, updating the highlightedIndex published property that drives the wedge animation in RadialMenuView.syncWedge.
Keyboard Navigation Capture
A local monitor added via addLocalMonitorForEvents(matching: .keyDown) captures all keystrokes while the panel is key. The handleKeyDown method implements vim-style navigation: arrow keys invoke rotateHighlight(), digits 1-9 call select(index) directly, Return activates the current selection, and Esc triggers stepBack() to navigate submenus.
Modifier Flag Monitoring
For users operating in hold mode (keeping modifiers pressed to maintain the menu), a flagsChanged monitor watches for the release of shortcut modifiers. When the trigger keys lift, handleFlagsChanged() invokes endHoldPhase(), automatically executing the highlighted action without requiring an explicit click.
Selection Logic and Action Execution
When the user commits to an action, the overlay must determine what was selected and execute it cleanly.
Geometry-Based Selection
The activatePointer() method distinguishes between three zones: clicks outside the wheel boundary dismiss the session via endSession(), clicks in the central dead-zone trigger stepBack() for submenu navigation, and clicks over a highlighted slice invoke select(index).
Dispatching Actions
The run(_:) method in RadialMenuService dispatches based on RadialMenuItem.kind:
- Apps, Files, URLs: Opened via
NSWorkspace.shared.open(). - Keyboard Shortcuts: Posted as synthetic
CGEventsequences throughpostWhenModifiersReleased(), ensuring held modifiers from the trigger don't interfere with the shortcut. - Media Keys: Dispatched as system-defined events via
postMediaKey(). - Tools and Toggles: Invoke internal Vorssaint services (screenshot, window layout) after a short delay to allow the overlay to fade.
All actions execute only after PanelDismissal completes the fade-out animation, guaranteeing the overlay has fully disappeared before sending events to the system.
Permissions and System Integration
The input overlay requires specific macOS permissions to function correctly. The ensureAccessibilityPermission() method checks for and requests accessibility access via Permissions.shared.requestAccessibility(). Without this permission, the mouse event tap cannot intercept extra buttons and the service emits a system beep to alert the user.
The overlay respects system accessibility preferences: it disables animations when the user enables Reduce Motion and adjusts transparency settings based on the Reduce Transparency flag.
Implementation Examples
Below are practical code snippets demonstrating common integration patterns for the radial menu input overlay.
Trigger via Global Shortcut:
// Register the stored hotkey at application launch
RadialMenuService.shared.syncWithPreferences()
// When pressed, the internal hotkeyPressed(for:) method
// automatically invokes beginSession()
Enable and Capture Extra Mouse Buttons:
// Enable mouse button trigger in preferences
UserDefaults.standard.set(true, forKey: DefaultsKey.radialMenuEnabled)
// Temporarily enable button reporting for configuration UI
RadialMenuService.shared.setReportingMouseButtons(true)
// The service now creates the event tap that swallows clicks
// via handleMouseTap -> beginSession
Present a Preview from Settings:
// Decode the stored profile and present non-interactive preview
if let profile = RadialMenuSupport.decodeProfiles(
UserDefaults.standard.data(forKey: DefaultsKey.radialMenuProfiles),
defaults: .standard
).first {
RadialMenuService.shared.presentPreview(for: profile)
}
Summary
- RadialMenuService (
RadialMenuService.swift) orchestrates the input overlay lifecycle, managing global hotkeys, event taps, and session state throughbeginSession()andendSession(). - The overlay captures input through temporary monitors that swallow events (returning
nilfor mouse taps, intercepting keyDown) to prevent underlying applications from receiving trigger inputs. - RadialMenuView renders the glass-styled wheel and responds to geometry calculations from
RadialMenuGeometry.highlightedIndexto provide real-time visual feedback. - Actions dispatch through
run(_:)only after the panel dismisses, usingpostWhenModifiersReleased()for shortcuts to avoid modifier interference. - The system requires Accessibility permissions to create the low-level event tap needed for extra mouse button capture.
Frequently Asked Questions
How does Vorssaint-utils prevent the underlying app from receiving the triggering mouse click?
The service creates a low-level Core Graphics event tap in RadialMenuService.swift that monitors otherMouseDown events. When the configured extra button presses, the handleMouseTap callback returns nil to the event stream, effectively consuming the click before it reaches the underlying application.
Can the radial menu appear at the current mouse position instead of screen center?
Yes. When beginSession() creates the overlay, it checks DefaultsKey.radialMenuAtPointer. If true, it positions the NSPanel at the current cursor coordinates; otherwise, it centers the wheel on the active screen.
What happens to keyboard shortcuts that use modifiers if I'm holding keys to keep the menu open?
The handleFlagsChanged() method monitors modifier key states. When you release the shortcut modifiers that initially triggered the menu in hold mode, it automatically invokes endHoldPhase(), which releases the modifiers before posting the final shortcut through postWhenModifiersReleased(), preventing stuck keys or conflicting inputs.
How does the overlay handle file and application icons without blocking the main thread?
RadialMenuIconStore.swift maintains a cache of file and custom icons loaded asynchronously. When RadialMenuView renders slices, it reads from this cache rather than accessing the file system directly, ensuring that pointer movement and highlight updates remain fluid even when displaying complex folder contents.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →