Skip to content

Instantly share code, notes, and snippets.

@xiexingwu
Last active November 26, 2025 03:22
Show Gist options
  • Select an option

  • Save xiexingwu/991ff58ba970038c7f14a28588a00b16 to your computer and use it in GitHub Desktop.

Select an option

Save xiexingwu/991ff58ba970038c7f14a28588a00b16 to your computer and use it in GitHub Desktop.
Wezterm - Quake/Quick Terminal using Hammerspoon (optional Aerospace)

Quick Terminal setup for Wezterm

Make Wezterm behave like Ghostty's Quick Terminal, i.e. use a keybind to focus/hide a scratch terminal that always stays on top.

wezterm_quickterm

Keybind: Alt-`

  • First press: Spawns a wezterm terminal with workspace "_scratch" and focuses it in the middle of the screen.
  • Subsequent pressees: Hides the terminal if it is focused. Otherwise bring the terminal to the centre and focus.

How it works

Wezterm

Define a "_scratch" workspace (or any other name of your choice) so Wezterm can do special handling for this specific GUI window, e.g. resize it to preference and always keep it above other apps. See wezterm.lua below.

Hammerspoon

Hammerspoon is responsible for summoning and hiding the scratch terminal. See hammerspoon.lua below.

Aerospace (Optional)

If you have a tiling window manager like Aerospace, you will probably want to set an exception to not manage the scratch terminal, e.g.

[[on-window-detected]]
if.app-name-regex-substring = 'wezterm'
if.window-title-regex-substring = '_scratch'
run = 'layout floating'

Version snapshot:

  • wezterm --version: 20250511-222021-ad44b159
  • Hammerspoon: Version 1.0.0 (6864)
  • aerospace --version: 0.18.5-Beta 4213dfd9d958dbfe3f801e07d7d5af53303baa75

FAQ

  • I don't want to use Wezterm's multiplexer.

    In the hammerspoon script, find the wez.spawn method and remove --domain unix --attach from the args variable. In fact, play around with this part of the script and please let me know if there's a better way to interact with wezterm.

  • I don't like the terminal in the center of the screen and prefer the curtain-like apperance of Quake mode.

    Update your wezterm configuration to change how the window is positioned. See https://gist.github.com/xieve/fc67361a2a0cb8fc23ab8369a8fc1170 for example.

  • Are there other ways to achieve this result? How about on other platforms?

    Yes, please see this discussion wezterm/wezterm#1751 and another relevant gist: https://gist.github.com/xieve/fc67361a2a0cb8fc23ab8369a8fc1170.

-- .config/hammerspoon/init.lua
Logger = hs.logger.new("default", "info")
local scratch = "_scratch" -- keep this consitent with Wezterm
local wez = {}
function wez.spawn(workspace)
local args = " --class org.wezfurlong.wezterm." ..
workspace .. " --workspace " .. workspace .. " --domain unix --attach"
local command = "wezterm start" .. args .. " zsh -i & detach"
io.popen(os.getenv("SHELL") .. [[ -l -i -c "]] .. command .. [["]])
-- hs.execute(os.getenv("SHELL") .. [[ -l -i -c "]] .. command .. [["]])
-- Wezterm may still be starting up, so we will keep looking for it and finally focus it when we do
local count = 0
hs.timer.waitUntil(
function()
count = count + 1
if count >= 20 then
Logger.w("Failed to find newly spawned Wezterm window after 20 tries.")
return true
end
return wez.findWindow(workspace) ~= nil
end,
function() wez.toggleFocus(workspace, { force_show = true }) end,
0.1)
end
---Return the first window whose name includes the workspace name
---TODO: This is a very fragile process since the window title may inadvertently contain the workspace name.
function wez.findWindow(workspace)
local terms = table.pack(hs.application.find("wezterm"))
for _, term in ipairs(terms) do
local windows = table.pack(term:findWindow(workspace))
if #windows > 0 then
return windows[1]
end
end
end
---Toggles the focus of the wezterm window named `workspace`
---@param workspace string The workspace name
---@param opts { force_show: boolean } Options:
--- force_show=nil: If true, always try to focus/center the window
---@return boolean isSuccess Whether the window was successfully focused
function wez.toggleFocus(workspace, opts)
opts = opts or {}
local win = wez.findWindow(workspace)
if win == nil then
Logger.e("Failed to find a wezterm window [" .. workspace .. "] to focus.")
return false
end
-- hide/unhide
if workspace == scratch then
local app = win:application()
-- Toggle scratch primary/minimised
if win:isVisible() then
if opts.force_show then
win:centerOnScreen('0,0')
win:focus()
else
app:hide()
end
elseif win:application():isHidden() then
app:unhide()
win:centerOnScreen('0,0')
win:focus()
end
else
win:focus()
end
return true
end
-- Setup keybinds
function wez.summon(workspace)
local terms = table.pack(hs.application.find("wezterm"))
if #terms == 0 then
Logger.e("No Wezterm found, spawning [" .. workspace .. "]")
-- No Wezterm instances
wez.spawn(workspace)
else
local found = wez.toggleFocus(workspace)
if not found then
Logger.e("[" .. workspace .. "] not found, spawning")
-- No windows to focus
wez.spawn(workspace)
end
end
end
hs.hotkey.bind({ "alt" }, "`", function() wez.summon(scratch) end)
hs.hotkey.bind({ "alt" }, "1", function() wez.summon("work") end)
hs.hotkey.bind({ "alt" }, "2", function() wez.summon("pers") end)
-- .config/wezterm/wezterm.lua
local wezterm = require("wezterm") --[[@as Wezterm]]
local mux = wezterm.mux
local act = wezterm.action
local scratch = "_scratch" -- Keep this consistent with Hammerspoon
wezterm.on("gui-attached", function(domain)
local workspace = mux.get_active_workspace()
if workspace ~= scratch then return end
-- Compute width: 66% of screen width, up to 1000 px
local width_ratio = 0.66
local width_max = 1000
local aspect_ratio = 16 / 9
local screen = wezterm.gui.screens().active
local width = math.min(screen.width * width_ratio, width_max)
local height = width / aspect_ratio
for _, window in ipairs(mux.all_windows()) do
local gwin = window:gui_window()
if gwin ~= nil then
gwin:perform_action(act.SetWindowLevel "AlwaysOnTop", gwin:active_pane())
gwin:set_inner_size(width, height)
end
end
end)
@BitCalSaul
Copy link

@xiexingwu Thank you for your detail reply. I have migrated to Ghostty too since its convenient quick terminal feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment