Basic window management hot keys
Create a bunch of hotkeys to move your application windows to any part of your screen(s).
Next ChapterAPIs used today | Description |
hs.alert | Draws an alert message on the screen. |
hs.fnutils | A collection of functional programming utility functions. |
hs.geometry | A coordinate struct of (x, y, w, h) with attached math functions. |
hs.grid | Partitions your screen into a grid, and allows you to position windows within the grid. |
hs.screen.watcher | Receive a callback when a new display is connected, resolution is changed, or monitor position is moved. |
hs.window | Manage window positions, sizes, and visibility states. |
What you’re building #
In this chapter, you’re going to combine a few object types to resize, move, maximize, and center your windows, all using keyboard hotkeys.
Object | What it represents |
---|---|
hs.window |
A single window in an application. This is generally the currently focused window that you wish to resize or move. |
hs.screen |
A single monitor screen - either your laptop screen, or an external monitor. |
hs.grid |
You can partition an hs.screen into a grid, for arranging your windows in a tiled manner. Each grid can be sliced into as many rows or columns you’d like: 8x4 , 4x10 , etc. Windows can then be easily moved or resized along this grid. |
In combining these objects together, you’ll:
- Configure grid sizes for each of your connected screens
- Reconfigure your grids whenever a monitor is plugged or unplugged
- Create functions to:
- throw windows to other monitors
- maximize or center your windows
- move your windows to the top, bottom, left, or right halves of the screen
- resize your windows up, down, left, and right along your defined grid
- nudge your windows up, down, left, and right along your defined grid
- and of course, bind these all to hot keys!
Create a new config file #
First, make a config file to hold all your audio switcher code.

touch ~/.hammerspoon/window-management.lua
And require it in your main config:

require("window-management")
Initial setup #
Create and return a module #
You’re going to structure this project as a module that can be required in another file. If you recall from the cheatsheet, any value you return
from a Lua file can be then loaded with require()
in other files. This is really handy for modularizing your code, and will be good practice today.

local module = {}
return module
Disable animations #
By default, Hammerspoon has animations enabled for window resizes and movements. The default animation duration is 200ms
. To me, this feels really slow, especially the 50th time you go to move a window. I’d highly recommend disabling animations, which you can do by adding this line of code to the top of the config file:

hs.window.animationDuration = 0
windowMeta
struct #
Each of the functions you’re going to write below rely on having a reference to the current window
, the current screen
, and the grid objects for both of those.
Since you have to use these objects over and over again, you’re just going to make a little struct that loads those objects for you, so we can reuse it for the rest of this tutorial:

-- Create a handy struct to hold the current window/screen and their grids.
local function windowMeta()
local meta = {}
meta.window = hs.window.focusedWindow()
meta.screen = meta.window:screen()
meta.windowGrid = hs.grid.get(meta.window)
meta.screenGrid = hs.grid.getGrid(meta.screen)
return meta
end
local module = {}
return module
Your first window management functions #
We don’t need to make fancy tile grids to get started here. You’ll take care of some basics first in this section, before diving into that material.
Maximizing and centering windows #
Now you can make your first window management functions. You’ll make one for resizing a window to full screen, and one for moving a window to the center of the screen.
maximizeWindow
#
This function will maximize the current window to fit the entire screen. Add this function between the local module
definition and the final return
statement:

local module = {}
-- Maximizes a window to fit the entire grid.
module.maximizeWindow = function ()
local meta = windowMeta()
hs.grid.maximizeWindow(meta.window)
end
return module
I bind this key to Super (⌘⌥⌃) F (“F” is for “fullscreen”). If you make a new file for your keybinds, you can require()
your window management functions there:

local wm = require('window-management')
hs.hotkey.bind(super, 'f', wm.maximizeWindow)
centerOnScreen
#
This function will move the current window to the center of the screen. Add this function:

local module = {}
-- Centers a window in the middle of the screen.
module.centerOnScreen = function ()
local meta = windowMeta()
meta.window:centerOnScreen(meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) C:

local wm = require('window-management')
hs.hotkey.bind(super, 'c', wm.centerOnScreen)
Optional: Throwing windows to other monitors #
If you have multiple monitors, you can use these functions to throw a window from one monitor to the other.
throwLeft
#
This function will take a window and throw it to the monitor to the left of its current screen:

