Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: Detect typed input without swallowing characters



On Thu, 2022-02-10 at 22:21 +0100, Marc Cornellà wrote:
> in Oh My Zsh we want to detect whether the user has typed characters
> before showing the prompt to auto-update, so that if they have, to
> skip it entirely and get them to the prompt as fast as possible.
> 
> The current solution is to detect whether there's input with `read -t
> -k 1`, but this solution has the downside of swallowing one
> character.
> 
> I've been looking at sysread and zselect to poll stdin (fd 0), but I
> believe they only tell whether stdin is ready for reading, not
> whether
> it holds any data. These are the commands I'm using by the way:
> 
>     sysread -t 0 -i 0
>     zselect -t 0 -r 0
> 
> Is there any way to do this or should I give up entirely?

You can use zselect, but you have to disable canonical mode with stty,
otherwise characters are not counted unless you press enter (the
terminal is set to read a whole line at once).

This function should work:

   type_ahead() {
     emulate -L zsh
     local termios=$(stty --save)
     stty -icanon
     zselect -t 0 -r 0
     local ret=$?
     stty $termios
     return ret
   }

There may be subtleties involved with the STTY variable as well as
ttyctl, check out the zsh documentation.  I only did minimal testing:

   sleep 5; if type_ahead; then echo TYPED AHEAD; fi

works as it should.

You can also get the number of bytes in the read buffer with the
FIONREAD ioctl() (man ioctl_tty for details).
This long python 1-liner does what you want:

   python -c 'import array, fcntl, termios; nbuffered=array.array("i", [0]); old=termios.tcgetattr(0); new=old[:]; new[3] &= ~termios.ICANON; termios.tcsetattr(0, termios.TCSANOW, new); fcntl.ioctl(0, termios.FIONREAD, nbuffered); termios.tcsetattr(0, termios.TCSANOW, old); print(nbuffered[0])'

Same script in a more readable form:

   #!/usr/bin/env python3
   
   import array, fcntl, termios
   
   nbuffered=array.array("i", [0])
   
   old=termios.tcgetattr(0)
   new=old[:]                # Array copy
   new[3] &= ~termios.ICANON # Disable canonical mode
   try: # try/finally cannot appear in the 1-liner above
     termios.tcsetattr(0, termios.TCSANOW, new)
     fcntl.ioctl(0, termios.FIONREAD, nbuffered)
   finally:
     termios.tcsetattr(0, termios.TCSANOW, old)
   print(nbuffered[0])

I doubt this would be useful for your use case, as firing up the whole
python interpreter to perform FIONREAD is likely to be slower than the
optimization gain you're trying to measure.

Phil.





Messages sorted by: Reverse Date, Date, Thread, Author