From 956e2333b5ddc6613d1c8c311e0ae73360509ae8 Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sun, 19 Sep 2021 16:23:47 +0200 Subject: [PATCH 01/11] Initial commit with optional three column grid --- init.lua | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/init.lua b/init.lua index 5809158..c14044f 100644 --- a/init.lua +++ b/init.lua @@ -42,16 +42,41 @@ local units = { top50 = { x = 0.00, y = 0.00, w = 1.00, h = 0.50 }, bot50 = { x = 0.00, y = 0.50, w = 1.00, h = 0.50 }, + right33 = { x = 0.67, y = 0.00, w = 0.33, h = 1.00 }, + left33 = { x = 0.00, y = 0.00, w = 0.33, h = 1.00 }, + upleft50 = { x = 0.00, y = 0.00, w = 0.50, h = 0.50 }, upright50 = { x = 0.50, y = 0.00, w = 0.50, h = 0.50 }, botleft50 = { x = 0.00, y = 0.50, w = 0.50, h = 0.50 }, botright50 = { x = 0.50, y = 0.50, w = 0.50, h = 0.50 }, + upleft33 = { x = 0.00, y = 0.00, w = 0.33, h = 0.50 }, + upright33 = { x = 0.67, y = 0.00, w = 0.33, h = 0.50 }, + botleft33 = { x = 0.00, y = 0.50, w = 0.33, h = 0.50 }, + botright33 = { x = 0.67, y = 0.50, w = 0.33, h = 0.50 }, + maximum = { x = 0.00, y = 0.00, w = 1.00, h = 1.00 }, } +local relatedUnits = {} + function obj:move(unit) self.hs.window.focusedWindow():move(unit, nil, true, 0) end +function obj:moveToggle(unit) + -- Fetch alternative unit, if any + local newUnit = relatedUnits[unit] + + local before = self.hs.window.focusedWindow():frame() + self:move(unit) + local after = self.hs.window.focusedWindow():frame() + + -- if the window is not moved or resized, it was already at the required location + -- if an alernative location is configured, move the window to that location + if before == after and newUnit then + self:move(newUnit) + end +end + function obj:resizeWindowInSteps(increment) local screen = self.hs.window.focusedWindow():screen():frame() local window = self.hs.window.focusedWindow():frame() @@ -110,21 +135,21 @@ function obj:resizeWindowInSteps(increment) self:move({ x = x, y = y, w = w, h = h }) end -function obj:left() self:move(units.left50) end +function obj:left() self:moveToggle(units.left50) end -function obj:right() self:move(units.right50) end +function obj:right() self:moveToggle(units.right50) end -function obj:up() self:move(units.top50) end +function obj:up() self:moveToggle(units.top50) end -function obj:down() self:move(units.bot50) end +function obj:down() self:moveToggle(units.bot50) end -function obj:upleft() self:move(units.upleft50) end +function obj:upleft() self:moveToggle(units.upleft50) end -function obj:upright() self:move(units.upright50) end +function obj:upright() self:moveToggle(units.upright50) end -function obj:botleft() self:move(units.botleft50) end +function obj:botleft() self:moveToggle(units.botleft50) end -function obj:botright() self:move(units.botright50) end +function obj:botright() self:moveToggle(units.botright50) end function obj:maximum() self:move(units.maximum) end @@ -196,4 +221,16 @@ function obj:bindHotkeys(mapping) return self end +function obj:enableThreeColumnGrid() + print(units) + relatedUnits = { + [units.left50] = units.left33, + [units.right50] = units.right33, + [units.upleft50] = units.upleft33, + [units.upright50] = units.upright33, + [units.botleft50] = units.botleft33, + [units.botright50] = units.botright33, + } +end + return obj From bd09ad0ca846a4ff773bfbb27af1d37412b55cf6 Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Wed, 17 Aug 2022 11:27:55 +0200 Subject: [PATCH 02/11] Changed implementation to have configurable steps, defaulting to only the 50% --- init.lua | 123 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 43 deletions(-) diff --git a/init.lua b/init.lua index c14044f..6a766b7 100644 --- a/init.lua +++ b/init.lua @@ -37,43 +37,53 @@ obj.mapping = { } local units = { - right50 = { x = 0.50, y = 0.00, w = 0.50, h = 1.00 }, - left50 = { x = 0.00, y = 0.00, w = 0.50, h = 1.00 }, - top50 = { x = 0.00, y = 0.00, w = 1.00, h = 0.50 }, - bot50 = { x = 0.00, y = 0.50, w = 1.00, h = 0.50 }, + left = function(x, y) return { x = 0.00, y = 0.00, w = x / 100, h = 1.00 } end, + right = function(x, y) return { x = 1 - (x / 100), y = 0.00, w = x / 100, h = 1.00 } end, + top = function(x, y) return { x = 0.00, y = 0.00, w = 1.00, h = y / 100 } end, + bot = function(x, y) return { x = 0.00, y = 1 - (y / 100), w = 1.00, h = y / 100 } end, - right33 = { x = 0.67, y = 0.00, w = 0.33, h = 1.00 }, - left33 = { x = 0.00, y = 0.00, w = 0.33, h = 1.00 }, - - upleft50 = { x = 0.00, y = 0.00, w = 0.50, h = 0.50 }, - upright50 = { x = 0.50, y = 0.00, w = 0.50, h = 0.50 }, - botleft50 = { x = 0.00, y = 0.50, w = 0.50, h = 0.50 }, - botright50 = { x = 0.50, y = 0.50, w = 0.50, h = 0.50 }, - - upleft33 = { x = 0.00, y = 0.00, w = 0.33, h = 0.50 }, - upright33 = { x = 0.67, y = 0.00, w = 0.33, h = 0.50 }, - botleft33 = { x = 0.00, y = 0.50, w = 0.33, h = 0.50 }, - botright33 = { x = 0.67, y = 0.50, w = 0.33, h = 0.50 }, + upleft = function(x, y) return { x = 0.00, y = 0.00, w = x / 100, h = y / 100 } end, + upright = function(x, y) return { x = 1 - (x / 100), y = 0.00, w = x / 100, h = y / 100 } end, + botleft = function(x, y) return { x = 0.00, y = 1 - (y / 100), w = x / 100, h = y / 100 } end, + botright = function(x, y) return { x = 1 - (x / 100), y = 1 - (y / 100), w = x / 100, h = y / 100 } end, maximum = { x = 0.00, y = 0.00, w = 1.00, h = 1.00 }, } local relatedUnits = {} +local latestMove = { + windowId = -1, + direction = 'unknown', + stepX = -1, + stepY = -1, +} + function obj:move(unit) self.hs.window.focusedWindow():move(unit, nil, true, 0) end function obj:moveToggle(unit) - -- Fetch alternative unit, if any - local newUnit = relatedUnits[unit] + local windowId = self.hs.window.focusedWindow():id() + local sameMoveAction = latestMove.windowId == windowId and latestMove.direction == unit + if sameMoveAction then + latestMove.stepX = obj.nextStepsX[latestMove.stepX] + latestMove.stepY = obj.nextStepsY[latestMove.stepY] + else + latestMove.stepX = obj.moveStepsX[1] + latestMove.stepY = obj.moveStepsY[1] + end + latestMove.windowId = windowId + latestMove.direction = unit local before = self.hs.window.focusedWindow():frame() - self:move(unit) - local after = self.hs.window.focusedWindow():frame() - - -- if the window is not moved or resized, it was already at the required location - -- if an alernative location is configured, move the window to that location - if before == after and newUnit then - self:move(newUnit) + self:move(unit(latestMove.stepX, latestMove.stepY)) + + if not sameMoveAction then + -- if the window is not moved or resized, it was already at the required location + -- if an alernative location is configured, move the window to that location + local after = self.hs.window.focusedWindow():frame() + if before == after then + self:moveToggle(unit) + end end end @@ -135,21 +145,21 @@ function obj:resizeWindowInSteps(increment) self:move({ x = x, y = y, w = w, h = h }) end -function obj:left() self:moveToggle(units.left50) end +function obj:left() self:moveToggle(units.left) end -function obj:right() self:moveToggle(units.right50) end +function obj:right() self:moveToggle(units.right) end -function obj:up() self:moveToggle(units.top50) end +function obj:up() self:moveToggle(units.top) end -function obj:down() self:moveToggle(units.bot50) end +function obj:down() self:moveToggle(units.bot) end -function obj:upleft() self:moveToggle(units.upleft50) end +function obj:upleft() self:moveToggle(units.upleft) end -function obj:upright() self:moveToggle(units.upright50) end +function obj:upright() self:moveToggle(units.upright) end -function obj:botleft() self:moveToggle(units.botleft50) end +function obj:botleft() self:moveToggle(units.botleft) end -function obj:botright() self:moveToggle(units.botright50) end +function obj:botright() self:moveToggle(units.botright) end function obj:maximum() self:move(units.maximum) end @@ -221,16 +231,43 @@ function obj:bindHotkeys(mapping) return self end -function obj:enableThreeColumnGrid() - print(units) - relatedUnits = { - [units.left50] = units.left33, - [units.right50] = units.right33, - [units.upleft50] = units.upleft33, - [units.upright50] = units.upright33, - [units.botleft50] = units.botleft33, - [units.botright50] = units.botright33, - } +local function join(items, separator) + local res = '' + for _, item in pairs(items) do + if res ~= '' then + res = res .. separator + end + res = res .. item + end + return res end +function obj:setSteps(stepsX, stepsY, skip_print) + if #stepsX < 1 or #stepsY < 1 then + print('Invalid arguments in setSteps, both dimensions should have at least 1 step') + return + end + local function listToNextMap(list) + local res = {} + for i, item in ipairs(list) do + local prev = (list[i - 1] == nil and list[#list] or list[i - 1]) + res[prev] = item + end + return res + end + + self.moveStepsX = stepsX + self.moveStepsY = stepsY + self.nextStepsX = listToNextMap(stepsX) + self.nextStepsY = listToNextMap(stepsY) + + if not skip_print then + print('Steps for horizontal:', join(stepsX, ' -> ')) + print('Steps for vertical:', join(stepsY, ' -> ')) + end +end + +-- Set default steps to 50%, as it's the ShiftIt default +obj:setSteps({ 50 }, { 50 }, true) + return obj From 911e5b6dfddbe262ce3b6143207655c4a334cb3b Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Tue, 23 Aug 2022 19:50:19 +0100 Subject: [PATCH 03/11] Fixed some lint warnings --- init.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/init.lua b/init.lua index 6a766b7..4b13265 100644 --- a/init.lua +++ b/init.lua @@ -37,10 +37,10 @@ obj.mapping = { } local units = { - left = function(x, y) return { x = 0.00, y = 0.00, w = x / 100, h = 1.00 } end, - right = function(x, y) return { x = 1 - (x / 100), y = 0.00, w = x / 100, h = 1.00 } end, - top = function(x, y) return { x = 0.00, y = 0.00, w = 1.00, h = y / 100 } end, - bot = function(x, y) return { x = 0.00, y = 1 - (y / 100), w = 1.00, h = y / 100 } end, + left = function(x, _) return { x = 0.00, y = 0.00, w = x / 100, h = 1.00 } end, + right = function(x, _) return { x = 1 - (x / 100), y = 0.00, w = x / 100, h = 1.00 } end, + top = function(_, y) return { x = 0.00, y = 0.00, w = 1.00, h = y / 100 } end, + bot = function(_, y) return { x = 0.00, y = 1 - (y / 100), w = 1.00, h = y / 100 } end, upleft = function(x, y) return { x = 0.00, y = 0.00, w = x / 100, h = y / 100 } end, upright = function(x, y) return { x = 1 - (x / 100), y = 0.00, w = x / 100, h = y / 100 } end, @@ -50,8 +50,6 @@ local units = { maximum = { x = 0.00, y = 0.00, w = 1.00, h = 1.00 }, } -local relatedUnits = {} - local latestMove = { windowId = -1, direction = 'unknown', From 5e35931ddd9a099ce6384c93ebc9fb496d79099d Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sun, 28 Aug 2022 14:37:13 +0100 Subject: [PATCH 04/11] Add tests for default 50% behaviour --- hammerspoon_mocks.lua | 16 +++++++++++ init_test.lua | 66 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/hammerspoon_mocks.lua b/hammerspoon_mocks.lua index f6a527d..209477b 100644 --- a/hammerspoon_mocks.lua +++ b/hammerspoon_mocks.lua @@ -24,6 +24,7 @@ end local window = { rect = defaultWindowRect, + _id = 42, _screen = screen, } @@ -35,6 +36,10 @@ function window:frame() return self.rect end +function window:id() + return self._id +end + function window:screen() return self._screen end @@ -44,6 +49,16 @@ function window:move(rect, _, _, _) lu.assertIsNumber(rect.y) lu.assertIsNumber(rect.w) lu.assertIsNumber(rect.h) + -- If the position contains a period (.), it is a relative coordinate + -- so multiple it with the screen size + if string.match(tostring(rect.x), '%.') ~= nil then + rect = { + x = self._screen.rect.x + (rect.x * self._screen.rect.w), + y = self._screen.rect.y + (rect.y * self._screen.rect.h), + w = rect.w * self._screen.rect.w, + h = rect.h * self._screen.rect.h, + } + end self.rect = rect end @@ -55,6 +70,7 @@ local mocks = { function mocks:reset() self.hotkey.bindings = {} self.window.rect = defaultWindowRect + self.window._id = 42 self.window._screen.rect = defaultScreenRect end diff --git a/init_test.lua b/init_test.lua index 1605e22..8b2416c 100644 --- a/init_test.lua +++ b/init_test.lua @@ -6,10 +6,11 @@ local hsmocks = require('hammerspoon_mocks') -- luacheck: ignore 112 TestShiftIt = {} -- luacheck: ignore 111 +shiftit.hs = hsmocks function TestShiftIt.setUp() - shiftit.hs = hsmocks hsmocks:reset() + shiftit:setSteps({ 50 }, { 50 }, true) end function TestShiftIt.testBindDefault() @@ -211,4 +212,67 @@ function TestShiftIt.testResizeWindowInStepsEdgeCases() end end +function TestShiftIt.testInitialiseSteps() + lu.assertEquals(shiftit.moveStepsX, { 50 }) + lu.assertEquals(shiftit.moveStepsY, { 50 }) + lu.assertEquals(shiftit.nextStepsX, { [50] = 50 }) + lu.assertEquals(shiftit.nextStepsY, { [50] = 50 }) + + shiftit:setSteps({ 80, 60, 40 }, { 75, 50, 25 }, true) + lu.assertEquals(shiftit.moveStepsX, { 80, 60, 40 }) + lu.assertEquals(shiftit.moveStepsY, { 75, 50, 25 }) + lu.assertEquals(shiftit.nextStepsX, { [40] = 80, [60] = 40, [80] = 60 }) + lu.assertEquals(shiftit.nextStepsY, { [25] = 75, [50] = 25, [75] =50 }) +end + +function TestShiftIt.testDefaultWindowShifts() + local tests = { + { + desc = 'shift to the left', + action = function () shiftit:left() end, + expect = { x = 0, y = 0, w = 600, h = 800 }, + }, + { + desc = 'shift to the right', + action = function() shiftit:right() end, + expect = { x = 600, y = 0, w = 600, h = 800 }, + }, + { + desc = 'shift to the top', + action = function() shiftit:up() end, + expect = { x = 0, y = 0, w = 1200, h = 400 }, + }, + { + desc = 'shift to the bottom', + action = function() shiftit:down() end, + expect = { x = 0, y = 400, w = 1200, h = 400 }, + }, + { + desc = 'shift to the left top', + action = function() shiftit:upleft() end, + expect = { x = 0, y = 0, w = 600, h = 400 }, + }, + { + desc = 'shift to the right top', + action = function() shiftit:upright() end, + expect = { x = 600, y = 0, w = 600, h = 400 }, + }, + { + desc = 'shift to the left bottom', + action = function() shiftit:botleft() end, + expect = { x = 0, y = 400, w = 600, h = 400 }, + }, + { + desc = 'shift to the right bottom', + action = function() shiftit:botright() end, + expect = { x = 600, y = 400, w = 600, h = 400 }, + }, + } + for _, test in pairs(tests) do + hsmocks:reset() + test.action() + lu.assertEquals(hsmocks.window.rect, test.expect, test.desc) + end +end + os.exit(lu.LuaUnit.run()) From ab017bb9826603a2e228612473f850e82b0b5497 Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sun, 28 Aug 2022 15:22:04 +0100 Subject: [PATCH 05/11] Add tests for multiple window size steps --- init.lua | 3 +- init_test.lua | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/init.lua b/init.lua index 4b13265..f202a59 100644 --- a/init.lua +++ b/init.lua @@ -79,7 +79,8 @@ function obj:moveToggle(unit) -- if the window is not moved or resized, it was already at the required location -- if an alernative location is configured, move the window to that location local after = self.hs.window.focusedWindow():frame() - if before == after then + if before.x == after.x and before.y == after.y + and before.w == after.w and before.h == after.h then self:moveToggle(unit) end end diff --git a/init_test.lua b/init_test.lua index 8b2416c..6d01981 100644 --- a/init_test.lua +++ b/init_test.lua @@ -275,4 +275,81 @@ function TestShiftIt.testDefaultWindowShifts() end end +function TestShiftIt.testMultipleWindowSizeSteps() + shiftit:setSteps({ 50, 75, 25 }, { 50 }, true) + local tests = { + { + desc = 'shift to the left through every option once', + action = function() shiftit:left() end, + expectSteps = { + { x = 0, y = 0, w = 600, h = 800 }, + { x = 0, y = 0, w = 900, h = 800 }, + { x = 0, y = 0, w = 300, h = 800 }, + }, + }, + { + desc = 'shift to the right bottom through every option twice', + action = function() shiftit:botright() end, + expectSteps = { + { x = 600, y = 400, w = 600, h = 400 }, + { x = 300, y = 400, w = 900, h = 400 }, + { x = 900, y = 400, w = 300, h = 400 }, + { x = 600, y = 400, w = 600, h = 400 }, + { x = 300, y = 400, w = 900, h = 400 }, + { x = 900, y = 400, w = 300, h = 400 }, + }, + }, + } + for _, test in pairs(tests) do + hsmocks:reset() + for i, expect in pairs(test.expectSteps) do + test.action() + lu.assertEquals(hsmocks.window.rect, expect, test.desc .. '> step ' .. i) + end + end +end + +function TestShiftIt.testMultipleWindowSizeStepsWithMultipleWindows() + shiftit:setSteps({ 50, 75, 25 }, { 50 }, true) + local windowId1 = 99 + local windowId2 = 88 + + hsmocks.window._id = windowId1 + shiftit:left() + lu.assertEquals(hsmocks.window.rect, { x = 0, y = 0, w = 600, h = 800 }) + shiftit:left() + lu.assertEquals(hsmocks.window.rect, { x = 0, y = 0, w = 900, h = 800 }) + + -- Mock switching windows, set the window size to differt from the last rect + hsmocks.window._id = windowId2 + hsmocks.window.rect = { x = 100, y = 100, w = 600, h = 500 } + shiftit:right() + lu.assertEquals(hsmocks.window.rect, { x = 600, y = 0, w = 600, h = 800 }) + shiftit:right() + lu.assertEquals(hsmocks.window.rect, { x = 300, y = 0, w = 900, h = 800 }) + shiftit:right() + lu.assertEquals(hsmocks.window.rect, { x = 900, y = 0, w = 300, h = 800 }) + + -- Mock switching windows, set the window size to the last size of this window + hsmocks.window._id = windowId1 + hsmocks.window.rect = { x = 0, y = 0, w = 900, h = 800 } + shiftit:left() + -- because the window was switched, we start at the beginning of the cycle, and expect the 50% value + lu.assertEquals(hsmocks.window.rect, { x = 0, y = 0, w = 600, h = 800 }) + + -- Mock switching windows, set the window size to the last size of this window + hsmocks.window._id = windowId2 + hsmocks.window.rect = { x = 900, y = 0, w = 300, h = 800 } + shiftit:right() + lu.assertEquals(hsmocks.window.rect, { x = 600, y = 0, w = 600, h = 800 }) + + -- Mock switching windows, set the window size to the last size of this window + hsmocks.window._id = windowId1 + hsmocks.window.rect = { x = 0, y = 0, w = 600, h = 800 } + shiftit:left() + -- because the window was switched, we start at the beginning of the cycle, and expect the 50% value + -- but because this did not cause any change, the function will call itself once, going to the 75% value + lu.assertEquals(hsmocks.window.rect, { x = 0, y = 0, w = 900, h = 800 }) +end + os.exit(lu.LuaUnit.run()) From 702fed315cffcff7b6a1f3c008a3b92953988b42 Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Thu, 1 Sep 2022 23:55:20 +0200 Subject: [PATCH 06/11] Fixed a bug where maximising or centering the window did not restart the size cycle --- init.lua | 10 ++++++++-- init_test.lua | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/init.lua b/init.lua index f202a59..19ce77a 100644 --- a/init.lua +++ b/init.lua @@ -160,13 +160,19 @@ function obj:botleft() self:moveToggle(units.botleft) end function obj:botright() self:moveToggle(units.botright) end -function obj:maximum() self:move(units.maximum) end +function obj:maximum() + latestMove.direction = 'maximum' + self:move(units.maximum) +end function obj:toggleFullScreen() self.hs.window.focusedWindow():toggleFullScreen() end function obj:toggleZoom() self.hs.window.focusedWindow():toggleZoom() end -function obj:center() self.hs.window.focusedWindow():centerOnScreen(nil, true, 0) end +function obj:center() + latestMove.direction = 'center' + self.hs.window.focusedWindow():centerOnScreen(nil, true, 0) + end function obj:nextScreen() self.hs.window.focusedWindow():moveToScreen(self.hs.window.focusedWindow():screen():next(), false, true, 0) diff --git a/init_test.lua b/init_test.lua index 6d01981..8641665 100644 --- a/init_test.lua +++ b/init_test.lua @@ -350,6 +350,11 @@ function TestShiftIt.testMultipleWindowSizeStepsWithMultipleWindows() -- because the window was switched, we start at the beginning of the cycle, and expect the 50% value -- but because this did not cause any change, the function will call itself once, going to the 75% value lu.assertEquals(hsmocks.window.rect, { x = 0, y = 0, w = 900, h = 800 }) + + shiftit:maximum() + shiftit:left() + -- because the window maximised, we expect the cycle to restart + lu.assertEquals(hsmocks.window.rect, { x = 0, y = 0, w = 600, h = 800 }) end os.exit(lu.LuaUnit.run()) From e886a2f36f67a3a9998543c355f110f728fdfa0a Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sat, 3 Sep 2022 17:48:37 +0200 Subject: [PATCH 07/11] Renamed variables and methods --- hammerspoon_mocks.lua | 2 +- init.lua | 56 +++++++++++++++++++++---------------------- init_test.lua | 24 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/hammerspoon_mocks.lua b/hammerspoon_mocks.lua index 209477b..6eaf5e1 100644 --- a/hammerspoon_mocks.lua +++ b/hammerspoon_mocks.lua @@ -50,7 +50,7 @@ function window:move(rect, _, _, _) lu.assertIsNumber(rect.w) lu.assertIsNumber(rect.h) -- If the position contains a period (.), it is a relative coordinate - -- so multiple it with the screen size + -- so multiply it with the screen size if string.match(tostring(rect.x), '%.') ~= nil then rect = { x = self._screen.rect.x + (rect.x * self._screen.rect.w), diff --git a/init.lua b/init.lua index 19ce77a..69cc54f 100644 --- a/init.lua +++ b/init.lua @@ -59,29 +59,29 @@ local latestMove = { function obj:move(unit) self.hs.window.focusedWindow():move(unit, nil, true, 0) end -function obj:moveToggle(unit) +function obj:moveWithCycles(unitFn) local windowId = self.hs.window.focusedWindow():id() - local sameMoveAction = latestMove.windowId == windowId and latestMove.direction == unit + local sameMoveAction = latestMove.windowId == windowId and latestMove.direction == unitFn if sameMoveAction then - latestMove.stepX = obj.nextStepsX[latestMove.stepX] - latestMove.stepY = obj.nextStepsY[latestMove.stepY] + latestMove.stepX = obj.nextCycleSizeX[latestMove.stepX] + latestMove.stepY = obj.nextCycleSizeY[latestMove.stepY] else - latestMove.stepX = obj.moveStepsX[1] - latestMove.stepY = obj.moveStepsY[1] + latestMove.stepX = obj.cycleSizesX[1] + latestMove.stepY = obj.cycleSizesY[1] end latestMove.windowId = windowId - latestMove.direction = unit + latestMove.direction = unitFn local before = self.hs.window.focusedWindow():frame() - self:move(unit(latestMove.stepX, latestMove.stepY)) + self:move(unitFn(latestMove.stepX, latestMove.stepY)) if not sameMoveAction then - -- if the window is not moved or resized, it was already at the required location - -- if an alernative location is configured, move the window to that location + -- if the window is not moved or resized, it was already at the required location, + -- in that case we'll call this method again, so it will go to the next cycle. local after = self.hs.window.focusedWindow():frame() if before.x == after.x and before.y == after.y and before.w == after.w and before.h == after.h then - self:moveToggle(unit) + self:moveWithCycles(unitFn) end end end @@ -144,21 +144,21 @@ function obj:resizeWindowInSteps(increment) self:move({ x = x, y = y, w = w, h = h }) end -function obj:left() self:moveToggle(units.left) end +function obj:left() self:moveWithCycles(units.left) end -function obj:right() self:moveToggle(units.right) end +function obj:right() self:moveWithCycles(units.right) end -function obj:up() self:moveToggle(units.top) end +function obj:up() self:moveWithCycles(units.top) end -function obj:down() self:moveToggle(units.bot) end +function obj:down() self:moveWithCycles(units.bot) end -function obj:upleft() self:moveToggle(units.upleft) end +function obj:upleft() self:moveWithCycles(units.upleft) end -function obj:upright() self:moveToggle(units.upright) end +function obj:upright() self:moveWithCycles(units.upright) end -function obj:botleft() self:moveToggle(units.botleft) end +function obj:botleft() self:moveWithCycles(units.botleft) end -function obj:botright() self:moveToggle(units.botright) end +function obj:botright() self:moveWithCycles(units.botright) end function obj:maximum() latestMove.direction = 'maximum' @@ -247,9 +247,9 @@ local function join(items, separator) return res end -function obj:setSteps(stepsX, stepsY, skip_print) +function obj:setWindowCyclingSizes(stepsX, stepsY, skip_print) if #stepsX < 1 or #stepsY < 1 then - print('Invalid arguments in setSteps, both dimensions should have at least 1 step') + print('Invalid arguments in setWindowCyclingSizes, both dimensions should have at least 1 step') return end local function listToNextMap(list) @@ -261,18 +261,18 @@ function obj:setSteps(stepsX, stepsY, skip_print) return res end - self.moveStepsX = stepsX - self.moveStepsY = stepsY - self.nextStepsX = listToNextMap(stepsX) - self.nextStepsY = listToNextMap(stepsY) + self.cycleSizesX = stepsX + self.cycleSizesY = stepsY + self.nextCycleSizeX = listToNextMap(stepsX) + self.nextCycleSizeY = listToNextMap(stepsY) if not skip_print then - print('Steps for horizontal:', join(stepsX, ' -> ')) - print('Steps for vertical:', join(stepsY, ' -> ')) + print('Cycle sizes for horizontal:', join(stepsX, ' -> ')) + print('Cycle sizes for vertical:', join(stepsY, ' -> ')) end end -- Set default steps to 50%, as it's the ShiftIt default -obj:setSteps({ 50 }, { 50 }, true) +obj:setWindowCyclingSizes({ 50 }, { 50 }, true) return obj diff --git a/init_test.lua b/init_test.lua index 8641665..8f1d96e 100644 --- a/init_test.lua +++ b/init_test.lua @@ -10,7 +10,7 @@ shiftit.hs = hsmocks function TestShiftIt.setUp() hsmocks:reset() - shiftit:setSteps({ 50 }, { 50 }, true) + shiftit:setWindowCyclingSizes({ 50 }, { 50 }, true) end function TestShiftIt.testBindDefault() @@ -213,16 +213,16 @@ function TestShiftIt.testResizeWindowInStepsEdgeCases() end function TestShiftIt.testInitialiseSteps() - lu.assertEquals(shiftit.moveStepsX, { 50 }) - lu.assertEquals(shiftit.moveStepsY, { 50 }) - lu.assertEquals(shiftit.nextStepsX, { [50] = 50 }) - lu.assertEquals(shiftit.nextStepsY, { [50] = 50 }) + lu.assertEquals(shiftit.cycleSizesX, { 50 }) + lu.assertEquals(shiftit.cycleSizesY, { 50 }) + lu.assertEquals(shiftit.nextCycleSizeX, { [50] = 50 }) + lu.assertEquals(shiftit.nextCycleSizeY, { [50] = 50 }) - shiftit:setSteps({ 80, 60, 40 }, { 75, 50, 25 }, true) - lu.assertEquals(shiftit.moveStepsX, { 80, 60, 40 }) - lu.assertEquals(shiftit.moveStepsY, { 75, 50, 25 }) - lu.assertEquals(shiftit.nextStepsX, { [40] = 80, [60] = 40, [80] = 60 }) - lu.assertEquals(shiftit.nextStepsY, { [25] = 75, [50] = 25, [75] =50 }) + shiftit:setWindowCyclingSizes({ 80, 60, 40 }, { 75, 50, 25 }, true) + lu.assertEquals(shiftit.cycleSizesX, { 80, 60, 40 }) + lu.assertEquals(shiftit.cycleSizesY, { 75, 50, 25 }) + lu.assertEquals(shiftit.nextCycleSizeX, { [40] = 80, [60] = 40, [80] = 60 }) + lu.assertEquals(shiftit.nextCycleSizeY, { [25] = 75, [50] = 25, [75] =50 }) end function TestShiftIt.testDefaultWindowShifts() @@ -276,7 +276,7 @@ function TestShiftIt.testDefaultWindowShifts() end function TestShiftIt.testMultipleWindowSizeSteps() - shiftit:setSteps({ 50, 75, 25 }, { 50 }, true) + shiftit:setWindowCyclingSizes({ 50, 75, 25 }, { 50 }, true) local tests = { { desc = 'shift to the left through every option once', @@ -310,7 +310,7 @@ function TestShiftIt.testMultipleWindowSizeSteps() end function TestShiftIt.testMultipleWindowSizeStepsWithMultipleWindows() - shiftit:setSteps({ 50, 75, 25 }, { 50 }, true) + shiftit:setWindowCyclingSizes({ 50, 75, 25 }, { 50 }, true) local windowId1 = 99 local windowId2 = 88 From b813d32dc22b1cf56cdb767d01f0b43617226d68 Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sat, 3 Sep 2022 18:36:09 +0200 Subject: [PATCH 08/11] Updated existing documentation formatting and key overriding example --- README.md | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 195e332..56ed86a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Install Hammerspoon if you haven't yet. Download the [latest release here](https Alternatively you can install it using brew: ```bash -brew install --cask hammerspoon +brew install --cask hammerspoon ``` #### Step 2 @@ -112,22 +112,22 @@ The default key mapping looks like this: ```lua { - left = {{ 'ctrl', 'alt', 'cmd' }, 'left' }, - right = {{ 'ctrl', 'alt', 'cmd' }, 'right' }, - up = {{ 'ctrl', 'alt', 'cmd' }, 'up' }, - down = {{ 'ctrl', 'alt', 'cmd' }, 'down' }, - upleft = {{ 'ctrl', 'alt', 'cmd' }, '1' }, - upright = {{ 'ctrl', 'alt', 'cmd' }, '2' }, - botleft = {{ 'ctrl', 'alt', 'cmd' }, '3' }, - botright = {{ 'ctrl', 'alt', 'cmd' }, '4' }, - maximum = {{ 'ctrl', 'alt', 'cmd' }, 'm' }, - toggleFullScreen = {{ 'ctrl', 'alt', 'cmd' }, 'f' }, - toggleZoom = {{ 'ctrl', 'alt', 'cmd' }, 'z' }, - center = {{ 'ctrl', 'alt', 'cmd' }, 'c' }, - nextScreen = {{ 'ctrl', 'alt', 'cmd' }, 'n' }, - previousScreen = {{ 'ctrl', 'alt', 'cmd' }, 'p' }, - resizeOut = {{ 'ctrl', 'alt', 'cmd' }, '=' }, - resizeIn = {{ 'ctrl', 'alt', 'cmd' }, '-' } + left = { { 'ctrl', 'alt', 'cmd' }, 'left' }, + right = { { 'ctrl', 'alt', 'cmd' }, 'right' }, + up = { { 'ctrl', 'alt', 'cmd' }, 'up' }, + down = { { 'ctrl', 'alt', 'cmd' }, 'down' }, + upleft = { { 'ctrl', 'alt', 'cmd' }, '1' }, + upright = { { 'ctrl', 'alt', 'cmd' }, '2' }, + botleft = { { 'ctrl', 'alt', 'cmd' }, '3' }, + botright = { { 'ctrl', 'alt', 'cmd' }, '4' }, + maximum = { { 'ctrl', 'alt', 'cmd' }, 'm' }, + toggleFullScreen = { { 'ctrl', 'alt', 'cmd' }, 'f' }, + toggleZoom = { { 'ctrl', 'alt', 'cmd' }, 'z' }, + center = { { 'ctrl', 'alt', 'cmd' }, 'c' }, + nextScreen = { { 'ctrl', 'alt', 'cmd' }, 'n' }, + previousScreen = { { 'ctrl', 'alt', 'cmd' }, 'p' }, + resizeOut = { { 'ctrl', 'alt', 'cmd' }, '=' }, + resizeIn = { { 'ctrl', 'alt', 'cmd' }, '-' } } ``` @@ -136,10 +136,13 @@ The default key mapping looks like this: You can pass the part of the key mappings that you want to override to the `bindHotkeys()` function. For example: ```lua +-- Use Vim arrow keys spoon.ShiftIt:bindHotkeys({ - upleft = {{ 'ctrl', 'alt', 'cmd' }, 'q' }, - upright = {{ 'ctrl', 'alt', 'cmd' }, 'w' }, -}); + left = { { 'ctrl', 'alt', 'cmd' }, 'h' }, + down = { { 'ctrl', 'alt', 'cmd' }, 'j' }, + up = { { 'ctrl', 'alt', 'cmd' }, 'k' }, + right = { { 'ctrl', 'alt', 'cmd' }, 'l' }, +}) ``` ## Alternative installations From 317879ddc66302a01a99c1cbc5d36a1f00ceb39b Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sat, 3 Sep 2022 18:37:55 +0200 Subject: [PATCH 09/11] Update documentation with new window cycle size configuration --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 56ed86a..f9492e4 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,13 @@ Go to `System Preferences > Security & Privacy > Accessibility` and make sure Ha _If you just enabled permissions for Hammerspoon, you might need to restart the application for the permissions to take effect._ +#### Step 5 (optional) + +Configure the Shiftit spoon to your preference. + +- [Multiple window cycle sizes](https://github.com/peterklijn/hammerspoon-shiftit#configure-multiple-window-cycle-sizes) allows you to override the default 50% window size for snapping to [sides](https://github.com/peterklijn/hammerspoon-shiftit#snap-to-sides) and [corners](https://github.com/peterklijn/hammerspoon-shiftit#snap-to-corners) +- [Override key mappings](https://github.com/peterklijn/hammerspoon-shiftit#overriding-key-mappings) allows you to override the default key bindings. + The ShiftIt spoon is now ready to use, enjoy. Having issues? Check out the [Known issues](https://github.com/peterklijn/hammerspoon-shiftit#known-issues) section, have a look in the [issues section](https://github.com/peterklijn/hammerspoon-shiftit/issues), or create a new issue. @@ -131,6 +138,17 @@ The default key mapping looks like this: } ``` +### Configure multiple window cycle sizes + +You can configure multiple window cycle sizes by adding the following line after loading the ShiftIt spoon: + +```lua +spoon.ShiftIt:setWindowCyclingSizes({ 50, 33, 67 }, { 50 }) +``` + +The first argument (`{ 50, 33, 67 }`) sets the horizontal window cycle sizes, in the provided order. +The second argument (`{ 50 }`) sets the vertical window cycle sizes, in this example it only sets one. + ### Overriding key mappings You can pass the part of the key mappings that you want to override to the `bindHotkeys()` function. For example: From 971cdb58df2df2ff7b8f264af01d2a7d68288d6d Mon Sep 17 00:00:00 2001 From: Peter Klijn Date: Sun, 11 Sep 2022 13:51:08 +0200 Subject: [PATCH 10/11] Bump version --- Spoons/ShiftIt.spoon.zip | Bin 2067 -> 2839 bytes init.lua | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Spoons/ShiftIt.spoon.zip b/Spoons/ShiftIt.spoon.zip index 473d44f82a127a9edce8220293d736900f8156d6..55a97eb5e0a0f1fb265fd3e0c74a5fd36c58d0c3 100644 GIT binary patch delta 2675 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 delta 1907 zcmZ{lc{~%0AIIHt-{d~)apoCo!jOlYt5*HOC}DFYIhq)ZWwYkLzxgWLc^N- zNUhw7qRlNiOMZ_(e*Znc@9Xt>egF8pKYzdTWO3rcP-ku)L5|}yzLO<<$}qb}8Gf9I zC?O6`o^4JJ4j$=$%46*KpZEvUyUv)!&`|HotdgHc4{9db|hfDN(hvGn>8UI=H^X z>FMcA>Xm|bZM}(~^<>w-Ue!PW^C25aZ5dI>%R(Y=F?SH})WYmm3)QJSxmebt>gJCcKd{jW{c_!Hj5ULUiu#wtAItHVL^8jkiBJ>;W7 zlGhT;9}#sebeI=eVC=#OHB$9W@-fudt)AIxs|xlG!vV%;Mpw$hrVZk-fR<#*6f}sv zlK_^_*Qd8LpFvocbVaH~yBxULfV!={;_bcE03LKddBDJ!p3pyk^YptX6=E@S&7Lf0 zx~CBPeH^TzeTjB5PCDc}#+NV2VYKa)_YQKHsu!uo^Py@Ia z)? zFyI$!RXX##6jZt}qR3*GIhW?zWnYX;5l58*Dq@T@u>w(QT0TplTvkmSm8L>Nmr%nHPE8Ibhia# z7w=9867Me7?jIn`&H-R9T+b1Oc%-0;fE~O=lQu~IS9ML?BbzSI-{4&RBGDR#F;_#LvDJj%5Tt^7Wx)NP*k_Pxv`@{MW*Q|t zvKKyNR^fSsbw}?z^HgeK(PF^PmLRFl8*2A$IUeS*6{7jc(yc7QVNbK17YLAq&1_2` zZ9lyVy*Bf3CxSZlNH#k0wg;gj!x$ed5EH4@PyJOyjIHiYGmrPL(90;E+rz&yl<%A<>nDZ{>c7SbpAA6#toSi7 zfHHPp<2LauQrugjs+{!|A!u0%Xj&n6Y?9TKmd#1`yV3XRHh7o$&Ed0i7}Kwx7Vezg zUxYKQES(Mab#5*sT_#+*&7Likjx+?5>QTuVX_^$8D6?!ratWH-c5imS2}bj4j+lu8w2Q&NC&O>cGEg<# zI`8>=#P6zxD#YFa(z^TJd6-{VDScKRJ+V5Ez6&{EI+wbS1P2xhsgRRw$koB@!ZAe2 zgDC+F7alfi_BOWSI#Kzy+(3N@?8<|tiykU#6suLt+V1cCXC_JA{Ndokh;OQb@L?a- zZll;gx63z#37T`~h9yeB@gdgIEi--hLYJ-}v?04rOJBCNCOekyLq8vKUMM;8--|c# z{|pe59+5*vrpqT*){a@`mi-K!J`a>8zHaz=PNZ0@+Vqr2caTY Date: Sun, 11 Sep 2022 14:15:43 +0200 Subject: [PATCH 11/11] Added a visualisation in for the window cycling sizes --- README.md | 4 + docs/window-cycling-sizes.html | 121 +++++++++++++++++++++ images/window-cycling-sizes-visualised.png | Bin 0 -> 48375 bytes 3 files changed, 125 insertions(+) create mode 100644 docs/window-cycling-sizes.html create mode 100644 images/window-cycling-sizes-visualised.png diff --git a/README.md b/README.md index f9492e4..214cc3b 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,10 @@ spoon.ShiftIt:setWindowCyclingSizes({ 50, 33, 67 }, { 50 }) The first argument (`{ 50, 33, 67 }`) sets the horizontal window cycle sizes, in the provided order. The second argument (`{ 50 }`) sets the vertical window cycle sizes, in this example it only sets one. +The above settings will toggle the window through these steps, when repeatingly hitting `ctrl(^) + alt(⌥) + cmd(⌘) + left`: + +![Window Cycling Sizes visualised for left action](https://github.com/peterklijn/hammerspoon-shiftit/blob/master/images/window-cycling-sizes-visualised.png?raw=true) + ### Overriding key mappings You can pass the part of the key mappings that you want to override to the `bindHotkeys()` function. For example: diff --git a/docs/window-cycling-sizes.html b/docs/window-cycling-sizes.html new file mode 100644 index 0000000..f2d2304 --- /dev/null +++ b/docs/window-cycling-sizes.html @@ -0,0 +1,121 @@ + + + + + + +
+ +
+
+
+ step 2: 33% +
+ step 1: 50% +
+ step 3: 67% +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/images/window-cycling-sizes-visualised.png b/images/window-cycling-sizes-visualised.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f541ece182ee061152b3a933af1857b0b1ff33 GIT binary patch literal 48375 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 M?Yb*>r`P#^1NEisYXATM literal 0 HcmV?d00001