-
-
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.