Hammerspoon / hammerspoon

Staggeringly powerful macOS desktop automation with Lua
http://www.hammerspoon.org
MIT License
12.08k stars 582 forks source link

Moving window to space at left our right #823

Closed spadino closed 7 years ago

spadino commented 8 years ago

I'm using that scripts to move my windows to the space at left or right:

-- Switch windows to next space at left or right
function moveWindowOneSpace(direction)
   _mouseOrigin = hs.mouse.getAbsolutePosition()
   local win = hs.window.focusedWindow()
   _clickPoint = win:zoomButtonRect()

   _clickPoint.x = _clickPoint.x + _clickPoint.w + 5
   _clickPoint.y = _clickPoint.y + (_clickPoint.h / 2)

   local mouseClickEvent = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, _clickPoint)
   mouseClickEvent:post()

   hs.timer.usleep(100000)

   local nextSpaceDownEvent = hs.eventtap.event.newKeyEvent({"ctrl"}, direction, true)
   nextSpaceDownEvent:post()
end

function moveWindowOneSpaceEnd(direction)
   local nextSpaceUpEvent = hs.eventtap.event.newKeyEvent({"ctrl"}, direction, false)
   nextSpaceUpEvent:post()
   hs.timer.usleep(100000)
   local mouseReleaseEvent = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, _clickPoint)
   mouseReleaseEvent:post()
   hs.timer.usleep(100000)
   hs.mouse.setAbsolutePosition(_mouseOrigin)
end

hk1 = hs.hotkey.bind({"ctrl","cmd"}, "right",
   function() moveWindowOneSpace("right") end)
hk2 = hs.hotkey.bind({"ctrl","cmd"}, "left",
   function() moveWindowOneSpace("left") end)

Although, the window is not fixed on the switched space. If I move the window, than moving back on the previous space, the window follow the space switching. Anyone know how can I prevent it? Also, anyone have a clue to just send a window to the space without actually switching to that space?

spadino commented 8 years ago

I noticed that if I click on the titlebar after the window switch, it correctly sticky to the space. So, I need to insert a click on the title bar after the window movement, but I'm not sure how to do this...

szymonkaliski commented 8 years ago

Check this thread for solutions: https://github.com/Hammerspoon/hammerspoon/issues/581

My working version: https://github.com/szymonkaliski/Dotfiles/blob/5ab45f6392a0c1a5354f14a9871589fea2e67d26/Dotfiles/hammerspoon/ext/window.lua#L197-L251

heptal commented 8 years ago

Note that szymonkaliski's solution uses asmagill's spaces module which you will need to put in your ~/.hammerspoon/ as detailed on the readme before using.

Spaces are not officially supported because there is no public API, but asmagill's module seems to work reliably.

szymonkaliski commented 8 years ago

Oh yeah, forgot to mention that, sorry :)

spadino commented 8 years ago

Many thanks for pointing me to the right direction. Anyway, it's really difficult to understand how can I put it in a simple start setup. By now, I just need that feature. Can someone kindly instruct me how to implement such feature? The Dotfiles repo are very difficult to understand...

Szymon Kaliski wrote:

Oh yeah, forgot to mention that, sorry :)

— Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/823#issuecomment-192958673.


Andrea Spada :: Liuteria d'Autore www.andreaspada.com

heptal commented 8 years ago

szymonkaliski's hammerspoon config is a bit more sophisticated than most, but full of great stuff and worth taking the time to decipher.

First download the latest release of asmagill's spaces module. Extract it and move it to your ~/.hammerspoon/ wholesale (so that ~/.hammerspoon/hs/_asm/undocumented/spaces/internal.so exists).