local module = {}
-- Throws a window 1 screen to the left
module.throwLeft = function ()
local meta = windowMeta()
meta.window:moveOneScreenWest()
end
return module
I bind this key to Super (⌘⌥⌃) Q:

local wm = require('window-management')
hs.hotkey.bind(super, 'q', wm.throwLeft)
throwRight
#
This function will take a window and throw it to the monitor to the right of its current screen:

local module = {}
-- Throws a window 1 screen to the right
module.throwRight = function ()
local meta = windowMeta()
meta.window:moveOneScreenEast()
end
return module
I bind this key to Super (⌘⌥⌃) W:

local wm = require('window-management')
hs.hotkey.bind(super, 'w', wm.throwRight)
Moving windows to different halves of the screen #
Next, you’ll create 4 functions to move and resize a window to take up half the screen. You can send it to the left half, right half, top half, or bottom half.
Because I use Vim and am used to the arrow keys being under hjkl
, I bind this keys to Super (⌘⌥⌃) HJKL, depending on the side of the screen I’m moving the window to:
Key | Where the window goes |
---|---|
Super (⌘⌥⌃) H | Left half |
Super (⌘⌥⌃) J | Bottom half |
Super (⌘⌥⌃) K | Top half |
Super (⌘⌥⌃) L | Right half |
These next functions rely on the hs.geometry module to compute new window sizes and positions. hs.geometry
is a utility object that represents either points, sizes, or rectangles, depending how you construct it (if you find this overloading confusing, you’re not alone…)
Today you’re just going to be using it as a rectangle. This code snippet creates a rectangle with its upper-left coordinate at (0, 0)
and a width/height of 400x300
:

x = 0
y = 0
width = 400
height = 300
rectangle = hs.geometry(x, y, width, height)
A lot of the hs.grid
functions for moving and resizing take hs.geometry
rectangles as arguments, so you’ll be using these a lot going forward.
leftHalf
#
This resizes the window to the left half of the screen. You’ll do this by computing a new position and size for the window with hs.geometry
, and then applying the geometry to the window within its current screen:

local module = {}
-- 1. Moves a window all the way left
-- 2. Resizes it to take up the left half of the screen (grid)
module.leftHalf = function ()
local meta = windowMeta()
-- We are computing the new desired geometry for our window:
--
-- The upper left corner of the window will end up at (0, 0).
-- The width of the window will be _half_ the screen's grid width (half the screen)
-- The height of the window will be equal to the entire screen's height (full height)
local cell = hs.geometry(0, 0, 0.5 * meta.screenGrid.w, meta.screenGrid.h)
-- With this call, we're positioning the `window` with the given `cell` dimensions,
-- inside of our current screen's grid (`meta.screen`).
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) H:

local wm = require('window-management')
hs.hotkey.bind(super, 'h', wm.leftHalf)
rightHalf
#
This resizes the window to the right half of the screen, and it looks a lot like the leftHalf
function. The only difference is that our x
coordinate is equal to half the screen’s width, so that the left edge of the window will end up in the center.

local module = {}
-- 1. Moves a window all the way right
-- 2. Resizes it to take up the right half of the screen (grid)
module.rightHalf = function ()
local meta = windowMeta()
local cell = hs.geometry(0.5 * meta.screenGrid.w, 0, 0.5 * meta.screenGrid.w, meta.screenGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) L:

local wm = require('window-management')
hs.hotkey.bind(super, 'l', wm.rightHalf)
topHalf
#
This resizes the window to the top half of the screen. It will be positioned at (0, 0)
, with full width and half height:

local module = {}
-- 1. Moves a window all the way to the top
-- 2. Resizes it to take up the top half of the screen (grid)
module.topHalf = function ()
local meta = windowMeta()
local cell = hs.geometry(0, 0, meta.screenGrid.w, 0.5 * meta.screenGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) K:

local wm = require('window-management')
hs.hotkey.bind(super, 'k', wm.topHalf)
bottomHalf
#
This resizes the window to the bottom half of the screen. It will be positioned halfway down the screen, with full width and half height:

