~/.config/heed/heed.conf

v0.12.3 github

# heed

# Focus follows mouse and directional focus for macOS, after Hyprland's follow_mouse and movefocus.

# Heed moves keyboard focus to the window under the pointer. It runs in the background with no Dock icon or main window; a menu bar icon and a global hotkey turn it on and off. macOS 14 or later, Apple Silicon.

# source releases raycast extension rbstp.dev

install {

$brew install --cask rbstp/tap/heed

# Or from source. make cert once, and only without a Developer ID; it keeps the Accessibility grant across rebuilds.

$make cert && make install# build, sign, install, start

# On first launch, grant Heed access in System Settings › Privacy & Security › Accessibility. It picks the grant up without a restart. Every release is signed with a Developer ID, notarized, and carries a build provenance attestation next to it:

$gh attestation verify Heed-0.12.3.zip --repo rbstp/heed

}

binds {

# Heed registers hotkeys exclusively: a combination it claims is gone from every other app. These work whether or not focus following is on.

$mod=CTRL CMD# Shortcut Modifier in the menu changes every bind at once
bind=$mod, H, toggle# focus follows mouse on or off, from anywhere
bind=$mod, right, movefocus, next# next visible window, screen by screen from left to right, then left to right within each screen
bind=$mod, left, movefocus, prev# spatial order, not stacking order: stepping through does not reorder the cycle
bind=$mod, 1..9, movefocus, window# the window with that number, counted the same way; a number with no window does nothing
bind=$mod (held), , shownumbers# each window numbered on screen after 100 ms, so the digit is read rather than counted

# Directional focus, off by default because every bind is taken from every other app. WASD rather than HJKL: the toggle is already on H. A window sharing a row or column with the focused one wins over a closer one that does not, and the edge is a dead end rather than a wrap.

bind=$mod ALT, A, movefocus, l# defaults write io.github.rbstp.heed focusLeftHotkey 'cmd+ctrl+alt+a'
bind=$mod ALT, S, movefocus, d# focusDownHotkey 'cmd+ctrl+alt+s'
bind=$mod ALT, W, movefocus, u# focusUpHotkey 'cmd+ctrl+alt+w'
bind=$mod ALT, D, movefocus, r# focusRightHotkey 'cmd+ctrl+alt+d'

# A hotkey needs at least one modifier other than Shift. If another app already registered it, Heed logs the refusal and registers nothing. An empty string disables a bind. The menu offers Command-Option with a warning and does not offer Command-Shift at all; defaults write accepts either.

$defaults write io.github.rbstp.heed hotkey 'cmd+ctrl+alt+f' && make restart

}

warp {

# Mouse follows focus. Focus moved by keyboard leaves the pointer over the window you just left, and the next flick of the mouse drags focus back there. Turn it around and the pointer follows focus into the new window: on Command-Tab, on the focus binds, on a window picked from Raycast's Switch Windows, and on any other keyboard-driven activation. Not for a click, mid-drag, into a window Heed would not focus by pointer, or when the pointer is already inside.

warpPointer=false# Move the pointer into a window that took keyboard focus. Needs handoverGuard on.
warpX=50# Where in that window the pointer lands, as a percentage across.
warpY=50# Where in that window the pointer lands, as a percentage down.
$defaults write io.github.rbstp.heed warpPointer -bool true && make restart

}

remote {

# Drive it from somewhere else. A URL reaches the running agent directly, and the same vocabulary works as flags on the installed binary, so Heed can be driven without registering anything.

$open 'heed://focus/next'# also prev, left, right, up, down
$open 'heed://focus/3'# numbers past 9 work here; heed://focus/id/<n> takes the window server's own number
$open 'heed://toggle'# also enable, disable
$~/Applications/Heed.app/Contents/MacOS/Heed --focus next
$~/Applications/Heed.app/Contents/MacOS/Heed --windows# the focus ring as JSON, whether or not Heed is running

# With Raycast. Raycast already tiles and switches windows; Heed adds the pointer. The extension has a command for each of these, so the hotkey is Raycast's and Heed claims nothing. Or a Quicklink to heed://focus/next with a hotkey, then free the combination Heed holds:

$defaults write io.github.rbstp.heed focusNextHotkey '' && make restart

# The flags reach the agent over a distributed notification, which any process in your login session can post and read. Heed holds Accessibility permission, so anything in your session can switch it off or move focus around while it runs. The vocabulary is fixed at toggle-and-focus: no arbitrary arguments, no window contents, nothing read back. If that is not a trade you want, leave the commands alone and use the binds; nothing else in Heed listens.

}

