Skip to content

Instantly share code, notes, and snippets.

@diogox
Created March 19, 2023 15:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save diogox/446cb80eb91b7f2009d8d496ba6f1949 to your computer and use it in GitHub Desktop.
Save diogox/446cb80eb91b7f2009d8d496ba6f1949 to your computer and use it in GitHub Desktop.
My Hammerspoon setup
-- Make sure commandline client (/usr/local/bin/hs) is installed
-- and IPC module is running.
require("hs.ipc")
local logger = hs.logger.new("ipc", "info")
local path = "/usr/local"
-- On Macs with Apple chips, install cli in /opt/homebrew/ with Homebrew
-- as /usr/local doesn't seem to be writable.
if hs.fs.attributes("/opt/homebrew/") then
path = "/opt/homebrew/"
end
-- Second parameter to cliStatus() and cliInstall() is silent
if hs.ipc.cliStatus(path, false) then
logger.d("HammerSpoon CLI already installed at " .. path)
else
logger.i("Installing HammerSpoon CLI in " .. path)
hs.alert.show("Installing HammerSpoon CLI tool")
local result = hs.ipc.cliInstall(path, true)
if result then
logger.i("CLI install successful.")
else
logger.e("CLI install failed.")
hs.alert.show("CLI install failed.")
end
end
hs.ipc.cliColors({
-- Logged stuff
initial = "\27[35m", -- Magenta
-- Prompt and typed input
input = "\27[33m", -- Yellow
-- Output from run commands
output = "\27[26m" -- Cyan
})
hs.alert.show("Hammerspoon Started!")
local hyper = {'cmd','ctrl','alt'}
local shiftHyper = {'cmd','ctrl','alt', 'shift'}
-- Startup
require("hs")
local ipc = require("hs.ipc")
local stackline = require "stackline"
stackline:init()
local yabai = require("yabai")
-- Spoons
hs.loadSpoon("ReloadConfiguration")
spoon.ReloadConfiguration:start()
-- Libraries
local function openApp(app_name, new)
new = new or false
local s = 'open'
if new then
s = s .. ' -n'
end
s = s .. " -a '/Applications/" .. app_name .. ".app'"
hs.execute(s)
end
function yabai_ipc(args)
-- Runs in background very fast
hs.task.new("/opt/homebrew/bin/yabai", nil, function(ud, ...)
print("stream", hs.inspect(table.pack(...)))
return true
end, args):start()
end
-- ------ --
-- SPOONS --
-- ------ --
hs.loadSpoon("RecursiveBinder")
-- ------- --
-- HOTKEYS --
-- ------- --
-- Terminal
hs.hotkey.bind(hyper, 'return', function()
openApp('Kitty', true)
end)
local singleKey = spoon.RecursiveBinder.singleKey
local recursiveMap =
hs.hotkey.bind(
hyper, "w",
spoon.RecursiveBinder.recursiveBind(
{
[singleKey("v", "Toggle VPN")] = function()
hs.alert.show("TODO")
end,
[singleKey(",", "Settings")] = function()
hs.application.launchOrFocus("System Preferences")
end,
[singleKey("d", "Open URL")] = {
[singleKey("y", "Youtube")] = function()
hs.urlevent.openURL("https://youtube.com")
end,
},
}
)
)
-- --------- --
-- yabai_ipc --
-- --------- --
local function getCurrentSpaceWindowIDs()
local windows = yabai_ipc({"-m", "query", "--windows", "--space"})
end
-- Focus Movement
-- TODO: Call "cursorcerer" keybinding to hide cursor upon moving focus.
-- TODO: Add logic to movements that checks if a window is currently focused, if not focus, then move.
hs.hotkey.bind(hyper, "h", function() yabai_ipc({"-m", "window", "--focus", "west"}) end)
hs.hotkey.bind(hyper, "l", function() yabai_ipc({"-m", "window", "--focus", "east"}) end)
hs.hotkey.bind(hyper, "j", function() yabai.focus_south_or_cycle_next() end)
hs.hotkey.bind(hyper, "k", function() yabai.focus_north_or_cycle_prev() end)
-- Window Movement
hs.hotkey.bind(shiftHyper, "h", function() yabai_ipc({"-m", "window", "--swap", "west"}) end)
hs.hotkey.bind(shiftHyper, "l", function() yabai_ipc({"-m", "window", "--swap", "east"}) end)
hs.hotkey.bind(shiftHyper, "j", function() yabai_ipc({"-m", "window", "--swap", "south"}) end)
hs.hotkey.bind(shiftHyper, "k", function() yabai_ipc({"-m", "window", "--swap", "north"}) end)
-- Desktops
for space_number=1,9 do
hs.hotkey.bind(hyper, tostring(space_number), function() yabai.focus_space(space_number) end)
end
hs.hotkey.bind(hyper, "0", function() yabai.focus_space(10) end)
hs.hotkey.bind(hyper, ",", function() yabai.focus_prev_space() end)
hs.hotkey.bind(hyper, ".", function() yabai.focus_next_space() end)
-- Move to Desktops
for space_number=1,9 do
hs.hotkey.bind(shiftHyper, tostring(space_number), function() yabai.move_to_space(space_number) end)
end
hs.hotkey.bind(shiftHyper, "0", function() yabai.move_to_space(10) end)
-- Tiling Modes
hs.hotkey.bind(hyper, "m", function() yabai:toggle_stack() end)
hs.hotkey.bind(shiftHyper, "space", function() yabai:toggle_float() end)
-- Screenshots
-- TODO: This doesn't work. I just set the hotkey in the flameshot configurations.
-- hs.hotkey.bind({"cmd", "alt", "shift"}, "5", function() hs.execute[["flameshot", gui"]] end)
---------------------------------------
-- _ _ _____ ___ _ ____
-- | | | |_ _|_ _| | / ___|
-- | | | | | | | || | \___ \
-- | |_| | | | | || |___ ___) |
-- \___/ |_| |___|_____|____/
---------------------------------------
function ipc(command, callback_success, callback_failure)
table.insert(command, 1, "-m")
local callback_success = callback_success or function(x)
return x
end
local callback_failure = callback_failure or function(x)
return x
end
local callback = function(exitCode, stdOut, stdErr)
if exitCode == 0 then
callback_success(stdOut)
else
callback_failure(stdErr)
end
end
hs.task.new(
"/opt/homebrew/bin/yabai",
callback,
command
):start()
end
-- For some reason, some windows get "topmost" status seemingly randomly.
-- This function removes topmost from all windows in the current space as a way to workaround this bug.
function remove_topmost()
ipc({ "query", "--windows", "--space" }, function(res)
local res_json_list = hs.json.decode(res)
for _, res_json in pairs(res_json_list) do
local window_id = res_json["id"]
local is_topmost = res_json["is-topmost"]
local is_floating = res_json["is-floating"]
if is_topmost and not is_floating then
ipc({ "window", window_id, "--toggle", "topmost" })
end
end
end)
end
-----------------------------------------------
-- __ __ ___ ____ _ _ _ _____
-- | \/ |/ _ \| _ \| | | | | | ____|
-- | |\/| | | | | | | | | | | | | _|
-- | | | | |_| | |_| | |_| | |___| |___
-- |_| |_|\___/|____/ \___/|_____|_____|
-----------------------------------------------
local module = {}
-- Toggles floating for the focused window.
function module.toggle_float()
ipc({ "window", "--toggle", "float" })
-- Center newly floated window
ipc({"window", "--grid", "5:5:1:1:3:3"})
end
-- Toggles the "stack mode" for all windows in a space.k
function module.toggle_stack()
ipc({ "query", "--windows", "--window" }, function(res)
local res_json = hs.json.decode(res)
local is_stacked = res_json["stack-index"] > 0
local layout
if is_stacked then
ipc({ "space", "--layout", "bsp" })
else
ipc({ "query", "--windows", "--space" }, function(res)
remove_topmost()
ipc({ "space", "--layout", "stack" })
end)
end
end)
end
-- Focuses the window to the south, or cycles to the next window if stacked.
function module.focus_south_or_cycle_next()
ipc({ "query", "--windows", "--window" }, function(res)
local res_json = hs.json.decode(res)
local is_stacked = res_json["stack-index"] > 0
if is_stacked then
remove_topmost()
ipc({ "window", "--focus", "stack.next" }, nil, function()
ipc({ "window", "--focus", "stack.first" })
end)
else
ipc({ "window", "--focus", "south" })
end
end)
end
-- Focuses the window to the north, or cycles to the previous window if stacked.
function module.focus_north_or_cycle_prev()
ipc({ "query", "--windows", "--window" }, function(res)
local res_json = hs.json.decode(res)
local is_stacked = res_json["stack-index"] > 0
if is_stacked then
remove_topmost()
ipc({ "window", "--focus", "stack.prev" }, nil, function()
ipc({ "window", "--focus", "stack.last" })
end)
else
ipc({ "window", "--focus", "north" })
end
end)
end
-- Focuses the space with the index provided.
function module.focus_space(space)
ipc({ "query", "--spaces", "--space"}, function(res)
local res_json = hs.json.decode(res)
local current_space_index = res_json.index
local is_already_focused = current_space_index == space
-- local create_new_space = function()
--ipc({ "space", "--create", "recent"})
-- yabai -m space --create && yabai -m space --focus last
-- end
if is_already_focused then
ipc({ "space", "--focus", "recent"})
else
ipc({ "space", "--focus", tostring(space)})
end
end)
end
-- Moves the focused window to the space with the provided index.
function module.move_to_space(space)
ipc({ "window", "--space", tostring(space)})
end
-- Focuses the space with the index number before the currently focused space.
function module.focus_prev_space()
ipc({ "space", "--focus", "prev"})
end
-- Focuses the space with the index number after the currently focused space.
function module.focus_next_space()
ipc({ "space", "--focus", "next"})
end
return module
@diogox
Copy link
Author

diogox commented Mar 19, 2023

This is a somewhat unpolished config.

There are a few dangling bits of code that don't connect anywhere.
Some TODOs here and there.
And it doesn't work very well with a multi-monitor setup. (I would like to have the hyper+h and hyper+l keybindings move to the right and left monitors if there are no more windows in the current space to that direction, for example)

Either way, it's pretty functional, and a great starting point!

Setup

Your ~/.hammerspoon directory should be look like this:

  • hs.lua
  • init.lua
  • yabai.lua
  • Spoons (directory where you should install the necessary spoons)

You'll need the stackline plugin.
I also have the RecursiveBinder spoon installed, though I have yet to find a good use for it, so feel free to delete the piece of code that pertains to it and skip this install.

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