A nifty window layout switcher for macOS using Hammerspoon


I’m a sucker for screen real estate, and thus I use 2 32” 4k monitors so I can see most of my apps’ windows simultaneously, instead of switching back and forth between them.

I use the awesome Rectangle app that provides shortcuts to move/resize an app window to my liking. However, this goes for a toss whenever I disconnect my laptop, or reboot it. macOS can’t seem to reliably remember where to put the windows at such events and I’ve to keep playing this game of doing a lot of alt-tabs and rectangle keyboard shortcuts to get my app windows back where I like them. So, I spent some time recently to whip up a short hammerspoon script that allows me to do this easily.

local laptopScreen = "Color LCD"
local leftMonitor = "Acer ET322QK"
local rightMonitor = "B326HK"

-- Define position values that don't exist by default in hs.layout.*
local positions = {
  leftTop = {x=0, y=0, w=0.5, h=0.5},
  leftBottom = {x=0, y=0.5, w=0.5, h=0.5},
  rightTop = {x=0.5, y=0, w=0.5, h=0.5},
  rightBottom = {x=0.5, y=0.5, w=0.5, h=0.5}
}

local layoutTripleScreen = {
  {"Google Chrome", nil, rightMonitor, positions.rightBottom, nil, nil},
  {"Joplin", nil, rightMonitor, positions.rightTop, nil, nil},
  {"Cider", nil, rightMonitor, hs.layout.left50, nil, nil},
  {"Google.com Mail", nil, leftMonitor, positions.leftBottom, nil, nil},
  {"Hangouts Chat", nil, leftMonitor, positions.leftTop, nil, nil},
  {"Firefox", nil, leftMonitor, positions.rightTop, nil, nil},
  {"iTerm2", nil, leftMonitor, positions.rightBottom, nil, nil},
}

local layoutSingleScreen = {
  {"Google Chrome", nil, laptopScreen, hs.layout.maximized, nil, nil},
  {"Joplin", nil, laptopScreen, hs.layout.maximized, nil, nil},
  {"Cider", nil, laptopScreen, hs.layout.maximized, nil, nil},
  {"Google.com Mail", nil, laptopScreen, hs.layout.maximized, nil, nil},
  {"Hangouts Chat", nil, laptopScreen, hs.layout.maximized, nil, nil},
  {"Firefox", nil, laptopScreen, hs.layout.maximized, nil, nil},
  {"iTerm2", nil, laptopScreen, hs.layout.maximized, nil, nil},
}

local appNames = {
  "Google Chrome",
  "Joplin",
  "Cider",
  "Google.com Mail",
  "Hangouts Chat",
  "Firefox",
  "iTerm",
}

local function launchApps()
  for i, appName in ipairs(appNames) do
    hs.application.launchOrFocus(appName)
  end
end

local menu = hs.menubar.new()
local function setSingleScreen()
  menu:setTitle("🖥1")
  menu:setTooltip("Single Screen Layout")
  hs.layout.apply(layoutSingleScreen)
end

local function setTripleScreen()
  menu:setTitle("🖥3")
  menu:setTooltip("Triple Screen Layout")
  hs.layout.apply(layoutTripleScreen)
end

local function enableMenu()
  menu:setTitle("🖥")
  menu:setTooltip("No Layout")
  menu:setMenu({
      { title = "Launch Apps", fn = launchApps },
      { title = "Set Triple Screen Layout", fn = setTripleScreen },
      { title = "Set Single Screen Layout", fn = setSingleScreen },
  })
end

enableMenu()

The script creates a list of apps that I like to be open while working and defines two layouts for them (One when monitors are connected thus giving me 3 screens, and one when I’m working solely off the laptop). It also puts a handy menu in the menubar that allows me to launch the apps or switch between various options. It also shows a number “1” or “3” adjacent to the menu icon to display what layout I’m currently using. Pretty nifty!

The Layout Switcher Menu


See also