# Settings live in the io.github.rbstp.heed defaults domain, flat: defaults write io.github.rbstp.heed <key> <value>, then restart Heed. The blocks below group them for reading; the values are the defaults.

general {

enabled=true# Turn focus following on or off.
menuBarIcon=true# Show the menu bar icon. Click toggles; right-click for the modifier, numbers, log, version, quit.
raise=true# Raise the selected window within its app. macOS does not separate focus from raising across apps.
verbose=false# Log each focus decision.
hotkey=cmd+ctrl+h# Global toggle. Empty string disables it.
focusNextHotkey=cmd+ctrl+right# Move focus to the next window.
focusPreviousHotkey=cmd+ctrl+left# Move focus to the previous window.
focusWindowHotkey=cmd+ctrl+1# Window 1; the same modifiers with 2 to 9 reach the others.
focusLeftHotkey=off# Nearest window to the left.
focusRightHotkey=off# Nearest window to the right.
focusUpHotkey=off# Nearest window above.
focusDownHotkey=off# Nearest window below.
windowNumbers=true# Number the windows on screen while the numbered shortcuts' modifier is held.
windowNumbersDelayMs=100# How long that modifier must be held first.

}

pointer {

dwellMs=0# Time the pointer must rest before focus changes. Try 200 if instant is too eager.
pollMs=40# Pointer sampling interval while active.
idlePollMs=1000# Heartbeat while idle. Mouse movement wakes the fast loop.
entryMotionPx=6# Travel required before a different window may take focus. 0 disables the guard.
verifyTimeoutMs=100# Time allowed to confirm a focus change before retrying.
requireStandardWindow=true# Only focus ordinary AXStandardWindow windows. Floating panels are not targets.

}

guards {

# Heed follows the pointer but avoids the common focus fights. A window that appears under a still pointer does not take focus; dialogs and prompts keep theirs; focus that arrived by shortcut, launch, menu or Command-Tab stays put until the pointer rests on another window, and crossing the menu bar, Dock or empty space on the way does not release it. Pointer focus is also suppressed while the window numbers are up.

typingCooldownMs=500# Ignore pointer focus after a keystroke.
clickGraceMs=150# Ignore pointer focus after a mouse press or release.
ignoreWhenCommandHeld=true# Suppress pointer focus while Command is held.
menuGuard=true# Suppress pointer focus while menus, popovers or drag images are visible.
handoverGuard=true# Keep focus that arrived without pointer movement.
handoverSettleMs=300# Rest time on another window before releasing held focus.
promptGuard=true# Keep focus on a prompt until it is answered.

}

exclude {

# Heed always excludes itself, Dock, WindowServer, loginwindow, Control Center, Notification Center, SystemUIServer, the screenshot UI, Spotlight, Raycast and AltTab.

excludedWindowTitles=[]# Case-insensitive regular expressions for window titles to skip.
excludedBundleIDs=[]# Extra application bundle IDs to skip.
$defaults write io.github.rbstp.heed excludedWindowTitles -array '^Picture in Picture$'
$defaults write io.github.rbstp.heed excludedBundleIDs -array com.example.Overlay

}

troubleshooting {

# See what Heed sees under the pointer, or at a point, and log each decision. The log is append-only; make logs-clear truncates it.

$make probe# or make probe X=960 Y=540
$defaults write io.github.rbstp.heed verbose -bool true && make restart && make logs

# Accessibility stopped working after an upgrade. Releases from 0.12.0 on are signed with a Developer ID and notarized, so the grant carries across versions. Upgrading from an older, ad-hoc signed build is the one exception: that grant was tied to the old binary and cannot transfer. If Heed stays dimmed:

$tccutil reset Accessibility io.github.rbstp.heed

}

# Limitations. Focusing another application raises it. Apps with incomplete Accessibility support may only work at application level: some games, XQuartz and Java applications expose no individual windows, so the pointer focuses them as a whole and the focus binds skip them. Stage Manager may override window ordering.

# Prior art. AutoRaise does the same in C++. yabai and AeroSpace include related features in larger window managers.