Last active
August 13, 2024 16:31
-
-
Save spartanatreyu/850788a0441e1c5565668a35ed9a1dfc to your computer and use it in GitHub Desktop.
Personal Window Management script (hammerspoon)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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).