Petr Gazarov

Maximize Mac Windows with Keyboard

If you have a Mac laptop and an external display, you probably know that plugging or unplugging the external display often messes up application window sizes. Windows that were maximized on the native display won’t be maximized on the external display when it is plugged in, and same when going back. It’s really annoying, so after a fruitless search in Mac settings, I rolled custom keyboard shortcuts using Hammerspoon. If you haven’t heard about Hammerspoon, it’s an amazing tool to script all kinds of things Mac.

After installing Hammerspoon, I created a custom Lua script in the ~/.hammerspoon/ directory. It contains 3 shortcuts:

  1. option + cmd + = - Maximizes the currently focused window to fill the entire screen.
  2. option + cmd + - - Minimizes the currently focused window to 70% height / 50% width of the screen size and centers it.
  3. ctrl + cmd + option + = - Maximizes all open windows to fill the entire screen.

For the third shortcut, some applications (Finder and Messages) are intentionally excluded from maximizing because I don’t like them maximized. You can configure this list however you like in the maximizeAllWindows function.

Here is the complete code:

-- ~/.hammerspoon/init.lua (init file).

-- Import and run custom script:
dofile(hs.configdir .. "/AdjustApplicationsWindowSize.lua")
-- ~/.hammerspoon/AdjustApplicationsWindowSize.lua (custom script).

local log = hs.logger.new('AAppWinSize', 'debug')

-- Higher order function that prevents animations from interfering with resizing.
local function adjustWindowSettings(action)
    -- Temporarily disable EnhancedUserInterface to prevent window animations
    -- (see https://github.com/Hammerspoon/hammerspoon/issues/3224#issuecomment-1294359070)
    local axApp = hs.axuielement.applicationElement(hs.window.frontmostWindow():application())
    local wasEnhanced = axApp.AXEnhancedUserInterface
    axApp.AXEnhancedUserInterface = false

    -- Temporarily disable window animations
    local originalAnimationDuration = hs.window.animationDuration
    hs.window.animationDuration = 0

    action()

    -- Restore the original settings
    hs.window.animationDuration = originalAnimationDuration
    axApp.AXEnhancedUserInterface = wasEnhanced
end

-- Get the display name for logging
local function getDisplayName(app, title)
    -- Use "app: title" format if title exists and is different from app name,
    -- otherwise just use "app"
    return (title ~= "" and title ~= app) and (app .. ": " .. title) or app
end

-- Maximizes the currently focused window to fill the entire screen.
local function maximizeCurrentWindow()
    adjustWindowSettings(function()
        log.d('Starting maximizeCurrentWindow')
        local window = hs.window.focusedWindow()
        if not window then
            log.d('No window is currently focused.')
            return
        end

        local screen = window:screen()
        local usableFrame = screen:frame() -- Usable screen area excluding dock and menu bar.
        local displayName = getDisplayName(window:application():name(), window:title())
        log.d('Attempting to maximize current window: ' .. displayName)
        window:setFrame(usableFrame)
        log.d('Finished maximizeCurrentWindow.')
    end)
end

-- Minimizes the currently focused window to % of the screen size and centers it.
local function minimizeCurrentWindow()
    adjustWindowSettings(function()
        log.d('Starting minimizeCurrentWindow')
        local window = hs.window.focusedWindow()
        if not window then
            log.d('No window is currently focused.')
            return
        end

        local frame = window:frame()
        local screen = window:screen()
        local max = screen:frame()
        frame.w = max.w * 0.5
        frame.h = max.h * 0.7
        frame.x = max.x + (max.w - frame.w) / 2
        frame.y = max.y + (max.h - frame.h) / 2

        local displayName = getDisplayName(window:application():name(), window:title())
        log.d('Minimizing current window: ' .. displayName)
        window:setFrame(frame)
        log.d('Finished minimizeCurrentWindow.')
    end)
end

-- Maximizes all open windows on the main screen, excluding specified applications.
local function maximizeAllWindows()
    adjustWindowSettings(function()
        log.d('Starting maximizeAllWindows')
        local screen = hs.screen.mainScreen()
        local max = screen:frame()
        local windows = hs.window.visibleWindows()
        local excludedApps = { "Finder", "Messages" } -- List of apps to exclude from maximizing

        log.d("Found " .. #windows .. " windows in total.")
        local maximizedCount = 0

        for _, window in ipairs(windows) do
            local app = window:application():name()
            local title = window:title()
            local displayName = getDisplayName(app, title)

            if not hs.fnutils.contains(excludedApps, app) then
                log.d('Maximizing window "' .. displayName .. '"')
                window:setFrame(max)
                maximizedCount = maximizedCount + 1
            else
                log.d('Skipping window "' .. displayName .. '"')
            end
        end

        log.d('Finished maximizeAllWindows. Maximized ' .. maximizedCount .. ' windows.')
    end)
end

-- Bind the keyboard shortcuts to the functions
hs.hotkey.bind({ "option", "cmd" }, "=", maximizeCurrentWindow)
hs.hotkey.bind({ "ctrl", "cmd", "option" }, "=", maximizeAllWindows)
hs.hotkey.bind({ "option", "cmd" }, "-", minimizeCurrentWindow)

After saving the code and restarting Hammerspoon, you should see something like this in the Hammerspoon console: Hammerspoon console