Skip to content

Instantly share code, notes, and snippets.

@spartanatreyu
Last active April 15, 2022 13:24
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save spartanatreyu/850788a0441e1c5565668a35ed9a1dfc to your computer and use it in GitHub Desktop.
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
--By Jayden Pearse (spartanatreyu)
--Sort of messy, forgive me. Never scripted in lua before
-------------------------------------------------------------------
-------------------------------------------------------------------
--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
Copy link

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.

@spartanatreyu
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment