- Dumping all open Safari tabs to an Obsidian doc
- Adding 'hyper' (Ctrl-Opt-Cmd) keybinds to pop a new window for:
- Safari
- Finder
- Terminal / Ghostty
- VS Code
- Notes
- Editing Hammerspoon/AeroSpace/Sketchybar config
- Reloading Hammerspoon config
- Reloading Sketchybar
- Quitting all Dock apps except Finder
- Screen lock
- System sleep
- Opening front Finder folder in VS Code
- Opening front Safari URL on Archive.today
- Showing front Safari window tab count
- Showing front app bundle ID
- Posting notification about current Music track
- Controlling my Logi Litra light (various color temps/brightnesses)
- Starting/stopping a client work timer
- Tying it to AeroSpace for:
- Pushing a window to another monitor
- Performing a two-up window layout
- Swapping those two windows
- Closing all other workspace windows
- Gathering all windows to first workspace
- Ensuring some background apps stay running if they crash
- Prompting to unmount disk images if trashed
- Binding into Skim to jump to specific sections of spec PDFs using terse Markdown URLs- check the list of open Teams windows; if there's a non-standard one, assume I'm in a meeting and webhook to HomeAssistant to select the "active"[2] preset on my meeting light[0].
- download my work ical[1] and, if there's a pending meeting (<~15m), webhook-HASS for the "pending" present on the meeting light.
[0] Just a short strip of WS2812B connected to an ESP32 running WLED.
[1] Originally this was a simple HTTP to my shared link on outlook.com but then they started requiring authentication (because that's exactly what you want on a SHARED link, you gufftarts); had a look at the Azure SDK and ... bag of milky spanners that is; ended up having to import my work ical into Apple Calendar and then use the ical link for that in Hammerspoon. Oh how we laughed. Especially when I realised it only has about 40% of the actual meetings because somehow "my calendar" is actually 4 or 5 bastardised conglomerations of pain and the ical for "my calendar" is actually just for one of those. AND NOT THE USEFUL ONE EITHER.
[2] There's various - "camera" for "the one meeting I'm forced to have my camera on", "active" is "I probably have to talk", "passive" is "I'm not going to be talking", and "silent" for things like company presentations where it's just watching a boring Powerpoint over Teams.
I've done something similar, but using the webcam watcher to hook on the webcam being enabled for any reason -- that way when I have that one external meeting on Google Meet or whatever the light still works.
(I also found it useful to have Hammerspoon flip a virtual switch in, well, Hubitat for me, and then automation based on that virtual switch, rather than triggering the light directly. Lets me hang other things off of that virtual switch instead of putting it in Hammerspoon.)
I'd love to do this too. Would you mind sharing how you do it? Or is it trivially easy and not worth explaining? (I haven't looked too deeply into HS yet.)
- Brings in the date path components for the dumped-to folder
- Makes a hash of the URL for an Obsidian doc (each tab gets their own doc)
- Uses Chrome command line (--headless --disable-gpu --dump-dom) to save a snapshot of the page contents
- Uses it again with --screenshot to make a thumbnail
- Create an Obsidian doc from a template
- If it's a single tab dump, pass -o to the script, which opens it in Obsidian for review
Lastly, I use the relatively-new Bases feature in Obsidian to make a nice "cards" view of the docs with their thumbnails.I'm hoping to clean it up at some point and maybe release it, but it's one of those classic one-shot systems that just works for me for now.
You could combine both of those into "run Archivebox somewhere and pass the URLs into that" (which is what I do for "URLs I save to Instapaper" - they go to my Linkhut, Pinboard, my Archivebox, and once I've fixed my code, to archive.org as well.)
Also if I may ask, how do you like Obsidian? I had never heard of it until now. Seems like a competitor to the Notes feature of iOS/macOS, but with its own subscription for syncing independently of iCloud?
Obsidian is good! This use of Bases is really my only "proprietary" use of anything Obsidian-specific. The rest is a combo of personal reference, brainstorms, intricate client work specs or outlines, and the beginnings of a personal wiki. The keybinds are great, everything is in one big folder for now, and the fuzzy search makes it fast. For sync, I just have my vault in a folder that is part of my overall Syncthing, so all my computers can access it. On mobile (iPhone moving to Android, and iPad) it's just read-only for now; not using their sync or doing any writing into the system from mobile.
Somewhat relatedly, I just got Standard Notes going on all systems (Mac/Linux/iPhone/Android/iPad) which is good for reliable capture at all places for me right now. I'm not paying, so I don't have (Markdown or other) formatting like in Apple Notes yet.
const fetchTabsScript = `
tell application "Brave Browser"
set output to ""
repeat with w in windows
repeat with t in tabs of w
set output to output & (URL of t) & "|||" & (title of t) & "\n"
end repeat
end repeat
return output
end tell
`
func GetOpenTabs() ([]Tab, error) {
cmd := exec.Command("osascript", "-e", fetchTabsScript)
output, err := cmd.Output()
// ...
}I use it for one thing only, as a window manager, and for that purpose it has made MacOS eminently more usable for me.
Would you mind elaborating on your vision for v2? Was there a certain limitation in the previous architecture that you’re trying to avoid this time around? Was there something in particular that drew you to choosing JavaScript for this version?
Honestly, in 2026, I do not want to be maintaining a 100k line Objective C program.
So, my current experimentation with a v2 is to see how easily I can catch up with where v1 is, just using Swift and JavaScriptCore.
There are lots of things about the Lua APIs that I don't like, and I'm addressing some of those as I go, but I'm currently in a phase where I'm targeting parity with everything I need for my v1 config, at which point I can cut over to running v2 and then see how things are looking and what can be refined/reworked.
Presumably that'll be released in [checks calendar] 18 days?
I'd take a minor quibble with one of the Reddit commentators:
> "Lua is a language that was built for people that are not programmers, and Hammerspoon (or at least building it's extensions) is targeted specifically at programmers."
Hammerspoon[0] isn't targeted at programmers because it's abstracting hard things (interfacing with macOS system libraries, etc.) into easier ones (the Lua spoons) where accessibility to non-programmers is surely a goal.
[0] I'm excluding extensions because the included spoons cover many scenarios people would be interesting in using and, to be honest, building "extensions" to something as tricky as Hammerspoon would be beyond many programmers[1], never mind non-programmers.
[1] I'm reasonably experienced and pretty fast at the "huh? <-> search <-> experiment <-> kludge <-> test <-> passable code" cycle even with completely new technologies and I definitely wouldn't be keen on attempting a Hammerspoon extension.
Where JS massively wins here, to my mind, is that there is so much tooling available for it, to the point that I'm already not actually writing my v2 config in JS at all, I'm writing it in TypeScript and compiling it to JS.
> Where JS massively wins here, to my mind, is that there is so much tooling available for it
Yeah, I can see that being a big win for development and people already invested in the JS ecosystem.
(I'll stick with Hammerspoon v1 until it breaks and then figure something else out because it'll be a cold day in hell before I subject myself to JS/Node/Typescript again. The trauma runs deep and wide.)
> I'm already not actually writing my v2 config in JS at all, I'm writing it in TypeScript
Will give v2 a go with `lua2js`[0] transliteration and see if that's workable.
Yay! :D
>.. enjoying ..
:)
>Lua to JavaScript
:\
Well, I have been a long user of Hammerspoon, and Lua, so thanks for the great app, it made a difference for me for a long time .. would be happy to hear why, but don’t feel obliged, the switch to JS over Lua, but anyway, thanks again!
The more complex answer is that Hammerspoon is currently about 100k lines of Objective C, and none of us really want to work on it anymore when Swift is the much nicer place to be doing macOS development.
Technically we could slowly convert in-place from ObjC to Swift, but there will always be a need for "LuaSkin", the bridging code we've accumulated over the last 13+ years, and rewriting that in Swift would be significantly complicated.
JavaScript, however, is already bridged for us because WebKit needs it.
What will v2 enable??
Beyond my enjoyment/productivity on the developer side though, I think v2 will be a big boost to user enjoyment/productivity, mostly because they'll be able to get much nicer IDE integrations for their config file, and be able to do things like write their config in TypeScript.
I have a feeling that letting people do what Hammerspoon can do, but in a TypeScript environment that they're much more likely to know, than Lua, will be huge for the project.
Lua is a better JS.
/ducks
That means the core of Hammerspoon goes from being incredibly complicated, to really just a protocol conformance.
Things programmers believe. It's interesting how some knowledge fundamentally assumed by default. For whatever reasons, the notion is widespread. You're a programmer? Therefore you must know JS, SQL, Bash and Python. In practice, what I've found after decades working with various teams - most programmers have pretty inadequate knowledge of any of these things.
I can't even work on Mac without it. It let's you do stuff like "alt+spc a b" (apps -> browser) or "alt+spc m j/k" (media -> vol up/down), or edit just about any text of any app in your editor (Emacs atm) - with all the tools you have there - spellchecking, thesaurus, translation, LLMs, etc.
You can plug it to your favorite WM (I'm currently using Yabai) and do tons of other interesting things. Because it's all written in Fennel, one can develop things in a tight feedback loop with a connected REPL - e.g., I can ask Claude to inspect things in the running Slack app or Firefox and make interesting automations - all without ever leaving my editor.
if only keyboards came with built in buttons for adjusting the volume… oh wait. Unless of course you are suffering on a touch bar mac, then I completely understand.
alt+cmd (was a typo, I meant to say alt+space), which is configurable - I myself prefer using cmd+space. That opens the "main" modal, from where you can configure "conditional branching" - e.g. "m" - for "media", or "a" - for "apps", so with "alt+space m j/k" you can do volume up/down, while pressing h/l could be "previous/next song". Then, "alt+spc a b" activates the browser, and "alt+spc a t" - could be bind to activate "terminal", etc.
It only looks like you have to press more keys to achieve anything, in practice - you quickly develop muscle memory. Then switching between the apps, moving windows around and resizing them, controlling playback, etc. - it all gains incredible productivity without affecting the focus point. You don't need to keep moving your hand for the mouse, you don't need to memorize and deal with myriad of modifier-driven key combinations - you control precisely what you need, without ever having to contort your fingers to hold modifiers, without ever thinking "what should I bind this action to, all memoizable keys are already taken, I suppose I'll just bind it to this impossible combo with a key that has no semantic meaning for the thing..." With Spacehammer you can create mnemonically-handy actions e.g., "o f" for "Open in Finder", while in another context that may work as "Open in Firefox".
99% of my working day, my fingers are on or near alt/cmd/m/j/k (a nice easy position in the centre of the keyboard.)
They are not on or indeed anywhere even vaguely near fn+f10/f11/f12 (which are, in fact, diametrically opposite corners of the keyboard.)
-- resize based on ratios
function ratioResize(xr, yr, wr, hr)
return function ()
local win = hs.window.focusedWindow()
win:moveToUnit({x=xr,y=yr,w=wr,h=hr})
end
end
-- 4 corners, different sizes
hs.hotkey.bind({"cmd", "ctrl"}, "w", ratioResize(0, 0, 2/5, 2/3))
hs.hotkey.bind({"cmd", "ctrl"}, "e", ratioResize(2/5, 0, 3/5, 2/3))
hs.hotkey.bind({"cmd", "ctrl"}, "s", ratioResize(0, 2/3, 2/5, 1/3))
hs.hotkey.bind({"cmd", "ctrl"}, "d", ratioResize(2/5, 2/3, 3/5, 1/3))
And to throw windows to other monitors: -- send to next screen
hs.hotkey.bind({"cmd", "ctrl"}, ";", function()
local win = hs.window.focusedWindow()
local screen = win:screen()
local next_screen = screen:next()
win:moveToScreen(next_screen)
end)- the switch goes through, Left displays workspace 1, right displays 3 (desired state)
- Application B is focused, presumably because its window on 3 becomes active (also desired)
- Display Left switches to display workspace 2, presumably because it contains a window belonging to the newly focused application B? (I don't want this)
- the window of application B on workspace 2 steals focus from the one on workspace 3 (???)
Charlie's paternal grandfather Reginald married twice—first to Mildred, mother of Charlie's father Arthur and his siblings Beatrice (a nun with spiritual godchildren) and Cecil (whose widow Dorothy married Charlie's maternal uncle Edward). What is the name of Charlie's goddaughter?
local mode = hs.screen.primaryScreen():currentMode()
local mods = {"ctrl", "alt", "cmd"} -- mash those keys
-- regular app windows
do
local w = 1094 -- no clip on GitHub, HN
local h = 1122 -- tallish
local x_1 = 0 -- left edge
local x_2 = math.max(0, (mode.w - w - w) / 2) -- left middle
local x_3 = (mode.w - w) / 2 -- middle
local x_4 = math.min(mode.w - w, x_2 + w + 1) -- right middle
local x_5 = mode.w - w -- right edge
local y = 23 -- top of screen below menu bar
hs.hotkey.bind(mods, "2", function() move_win( 0, y, mode.w, mode.h) end) -- max
hs.hotkey.bind(mods, "3", function() move_win(x_1, y, w, h) end)
hs.hotkey.bind(mods, "4", function() move_win(x_2, y, w, h) end)
hs.hotkey.bind(mods, "5", function() move_win(x_3, y, w, h) end)
hs.hotkey.bind(mods, "6", function() move_win(x_4, y, w, h) end)
hs.hotkey.bind(mods, "7", function() move_win(x_5, y, w, h) end)
end
function move_win(x, y, w, h)
hs.window.focusedWindow():setFrame(hs.geometry.rect(x, y, w, h))
endI use ShiftIt (a lovely project, but dead) reimplemented in Hammerspoon. It is very comprehensive.
It's nice to be able to iterate through the halves/thirds configurations for different cases.
The mail program has a folder tree on the left, the list of messages in the center, and the current message on the right. The IDE has all these tool windows that need showing, in addition to the actual editor. Websites also like it if the window size is a bit more.
Back when I was using Emacs and xterm, mainly, it was nice to show Emacs in the left half and then two xterms on the right.
So instead of tiling, I've come to realize that I only need a couple of window positions and sizes: Mail program and IDE are full screen. The browser occupies 70% width and height, in the top right corner, and the terminal is in the bottom left corner, 200 columns by 44 rows or so. (Lazygit works better if the terminal is a bit larger.) The chat program is full height, 60% width, left edge.
In this way, while the IDE is building or running tests, I can summon the web browser and still see at the bottom and on the left what is the progress of build or test. Also, when I use the software through the browser, I can see a couple of lines of log messages, which is enough to tell me whether to switch.
So I'm now happy with hotkeys in Hammerspoon that reposition and resize the current window to one of these presets, and to jump to a specific app with a keypress. I use a modal for this.
I dig the idea of having multi-level modals, somehow this idea never occurred to me.
hs.hotkey.bind({"ctrl"}, "D", function()
hs.grid.show()
end)
i've tried all of the other fancy window managers and for me nothing has ever beat the ease of use of just(1) ctrl-d to see the grid, (2) type the letter where you want the top left corner of your window to be, (3) type the letter where you want the bottom right corner to be
window resized
Just messing around I found you can extend the grid size with `hs.grid.setGrid('4x4')`, which you also may then want to shrink the text size with `hs.grid.ui.textSize = 30`, and finally if you use an alternative keyboard layout (eg: Colemak), you can set the grid to use it with `hs.grid.HINTS`. They really thought of everything with this feature.
Thinking of the usecase where every task or a project deserves a certain arrangement of windows and it would be good to summon them into existence as and when needed?
-- Hide Zoom's "share" windows so it doesn't come back on ESC keypress
local zoomWindow = nil
local originalFrame = nil
hs.hotkey.bind({"cmd", "ctrl", "alt"}, "H", function()
print("> trying to hide zoom")
if not zoomWindow then
print("> looking for window")
zoomWindow = hs.window.find("zoom share statusbar window")
end
if zoomWindow then
print("> found window")
if originalFrame then
print("> restoring")
zoomWindow:setFrame(originalFrame)
originalFrame = nil
zoomWindow = nil
else
print("> hiding")
originalFrame = zoomWindow:frame()
local screen = zoomWindow:screen()
local frame = zoomWindow:frame()
frame.x = screen:frame().w + 99000
frame.y = screen:frame().h + 99000
zoomWindow:setFrame(frame)
end
else
print("> window not found")
end
end)- Vim mode everywhere in macOS: https://github.com/dbalatero/VimMode.spoon
- Modifier keys + click/drag to resize or move windows: https://github.com/dbalatero/SkyRocket.spoon
- Show an overlay helper of all your keybinds when you hold modifier keys down: https://github.com/dbalatero/HyperKey.spoon
And my huge pile of random scripts/configs: https://github.com/dbalatero/nixpkgs/tree/main/home/modules/...
local usbWatcher = hs.usb.watcher.new(function(device)
if device.productName == "EMEET SmartCam C960" then
if device.eventType == "added" then
hs.execute("networksetup -setairportpower en0 off")
hs.notify.new({title="Wi-Fi", informativeText="Disabled (USB device connected)"}):send()
elseif device.eventType == "removed" then
hs.execute("networksetup -setairportpower en0 on")
hs.notify.new({title="Wi-Fi", informativeText="Re-enabled (USB device removed)"}):send()
end
end
end)
usbWatcher:start()https://support.apple.com/en-ca/guide/mac-help/mchlp2711/mac
Some previous discussion:
Then I just figured out that I have Hammerspoon, it can control windows -> recreate one exactly how I like it. Been using it for a year now and it's 99% perfect. Some specific applications (coughFirefoxcough) sometimes get into a weird state that doesn't work, but I can live with that.
It can also pop all windows to a specific layout with a single shortcut by combining the active wifi + monitor setup to detect if I'm at home, at work, or working at home.
Hammerspoon seems like a superset and it’s probably better to just have one, instead of two tools warring about who gets the keypresses?
I was hoping I could be lazy and ask, and a not-lazy person could give a ready made answer :)
It has several features from Aerospace, but Hammerspoon's window management performance is not nearly as good as Aerospace's (not surprising!).
Overall, I've found it easier to just fork Aerospace and add various extra features to it, so that's what I'm doing now.
I am writing a window manager bundled with other knick-knacks for myself. I have a "solution" for moving windows between spaces, but in the most vile way possible.
The only way I have managed to move windows between spaces is by, and this is no joke, recording the mouse position, moving the mouse to an app's titlebar, automating the 'click and hold' on the window's titlebar, then having the keybindings for "Switch to Next/Previous Space" fire off, and then moving the mouse back to the original position.
Because of the animations, all of junk requires carefully timed, short sleeps, which are also not likely consistent across various hardware/OS versions (can't test it myself).
Also, I have no idea what happens if my solution is tried on apps with pop-up windows, 'headless' apps (no title bar), electron apps, etc..
Apple's support for spaces is notoriously atrocious. There is no clean way to move windows from one space to another or to create/delete spaces. Though there was a built in way in OSX Snow Leopard, IIRC. Why it was removed? I have no idea.
Aerospace creates its own virtual desktops/spaces instead of trying to fight against the OS. I have never used Aerospace, so I cannot comment on its efficacy. But that is probably the cleanest solution we currently have available.
Aerospace is pretty cool, i recommend it, but I have not really worked out how full screen interacts with spaces. It’s a mess with and without aerospace.
local appHotkeys = {}
local function remapAppHotkey(appName, fromMods, fromKey, toMods, toKey, delay)
if not appHotkeys[appName] then
appHotkeys[appName] = {}
end
local hotkey = hs.hotkey.new(fromMods, fromKey, function()
hs.eventtap.keyStroke(toMods, toKey, delay or 0)
end)
table.insert(appHotkeys[appName], hotkey)
end
local appWatcher = hs.application.watcher.new(function(appName, eventType)
local hotkeys = appHotkeys[appName]
if not hotkeys then return end
for _, hotkey in ipairs(hotkeys) do
if eventType == hs.application.watcher.activated then
hotkey:enable()
elseif eventType == hs.application.watcher.deactivated then
hotkey:disable()
end
end
end)
appWatcher:start()
-- Remap app hotkeys
remapAppHotkey("Finder", { "cmd" }, "q", { "cmd" }, "w", 0.5)
... etc ...I’ll have a hell of a time rewriting everything into Lua when I have soooo many node packages I leverage.
It's fun to combine with qmk [0], which gives you a bunch more options for hotkeys on your keyboard via layers. I've ended up with a layer where half the keyboard is Hammerspoon shortcuts directly to apps (e.g. go to Slack, to Chrome, etc.) and half of it is in-app shortcuts (like putting cmd-number on the home row, for directly addressing chrome tabs).
Between this and one of the tiling window manager-adjacent tools (I use Sizeup), I can do all my OS-level navigation directly. "Oh I want to go to Slack and go to this DM" is a few keystrokes away, and not dependent on what else I was doing.
[0] https://qmk.fm/
It uses a swipe gesture detection spoon I found after searching for something similar[1].
Can't live without AutoHotkey on Windows.
Thanks to everyone who contributed to both!
otherwise I'm slowly working on a Spoon that figures out if there is an active meeting in Zoom, Teams, Huddle, Google Meet and will allow for muting, video enable/disable and screen sharing etc
Yabai supports this perfectly (especially combined with instant, animation-free space switching) but it requires disabling system integrity protection--which is a non-starter on a work computer.
Aerospace solves it with their own spaces implementation.
I was able to put together a hammerspoon script that does the job decently enough for my purposes: https://gist.github.com/kcrwfrd/6f3dcaec0e08e0e77b2884588a34...
But modern MacOS is automatable using Javascript in Script Editor, so they're catching up.
cmd+, and it transcribes on release.
It's lua, so you can get creative with https://fennel-lang.org/
I'll be sad when it moves from Lua to JavaScript, but I guess that's better than moving to Tcl.
Will be using it for more automation tools moving forward.
hs.loadSpoon("MicMute")
binding = { toggle = { {"ctrl", "alt"}, "m" } }
spoon.MicMute:bindHotkeys(binding)
```
You'll have to add the MicMute spoon which just mean downloading the zip here, unzipping, and opening the .spoon. https://www.hammerspoon.org/Spoons/MicMute.html
I think GP is asking about a global (from any application) mute/unmute teams Mic. I have wished for one for ever.
Is completely muting your mic sufficient? If so, I have an Applescript solution that seems to work if you want it. I tested it in VoiceMemos and it worked even if I was in a different app in a different space. You can bind the script to a global hotkey very easily via many different apps like Alfred, Karabiner, etc..
AppleScript: https://pastebin.com/xHE1uQym
Still wouldn't work without it though (I run Niri at home)
KeyboardMaestro
Automator and AppleScript
Raycast