From df93bad2ec9a71a9e917f7013a0a6fe945817dde Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Sun, 29 Dec 2024 19:36:07 -0500 Subject: [PATCH] Improved performance; adaditional built-in geometries, improved state management --- wm/spoon.lua | 115 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 47 deletions(-) diff --git a/wm/spoon.lua b/wm/spoon.lua index 924bd99..dbae0cb 100644 --- a/wm/spoon.lua +++ b/wm/spoon.lua @@ -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 } @@ -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, @@ -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) @@ -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 @@ -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 @@ -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 @@ -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