Skip to content

Instantly share code, notes, and snippets.

@rodgtr1
Created July 27, 2025 02:17
Show Gist options
  • Save rodgtr1/8374e463c0cbee9f8834750683084951 to your computer and use it in GitHub Desktop.
Save rodgtr1/8374e463c0cbee9f8834750683084951 to your computer and use it in GitHub Desktop.
Hammerspoon to toggle terminal, 40% to the right, on top
-- Hammerspoon Terminal Toggle with Double-Shift
-- Toggles terminal (Ghostty) visibility with double-shift keypress
-- Positions terminal on right side of screen
-- I use it for quick Claude Code access
-- Basic Hammerspoon setup
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", function()
hs.reload()
hs.alert.show("Hammerspoon config reloaded")
end)
-- Auto-reload config when files change (debounced)
local function reloadConfig(files)
local doReload = false
for _, file in pairs(files) do
if file:sub(-4) == ".lua" then
doReload = true
break
end
end
if doReload then
hs.timer.doAfter(0.5, hs.reload)
end
end
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()
hs.alert.show("Config loaded")
-- Configuration
local TERMINAL_APP = "Ghostty" -- Change this to your preferred terminal
local TERMINAL_WIDTH_RATIO = 0.4 -- 40% of screen width
local LAUNCH_DELAY = 0.5 -- Seconds to wait for app launch
local DOUBLE_PRESS_TIME = 0.3 -- Max time between shift presses
-- Calculate terminal dimensions and position
local screen = hs.screen.mainScreen()
local frame = screen:frame()
local terminalWidth = frame.w * TERMINAL_WIDTH_RATIO
local terminalHeight = frame.h
local terminalX = frame.x + frame.w - terminalWidth
local terminalY = frame.y
-- Check if terminal is currently visible
function isTerminalVisible()
local app = hs.application.get(TERMINAL_APP)
if not app then return false end
local windows = app:allWindows()
if #windows == 0 then return false end
-- Check if any window is visible and not minimized
for _, win in ipairs(windows) do
if not win:isMinimized() and win:isVisible() then
return true
end
end
return false
end
-- Main toggle function
function toggleTerminal()
local app = hs.application.get(TERMINAL_APP)
-- If app doesn't exist, launch it
if not app then
hs.application.launchOrFocus(TERMINAL_APP)
-- Wait for app to launch and position window
hs.timer.doAfter(LAUNCH_DELAY, function()
local newApp = hs.application.get(TERMINAL_APP)
if newApp then
local windows = newApp:allWindows()
if #windows > 0 then
local win = windows[1]
win:setFrame({
x = terminalX,
y = terminalY,
w = terminalWidth,
h = terminalHeight
})
win:focus()
end
end
end)
return
end
local windows = app:allWindows()
-- If no windows exist, try to create one
if #windows == 0 then
app:activate()
-- Uncomment next line if your terminal doesn't auto-create windows:
-- hs.eventtap.keyStroke({"cmd"}, "n")
return
end
local win = windows[1]
-- Toggle based on actual visibility
if isTerminalVisible() then
win:minimize()
else
win:unminimize()
win:setFrame({
x = terminalX,
y = terminalY,
w = terminalWidth,
h = terminalHeight
})
win:focus()
app:activate()
end
end
-- Double-shift detection system
local doublePressTime = DOUBLE_PRESS_TIME
local lastShiftReleaseTime = 0
local shiftPressCount = 0
local shiftWasPressed = false
local resetTimer = nil
local shiftTap = hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, function(event)
local flags = event:getFlags()
if flags.shift then
shiftWasPressed = true
else
if shiftWasPressed then
shiftWasPressed = false
local currentTime = hs.timer.secondsSinceEpoch()
-- Cancel existing reset timer
if resetTimer then
resetTimer:stop()
resetTimer = nil
end
if currentTime - lastShiftReleaseTime < doublePressTime then
shiftPressCount = shiftPressCount + 1
else
shiftPressCount = 1
end
lastShiftReleaseTime = currentTime
if shiftPressCount == 2 then
toggleTerminal()
shiftPressCount = 0
return true -- Consume the event
else
-- Reset count after timeout
resetTimer = hs.timer.doAfter(doublePressTime, function()
shiftPressCount = 0
resetTimer = nil
end)
end
end
end
return false
end)
-- Start the event tap with retry on failure
local function startShiftTap()
if shiftTap then
shiftTap:start()
if not shiftTap:isEnabled() then
-- Retry after brief delay if startup fails
hs.timer.doAfter(2, startShiftTap)
end
end
end
startShiftTap()
-- Clean up when terminal app quits
local appWatcher = hs.application.watcher.new(function(name, eventType, app)
if name == TERMINAL_APP and eventType == hs.application.watcher.terminated then
if resetTimer then
resetTimer:stop()
resetTimer = nil
end
end
end)
appWatcher:start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment