Skip to content

Instantly share code, notes, and snippets.

@kizzx2
Last active December 19, 2022 06:47
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kizzx2/e542fa74b80b7563045a to your computer and use it in GitHub Desktop.
Save kizzx2/e542fa74b80b7563045a to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link

deryni 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
Copy link

deryni commented Apr 19, 2016

@kizzx2 Is there a license for this?

@jdtsmith
Copy link

jdtsmith 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
Copy link

kynetiv 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
Copy link

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

@harob
Copy link

harob commented Nov 27, 2019

I've been happily using this for years, but now that I've upgraded to OSX Catalina it's extremely laggy. The window moves with delay relative to cursor movement, and the modifier keys seem to stay activated for a few seconds after unpressing them. Has anyone else encountered this or found a fix?

@joaofnds
Copy link

Hi guys, I just found out about hammerspoon. After spending a weekend on linux I found myself trying to alt+leftClick to drag and alt+rightClick to resize, so I went looked for a solution and here I am :)

Here's what I did based on the script I found here. This implements cmd + leftClick for moving and cmd + rightClick for resizing.

function get_window_under_mouse()
  local pos = hs.geometry.new(hs.mouse.getAbsolutePosition())
  local screen = hs.mouse.getCurrentScreen()

  return hs.fnutils.find(hs.window.orderedWindows(), function(w)
    return screen == w:screen() and pos:inside(w:frame())
  end)
end

dragging_window = nil

drag_event = hs.eventtap.new(
  {
    hs.eventtap.event.types.leftMouseDragged,
    hs.eventtap.event.types.rightMouseDragged,
  }, function(e)
    if not dragging_win then return nil end

    local dx = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaX)
    local dy = e:getProperty(hs.eventtap.event.properties.mouseEventDeltaY)
    local mouse = hs.mouse:getButtons()

    if mouse.left then
      dragging_win:move({dx, dy}, nil, false, 0)
    elseif mouse.right then
      local sz = dragging_win:size()
      local w1 = sz.w + dx
      local h1 = sz.h + dy
      dragging_win:setSize(w1, h1)
    end
end)

flag_event = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e)
  local flags = e:getFlags()

  if flags.cmd then
    dragging_win = get_window_under_mouse()
    drag_event:start()
  else
    draggin_win = nil
    drag_event:stop()
  end
end)

flag_event:start()

As @harob has said, hammerspoon is getting a little still, I don't know why. If one of you finds a solution, share it with us! :)

@dbalatero
Copy link

Regarding slow FPS when resizing, check out this (closed) issue. Sounds like it needs to be fixed internally somehow: Hammerspoon/hammerspoon#923

@dbalatero
Copy link

Hey all,

I created a spoon that combines some of the techniques here and fixes the slow framerate while resizing:

https://github.com/dbalatero/SkyRocket.spoon

Instead of resizing the window in realtime, I draw an transparent preview box on the screen with hs.canvas, resize that as you drag, then commit the actual resize to the window when you let go of the mouse. It's a compromise for sure, but it's way better than dealing with the laggy framerate when naively trying to resize the window in realtime.

@arrelid
Copy link

arrelid commented Nov 15, 2021

@dbalatero Thanks for the workaround - works like a charm!

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