Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hammerspoon script to move/resize window under cursor
-- Inspired by Linux alt-drag or Better Touch Tools move/resize functionality
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
dragging_win = nil
dragging_mode = 1
drag_event = hs.eventtap.new({ hs.eventtap.event.types.mouseMoved }, function(e)
if dragging_win then
local dx = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaX)
local dy = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaY)
local mods = hs.eventtap.checkKeyboardModifiers()
-- Ctrl + Shift to move the window under cursor
if dragging_mode == 1 and mods.ctrl and mods.shift then
dragging_win:move({dx, dy}, nil, false, 0)
-- Alt + Shift to resize the window under cursor
elseif mods.alt and mods.shift then
local sz = dragging_win:size()
local w1 = sz.w + dx
local h1 = sz.h + dy
dragging_win:setSize(w1, h1)
end
end
return nil
end)
flags_event = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e)
local flags = e:getFlags()
if flags.ctrl and flags.shift and dragging_win == nil then
dragging_win = get_window_under_mouse()
dragging_mode = 1
drag_event:start()
elseif flags.alt and flags.shift and dragging_win == nil then
dragging_win = get_window_under_mouse()
dragging_mode = 2
drag_event:start()
else
drag_event:stop()
dragging_win = nil
end
return nil
end)
flags_event:start()
@atsepkov

This comment has been minimized.

Copy link

commented Mar 25, 2016

Out of curiosity, how would one use this? Do I need to map get_window_under_mouse to a key combination? Is it possible to map it to windows + click/drag?

Thanks

@deryni

This comment has been minimized.

Copy link

commented Apr 19, 2016

@atsepkov The code as written is already listening for Ctrl+Shift and Alt+Shift and mouse movement (no clicking though see the next paragraph).

So this doesn't quite work the way I expected it would. It appears to work such that simply holding Ctrl+Shift or Alt+Shift and then moving the mouse will move or resize the window that was under the cursor. That's certainly interesting but not quite what I was imagining. It also doesn't support cancelling the move/resize with escape or similar.

@deryni

This comment has been minimized.

Copy link

commented Apr 19, 2016

@kizzx2 Is there a license for this?

@jdtsmith

This comment has been minimized.

Copy link

commented Oct 21, 2016

Thanks for this. I altered it somewhat to use Command+Ctrl (since that's my default key binding in HS). I also weed out non-standard and maximized windows, which avoids various issues, and use a different resize method that is more reliable for me. You can also switch mid-action between move or resize and the action isn't interrupted. Works great for me! Thanks again.

-- Inspired by Linux alt-drag or Better Touch Tools move/resize functionality
-- from https://gist.github.com/kizzx2/e542fa74b80b7563045a
-- Command-Ctrl-move: move window under mouse
-- Command-Control-Shift-move: resize window under mouse
function get_window_under_mouse()
   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
                                w:isStandard() and
                                (not w:isFullScreen()) and
                                my_pos:inside(w:frame())
   end)
end

dragging = {}                   -- global variable to hold the dragging/resizing state

drag_event = hs.eventtap.new({ hs.eventtap.event.types.mouseMoved }, function(e)
      if not dragging then return nil end 
      if dragging.mode==1 then -- just move
         local dx = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaX)
         local dy = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaY)
         dragging.win:move({dx, dy}, nil, false, 0)
      else -- resize
         local pos=hs.mouse.getAbsolutePosition()
         local w1 = dragging.size.w + (pos.x-dragging.off.x)
         local h1 = dragging.size.h + (pos.y-dragging.off.y)
         dragging.win:setSize(w1, h1)
      end
end)

flags_event = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e)
      local flags = e:getFlags()
      local mode=(flags.ctrl and flags.cmd and 1 or 0) + (flags.shift and 2 or 0)
      if mode==1 or mode==3 then -- valid modes
         if dragging then
            if dragging.mode == mode then return nil end -- already working
         else
            -- only update window if we hadn't started dragging/resizing already
            dragging={win = get_window_under_mouse()}
            if not dragging.win then -- no good window
               dragging=nil
               return nil
            end 
         end
         dragging.mode = mode   -- 1=drag, 3=resize
         if mode==3 then
            dragging.off=hs.mouse.getAbsolutePosition()
            dragging.size=dragging.win:size()
         end
         drag_event:start()
      else                      -- not a valid mode
         if dragging then
            drag_event:stop()
            dragging = nil
         end 
      end
      return nil
end)
flags_event:start()
@kynetiv

This comment has been minimized.

Copy link

commented Mar 10, 2017

Great work here. I've just tweaked @jdtsmith's to use cmd + shift (drag) and alt + shift (resize) as I'm used to zooom2 keybindings.

-- Inspired by Linux alt-drag or Better Touch Tools move/resize functionality
-- from https://gist.github.com/kizzx2/e542fa74b80b7563045a
-- Command-shift-move: move window under mouse
-- Alt-Shift-move: resize window under mouse
function get_window_under_mouse()
   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
                                w:isStandard() and
                                (not w:isFullScreen()) and
                                my_pos:inside(w:frame())
   end)
end

dragging = {}                   -- global variable to hold the dragging/resizing state

drag_event = hs.eventtap.new({ hs.eventtap.event.types.mouseMoved }, function(e)
      if not dragging then return nil end
      if dragging.mode==3 then -- just move
         local dx = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaX)
         local dy = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaY)
         dragging.win:move({dx, dy}, nil, false, 0)
      else -- resize
         local pos=hs.mouse.getAbsolutePosition()
         local w1 = dragging.size.w + (pos.x-dragging.off.x)
         local h1 = dragging.size.h + (pos.y-dragging.off.y)
         dragging.win:setSize(w1, h1)
      end
end)

flags_event = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e)
      local flags = e:getFlags()
      local mode=(flags.shift and 1 or 0) + (flags.cmd and 2 or 0) + (flags.alt and 4 or 0)
      if mode==3 or mode==5 then -- valid modes
         if dragging then
            if dragging.mode == mode then return nil end -- already working
         else
            -- only update window if we hadn't started dragging/resizing already
            dragging={win = get_window_under_mouse()}
            if not dragging.win then -- no good window
               dragging=nil
               return nil
            end
         end
         dragging.mode = mode   -- 3=drag, 5=resize
         if mode==5 then
            dragging.off=hs.mouse.getAbsolutePosition()
            dragging.size=dragging.win:size()
         end
         drag_event:start()
      else                      -- not a valid mode
         if dragging then
            drag_event:stop()
            dragging = nil
         end
      end
      return nil
end)
flags_event:start()
@feifanzhou

This comment has been minimized.

Copy link

commented Aug 26, 2019

Thank you all — this is exactly what I've been looking for!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.