Skip to content

Commit

Permalink
Improved performance; adaditional built-in geometries, improved state…
Browse files Browse the repository at this point in the history
… management
  • Loading branch information
cmpadden committed Dec 30, 2024
1 parent f385fd0 commit df93bad
Showing 1 changed file with 68 additions and 47 deletions.
115 changes: 68 additions & 47 deletions wm/spoon.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ local obj = {
state_alert = "/",
},
},
-- Store the most layout index for each application in each layout
state = {
-- [1] = { application_name_1 = 1, application_name_2 = 1 }
-- [2] = { application_name_1 = 1, application_name_2 = 3 }
Expand All @@ -35,6 +36,13 @@ local pip_width = 0.142

--- Predefined geometries
obj.builtins = {
full = hs.geometry({
h = 1,
w = 1,
x = 0,
y = 0,
}),

padded_center = hs.geometry({
h = (1 - (2 * padding)),
w = window_width_centered,
Expand Down Expand Up @@ -95,48 +103,19 @@ local function get_config(...)
return value
end

--- Converts a unitrect (relative coordinates) to a rect (absolute coordinates) based on the main screen's frame.
-- --- @param unit_rect hs.geometry A unitrect representing relative coordinates.
-- --- @return hs.geometry A rect representing absolute coordinates.
local function _unit_rect_to_rect(unit_rect)
local screen_frame = hs.screen.mainScreen():frame()
return hs.geometry.rect(
screen_frame.x + (unit_rect.x * screen_frame.w),
screen_frame.y + (unit_rect.y * screen_frame.h),
unit_rect.w * screen_frame.w,
unit_rect.h * screen_frame.h
)
end

-- Temporary workaround: move windows until we confirm that they are at the frame that
-- we expect. Have a retry of 3 to prevent any unwanted infinite loops. For more
-- information reference the open github issue:
--
-- https://github.com/Hammerspoon/hammerspoon/issues/3624
local function _move_to_unit_with_retries(geometry, window)
window:moveToUnit(geometry)
local retries = 3
hs.timer.doUntil(function()
return retries == 0 or window:frame():equals(_unit_rect_to_rect(geometry):floor())
end, function()
window:moveToUnit(geometry)
retries = retries - 1
end, 0.25)
end

-- Gets the layout index for a given `application_name` and `layout`, defaulting to `1`.
local function get_application_geometry_index(layout, application_name)
if obj.state[layout] == nil then
obj.state[layout] = {}
obj.state[layout][application_name] = 1
return 1
local layout_state = obj.state[layout]
if layout_state == nil then
layout_state = { application_name = 1 }
obj.state[layout] = layout_state
end

if obj.state[layout][application_name] == nil then
obj.state[layout][application_name] = 1
return 1
if layout_state[application_name] == nil then
layout_state[application_name] = 1
end

return obj.state[layout][application_name]
return layout_state[application_name]
end

local function set_application_geometry_index(layout, application_name, index)
Expand All @@ -154,9 +133,10 @@ end
-- however, instead we have to do this hacky workaround. See another user with the same
-- bewilderment: https://devforum.roblox.com/t/wrapping-index-in-an-array/1476197/2
--
---@param table table table to traverse
---@param index integer current index of table
---@param step integer positive or negative value to iterate over table
-- @param table table table to traverse
-- @param index integer current index of table
-- @param step integer positive or negative value to iterate over table
--
local function next_index_circular(table, index, step)
if #table == 1 then
return 1
Expand Down Expand Up @@ -188,12 +168,30 @@ function obj:set_layout(layout)
self.layout = layout
local active_layout = self.layouts[layout]

-- NOTE: Getting windows on each layout change can be removed if window creation stores window in state
local active_windows = self.window_filter_all:getWindows()
for _, window in ipairs(active_windows) do
local app_name = window:application():name()
local ix = get_application_geometry_index(layout, app_name)
local target_geometry = active_layout[ix]
_move_to_unit_with_retries(target_geometry, window)
-- Non-blocking `moveToUnit` by using timers
hs.timer.doAfter(0, function()
-- Disabling `AXEnhancedUserInterface` fixes the issue where applications like Firefox
-- require multiple retries to resize. Ideally, we would re-set this value to `true` after
-- resizing the window, as it's required for voice controls, but for now we'll just set it
-- once.
--
-- See: https://github.com/Hammerspoon/hammerspoon/issues/3224#issuecomment-2155567633
-- See: https://github.com/Hammerspoon/hammerspoon/issues/3624
local app_name = window:application():name()
local axApp = hs.axuielement.applicationElement(window:application())
if axApp.AXEnhancedUserInterface then
axApp.AXEnhancedUserInterface = false
end

-- obj.state[layout][application_name]

local ix = get_application_geometry_index(layout, app_name)
local target_geometry = active_layout[ix]
window:moveToUnit(target_geometry)
end)
end
end

Expand Down Expand Up @@ -222,9 +220,18 @@ function obj:init()
-- Consider usage of `windowCreated` and `windowFocused` for ideal resizing trigger
-- TODO refactor this so that movement and getting layout is shared
self.window_filter_all:subscribe(hs.window.filter.windowCreated, function(window, app_name)
local ix = get_application_geometry_index(self.layout, app_name)
local target_geometry = self.layouts[self.layout][ix]
_move_to_unit_with_retries(target_geometry, window)
-- Prevent resizing of floating windows
--
-- http://www.hammerspoon.org/docs/hs.window.html#isStandard
--
-- > "Standard window" means that this is not an unusual popup window, a modal dialog, a floating window, etc.
--
if window:isStandard() then
hs.alert("Initializing " .. app_name)
local ix = get_application_geometry_index(self.layout, app_name)
local target_geometry = self.layouts[self.layout][ix]
window:moveToUnit(target_geometry)
end
end)

-- bind layouts to corresponding 1, 2, ..., n
Expand Down Expand Up @@ -266,6 +273,20 @@ function obj:init()
hs.hotkey.bind(_prefix, get_config("bindings", "state_restore"), function()
self:load_state()
end)

local function moveToScreen(index)
local screens = hs.screen.allScreens()
hs.window.focusedWindow():moveToScreen(screens[index])
end

-- todo - move to next / previous screen
hs.hotkey.bind({ "cmd", "ctrl" }, "h", function()
moveToScreen(1)
end)

hs.hotkey.bind({ "cmd", "ctrl" }, "l", function()
moveToScreen(2)
end)
end

return obj

0 comments on commit df93bad

Please sign in to comment.