Closed TwoLeaves closed 9 years ago
So, we now have hs.window:zoomButtonRect(), which is as far, I think, as Hammerspoon itself should go, so I have closed the issue.
However, we can continue to discuss how to make a Hammerspoon config that uses zoomButtonRect() and hs.eventtap, to implement the kludge :)
(I just don't think the kludge itself should be part of Hammerspoon, at least until we get some clearer idea of whether Apple is going to rewrite Spaces again, or add an API, or what)
(and 0.9.24 is now released, so zoomButtonRect() is now available)
So, once 0.9.25 is out, the following will work:
function moveWindowOneSpace(direction)
local win = hs.window.focusedWindow()
local clickPoint = win:zoomButtonRect()
local mouseOrigin = hs.mouse.getAbsolutePosition()
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)
local mouseReleaseEvent = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftmouseup, clickPoint)
local nextSpaceDownEvent = hs.eventtap.event.newKeyEvent({["ctrl"]=true}, direction, true)
local nextSpaceUpEvent = hs.eventtap.event.newKeyEvent({["ctrl"]=true}, direction, false)
mouseClickEvent:post()
hs.timer.usleep(200000)
nextSpaceDownEvent:post()
hs.timer.usleep(200000)
nextSpaceUpEvent:post()
hs.timer.usleep(200000)
mouseReleaseEvent:post()
hs.timer.usleep(200000)
hs.mouse.setAbsolutePosition(mouseOrigin)
end
hk1 = hs.hotkey.bind({"cmd", "shift", "ctrl", "alt"}, "right", function() moveWindowOneSpace("right") end)
hk2 = hs.hotkey.bind({"cmd", "shift", "ctrl", "alt"}, "left", function() moveWindowOneSpace("left") end)
It's not 100% perfect, because we're doing some pretty crazy stuff here, but it seems to work for me almost all of the time.
(fwiw, I'd expect 0.9.25 to be out within the next few days)
(also while I'm talking about eventtap stuff - @asmagill you might be interested to look at the last few commits to extensions/eventtap/event.m, I believe I have fixed at least one surprisingly significant bug. I'd be curious to hear if you disagree with what I've done)
Thank you cmsj, I'll check it out when 0.9.25 is released (I tried building the app but my build behaves differently to the official build).
The above code is not working for me cmsj. It worked once. But barring that one time the space doesn't move. I tried changing the spaces using asmagill's spaces extension, and the space changed successfully. The window however did not move with it. I disabled the mouse event and emulated what the code does myself with the mouse, and this worked. So it seems that for me the OSX space changing does not work (but the mostly-working space change extension can replace this) and the programmatic mouse up/down does not work at all. I am using version 0.9.25 on OSX 10.10.2.
@TwoLeaves you shouldn't need the spaces extension to use the code I pasted above. It relies on the system keyboard shortcuts for changing space (ctrl-left an ctrl-right). Having said that, it is possible that the spaces extension is interacting with things badly.
This is all horribly hacky and experimental - until Apple provides an API we can use to interact with Spaces, that seems unlikely to change. Perhaps you need slightly longer usleep() times between the events? Perhaps the particular window you were using didn't have draggable space at the exact co-ordinate we simulated the mouse down/up events. It's so hard to know :/
Yeah ctrl-left and ctrl-right doesn't move the space for me (when pressed via Hammerspoon). If I click the hotkey twice it sometimes works. Tried to no avail longer usleep times. Don't think it's the co-ordinates thing either.
Oh well, I'll continue to move windows by hand for now. Thanks for working on it Chris Jones. :)
In case anyone is trying this I modified a bit for 0.9.29 and the following is working for moving a window one space right or left. For some reason I had to create and then post the event in succession or it would not trigger.
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(150000)
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(150000)
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","shift"}, "right",
function() moveWindowOneSpace("right") end,
function() moveWindowOneSpaceEnd("right") end)
hk2 = hs.hotkey.bind({"ctrl","shift"}, "left",
function() moveWindowOneSpace("left") end,
function() moveWindowOneSpaceEnd("left") end)
@jdtsmith -
I tired your example on 0.9.29 and the context moved to the second space when I did shift+control+right
however the application didn't move with it. I tried ot move back to the previous space ( shift+control+left
) and I go the following error:
/Users/jdyer/.hammerspoon/init.lua:401: attempt to index global '_clickPoint' (a nil value)
stack traceback:
/Users/jdyer/.hammerspoon/init.lua:401: in function 'moveWindowOneSpace'
/Users/jdyer/.hammerspoon/init.lua:427: in function </Users/jdyer/.hammerspoon/init.lua:427>
[C]: in function 'xpcall'
...oon.app/Contents/Resources/extensions/hs/hotkey/init.lua:29: in function <...oon.app/Contents/Resources/extensions/hs/hotkey/init.lua:27>
...n.app/Contents/Resources/extensions/hs/eventtap/init.lua:53: bad argument #2 to '_newMouseEvent' (table expected, got nil)
stack traceback:
[C]: in function '_newMouseEvent'
...n.app/Contents/Resources/extensions/hs/eventtap/init.lua:53: in function 'newMouseEvent'
/Users/jdyer/.hammerspoon/init.lua:417: in function 'moveWindowOneSpaceEnd'
/Users/jdyer/.hammerspoon/init.lua:428: in function </Users/jdyer/.hammerspoon/init.lua:428>
[C]: in function 'xpcall'
...oon.app/Contents/Resources/extensions/hs/hotkey/init.lua:29: in function <...oon.app/Contents/Resources/extensions/hs/hotkey/init.lua:27>
However if I did shift+control+right
a second time I switched back to the first space. However as I mentioned the application in context did not switch with me when I went from space to space.
ok, I have no idea what changed but now its working.... I did reload the config before, no idea why it is not cooperating. I have noticed that it only appears to work now on applications on my "primary" screen ( I have two cinema displays ). If i move the app to the second display I am unable to then move that app to another space.
I was having some trouble with the start/end formulation, so went back to a single function which seems to be working well. You may need to adjust the sleep timings. Critical was binding and posting the event in short succession.
function moveWindowOneSpace(direction)
local mouseOrigin = mouse.getAbsolutePosition()
local win = hs.window.focusedWindow()
local 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(150000)
local nextSpaceDownEvent = hs.eventtap.event.newKeyEvent({"ctrl"}, direction, true)
nextSpaceDownEvent:post()
hs.timer.usleep(150000)
local nextSpaceUpEvent = hs.eventtap.event.newKeyEvent({"ctrl"}, direction, false)
nextSpaceUpEvent:post()
hs.timer.usleep(150000)
local mouseReleaseEvent = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftmouseup, clickPoint)
mouseReleaseEvent:post()
hs.timer.usleep(150000)
mouse.setAbsolutePosition(mouseOrigin)
end
hk1 = hs.hotkey.bind(mash, "s",
function() moveWindowOneSpace("right") end)
hk2 = hs.hotkey.bind(mash, "a",
function() moveWindowOneSpace("left") end)
HI @jdtsmith @cmsj sorry to reopen this, I am using @jdtsmith code above and it works fine on everything except chrome, it moves to different screen it just that chrome doesn't move. Is anyone having the same issue ? and if so is there way to fix it.
@lanox I recently switched to using hs._asm.undocumented.spaces
moveWindowToSpace
and it's much more reliable, I'd encourage checking it out :)
@szymonkaliski I will be completely honest and say I have not clue how to use lua.
@lanox my dotfiles are a mess right now so they are not online, but I'll try to cut out relevant parts for how I use it:
local spaces = require('hs._asm.undocumented.spaces')
-- get ids of spaces in same layout as mission control has them (hopefully)
local getSpacesIdsTable = function()
local spacesLayout = spaces.layout()
local spacesIds = {}
hs.fnutils.each(hs.screen.allScreens(), function(screen)
local spaceUUID = screen:spacesUUID()
local userSpaces = hs.fnutils.filter(spacesLayout[spaceUUID], function(spaceId)
return spaces.spaceType(spaceId) == spaces.types.user
end)
hs.fnutils.concat(spacesIds, userSpaces or {})
end)
return spacesIds
end
local throwToSpace = function(win, spaceIdx)
local spacesIds = getSpacesIdsTable()
local spaceId = spacesIds[spaceIdx]
spaces.moveWindowToSpace(win:id(), spaceId)
end
hs.hotkey.bind({ 'ctrl', 'shift' }, 1, nil, function()
local win = hs.window.focusedWindow()
if not win then return end
-- throw window to space
throwToSpace(win, 1)
-- move to that space
hs.eventtap.keyStroke({ 'ctrl' }, '1')
end)
Making it work across multiple screens etc is bit more complex, but doable (I have it working in my setup).
@szymonkaliski thanks will give that a go, yes I have had a look at your dotfiles and i was scratching my head lol ..
but does that mean I need to get spaces ? module or plugin.
@lanox: For reference, the issue you were having with Chrome is perhaps due to the hidden window placed on top of all visible Chrome windows. These hidden windows have an empty title, so you can avoid selecting them by checking if a window's title is empty and if it belongs to Chrome (because some programs give their main window an empty title). That is unless Chrome has changed this behaviour (I haven't used Hammerspoon or Chrome on OSX for a couple of years now). The code I used to perform this task was as such:
local function spaceChange()
visibleWindows = hs.window.orderedWindows()
for i, window in ipairs(visibleWindows) do
if window:application():title() == "Google Chrome" then
if window:title() ~= "" then
window:focus()
break
end
else
window:focus()
break
end
end
end
@TwoLeaves do you have a working solution with the code from jdsmith, sorry I am not that good wiht lua, I am just starting out with hammerspoon , thanks so much
I don't I'm sorry. I only have Thinkpads around at the moment. You would have to extend the line from jdsmith which reads "local win = hs.window.focusedWindow()".
if anyone has a working solution would be much appreciated, sorry never worked with lua.
Not sure if this helps the Chrome issue, but I hadn't yet heard of the undocumented Spaces plugin, thanks for the tip. Here's my attempt at using it to replace the old simulate-titlebar-mouse-press-and-move hack. Note that this version still requires the use of Ctrl-left
and Ctrl-right
shortcuts for switching to the next space, because the hs._asm.undocumented.spaces
space switching requires killing the Dock, which is slow and ugly.
Advantages of the old method over the new:
And disadvantages:
Advantages of the new method:
Disadvantages:
Ctrl-direction
keyboard shortcuts) gets you to the correct space, but rearranges Full/Split screen spaces as a side effect.Here's the code. I put it in move_space.lua
and require("move_space")
in my init.lua
local hotkey = require "hs.hotkey"
local window = require "hs.window"
local spaces = require "hs._asm.undocumented.spaces"
function getGoodFocusedWindow(nofull)
local win = window.focusedWindow()
if not win or not win:isStandard() then return end
if nofull and win:isFullScreen() then return end
return win
end
function flashScreen(screen)
local flash=hs.canvas.new(screen:fullFrame()):appendElements({
action = "fill",
fillColor = { alpha = 0.25, red=1},
type = "rectangle"})
flash:show()
hs.timer.doAfter(.15,function () flash:delete() end)
end
function switchSpace(skip,dir)
for i=1,skip do
hs.eventtap.keyStroke({"ctrl"},dir)
end
end
function moveWindowOneSpace(dir,switch)
local win = getGoodFocusedWindow(true)
if not win then return end
local screen=win:screen()
local uuid=screen:spacesUUID()
local userSpaces=nil
for k,v in pairs(spaces.layout()) do
userSpaces=v
if k==uuid then break end
end
if not userSpaces then return end
local thisSpace=win:spaces() -- first space win appears on
if not thisSpace then return else thisSpace=thisSpace[1] end
local last=nil
local skipSpaces=0
for _, spc in ipairs(userSpaces) do
if spaces.spaceType(spc)~=spaces.types.user then -- skippable space
skipSpaces=skipSpaces+1
else -- A good user space, check it
if last and
((dir=="left" and spc==thisSpace) or
(dir=="right" and last==thisSpace))
then
win:spacesMoveTo(dir=="left" and last or spc)
if switch then
switchSpace(skipSpaces+1,dir)
win:focus()
end
return
end
last=spc -- Haven't found it yet...
skipSpaces=0
end
end
flashScreen(screen) -- Shouldn't get here, so no space found
end
mash = {"ctrl", "cmd"}
mashshift = {"ctrl", "cmd","shift"}
hotkey.bind(mash, "s",nil,
function() moveWindowOneSpace("right",true) end)
hotkey.bind(mash, "a",nil,
function() moveWindowOneSpace("left",true) end)
hotkey.bind(mashshift, "s",nil,
function() moveWindowOneSpace("right",false) end)
hotkey.bind(mashshift, "a",nil,
function() moveWindowOneSpace("left",false) end)
And by the way, it appears I had altered my version of the old hackish method to handle multiple moves in quick succession (reentrancy) without reporting it, so for full disclosure here it is. The new version doesn't have this issue, since the window can be moved arbitrarily far ahead and the space switching animation catches up after the fact.
local mouseOrigin
local inMove=0
-- move a window to an adjacent Space
function moveWindowOneSpaceOld(direction)
local win = window.focusedWindow()
if not win then return end
local clickPoint = win:zoomButtonRect()
if inMove==0 then mouseOrigin = mouse.getAbsolutePosition() end
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()
local nextSpaceDownEvent = hs.eventtap.event.newKeyEvent(
{"ctrl"},direction, true)
nextSpaceDownEvent:post()
inMove=inMove+1 -- nested moves possible, ensure reentrancy
hs.timer.doAfter(.1,function()
local nextSpaceUpEvent = hs.eventtap.event.newKeyEvent(
{"ctrl"}, direction, false)
nextSpaceUpEvent:post()
-- wait to release the mouse to avoid sticky window syndrome
hs.timer.doAfter(.25,
function()
local mouseReleaseEvent = hs.eventtap.event.newMouseEvent(
hs.eventtap.event.types.leftMouseUp, clickPoint)
mouseReleaseEvent:post()
inMove=math.max(0,inMove-1)
if inMove==0 then mouse.setAbsolutePosition(mouseOrigin) end
end)
end)
end
@jdtsmith woha thanks, I have trued to use the old method but I get
attempt to index a nil value (global 'window')stack traceback
this is how my code looks(well your code)
local mouseOrigin
local inMove=0
-- move a window to an adjacent Space
function moveWindowOneSpaceOld(direction)
local win = window.focusedWindow()
if not win then return end
local clickPoint = win:zoomButtonRect()
if inMove==0 then mouseOrigin = mouse.getAbsolutePosition() end
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()
local nextSpaceDownEvent = hs.eventtap.event.newKeyEvent(
{"ctrl"},direction, true)
nextSpaceDownEvent:post()
inMove=inMove+1 -- nested moves possible, ensure reentrancy
hs.timer.doAfter(.1,function()
local nextSpaceUpEvent = hs.eventtap.event.newKeyEvent(
{"ctrl"}, direction, false)
nextSpaceUpEvent:post()
-- wait to release the mouse to avoid sticky window syndrome
hs.timer.doAfter(.25,
function()
local mouseReleaseEvent = hs.eventtap.event.newMouseEvent(
hs.eventtap.event.types.leftMouseUp, clickPoint)
mouseReleaseEvent:post()
inMove=math.max(0,inMove-1)
if inMove==0 then mouse.setAbsolutePosition(mouseOrigin) end
end)
end)
end
hk1 = hs.hotkey.bind(mash3, "Right",
function() moveWindowOneSpaceOld("right", true) end)
hk2 = hs.hotkey.bind(mash3, "Left",
function() moveWindowOneSpaceOld("left", true) end)
another thing tyring out the new version with undocumented methode, I can move chrome but I cant move anything else ? also why do you need to have 2 mash ? thanks
I didn't provide complete code for the old method. You need, at the top:
local window = require "hs.window"
mash3 = {"ctrl", "cmd"}
or similar to set up which modifier keys to use. Probably worth spending some time familiarizing yourself with a variety of example dotfiles and some Lua coding to make good use of HS.
@jdtsmith thanks mate, I dunno but no matter what I do i cant seem to get past this error
2017-11-03 09:24:19: 09:24:19 ERROR: LuaSkin: hs.hotkey callback error: attempt to index a nil value
stack traceback:
[C]: in for iterator 'for iterator'
/Users/lxxx/.hammerspoon/init.lua:37: in function 'moveWindowOneSpace'
/Users/lxxx/.hammerspoon/init.lua:62: in function </Users/lxxx/.hammerspoon/init.lua:62>
2017-11-03 09:24:19: ********
This is using this code.
local hotkey = require "hs.hotkey"
local window = require "hs.window"
local spaces = require "hs._asm.undocumented.spaces"
function getGoodFocusedWindow(nofull)
local win = window.focusedWindow()
if not win or not win:isStandard() then return end
if nofull and win:isFullScreen() then return end
return win
end
function flashScreen(screen)
local flash=hs.canvas.new(screen:fullFrame()):appendElements({
action = "fill",
fillColor = { alpha = 0.25, red=1},
type = "rectangle"})
flash:show()
hs.timer.doAfter(.15,function () flash:delete() end)
end
function switchSpace(skip,dir)
for i=1,skip do
hs.eventtap.keyStroke({"ctrl"},dir)
end
end
function moveWindowOneSpace(dir,switch)
local win = getGoodFocusedWindow(true)
if not win then return end
local screen=win:screen()
local uuid=screen:spacesUUID()
local userSpaces=spaces.layout()[uuid]
local thisSpace=win:spaces() -- first space win appears on
if not thisSpace then return else thisSpace=thisSpace[1] end
local last=nil
local skipSpaces=0
for _, spc in ipairs(userSpaces) do
if spaces.spaceType(spc)~=spaces.types.user then -- skippable space
skipSpaces=skipSpaces+1
else -- A good user space, check it
if last and
(dir=="left" and spc==thisSpace) or
(dir=="right" and last==thisSpace)
then
win:spacesMoveTo(dir=="left" and last or spc)
if switch then
switchSpace(skipSpaces+1,dir)
win:focus()
end
return
end
last=spc -- Haven't found it yet...
skipSpaces=0
end
end
flashScreen(screen) -- Shouldn't get here, so no space found
end
mash = {"ctrl", "cmd"}
mashshift = {"ctrl", "cmd","shift"}
hotkey.bind(mash, "s",nil,
function() moveWindowOneSpace("right",true) end)
hotkey.bind(mash, "a",nil,
function() moveWindowOneSpace("left",true) end)
hotkey.bind(mashshift, "s",nil,
function() moveWindowOneSpace("right",false) end)
hotkey.bind(mashshift, "a",nil,
function() moveWindowOneSpace("left",false) end)
Throw a print(uuid)
line after the local uuid=...
line and see what it says. Sounds like an issue of not finding any spaces.
I dunno all the suddenly it started working. Thanks so much mate..
@jdtsmith Ah I found out why it was not working, when I have multiple 2 external monitors connected, if I move window to second monitor then it doesnt work. If the window is on primary screen it works fine.
Multi screens work for me. Do you have different spaces per screen?
Yes I do have Display have separate Spaces un ticket is it possible to work with aout been ticket ?
If i thick the box all works fine. Thanks so much for you time and effort on helping me with this
OK, I see what happens when spaces are shared between screens (only one screen is listed in layout()
). I've fixed the original code above so it can handle both cases.
thanks so much man, all works well now.
Can we implement moving a window to another Space? The kludgy process of achieving this is documented here (http://ianyh.com/blog/2013/06/05/accessibility/) and code that achieves this is here (https://github.com/ianyh/Silica/blob/c10cfeda040ebcf475a73307614ed2576747bba8/Silica/SIWindow.m#L189). The process in general is programmatically clicking on a free point on the toolbar (5 pixels to the right of the 'zoom button' is likely to be free), changing the Space (using private APIs via this Hammerspoon extension: https://github.com/asmagill/hammerspoon_asm.undocumented/tree/master/spaces) and releasing the click. Thank you.