yabai: Commands fail from Hammerspoon
Apologies if this is not the right place to post a question like I have, but I’m not sure where else to go.
I am attempting to invoke yabai through key bindings implemented in Hammerspoon. Both Hammerspoon and yabai have accessibility permissions granted. Yabai commands work fine from the terminal. But attempting to execute yabai commands, e.g. os.execute("/usr/local/bin/yabai -m window --focus west") in Hammerspoon fails with exit code 1. The Yabai log prints EVENT_HANDLER_DAEMON_MESSAGE: window --focus west, but then it hangs briefly and fails. There’s nothing in the error log either.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 31 (6 by maintainers)
Yes, it’s “blocking” on the
fifo:read('a')line.And to answer your other question, no… and yes, sorta.
Lua is by default a single application threaded language… there are some various projects out there that attempt to make Lua truly multi-threaded in the sense most people mean when they talk about multi-threaded applications, but their statuses vary and in any case, we’re not using any of them.
And while Lua is running code (i.e. in the middle of your function), Hammerspoon can’t do anything else (timers, tasks, events, hotkeys, etc.) on the main application thread because that’s where Lua is currently processing code.
However, Lua does support coroutines, which is a way for a particular chunk of Lua code to “pause” it’s execution and wait for a separate “thread” (poor choice of words, in my opinion, but that’s what the Lua docs use) of Lua code to perform some action. This is an extreme over simplification and more detail is beyond the scope of this thread – if you’re really interested check out the Lua docs at https://www.lua.org/docs.html. I’ve found the third and fourth editions of the “Programming in Lua” book they mention very useful over the years, but the online reference is good, if a little more formal and reference like (as opposed to the books which provide demonstrations), as well.
BUT because of programming choices made early on with Hammerspoon, coroutines are not safe to use in the current release or earlier versions of Hammerspoon if your code uses any functionality that Hammerspoon has added beyond just stock Lua code within the coroutine – none of our modules work, none of the hooks into hotkeys, timers, tasks, etc. work, etc.
This has been fixed in the current development version of Hammerspoon which means it will be fixed in the next release of Hammerspoon, but I don’t have any idea what the time frame on that is. I would assume it’s relatively soon as there have been some other additions and bug fixes recently, but there are a couple of pull requests that I think we’re still trying to nail down first.
Once that version (or a development version if you want to try your hand at building it yourself – you’ll need XCode. Search the Hammerspoon issues and you should find at least a couple of threads about building it yourself) is installed, the following would work (also just tested this myself):
What
coroutine.applicationYielddoes is “yield” the coroutine (i.e. pause it and allow other code to do something) and then start a timer that automatically “resumes” the coroutine when the timer fires. In between, the Hammerspoon main application thread is allowed to respond to other events (timers, etc.) that have queued up for action. But as stated in the comments, it requires your code to be fully contained within a coroutine to work properly.So, yes, it will be possible to pause and give Hammerspoon the time it needs to respond to external queries (a) with the next version of Hammerspoon, and (b) if you write your code appropriately and enclose the bulk of it within a coroutine.
I’m not sure what I was thinking… the coroutine support won’t help in that case because the function isn’t in a coroutine…
If you truly need to wait until the result is available, your best bet really is to use
hs.executeinstead ofhs.task.I added error checking in there… if you really don’t care and can reasonably assume it won’t fail, then you could simplify it down to:
If it does fail in this simplified version, the first argument returned will be an empty string (because it just returns the results from
hs.execute, all four of the return values are actually returned, but if you do something likeoutput = getYabaiWindows()then the other three are just silently dropped.)hs.taskis designed for more complex cases – if you specifically require stderr, if you need to programmatically provide data for stdin, if you are invoking something long running and you don’t want to block or wait until it’s finished to do something else while waiting for the callback function to be invoked, etc.I’m really not sure why your original function seemed to work for me – it really shouldn’t have, the more I understand how
hs.taskis written. I’m not sure if we can makewaitUntilExitreliably work like you want it to or not, but I’ll give it some thought. It will probably be sometime next week, though, as I have other priorities I’m trying to get finished first.However, for your use case, I really think
hs.executeshould be sufficient.