Skip to content

Instantly share code, notes, and snippets.

@Sebas-h
Last active September 3, 2019 10:41
Show Gist options
  • Save Sebas-h/414e2dc4249df4d2c07de40e5bfcfee1 to your computer and use it in GitHub Desktop.
Save Sebas-h/414e2dc4249df4d2c07de40e5bfcfee1 to your computer and use it in GitHub Desktop.
Hammerspoon... spoon. Binds Hyper+Key to switch to a particular app and when pressed again switches between the windows of that app (across spaces). Cycle order between windows is determined by the order of focus received, most recently first. To use: place file in .hammerspoon directory and add 'require("hyper_plus")' to 'init.lua'
--------------------------
-- DEFINE YOUR APP + SHORTCUT KEY COMBINATIONS
--------------------------
shortcut_key_and_app = {
{ 'f', 'Firefox' }, -- "F" for "Browser"
{ 'e', 'Code' }, -- "E" for "Editor"
{ 'v', 'Finder' }, -- "V" for "Finder"
{ 't', 'iTerm2' }, -- "T" for "Terminal"
}
--------------------------
-- GLOBAL VARIABLES
--------------------------
hyper_modifiers_keys = {'shift', 'ctrl', 'alt', 'cmd'} -- Mapped to Caps Lock in Karabiner-Elements
hyper_pressed = false
idx_to_focus_hcl = 1 -- index of the window to focus from the hyper cycle window list
-- Window filter: will keep track of all visible windows across spaces
-- sources: https://github.com/Hammerspoon/hammerspoon/issues/1460
-- https://github.com/Hammerspoon/hammerspoon/issues/1260
-- When reloading hs config, we have to go through our workspaces
-- manually for the wf to detect the windows there
wf = hs.window.filter.new():setCurrentSpace(nil):keepActive()
--------------------------
-- SET UP
--------------------------
registered_apps = {}
for i, app in ipairs(shortcut_key_and_app) do
registered_apps[app[2]] = {shortcut_key=app[1], window_focus_list={}, window_hyper_cycle_list={}}
end
--------------------------
-- HELPER FUNCTIONS
--------------------------
function deepcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[deepcopy(orig_key)] = deepcopy(orig_value)
end
setmetatable(copy, deepcopy(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
function window_list_remove(window_list, id)
for i, winfo in ipairs(window_list) do
if winfo.id == id then
-- Removes element at index i and re-orders/re-indexes the list
table.remove(window_list, i)
return i
end
end
end
function window_list_add(window_list, window_info)
window_list_remove(window_list, window_info.id)
-- Append window to window list:
window_list[#window_list + 1] = window_info
end
function get_index_after_remove(next_index, index_to_remove, length_list)
if length_list == 1 then return 1 end
local current_index = next_index + 1
if current_index > length_list then current_index = 1 end
if current_index > index_to_remove then
current_index = current_index - 1
elseif current_index == index_to_remove and current_index == length_list then
current_index = 1
end
next_index = current_index - 1
if next_index == 0 then next_index = (length_list - 1) end
return next_index
end
--------------------------
-- WF WINDOW WATHCER
--------------------------
function wf_callback(window_object, app_name, event_name)
if registered_apps[app_name] ~= nil then -- check if app to which window belongs is in registered apps
if event_name == hs.window.filter.windowFocused then
-- Just before closing a window with focus the focus will transfer onto the previous window
-- that had focus before. The closed window won't exists anymore (i.e. is not standard anymore).
-- In order to keep the window_hyper_cycle_list order we should skip any further code in this block
for i, w in pairs(registered_apps[app_name].window_focus_list) do
if w.window:isStandard() ~= true then return end
end
window_list_add(registered_apps[app_name].window_focus_list, {id=window_object:id(), window=window_object})
if hyper_pressed ~= true then
registered_apps[app_name].window_hyper_cycle_list = deepcopy(registered_apps[app_name].window_focus_list)
idx_to_focus_hcl = #registered_apps[app_name].window_hyper_cycle_list - 1
end
elseif event_name == hs.window.filter.windowDestroyed
or event_name == hs.window.filter.windowHidden
or event_name == hs.window.filter.windowMinimized
then
local length_list = #registered_apps[app_name].window_hyper_cycle_list
-- Remove window from both window lists:
window_list_remove(registered_apps[app_name].window_focus_list, window_object:id())
index_to_remove = window_list_remove(registered_apps[app_name].window_hyper_cycle_list, window_object:id())
-- Calculate index of window to focus on next hyper cycle iteration:
idx_to_focus_hcl = get_index_after_remove(idx_to_focus_hcl, index_to_remove, length_list)
end
end
hyper_pressed = false
end
-- Listen for following window events and execute callback when event observered
wf:subscribe(
{hs.window.filter.windowDestroyed, hs.window.filter.windowHidden,
hs.window.filter.windowMinimized, hs.window.filter.windowFocused,},
wf_callback
)
--------------------------
-- BINDING HYPER SHORTCUTS
--------------------------
for app_name, app_info in pairs(registered_apps) do
hs.hotkey.bind(hyper_modifiers_keys, app_info.shortcut_key, function()
if hs.application.get(app_name):isFrontmost() and #app_info.window_focus_list > 1
then
hyper_pressed = true
-- Focus window
app_info.window_hyper_cycle_list[idx_to_focus_hcl].window:focus()
-- Update next index
idx_to_focus_hcl = idx_to_focus_hcl - 1
if idx_to_focus_hcl == 0 then idx_to_focus_hcl = #app_info.window_hyper_cycle_list end
else
hs.application.get(app_name):activate(true)
-- hs.application.open(app) -- does not work with Finder
end
end)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment