[HN Gopher] Hammerspoon
       ___________________________________________________________________
        
       Hammerspoon
        
       Author : tosh
       Score  : 161 points
       Date   : 2026-03-13 18:34 UTC (4 hours ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | golem14 wrote:
       | Has anyone worked on making a config replicating aerospace?
       | 
       | Hammerspoon seems like a superset and it's probably better to
       | just have one, instead of two tools warring about who gets the
       | keypresses?
        
         | hirvi74 wrote:
         | What features are you trying to replicate from Aerospace?
        
           | golem14 wrote:
           | Well, a tiling window and workspace manager. But as I am
           | typing this, I'm realizing they hammerspoon can probably do
           | some of the window placement, but maybe not handling
           | workspaces and global state.
           | 
           | I was hoping I could be lazy and ask, and a not-lazy person
           | could give a ready made answer :)
        
             | hirvi74 wrote:
             | If workspace management is an another term for managing
             | desktop/spaces, then you are going to be hard pressed to
             | find anything that is not a brittle hack.
             | 
             | 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.
        
               | golem14 wrote:
               | Sadly, I think you're right.
               | 
               | 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.
        
             | r5Khe wrote:
             | It can definitely handle virtual workspaces and global
             | state (if I'm understanding what you mean). I have an
             | Aerospace-like implementation here:
             | https://github.com/mybuddymichael/Helm.spoon
             | 
             | 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.
        
       | pjm331 wrote:
       | here is my entire config
       | 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
        
         | stackghost wrote:
         | Not that I insert EOFs very often, but does that conflict with
         | CTRL+D in the terminal?
        
           | xyzzy_plugh wrote:
           | I use EOF all the time to end terminal sessions.
        
             | commandertso wrote:
             | Great handle, btw.
        
           | pjm331 wrote:
           | yeah the CTRL+D definitely gives me problems from time to
           | time but thus far i have been too lazy to fix it
        
           | theshrike79 wrote:
           | Make caps lock a hyperkey (shift+ctrl+option+cmd) and use
           | that for non-overlapping shortcuts
        
             | 1-more wrote:
             | make caps lock control on hold, double quote on tap. Make
             | control hyperkey on hold, angle bracket on tap. My keyboard
             | firmware is very odd. This is not easily done with soft
             | remaps to the point that I don't bother trying.
        
         | elAhmo wrote:
         | This is amazing! I have a slightly more elaborate setup that
         | allows me to resize from one or another side, similar to what
         | Apple added recently but with more flexibility, but this is
         | super interesting, thanks for sharing!
        
         | hrmtst93837 wrote:
         | Neat until you need to sync configs or keep multiple machines
         | in harmony, at which point dotfile headaches stack up with
         | Hammerspoon and Lua. Adding complex logic like window rules,
         | app-specific behavior, or handling monitor changes strips away
         | some of that hotkey simplicity and leads to endless tweaking.
         | Still, for avoiding the mouse, it's one of the few flexible
         | options left on macOS that doesn't feel ancient. Tradeoffs
         | everywhere but nowhere else really compares in control.
        
           | dbalatero wrote:
           | Syncing configs is a pretty solved problem with dotfile
           | repos. I even made a starter repo anyone can fork & use:
           | https://github.com/dbalatero/dotfiles-starter
        
       | john-tells-all wrote:
       | I'd love to have a global "toggle Teams mute" button.
        
         | hirvi74 wrote:
         | What do you mean? Like muting the entire application so no
         | sound comes from Teams or muting yourself while on a call? For
         | the latter, I thought 'Option + Space' worked (or used to)?
        
         | roxolotl wrote:
         | ```
         | 
         | 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
        
       | jjmiv wrote:
       | is there a particular reason this was shared?
       | 
       | 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
        
         | al_borland wrote:
         | My guess is someone just found it. When I was moving to macOS
         | for my work laptop, I was looking for something to replace
         | AutoHotKey. It took me a long time and a lot of digging to
         | actually find HammerSpoon.
        
       | rolymath wrote:
       | Is paperwm jittery for everyone?
        
       | trjordan wrote:
       | I utterly love Hammerspoon.
       | 
       | 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/
        
         | henrebotha wrote:
         | My QMK Tmux "layer" is still one of my favourite
         | customisations. Prepends Ctrl-B to everything I type.
        
       | incanus77 wrote:
       | Hammerspoon is the glue that holds my Mac together. For a starter
       | list of things to do with this app, a partial list of the things
       | that I'm using it for:                 - 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
        
         | piskov wrote:
         | Keyboard stuff is better handled with Karabiner elements
        
           | dbalatero wrote:
           | Karabiner can _create_ new keys like hyper, but you _bind_
           | them with Hammerspoon.
        
         | joemi wrote:
         | > - Dumping all open Safari tabs to an Obsidian doc
         | 
         | 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.)
        
       | mwagstaff wrote:
       | Can't live without Hammerspoon on Mac.
       | 
       | Can't live without AutoHotkey on Windows.
       | 
       | Thanks to everyone who contributed to both!
        
       | hirvi74 wrote:
       | I have fond memories of this app. However, after many years, I
       | have moved on. I am in the process of writing my own replacement
       | for some of the various use cases that Hammerspoon once provided
       | me. Though, Hammerspoon will always be a source of great
       | inspiration.
        
       | weitzj wrote:
       | I love hammerspoon. That's it :D
       | 
       | It's lua, so you can get creative with https://fennel-lang.org/
        
       | hmokiguess wrote:
       | what's your favourite spoon?
        
       | zdw wrote:
       | I fake a tiling window manager on Mac with Hammerspoon, resizing
       | to fit in specific corners/sizes:                    -- 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)
        
         | comboy wrote:
         | I highly recommend Aerospace[1], went through a few approaches,
         | I cared about not completely compromising security either, it
         | works really well if you come from something like i3
         | 
         | 1. https://github.com/nikitabobko/AeroSpace
        
           | idle_zealot wrote:
           | Seconding this. I find MacOS unusable without it. I'll ask
           | here because websearch is failing me: is there a way to fix
           | the focus stealing that happens when you have multiple
           | windows of an application on different displays?
           | Specifically, say workspace 1 and 2 are on monitor Left,
           | while 3 and 4 are on Right. Application A has a window on
           | workspace 1, B has one window on 2 and another on 3, and C
           | has a window on 4. Workspace 1 is active on display Left,
           | workspace 4 is active on Right. If I switch to workspace 3
           | the following happens:
           | 
           | - 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 (???)
        
             | airstrike wrote:
             | Thirding the recommendation, and I also have this same
             | issue. It's quite frustrating--but still better than no
             | Aerospace!
        
             | JSR_FDED wrote:
             | So what you're saying is:
             | 
             | 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?
        
         | jeberle wrote:
         | I use it similarly, but I add spots for side x side as well as
         | left, center, right. I only use Hammerspoon for this and a
         | couple tiny things, but it's completely worth it for this
         | alone. Use math to specify window sizes & location. Insanity.
         | 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))       end
        
         | piskov wrote:
         | Moom is really the best
        
         | apazzolini wrote:
         | I like the miro windows manager plugin:
         | https://github.com/miromannino/miro-windows-manager
         | 
         | It's nice to be able to iterate through the halves/thirds
         | configurations for different cases.
        
       | jmcguckin wrote:
       | I use it to give me focus-follows-mouse and to have a large
       | circle surrounding the mouse when i move it, to aid finding it.
        
       | juancn wrote:
       | I use it to hide Zoom's screen sharing controls so they don't
       | come back when pressing Esc:                   -- 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)
        
         | henrebotha wrote:
         | Oh my god, immediately worth installing just for this
        
       | overflowy wrote:
       | I use this to remap app keys:                   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 ...
        
       | alexfortin wrote:
       | I use it to enable/disable the wifi when I disconnec/connect the
       | macbook to a specific usb hub with ethernet connection:
       | 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()
        
         | jmarcher wrote:
         | That should not be required if the Ethernet adapter has a
         | higher priority. That being said, you might different reason.
         | 
         | https://support.apple.com/en-ca/guide/mac-help/mchlp2711/mac
        
       | swiftcoder wrote:
       | I always confuse "hammerspoon" and "rowhammer"
        
         | iLemming wrote:
         | So, the trivia behind the name is interesting. It all starts
         | with a project called "Mjolnir" (Thor's Hammer). The original
         | idea was for a lightweight automation engine with pluggable
         | architecture. Someone wished to have "batteries included"
         | version, the author said they're not interested in that
         | direction, so the fork was born. What's meant to be a hard-
         | fork, so really not a fork, but rather a "spoon", hence the
         | name.
        
       | selectnull wrote:
       | Love hammerspoon. I use it to map double CMD to swap between the
       | terminal and the browser.
        
       | theshrike79 wrote:
       | I tried to find a proper window control tool for macOS for a
       | while, tested Rectangle and Magnet and dunno how many.
       | 
       | 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 (
       | _cough_ Firefox _cough_ ) 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.
        
         | piskov wrote:
         | Moom works like a charm
        
       | regus wrote:
       | How does this compare to AppleScript?
        
         | dbalatero wrote:
         | Insanely easier to use, way better programming language, a
         | kitchen sink of macOS APIs, and you can still call out to
         | AppleScript when you need it:
         | https://www.hammerspoon.org/docs/hs.osascript.html#applescri...
        
       | cjbarber wrote:
       | See also:
       | 
       | KeyboardMaestro
       | 
       | Automator and AppleScript
       | 
       | Raycast
        
       | ChrisArchitect wrote:
       | https://www.hammerspoon.org/
       | 
       | Some previous discussion:
       | 
       | 2021 https://news.ycombinator.com/item?id=29533495
       | 
       | 2019 https://news.ycombinator.com/item?id=21801178
        
       | iLemming wrote:
       | Shameless plug/proud self-promotion -
       | https://github.com/agzam/spacehammer "Spacemacs|Doom inspired
       | Hammerspoon modal toolkit"
       | 
       | 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.
        
         | jedbrooke wrote:
         | > "alt+cmd m j/k" (media -> vol up/down)
         | 
         | 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.
        
           | iLemming wrote:
           | It's not about "having" or "not having" keys for specific
           | actions, it's all about freedom and feeling of control. When
           | you take and apply the idea of modality, you quickly realize
           | that you are no longer constrained with the number of
           | combinations you can have or the type of keyboard you're
           | using. Everything can be controlled by (mostly) using home-
           | row keys - h/j/k/l - without having to memorize weird
           | combinations of modifiers and keys - "was it Ctrl+Alt+Cmd F,
           | or just Ctrl+Cmd F?"
           | 
           | 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.
        
       | gedy wrote:
       | If you are on macOS but miss Paperwm or niri, etc this is a good
       | compromise thanks to Hammerspoon:
       | https://github.com/mogenson/PaperWM.spoon
        
       | ifh-hn wrote:
       | It's this like Mac equivalent to autohotkey on windows?
        
       | dbalatero wrote:
       | I have an unreleased pile of tutorial projects with Hammerspoon.
       | I think not all chapters are finished, so I need to clean up and
       | finish it at some point soon: https://learnhammerspoon.com
        
       | kcrwfrd_ wrote:
       | I despair at not being able to easily send a window to another
       | space with a keyboard shortcut on macOS.
       | 
       | 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...
        
       | dbalatero wrote:
       | Some stuff I've made in Hammerspoon you can use:
       | 
       | - 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/...
        
       ___________________________________________________________________
       (page generated 2026-03-13 23:00 UTC)