-
-
Save spartanatreyu/850788a0441e1c5565668a35ed9a1dfc to your computer and use it in GitHub Desktop.
| --Hammerspoon config to replace Cinch & Size-up (Microsoft Windows style) window management for free | |
| --Windows Vista/7's Areo Snap on MacOS | |
| --By Jayden Pearse (spartanatreyu) | |
| ------------------------------------------------------------------- | |
| --Options, feel free to edit these: | |
| ------------------------------------------------------------------- | |
| --Set this to true to snap windows by dragging them to the edge of your screen | |
| enable_window_snapping_with_mouse = true | |
| --Set this to true to snap windows using keyboard shortcuts (eg. Ctrl + Option + Right Arrow) | |
| enable_window_snapping_with_keyboard = true | |
| --Will say the name of the window and its position for (in seconds) | |
| --Set to 0 to disable | |
| show_window_notification_duration = 1 | |
| --the height of the window's title area (in pixels), can change if you have different sized windows (might happen one day) | |
| --or need a different window grabbing sensitivity. Chrome is a little weird since its title area's height is non-standard | |
| window_titlebar_height = 21 | |
| --the amount (in pixels) around the edge of the screen in which the mouse has to be let go for the drag window to count | |
| monitor_edge_sensitivity = 1 | |
| --The time (in seconds) it takes for a window to transition to its new position and size | |
| hs.window.animationDuration = 0 | |
| ------------------------------------------------------------------- | |
| --Don't edit this section | |
| --Boilerplate init code, don't edit this section | |
| ------------------------------------------------------------------- | |
| --required to be non zero for dragging windows to work some weird timing issue with hammerspoon fighting against osx events | |
| if hs.window.animationDuration <= 0 then | |
| hs.window.animationDuration = 0.00000001 | |
| end | |
| --flag for dragging, 0 means no drag, 1 means dragging a window, -1 means dragging but not dragging the window | |
| dragging = 0 | |
| --the window being dragged | |
| dragging_window = nil | |
| -- Exists because lua doesn't have a round function. WAT?! | |
| function round(num) | |
| return math.floor(num + 0.5) | |
| end | |
| --based on kizzx2's hammerspoon-move-resize.lua | |
| function get_window_under_mouse() | |
| -- Invoke `hs.application` because `hs.window.orderedWindows()` doesn't do it | |
| -- and breaks itself | |
| local _ = hs.application | |
| local my_pos = hs.geometry.new(hs.mouse.getAbsolutePosition()) | |
| local my_screen = hs.mouse.getCurrentScreen() | |
| return hs.fnutils.find(hs.window.orderedWindows(), function(w) | |
| return my_screen == w:screen() and my_pos:inside(w:frame()) | |
| end) | |
| end | |
| ------------------------------------------------------------------- | |
| --Window snapping with mouse, Windows style (Cinch Alternative) | |
| ------------------------------------------------------------------- | |
| --Setup drag start and dragging | |
| click_event = hs.eventtap.new({hs.eventtap.event.types.leftMouseDragged}, function(e) | |
| --if drag is just starting... | |
| if dragging == 0 then | |
| dragging_window = get_window_under_mouse() | |
| --if mouse over a window... | |
| if dragging_window ~= nil then | |
| local m = hs.mouse.getAbsolutePosition() | |
| local mx = round(m.x) | |
| local my = round(m.y) | |
| --print('mx: ' .. mx .. ', my: ' .. my) | |
| local f = dragging_window:frame() | |
| local screen = dragging_window:screen() | |
| local max = screen:frame() | |
| --print('fx: ' .. f.x .. ', fy: ' .. f.y .. ', fw: ' .. f.w .. ', fh: ' .. f.h) | |
| --if mouse inside titlebar horizontally | |
| if mx > f.x and mx < (f.x + f.w) then | |
| --print('mouse is inside titlebar horizontally') | |
| --if mouse inside titlebar vertically | |
| if my > f.y and my < (f.y + window_titlebar_height) then | |
| --print('mouse is inside titlebar') | |
| dragging = 1 | |
| --print(' - start dragging - window: ' .. dragging_window:id()) | |
| else | |
| --print('mouse is not inside titlebar') | |
| dragging = -1 | |
| dragging_window = nil | |
| end | |
| else | |
| --print('mouse is not inside titlebar horizontally') | |
| dragging = -1 | |
| dragging_window = nil | |
| end | |
| end | |
| --else if drag is already going | |
| --[[ | |
| else | |
| if dragging_window ~= nil then | |
| local dx = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaX) | |
| local dy = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaY) | |
| local m = hs.mouse.getAbsolutePosition() | |
| local mx = round(m.x) | |
| local my = round(m.y) | |
| print(' - dragging: ' .. mx .. "," .. my .. ". window id: " .. dragging_window:id()) | |
| end | |
| ]]-- | |
| end | |
| end) | |
| --Setup drag end | |
| unclick_event = hs.eventtap.new({hs.eventtap.event.types.leftMouseUp}, function(e) | |
| --print('unclick, dragging: ' .. dragging) | |
| --if dragging the mouse | |
| if dragging == 1 then | |
| --if the mouse is dragging a window | |
| if dragging_window ~= nil then | |
| --print('letting go of window: ' .. dragging_window:id()) | |
| local m = hs.mouse.getAbsolutePosition() | |
| local mx = round(m.x) | |
| local my = round(m.y) | |
| --print('mx: ' .. mx .. ', my: ' .. my) | |
| local win = dragging_window | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| if mx < monitor_edge_sensitivity and my < monitor_edge_sensitivity then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Top Left", show_window_notification_duration) | |
| f.x = max.x | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| elseif mx > monitor_edge_sensitivity and mx < (max.w - monitor_edge_sensitivity) and my < monitor_edge_sensitivity then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Full", show_window_notification_duration) | |
| f.x = max.x | |
| f.y = max.y | |
| f.w = max.w | |
| f.h = max.h | |
| win:setFrame(f) | |
| elseif mx > (max.w - monitor_edge_sensitivity) and my < monitor_edge_sensitivity then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Top Right", show_window_notification_duration) | |
| f.x = max.x + (max.w / 2) | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| elseif mx < monitor_edge_sensitivity and my < (max.h - monitor_edge_sensitivity) and my > monitor_edge_sensitivity then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Left", show_window_notification_duration) | |
| f.x = max.x | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h | |
| win:setFrame(f) | |
| elseif mx > (max.w - monitor_edge_sensitivity) and my > monitor_edge_sensitivity and my < (max.h - monitor_edge_sensitivity) then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Right", show_window_notification_duration) | |
| f.x = max.x + (max.w / 2) | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h | |
| win:setFrame(f) | |
| elseif mx < monitor_edge_sensitivity and my > (max.h - monitor_edge_sensitivity) then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Bottom Left", show_window_notification_duration) | |
| f.x = max.x | |
| f.y = max.y + (max.h / 2) | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| elseif mx > (max.w - monitor_edge_sensitivity) and my > (max.h - monitor_edge_sensitivity) then | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Bottom Right", show_window_notification_duration) | |
| f.x = max.x + (max.w / 2) | |
| f.y = max.y + (max.h / 2) | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| end | |
| end | |
| --print("end dragging") | |
| end | |
| dragging = 0 | |
| dragging_window = nil | |
| end) | |
| --Start watching for dragging (AKA: turn dragging on) | |
| if enable_window_snapping_with_mouse == true then | |
| click_event:start() | |
| unclick_event:start() | |
| end | |
| ------------------------------------------------------------------- | |
| --Window snapping with Keyboard, Windows style (Sizeup Alternative) | |
| ------------------------------------------------------------------- | |
| if enable_window_snapping_with_keyboard == true then | |
| --Decided to bind the keys to Control and Option because these keys are unbinded by default in all the applications | |
| --that i've personally come across. I could bind it to command (the same place as the windows key on a non-mac keyboard) | |
| --but then i'd have to go through and change a lot of shortcuts in a lot of programs. | |
| hs.hotkey.bind({"alt", "ctrl"}, "Left", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Left", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h | |
| win:setFrame(f) | |
| end) | |
| hs.hotkey.bind({"alt", "ctrl"}, "Right", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Right", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x + (max.w / 2) | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h | |
| win:setFrame(f) | |
| end) | |
| hs.hotkey.bind({"alt", "ctrl"}, "Up", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Full", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x | |
| f.y = max.y | |
| f.w = max.w | |
| f.h = max.h | |
| win:setFrame(f) | |
| end) | |
| hs.hotkey.bind({"alt", "ctrl"}, "Down", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Center", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x + (max.w / 4) | |
| f.y = max.y + (max.h / 4) | |
| f.w = max.w / 2 | |
| f.h = max.h / 1.5 | |
| win:setFrame(f) | |
| end) | |
| --You either can't assign keyboard shortcuts with two arrows or I haven't figured out how yet | |
| --So instead we just push the three bottom modifier keys (Control + Option + Command) for corners | |
| --Feel free to switch bindings around, some peple like the twisted keys one way and some the other | |
| hs.hotkey.bind({"alt", "ctrl", "cmd"}, "Up", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Top Left", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| end) | |
| hs.hotkey.bind({"alt", "ctrl", "cmd"}, "Right", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Top Right", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x + (max.w / 2) | |
| f.y = max.y | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| end) | |
| hs.hotkey.bind({"alt", "ctrl", "cmd"}, "Down", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Bottom Right", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x + (max.w / 2) | |
| f.y = max.y + (max.h / 2) | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| end) | |
| hs.hotkey.bind({"alt", "ctrl", "cmd"}, "Left", function() | |
| hs.alert.show(hs.application.frontmostApplication():title() .. " Bottom Left", show_window_notification_duration) | |
| local win = hs.window.focusedWindow() | |
| local f = win:frame() | |
| local screen = win:screen() | |
| local max = screen:frame() | |
| f.x = max.x | |
| f.y = max.y + (max.h / 2) | |
| f.w = max.w / 2 | |
| f.h = max.h / 2 | |
| win:setFrame(f) | |
| end) | |
| -- this "end" is to make sure the keyboard shortcuts don't work if enable_window_snapping_with_keyboard is set to false | |
| end |
@dmitrym0 Yeah, I've noticed this too on one of the Macbooks someone where I work has. For all of Hammerspoon's api style woes it does support multi-monitor: http://www.hammerspoon.org/docs/hs.screen.html
It would probably be simple to check which monitor the window is in when moving and to setup a shortcut to move the windows between the screens. Unfortunately I don't have multiple monitors or even a mac at home so it's a little difficult to test.
Would be a good little holiday project if you want to learn a new language. Lua has well defined docs with plenty of examples on Stack Overflow (I made this gist in a day).
Hammerspoon also has a debug console that runs while using it so it wouldn't be hard to put in a print statement print('mx: ' .. mx .. ', my: ' .. my) to output any variables in any function if you want to play around with it. (.. is how you add strings and/or variable in lua).
Awesome work. Works really well for a single monitor setup, but with multiple monitors, freaks out when moving windows from primary to secondary, and refuses to work on secondary.