local module = {}
-- 1. Moves a window all the way to the bottom
-- 2. Resizes it to take up the bottom half of the screen (grid)
module.bottomHalf = function ()
local meta = windowMeta()
local cell = hs.geometry(0, 0.5 * meta.screenGrid.h, meta.screenGrid.w, 0.5 * meta.screenGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) J:

local wm = require('window-management')
hs.hotkey.bind(super, 'j', wm.bottomHalf)
Give these keys a try with one of your application windows!
Set up screen grids for your monitors #
For the remainder of the chapter, we’re going to create some functions to resize and nudge your windows along a grid. Before you do that, you need to decide how many rows and columns you want to break your screen into.
For example, if you sliced up your monitor into a 8x4
grid, you’d end up with the following cells:
<-------------- 8 -------------->
^ |---|---|---|---|---|---|---|---|
| | | | | | | | | |
| |---|---|---|---|---|---|---|---|
| | | | | | | | |
4 |---|---|---|---|---|---|---|---|
| | | | | | | | |
| |---|---|---|---|---|---|---|---|
| | | | | | | | | |
v |---|---|---|---|---|---|---|---|
The functions you’re going to write next will either move or resize a window along this grid, one cell at a time. This is nice, as it lets you arrange windows in a tiled layout, and they’ll look nice and aligned.
Set your grid’s margins #
hs.grid
lets you configure how much margin you want between each grid cell. Personally, I like the windows to have no space between them, so I set my x
and y
margins to (0, 0)
:

hs.grid.setMargins('0, 0')
For now, put this line in your config file, and tweak it later if you want to.
Set a grid for each of your screens #
Next, you need to choose grid dimensions for each of your screens. I’ve arbitrarily decided on the following grid values:
Screen orientation | Grid dimensions |
---|---|
Landscape | 8 cols, 4 rows |
Portrait (rotated) | 4 cols, 8 rows |
Ultrawide | 10 cols, 4 rows |
Write a function that loops through all your connected screens and sets the correct grid dimension for the screen’s orientation:

local function setGridForScreens()
-- Set a screen grid depending on the resolution
for _, screen in pairs(hs.screen.allScreens()) do
if screen:frame().w / screen:frame().h > 2 then
-- 10 * 4 for ultra wide screen
hs.grid.setGrid('10 * 4', screen)
else
if screen:frame().w < screen:frame().h then
-- 4 * 8 for vertically aligned screen
hs.grid.setGrid('4 * 8', screen)
else
-- 8 * 4 for normal screen
hs.grid.setGrid('8 * 4', screen)
end
end
end
end
-- Call it once on config load.
setGridForScreens()
In the next step, you’ll set up a watcher that refreshes these grids when a monitor is connected/disconnected.
Refresh your grids when a monitor is plugged in or unplugged #
If you add a monitor to your computer, you want to make sure that it also gets a grid defined. In just a couple lines of code, hs.screen.watcher
lets us create a function that gets called whenever the monitor configuration changes.
Add these lines to the config file:

-- Set screen watcher, in case you connect a new monitor, or unplug a monitor
screenWatcher = hs.screen.watcher.new(function()
setGridForScreens()
end)
screenWatcher:start()
Nudge your windows by 1 grid cell #
Now that your screens have grids defined, you can move onto the next 4 functions. These functions will nudge a window left, right, down, or up by 1 grid cell.
I bound the previous functions for sending windows to halves to Super (⌘⌥⌃) HJKL. I’m going to follow the same scheme, and put my nudge keys one keyboard row above, at Super (⌘⌥⌃) YUIO. You can do the same, or find your own that you like!
nudgeLeft
#
This function computes a new hs.geometry
using the existing meta.windowGrid
. By setting the x
coordinate to meta.windowGrid.x - 1
, we are saying we want to move the window 1 cell to the left. Expressing this as x - 1
is really nice, as the row and column width is abstracted away from us with the hs.grid
system we set up on each screen.
Add this function to your config file:

local module = {}
module.nudgeLeft = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x - 1, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) Y:

local wm = require('window-management')
hs.hotkey.bind(super, 'y', wm.nudgeLeft)
nudgeRight
#
You’ll make a similar function to nudge a window right, but this time set the x
coordinate to x + 1
:

local module = {}
module.nudgeRight = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x + 1, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) o:

local wm = require('window-management')
hs.hotkey.bind(super, 'o', wm.nudgeRight)
nudgeUp
#
Again, it’s similar to the previous functions for nudging up, but this time we set y = y - 1
:

