Racket: Direct TTY Control

Written by Dominik Joe Pantůček on 2020-02-13

racket

The Racket environment can keep surprising you even after very long time. Many people are complaining that it lacks a standard library for Unix terminal (TTY) control. But it turns out, it is perfectly possible to control the TTY without any further libraries. Read on to find out how.


It is not entirely true that there are no libraries to control the terminal. For example the excellent package charterm[1] can do that for you. However, it relies on external executable stty[2] which is unfortunate. Although the stty command is a part of the POSIX standard, it means that your application has to run external program to achieve relatively simple tasks.

If you dig deeper, you find out about termios[3] bindings[4] for Racket - but these come with the price that you need to compile them as C extension[5]. Yes, it is not a big obstacle at all, but it is strange such simple task cannot be done directly.

If you think about it, the Racket environment is linked to the systems C library - libc[6] for short. And libc contains all that is needed for controlling the terminal directly. Putting it into a raw mode[7] and allowing you to control everything on character-by-character basis.

To use this functionality, you need to open the running executable as a shared library. That is easy thanks to the ffi/unsafe[8] and ffi/unsafe/define bindings:

(require ffi/unsafe
         ffi/unsafe/define)
(define-ffi-definer define-c (ffi-lib #f))

Now with the FFI definer, you just pull in the required libc functions:

; int tcgetattr(int fd, struct termios *termios_p);
(define-c tcgetattr (_fun _int _pointer -> _int))

; int tcsetattr(int fd, int optional_actions,
;               const struct termios *termios_p);
(define-c tcsetattr (_fun _int _int _pointer -> _int))

; void cfmakeraw(struct termios *termios_p);
(define-c cfmakeraw (_fun _pointer -> _void))

To tell the truth, this is all that is needed. Of course, you need to allocate a large enough buffer to hold the termios structure - but we have already covered opaque structure size auto-detection[9].

So to put the terminal in raw mode, all you need to do is:

(define termios (malloc 'atomic (termios-struct-size)))
(tcgetattr 0 termios)
(cfmakeraw termios)
(tcsetattr 0 0 termios)

 

Just remember to store the original settings and restore them just before your program finishes. A typical wrapper would provide a (with-termios ...) form to handle that transparently.

 

And that's all for today. Hope you liked the simplicity of controlling the terminal without any external dependencies! See you next Thursday again...


References

  1. https://pkgs.racket-lang.org/package/charterm

  2. https://linux.die.net/man/1/stty

  3. https://linux.die.net/man/3/termios

  4. https://pkgs.racket-lang.org/package/termios

  5. https://docs.racket-lang.org/inside/Writing_Racket_Extensions.html

  6. Wikipedia contributors. (2020, January 4). C standard library. In Wikipedia, The Free Encyclopedia. Retrieved 20:27, February 12, 2020, from https://en.wikipedia.org/w/index.php?title=C_standard_library&oldid=934031456

  7. Wikipedia contributors. (2020, February 8). Computer terminal. In Wikipedia, The Free Encyclopedia. Retrieved 20:29, February 12, 2020, from https://en.wikipedia.org/w/index.php?title=Computer_terminal&oldid=939678312#Modes

  8. https://docs.racket-lang.org/foreign/index.html

  9. https://trustica.cz/en/2019/12/26/auto-detecting-opaque-struct-size-in-racket-ffi/