macOS doesn't like polling /dev/tty
macOS (more specifically Darwin) doesn’t support using kqueue(2) or poll(2) to monitor the
/dev/tty file.1 You are required to use select(2) or pselect(2) instead. See below for some possible solutions I am aware of.
I recently ran into this issue while updating zf to support
SIGWINCH (terminal window resize) signal handling. I couldn’t find much information about the
/dev/tty issue online, so hopefully what I write here will save others the trouble I went through.
# Why use /dev/tty
Many common command line tools read from stdin and write to stdout. The device file
/dev/tty represents the terminal for the current process. Even if a program has input or output redirected, using the
/dev/tty file always allows reading or writing to the terminal.
Here’s an example shell session that illustrates this:
$ # execute pwd in a shell $ bash -c 'pwd' /Users/nathan $ # execute pwd in a shell that has stdout redirected to /dev/null (no output) $ bash > /dev/null -c 'pwd' $ # this time redirect pwd to explicitly write to the terminal device file /dev/tty $ bash > /dev/null -c 'pwd > /dev/tty' /Users/nathan
Even though the bash subprocess was redirecting all stdout to
/dev/null, we were still able to write directly to the terminal with
So why is this useful? Any program that wants to control both the terminal and support I/O redirection needs to use
Example programs that rely on
/dev/tty include fuzzy finders like fzf or zf. That is why you can run
vim $(fd -tf | fzf) without issue: fzf will read the list of files on stdin and open the selected file in vim while the interactive fuzzy finding interface uses
/dev/tty to directly access the terminal.
# macOS makes things difficult
If you only need access to
/dev/tty in the main loop of your program, then there isn’t an issue. Read and write to that file like normal and smile because you don’t have to worry about the
/dev/tty polling problem.
But many programs need to asynchronously monitor events like multiple files, signals, sockets, etc. To monitor multiple events efficiently on macOS you would typically use kqueue(2). Sadly, macOS doesn’t allow using kqueue(2) or even poll(2) to monitor
I cannot find any authoritative documentation describing this limitation. If there is a good source to document this, please let me know and I will update this post. Most mentions I found online were issues in open source projects that are in various stages of dealing with this issue: crossterm and helix for example.2
If you want to poll
/dev/tty on macOS, you must use select(2) or pselect(2). These functions have limitations, primarily being the max file descriptor number they can monitor. From the Linux
select(2) manual page:
select()can monitor only file descriptors numbers that are less than
FD_SETSIZE(1024)—an unreasonably low limit for many modern applications—and this limitation will not change. All modern applications should instead use
epoll(7), which do not suffer this limitation.
# Possible solutions
Here are some ideas on how to work around these limitations:
On macOS you can define
_DARWIN_UNLIMITED_SELECTto bypass the file descriptor limit, but this isn’t portable.
For me, a max file descriptor limit is not an issue because zf shouldn’t require file descriptors anywhere near the limit of 1024. To handle
SIGWINCHI am using pselect(2), which is explained well in this LWN.net article. This relies on pselect(2) being more predictable with signal handling by atomically masking signals and waiting at the kernel level.
libuv is an example of a library that needs to support large file descriptor numbers, and also offers support for
/dev/tty. To do this they create a separate thread that uses select(2) and the self-pipe trick to communicate with the main event loop. See this blog post or this commit for more details. You could also use libuv or another similar library directly.
Hopefully this helps point you in the right direction if you are struggling with this problem like I was!
Coincidentally, there was an issue created just today on the Zig repository to add a select(2) wrapper to the standard library motivated by the
/dev/ttylimitation on macOS. It links to this wonderful blog post which I would have loved to find sooner.
I had already written a draft of this post so I figured I would share it anyway. I struggled to find information on this issue, and it doesn’t hurt to put more out there in case it helps others. ↩︎