local module = {}
module.nudgeUp = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y - 1, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) I:

local wm = require('window-management')
hs.hotkey.bind(super, 'i', wm.nudgeUp)
nudgeDown
#
Same deal as before, but with y = y + 1
:

local module = {}
module.nudgeDown = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y + 1, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) U:

local wm = require('window-management')
hs.hotkey.bind(super, 'u', wm.nudgeDown)
Resize your windows by 1 grid cell #
In this final section, you’ll create 4 functions to grow or shrink your windows horizontally or vertically, 1 cell at a time.
I bound the previous functions for sending windows to halves to Super (⌘⌥⌃) HJKL. I’m going to follow the same scheme, and put my resize keys one keyboard row below this time, at Super (⌘⌥⌃) NM <comma> <period>. You can do the same, or find your own that you like!
shrinkLeft
#
This function is similar to the previous set of movement functions, but instead of moving the (x, y)
coordinates by 1 grid cell, you’ll decrease the width
by 1 grid cell instead, setting it to meta.windowGrid.w - 1
.
Add this function to your config:

local module = {}
-- Shrinks a window's size horizontally to the left.
module.shrinkLeft = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w - 1, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) N:

local wm = require('window-management')
hs.hotkey.bind(super, 'n', wm.shrinkLeft)
growRight
#
To grow a window to the right by 1 cell, you just need to set the width equal to width + 1
:

local module = {}
-- Grows a window's size horizontally to the right.
module.growRight = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w + 1, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) <period>:

local wm = require('window-management')
hs.hotkey.bind(super, '.', wm.growRight)
shrinkUp
#
To shrink the window upwards, subtract 1 cell from the current height
:

local module = {}
-- Shrinks a window's size vertically up.
module.shrinkUp = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h - 1)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) <comma>:

local wm = require('window-management')
hs.hotkey.bind(super, ',', wm.shrinkUp)
growDown
#
To grow the window downwards, subtract 1 cell from the current height
:

local module = {}
-- Grows a window's size vertically down.
module.growDown = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h + 1)
hs.grid.set(meta.window, cell, meta.screen)
end
return module
I bind this key to Super (⌘⌥⌃) M:

local wm = require('window-management')
hs.hotkey.bind(super, 'm', wm.growDown)
Conclusion #
That was a lot of work! But now you hopefully have a good handle on how to interact with your application windows, and make them do crazy tricks with just the power of your keyboard!
In the next chapter, you’ll reuse this library to make a cool snippet to put 2 browser tabs side by side.
Put two Chrome tabs side-by-side
Get the entire script #
Want to just paste in this whole project to a file?
Create a window-keybinds.lua
file:

local wm = require('window-management')
hs.hotkey.bind(super, 'f', wm.maximizeWindow)
hs.hotkey.bind(super, 'c', wm.centerOnScreen)
-- Throw windows to secondary monitors
hs.hotkey.bind(super, 'q', wm.throwLeft)
hs.hotkey.bind(super, 'w', wm.throwRight)
-- Move windows to halves
hs.hotkey.bind(super, 'h', wm.leftHalf)
hs.hotkey.bind(super, 'j', wm.bottomHalf)
hs.hotkey.bind(super, 'k', wm.topHalf)
hs.hotkey.bind(super, 'l', wm.rightHalf)
-- Nudge windows by one grid cell
hs.hotkey.bind(super, 'y', wm.nudgeLeft)
hs.hotkey.bind(super, 'u', wm.nudgeDown)
hs.hotkey.bind(super, 'i', wm.nudgeUp)
hs.hotkey.bind(super, 'o', wm.nudgeRight)
-- Shrink/grow windows by one grid cell
hs.hotkey.bind(super, 'n', wm.shrinkLeft)
hs.hotkey.bind(super, 'm', wm.growDown)
hs.hotkey.bind(super, ',', wm.shrinkUp)
hs.hotkey.bind(super, '.', wm.growRight)
And then a window-management.lua
file:

