📖   Chapter 11

Basic window management hot keys

Create a bunch of hotkeys to move your application windows to any part of your screen(s).

Next Chapter
APIs used today Description
hs.alertDraws an alert message on the screen.
hs.fnutilsA collection of functional programming utility functions.
hs.geometryA coordinate struct of (x, y, w, h) with attached math functions.
hs.gridPartitions your screen into a grid, and allows you to position windows within the grid.
hs.screen.watcherReceive a callback when a new display is connected, resolution is changed, or monitor position is moved.
hs.windowManage 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.

copy
touch ~/.hammerspoon/window-management.lua

And require it in your main config:

copy
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.

copy
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:

copy
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:

copy
-- 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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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.

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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):

copy
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:

copy
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:

copy
-- 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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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:

copy
local wm = require('window-management')

hs.hotkey.bind(super, 'i', wm.nudgeUp)

nudgeDown #

Same deal as before, but with y = y + 1:

copy
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:

copy
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:

copy
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:

copy
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:

copy
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>:

copy
local wm = require('window-management')

hs.hotkey.bind(super, '.', wm.growRight)

shrinkUp #

To shrink the window upwards, subtract 1 cell from the current height:

copy
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>:

copy
local wm = require('window-management')

hs.hotkey.bind(super, ',', wm.shrinkUp)

growDown #

To grow the window downwards, subtract 1 cell from the current height:

copy
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:

copy
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:

copy
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:

copy
-- 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
Put two Chrome tabs side-by-side