Anyway, to rip out his screen-switching setup as a single-file (hope he doesn't mind 😬), put this in your init.lua (you can just comment out the os.execute(template([[ /usr/local/bin/cliclick... line in focusScreen or simply brew install cliclick first):

-- require traverses directories in your ~/.hammerspoon folder, with directory levels separated by dots
local spaces = require('hs._asm.undocumented.spaces')

local cache = {
  mousePosition = nil
}

-- grabs screen with active window, unless it's Finder's desktop
-- then we use mouse position
local function activeScreen()
  local mousePoint = hs.geometry.point(hs.mouse.getAbsolutePosition())
  local activeWindow = hs.window.focusedWindow()

  if activeWindow and activeWindow:role() ~= 'AXScrollArea' then
    return activeWindow:screen()
  else
    return hs.fnutils.find(hs.screen.allScreens(), function(screen)
        return mousePoint:inside(screen:frame())
      end)
  end
end

local function focusScreen(screen)
  local frame = screen:frame()

  -- if mouse is already on the given screen we can safely return
  if hs.geometry(hs.mouse.getAbsolutePosition()):inside(frame) then return false end

  -- "hide" cursor in the lower right side of screen
  -- it's invisible while we are changing spaces
  local mousePosition = {
    x = frame.x + frame.w - 1,
    y = frame.y + frame.h - 1
  }

  -- hs.mouse.setAbsolutePosition doesn't work for gaining proper screen focus
  -- moving the mouse pointer with cliclick (available on homebrew) works
  os.execute(template([[ /usr/local/bin/cliclick m:={X},{Y} ]], { X = mousePosition.x, Y = mousePosition.y }))
  hs.timer.usleep(1000)

  return true
end

local function activeSpaceIndex(screenSpaces)
  return hs.fnutils.indexOf(screenSpaces, spaces.activeSpace()) or 1
end

local function screenSpaces(currentScreen)
  currentScreen = currentScreen or activeScreen()
  return spaces.layout()[currentScreen:spacesUUID()]
end

local function spaceInDirection(direction)
  local screenSpaces = screenSpaces()
  local activeIdx = activeSpaceIndex(screenSpaces)
  local targetIdx = direction == 'west' and activeIdx - 1 or activeIdx + 1

  return screenSpaces[targetIdx]
end

local function moveToSpaceInDirection(win, direction)
  local clickPoint = win:zoomButtonRect()
  local sleepTime = 1000
  local targetSpace = spaceInDirection(direction)

  -- check if all conditions are ok to move the window
  local shouldMoveWindow = hs.fnutils.every({
      clickPoint ~= nil,
      targetSpace ~= nil,
      not cache.movingWindowToSpace
    }, function(test) return test end)

  if not shouldMoveWindow then return end

  cache.movingWindowToSpace = true

  cache.mousePosition = cache.mousePosition or hs.mouse.getAbsolutePosition()

  clickPoint.x = clickPoint.x + clickPoint.w + 5
  clickPoint.y = clickPoint.y + clickPoint.h / 2

  -- fix for Chrome UI
  if win:application():title() == 'Google Chrome' then
    clickPoint.y = clickPoint.y - clickPoint.h
  end

  -- focus screen before switching window
  focusScreen(win:screen())

  hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, clickPoint):post()
  hs.timer.usleep(sleepTime)

  hs.eventtap.keyStroke({ 'ctrl' }, direction == 'east' and 'right' or 'left')

  hs.timer.waitUntil(
    function()
      return spaces.activeSpace() == targetSpace
    end,
    function()
      hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, clickPoint):post()

      -- resetting mouse after small timeout is needed for focusing screen to work properly
      hs.mouse.setAbsolutePosition(cache.mousePosition)
      cache.mousePosition = nil

      -- reset cache
      cache.movingWindowToSpace = false

    end,
    0.01 -- check every 1/100 of a second
  )
end

hs.fnutils.each({
    { key = 'home', direction = 'west' },
    { key = 'end', direction = 'east' }
  }, function(object)
    hs.hotkey.bind({"cmd", "option"}, object.key, nil, function() moveToSpaceInDirection(hs.window.frontmostWindow(), object.direction) end)
  end)

It's definitely worth breaking big functionality like this into separate files. You can save the above as something like switchspaces.lua and require 'switchspaces' in your init.lua.

spadino commented 8 years ago

Wow, thank's a lot for your help! I did as you wrote, and when reloading the config the console display this:

-- Lazy extension loading enabled -- Loading ~/.hammerspoon/init.lua -- Loading extension: fnutils -- Loading extension: hotkey 19:55:04 hotkey: Enabled hotkey ⌘⌥HOME hotkey: Enabled hotkey ⌘⌥END -- Done. -- Loading extension: window -- Loading extension: uielement -- Loading extension: geometry -- Loading extension: mouse

