A small Hammerspoon native module that focuses a window without raising it
(X11-style "sloppy focus" on macOS). Designed to be called from the
FocusFollowsMouse spoon's _maybeFocus
hook, but usable from any Lua code that has an hs.window.
AutoRaise does the same thing as a
standalone app, and that's the simpler option for most people. It has one
limitation though: it doesn't reliably focus Chromium-based PWAs (Brave/Chrome
"Install as App" windows). That's the gap this module fills — hs.window
identifies PWAs correctly, so passing its pid and window id straight into the
same SkyLight calls AutoRaise uses gets us focus-without-raise that works on
native apps and PWAs.
Uses three private macOS APIs (resolved via dlopen/dlsym so a future
macOS that removes them just makes us degrade to a no-op):
_SLPSSetFrontProcessWithOptions(psn, wid, flags)— tells SkyLight to make a process front for a specific window. Passing the real window id (not 0) is what avoids the raise.SLPSPostEventRecordTo(psn, bytes)— used to post synthetic key-window events (the byte layout was lifted from yabai via AutoRaise).GetProcessForPIDfrom ApplicationServices — to get a PSN from a pid.
The full recipe is documented inline in internal.m; it's a verbatim
port of AutoRaise.mm lines 165-221
with the gating logic stripped out (we let hs.window decide what to
focus).
Each release ships a sloppyfocus-<version>-macos-universal.zip containing
a fat internal.so (arm64 + x86_64) plus init.lua, built against
-mmacosx-version-min=13.0. Pull the latest release artifact and unzip
straight into ~/.hammerspoon:
# 1. Download (replace <version> with whatever's current on the Releases page).
curl -L -o sloppyfocus.zip \
https://github.com/catokolas/HS_ModulesContrib-sloppyfocus/releases/latest/download/sloppyfocus-<version>-macos-universal.zip
# 2. macOS may quarantine a downloaded .so; clear the flag so dlopen accepts it.
xattr -dr com.apple.quarantine sloppyfocus.zip 2>/dev/null || true
# 3. Unzip into ~/.hammerspoon. The archive's top-level is hs/, so this
# lands at ~/.hammerspoon/hs/_ckol/sloppyfocus/.
unzip -o sloppyfocus.zip -d ~/.hammerspoon
# 4. Quit and relaunch Hammerspoon (Reload Config will NOT pick up a fresh .so).
# Then verify in the Console:
# require("hs._ckol.sloppyfocus")If dlopen still complains about the quarantine after step 2, repeat the
xattr after step 3 on the unpacked .so:
xattr -dr com.apple.quarantine ~/.hammerspoon/hs/_ckol/sloppyfocus.
cd sloppyfocus
make install # copies into ~/.hammerspoon/hs/_ckol/sloppyfocus/
# or for development:
make link # symlinks instead, so future `make` picks up automaticallyThen quit and relaunch Hammerspoon (Reload Config does not refresh native
modules — already-loaded .so files stay pinned in package.loaded).
To produce a release artifact (universal binary zip) yourself:
cd sloppyfocus
VERSION=0.1
make dist $VERSION # → dist/sloppyfocus-0.1-macos-universal.zipIf you have access — publish the artifact as a GitHub Release with the
gh CLI:
# From the repo root (the parent of the `sloppyfocus/` subdir):
gh release create v$VERSION \
sloppyfocus/dist/sloppyfocus-$VERSION-macos-universal.zip \
--title "v$VERSION" \
--notes "Initial release. Universal arm64 + x86_64 binary built against macOS 13.0+."This creates the git tag v0.1, drafts a release named "v0.1" on
GitHub, and attaches the zip as a downloadable asset. The
curl https://.../releases/latest/download/... URL in the
install-without-compiling section above resolves to whatever the most
recent release uploads.
local sloppy = require("hs._ckol.sloppyfocus")
-- Focus the window under the cursor without raising it:
local win = hs.window.windowsForApplication(app)[1]
sloppy.focusWithoutRaise(win)
-- When switching between two windows of the same app, also pass the
-- currently-focused window so SkyLight performs the extra deactivate/
-- activate dance it needs:
local current = hs.window.focusedWindow()
sloppy.focusWithoutRaise(target, current)This module emits no log output of its own and intentionally avoids
NSLog in internal.m — focus-without-raise runs on every mouseover
in the calling Spoon, so per-call logging would be very noisy. All
diagnostic output is the responsibility of the calling Spoon (e.g.
FocusFollowsMouse.spoon exposes a logger variable; see its README
for how to set the level).
For native-side debugging, build a debug copy and printf/NSLog
ad-hoc:
cd sloppyfocus
make clean && make DEBUG_CFLAGS="-g -O0"
# add NSLog(@"...") calls in internal.m, rebuild, quit & relaunch HS.
# Output lands in Console.app under the Hammerspoon process.Gives keyboard focus to win without changing its position in the Z-order.
win— anhs.windowcurrentlyFocused— optionalhs.windowcurrently holding focus. Required only when switching between two windows of the same app (e.g. two iTerm windows); if omitted, same-app focus changes may appear to no-op.- returns
trueon success,falseif the SkyLight symbols couldn't be resolved,GetProcessForPIDfailed, or SLPS returned an error.
- AutoRaise — the focus-without-raise recipe, including the same-process dance, is a direct port of its logic.
- yabai — the original
make_key_windowbyte layout came from there.
MIT — see LICENSE (sibling file at the repo root). Compatible
with the upstream projects this module derives from (AutoRaise and yabai are
both MIT).