Creating an Alacritty Hotkey Window
June 23 2024
workflow
software
iTerm2 Hotkey Window

iTerm2 has this great feature called the hotkey window. It's a persistent window that can be accessed from any desktop with a hotkey. When you toggle it on, the iTerm2 hotkey window shows up on top of all of your existing windows in full screen. And when you toggle it off it disappears. It's convenient because it makes a lot of the: create a terminal, do something, close the terminal, kind of workflows a lot easier.

Alacritty

I recently switched to Alacritty, which is a snappier and more lightweight shell. It doesn't have a hotkey window and has no plans of adding one. So, missing this feature, I came up with a hacky hotkey window for Alacritty myself.

Alacritty Hotkey Window

Requirements:

Code: GitHub

Creation

If you're familiar with Yabai, you might know that you can use queries:

yabai -m query <topic> <options>

to query for information about windows, spaces, and displays.

# Example: find information about the "focused" window.
yabai -m query --windows --window
# {
# 	"id":142173,
# 	"pid":21428,
# 	"app":"Alacritty",
# 	"title":"yabai -m query --win ~",
# 	"frame":{
# 		"x":3.0000,
# 		"y":35.0000,
# 		"w":1722.0000,
# 		"h":1047.0000
# 	},
#   [...]
# }

Each window has a unique id, so we use the id of our hotkey window to identify it. First, we create the hotkey window and get its id. On MacOS, an application can be opened programmatically using the open command.

open -a "Alacritty" -n
# -a: Application to open
# -n: Open a new instance, if one is already open

We can then get the pid of the Alacritty shell we just created:

pgrep -n -f "Alacritty"
# -n: Select only the newest of the matching processes
# -f: Match against "full argument lists", which includes the program name

We can then query, using Yabai, for the window with the corresponding pid to find its window id. Once we have the window id, we write it to a file, ~/.config/hotkey_window/window_id.txt, for future reference.

Using hammerspoon's APIs, this all looks like:

local function createHotkeyWindow()
    -- Create a new Alacritty window and get its PID.
    hs.execute("open -a " .. utils.quote("Alacritty") .. " -n")

    utils.sleep(1) -- wait for the application to launch

    local windowPid = hs.execute('pgrep -n -f "Alacritty"')
    if windowPid == nil then
        print("Failed to get pid of hotkey window")
        return nil
    end
    windowPid = math.floor(windowPid)
    print("Alacritty hotkey window pid: ", windowPid)

    -- Find the window id of the new window and write it to the file.
    local windows = yabai.query("windows")
    local windowId = nil

    -- Check if any of the active windows are the hotkey window.
    for _, window in ipairs(windows) do
        if math.floor(window["pid"]) == windowPid then
            windowId = math.floor(window["id"])
        end
    end

    if windowId == nil then
        print("Failed to get window id for hotkey window")
        return nil
    end

    local file = io.open(HOTKEY_WINDOW_ID_FILE, "w")
    if not file then
        print("Failed to write to hotkey window id file")
        return nil
    end

    file:write(windowId)
    file:close()

    -- Make the window: see-through, unmanaged, and full screen.
    yabai.command("-m window " .. windowId .. " --opacity 0.9")
    yabai.command("-m window " .. windowId .. " --toggle float")
    yabai.command("-m window " .. windowId .. " --grid 1:1:0:0:1:1")

    return windowId
end

Note: yabai.* and utils.* are custom helper functions - see full source for details.

When we trigger our hotkey window, we first want to check if we already have one, before making a new one.

Any hotkey window we've made with have an id that's been stored in ~/.config/hotkey_window/window_id.txt, so we read the id and then use Yabai to check if there are any windows that have that id. If there aren't, then our hotkey window got destroyed.

-- Check if a hotkey window has already been created.
local function exists()
    local file = io.open(HOTKEY_WINDOW_ID_FILE, "r")
    if not file then
        return false
    end

    local hotkeyWindowId = math.floor(file:read("*all"))
    file:close()

    local windows = yabai.query("windows")

    -- Check if any of the active windows are the hotkey window.
    for _, window in ipairs(windows) do
        local id = math.floor(window["id"])
        if id == hotkeyWindowId then
            return true, hotkeyWindowId
        end
    end

    return false
end

We check whether we have a hotkey window, and make one if we don't. Now we've got to check whether we're toggling the window on (making it visible) or off (hiding it).

To determine whether the hotkey window is focused we check whether the focused window has the same id as the hotkey window.

If we're toggling on the window we:

  • Move the hotkey window to the focused space.
  • Make it focused.
  • Make it full screen.

Otherwise, we hide the hotkey window by moving it to a space that is not currently visible.

local function toggle()
    -- Query for the focused window and space before doing anything else because
    -- showing/hinding the hotkey window may change the focused space.
    --
    -- Note: focusedWindow will be nil if there is no focused window.
    local focusedSpace = yabai.query("spaces", "--space")
    local focusedWindow = yabai.query("windows", "--window")

    local windowExists, windowId = exists()

    if not windowExists then
        print("Creating new hotkey window")
        windowId = createHotkeyWindow()
    end

    if windowId == nil then
        print("Failed to toggle hotkey window.")
        return
    end

    if focusedWindow ~= nil and focusedWindow["id"] == windowId then
        -- Hide the hotkey window.
        yabai.command("-m window " .. windowId .. " --space 9")
    else
        -- Show the hotkey window.
        --
        -- 1. Move it to the focused space.
        -- 2. Make it focused.
        -- 3. Make it full screen.

        yabai.command("-m window " .. windowId .. " --space " .. math.floor(focusedSpace["index"]))
        yabai.command("-m window " .. windowId .. " --focus")
        yabai.command("-m window " .. windowId .. " --grid 1:1:0:0:1:1")
    end
end

All that remains to be done is hook up the toggle function to a hotkey:

hs.hotkey.bind({ "ctrl", "cmd" }, 'k', toggle)

Now, hitting CTRL+CMD+K will toggle an Alacritty hotkey window.

I glanced over some of the details, like querying Yabai from Lua, or how this code can fit into your HammerSpoon configuration, so I've linked the full source code below. Cheers.

Code: GitHub