However, if I do ⌘⌥HOME in a window, it displays that: *\ ERROR: hs.hotkey callback error: /Users/andrea/.hammerspoon/init.lua:50: attempt to call a nil value (field 'layout') stack traceback: /Users/andrea/.hammerspoon/init.lua:50: in upvalue 'screenSpaces' /Users/andrea/.hammerspoon/init.lua:54: in upvalue 'spaceInDirection' /Users/andrea/.hammerspoon/init.lua:64: in upvalue 'moveToSpaceInDirection' /Users/andrea/.hammerspoon/init.lua:118: in function </Users/andrea/.hammerspoon/init.lua:118>

I do not know enough Hammerspoon to understand what is wrong here...

Michael B wrote:

szymonkaliski's hammerspoon config is a bit more sophisticated than most, but full of great stuff and worth taking the time to decipher.

First download the latest release of asmagill's spaces module https://github.com/asmagill/hs._asm.undocumented.spaces/releases. Extract it and move it to your |~/.hammerspoon/| wholesale (so that |~/.hammerspoon/hs/_asm/undocumented/spaces/internal.so| exists).

Anyway, to rip out his screen-switching setup as a single-file (hope he doesn't mind 😬), put this in your |init.lua| (you can just comment out the |os.execute(template([[ /usr/local/bin/cliclick...| line in |focusScreen| or simply |brew install cliclick| first):

-- require traverses directories in your ~/.hammerspoon folder, with directory levels separated by dots local spaces= require('hs._asm.undocumented.spaces')

local cache= { mousePosition= nil }

-- grabs screen with active window, unless it's Finder's desktop -- then we use mouse position local function activeScreen() local mousePoint= hs.geometry.point(hs.mouse.getAbsolutePosition()) local activeWindow= hs.window.focusedWindow()

if activeWindowand activeWindow:role()~= 'AXScrollArea' then return activeWindow:screen() else return hs.fnutils.find(hs.screen.allScreens(),function(screen) return mousePoint:inside(screen:frame()) end) end end

local function focusScreen(screen) local frame= screen:frame()

-- if mouse is already on the given screen we can safely return if hs.geometry(hs.mouse.getAbsolutePosition()):inside(frame)then return false end

-- "hide" cursor in the lower right side of screen -- it's invisible while we are changing spaces local mousePosition= { x= frame.x + frame.w - 1, y= frame.y + frame.h - 1 }

-- hs.mouse.setAbsolutePosition doesn't work for gaining proper screen focus -- moving the mouse pointer with cliclick (available on homebrew) works os.execute(template([[ /usr/local/bin/cliclick m:={X},{Y}]], { X= mousePosition.x, Y= mousePosition.y })) hs.timer.usleep(1000)

return true end

local function activeSpaceIndex(screenSpaces) return hs.fnutils.indexOf(screenSpaces, spaces.activeSpace())or 1 end

local function screenSpaces(currentScreen) currentScreen= currentScreenor activeScreen() return spaces.layout()[currentScreen:spacesUUID()] end

local function spaceInDirection(direction) local screenSpaces= screenSpaces() local activeIdx= activeSpaceIndex(screenSpaces) local targetIdx= direction== 'west' and activeIdx- 1 or activeIdx+ 1

return screenSpaces[targetIdx] end

local function moveToSpaceInDirection(win, direction) local clickPoint= win:zoomButtonRect() local sleepTime= 1000 local targetSpace= spaceInDirection(direction)

-- check if all conditions are ok to move the window local shouldMoveWindow= hs.fnutils.every({ clickPoint~= nil, targetSpace~= nil, not cache.movingWindowToSpace },function(test)return testend)

if not shouldMoveWindowthen return end

cache.movingWindowToSpace = true

cache.mousePosition = cache.mousePosition or hs.mouse.getAbsolutePosition()

clickPoint.x = clickPoint.x + clickPoint.w + 5 clickPoint.y = clickPoint.y + clickPoint.h / 2

-- fix for Chrome UI if win:application():title()== 'Google Chrome' then clickPoint.y = clickPoint.y - clickPoint.h end

-- focus screen before switching window focusScreen(win:screen())

hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, clickPoint):post() hs.timer.usleep(sleepTime)

hs.eventtap.keyStroke({'ctrl' }, direction== 'east' and 'right' or 'left')