-- Disable animations
hs.window.animationDuration = 0
-- Set grid margins
hs.grid.setMargins('0, 0')
-- Compute the grid dimensions for all connected screens
local function setGridForScreens()
-- Set a screen grid depending on the resolution
for _, screen in pairs(hs.screen.allScreens()) do
if screen:frame().w / screen:frame().h > 2 then
-- 10 * 4 for ultra wide screen
hs.grid.setGrid('10 * 4', screen)
else
if screen:frame().w < screen:frame().h then
-- 4 * 8 for vertically aligned screen
hs.grid.setGrid('4 * 8', screen)
else
-- 8 * 4 for normal screen
hs.grid.setGrid('8 * 4', screen)
end
end
end
end
-- Call this once on config load.
setGridForScreens()
-- Set up a screen watcher to fire in case you connect a new monitor
screenWatcher = hs.screen.watcher.new(function()
setGridForScreens()
end)
-- Start the watcher, or it won't run :)
screenWatcher:start()
-- Create a handy struct to hold the current window/screen and their grids.
local function windowMeta()
local meta = {}
meta.window = hs.window.focusedWindow()
meta.screen = self.window:screen()
meta.windowGrid = hs.grid.get(self.window)
meta.screenGrid = hs.grid.getGrid(self.screen)
return meta
end
local module = {}
-- Maximizes a window to fit the entire grid.
module.maximizeWindow = function ()
local meta = windowMeta()
hs.grid.maximizeWindow(meta.window)
end
-- Centers a window in the middle of the screen.
module.centerOnScreen = function ()
local meta = windowMeta()
meta.window:centerOnScreen(meta.screen)
end
-- Throws a window 1 screen to the left
module.throwLeft = function ()
local meta = windowMeta()
meta.window:moveOneScreenWest()
end
-- Throws a window 1 screen to the right
module.throwRight = function ()
local meta = windowMeta()
meta.window:moveOneScreenEast()
end
-- 1. Moves a window all the way left
-- 2. Resizes it to take up the left half of the screen (grid)
module.leftHalf = function ()
local meta = windowMeta()
-- We are computing the new desired geometry for our window:
--
-- The upper left corner of the window will end up at (0, 0).
-- The width of the window will be _half_ the screen's grid width (half the screen)
-- The height of the window will be equal to the entire screen's height (full height)
local cell = hs.geometry(0, 0, 0.5 * meta.screenGrid.w, meta.screenGrid.h)
-- With this call, we're positioning the `window` with the given `cell` dimensions,
-- inside of our current screen's grid (`meta.screen`).
hs.grid.set(meta.window, cell, meta.screen)
end
-- 1. Moves a window all the way right
-- 2. Resizes it to take up the right half of the screen (grid)
module.rightHalf = function ()
local meta = windowMeta()
local cell = hs.geometry(0.5 * meta.screenGrid.w, 0, 0.5 * meta.screenGrid.w, meta.screenGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- 1. Moves a window all the way to the top
-- 2. Resizes it to take up the top half of the screen (grid)
module.topHalf = function ()
local meta = windowMeta()
local cell = hs.geometry(0, 0, meta.screenGrid.w, 0.5 * meta.screenGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- 1. Moves a window all the way to the bottom
-- 2. Resizes it to take up the bottom half of the screen (grid)
module.bottomHalf = function ()
local meta = windowMeta()
local cell = hs.geometry(0, 0.5 * meta.screenGrid.h, meta.screenGrid.w, 0.5 * meta.screenGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Nudges a window 1 grid cell to the left
module.nudgeLeft = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x - 1, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Nudges a window 1 grid cell to the right
module.nudgeRight = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x + 1, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Nudges a window 1 grid cell up
module.nudgeUp = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y - 1, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Nudges a window 1 grid cell down
module.nudgeDown = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y + 1, meta.windowGrid.w, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Shrinks a window's size horizontally to the left.
module.shrinkLeft = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w - 1, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Grows a window's size horizontally to the right.
module.growRight = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w + 1, meta.windowGrid.h)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Shrinks a window's size vertically up.
module.shrinkUp = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h - 1)
hs.grid.set(meta.window, cell, meta.screen)
end
-- Grows a window's size vertically down.
module.growDown = function()
local meta = windowMeta()
local cell = hs.geometry(meta.windowGrid.x, meta.windowGrid.y, meta.windowGrid.w, meta.windowGrid.h + 1)
hs.grid.set(meta.window, cell, meta.screen)
end
return module