Hammerspoon: Moving window to space at left our right

Created on 6 Mar 2016  Â·  14Comments  Â·  Source: Hammerspoon/hammerspoon

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?

Most helpful comment

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.

All 14 comments

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

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.

Oh yeah, forgot to mention that, sorry :)

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

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.

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

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

Are you using the latest version of the spaces module?

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

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.

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

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

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

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

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 :)

Was this page helpful?
0 / 5 - 0 ratings