Created
March 19, 2023 15:27
-
-
Save diogox/446cb80eb91b7f2009d8d496ba6f1949 to your computer and use it in GitHub Desktop.
My Hammerspoon setup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--------------------------------------- | |
-- _ _ _____ ___ _ ____ | |
-- | | | |_ _|_ _| | / ___| | |
-- | | | | | | | || | \___ \ | |
-- | |_| | | | | || |___ ___) | | |
-- \___/ |_| |___|_____|____/ | |
--------------------------------------- | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a somewhat unpolished config.
There are a few dangling bits of code that don't connect anywhere.
Some
TODO
s here and there.And it doesn't work very well with a multi-monitor setup. (I would like to have the
hyper+h
andhyper+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
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.