hs.timer.waitUntil( function() return spaces.activeSpace()== targetSpace end, function() hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, clickPoint):post()

   -- resetting mouse after small timeout is needed for focusing screen to work properly
   hs.mouse.setAbsolutePosition(cache.mousePosition)
   cache.mousePosition  =  nil

   -- reset cache
   cache.movingWindowToSpace  =  false

 end,
 0.01  -- check every 1/100 of a second

) end

hs.fnutils.each({ { key= 'home', direction= 'west' }, { key= 'end', direction= 'east' } },function(object) hs.hotkey.bind({"cmd","option"}, object.key,nil,function()moveToSpaceInDirection(hs.window.frontmostWindow(), object.direction)end) end)

It's definitely worth breaking big functionality like this into separate files. You can save the above as something like |switchspaces.lua| and |require 'switchspaces'| in your |init.lua|.

— Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/823#issuecomment-193008104.


Andrea Spada :: Liuteria d'Autore www.andreaspada.com

heptal commented 8 years ago

Are you using the latest version of the spaces module?

spadino commented 8 years ago

Yes!

Michael B wrote:

Are you using the latest version of the spaces module?

— Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/823#issuecomment-193014766.


Andrea Spada :: Liuteria d'Autore www.andreaspada.com

heptal commented 8 years ago

Well, it must not latest because your error says that spaces.layout is nil, and the layout function was not present in the earliest version.

spadino commented 8 years ago

I'll check, but I just downloaded it from Github. I'll try to compile from source, synched with master, than I'll let you know. May thanks for your help, really! A.

Michael B wrote:

Well, it must not latest because your error says that |spaces.layout| is |nil|, and the layout function was not present in the earliest version.

— Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/823#issuecomment-193038459.


Andrea Spada :: Liuteria d'Autore www.andreaspada.com

szymonkaliski commented 8 years ago

@andreaspada you actually don't need to call focusScreen at all, it's just something I've added for myself, to make switching spaces on multiple displays switch the space with active window, and not where the mouse actually is, but you might not want that, so it could be even more simplified. And I compile spaces addon from source, so you might want to do that as well. Good luck! And thanks @heptal for jumping in while I was sleeping :)

spadino commented 8 years ago

So, compiled from source - synched with master branch, installed and config reload. On OSX 10.11.3. Everything ok. Xcode updated - gui and cli. Brew updated. Not working yet. Here it is the output of the console...

-- Lazy extension loading enabled -- Loading ~/.hammerspoon/init.lua -- Loading extension: uielement -- Loading extension: fnutils -- Loading extension: hotkey 09:33:19 hotkey: Enabled hotkey ⌘⌃LEFT hotkey: Enabled hotkey ⌘⌃RIGHT -- Done.

Than, after try to move the windows:

-- Loading extension: window -- Loading extension: geometry -- Loading extension: mouse *\ ERROR: hs.hotkey callback error: ...andrea/.hammerspoon/hs/_asm/undocumented/spaces/init.lua:419: attempt to call a nil value (field 'details') stack traceback: ...andrea/.hammerspoon/hs/_asm/undocumented/spaces/init.lua:419: in function 'hs._asm.undocumented.spaces.layout' /Users/andrea/.hammerspoon/init.lua:50: in upvalue 'screenSpaces' /Users/andrea/.hammerspoon/init.lua:54: in upvalue 'spaceInDirection' /Users/andrea/.hammerspoon/init.lua:64: in upvalue 'moveToSpaceInDirection' /Users/andrea/.hammerspoon/init.lua:118: in function </Users/andrea/.hammerspoon/init.lua:118>

Here it is my local setup. https://github.com/andreaspada/DOThammerspoon

Hope I understand what is wrong with it...

Cheers, Andrea

Szymon Kaliski wrote:

@andreaspada https://github.com/andreaspada you actually don't need to call |focusScreen| at all, it's just something I've added to myself, to make switching spaces on multiple displays switch the space with active window, and not where the mouse actually is, but you might not want that, so it could be even more simplified. And I compile spaces addon from source, so you might want to do that as well. Good luck! And thanks @heptal https://github.com/heptal for jumping in while I was sleeping :)

— Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/823#issuecomment-193163742.


Andrea Spada :: Liuteria d'Autore www.andreaspada.com

cmsj commented 7 years ago

I'm going to close this because I'm gardening the bugs and this has been open for ages with no further discussion, feel free to re-open it if there are specific things you want to do :)