'Reading a character without requiring the Enter button pressed

read-line and read-char both require you press Enter key after typing something. Is there any mechanism in Common Lisp that would allow the program to continue upon the press of any single character immediately, without requiring the additional step of pressing Enter?

I'm trying to build a quick, dynamic text input interface for a program so users can quickly navigate around and do different things by pressing numbers or letters corresponding to onscreen menus. All the extra presses of the Enter key seriously interrupt the workflow. This would also be similar to a "y/n" type of interrogation from a prompt, where just pressing "y" or "n" is sufficient.

I am using SBCL, if that makes a difference. Perhaps this is implementation specific, as I tried both examples on this page and it does not work (I still need to press Enter); here's the first one:

(defun y-or-n ()
(clear-input *standard-input*)
(loop as dum = (format t "Y or N for yes or no: ")
    as c = (read-char)
    as q = (and (not (equal c #\n)) (not (equal c #\y)))
    when q do (format t "~%Need Y or N~%")
    unless q return (if (equal c #\y) 'yes 'no)))


Solution 1:[1]

Adding another answer to point out the existence of this tutorial: cl-charms crash course, by Daniel "jackdaniel" Kochma?ski. Disabling buffering is related to how the terminal is configured, and cl-charms is a library that exploit the ncurses C library to configure the terminal for interactive usage.

Solution 2:[2]

I found cl-charms, which seems to be a fork of the abandoned cl-curses. However, the included example program charms-paint uses 100 % CPU to run the trivial paint application. The problem seems to be that the main loop busy-waits for input.

Solution 3:[3]

You could use the trivial-raw-io library to read a single character without pressing Enter. Usage: (trivial-raw-io:read-char). It works on SBCL, CCL, CMUCL, and CLISP on Linux, and should work on other Unix-like operating systems as well. The library has only one simple dependency (the alexandria library), and it is licensed under the BSD 2-clause license.

Solution 4:[4]

I've struggled recently with the same issue and I ended up solving it simply by using FFI to interact with termios and disable canonical mode. Essentially that what's mentioned in @JoshuaTaylor's response. I couldn't get that code to work with SBCL, for whatever reason, so I made a few changes. Here is the complete working code (only tested with SBCL):

(define-alien-type nil
  (struct termios
          (c_iflag unsigned-long)
          (c_oflag unsigned-long)
          (c_cflag unsigned-long)
          (c_lflag unsigned-long)
          (c_cc (array unsigned-char 20))
          (c_ispeed unsigned-long)
          (c_ospeed unsigned-long)))

(declaim (inline tcgetattr))
(define-alien-routine "tcgetattr" int
                      (fd int)
                      (term (* (struct termios))))

(declaim (inline tcsetattr))
(define-alien-routine "tcsetattr" int
                      (fd int)
                      (action int)
                      (term (* (struct termios))))

(defun read-single-byte (&optional (s *standard-input*))
  (with-alien ((old (struct termios))
               (new (struct termios)))
    (let ((e0 (tcgetattr 0 (addr old)))
          (e1 (tcgetattr 0 (addr new)))
          (n-lflag (slot new 'c_lflag)))
      (declare (ignorable e0 e1))           
      (unwind-protect
        (progn
          (setf (ldb (byte 1 8) n-lflag) 0) ; disables canonical mode
          (setf (ldb (byte 1 3) n-lflag) 0) ; disables echoing input char
          (setf (slot new 'c_lflag) n-lflag)
          (tcsetattr 0 0 (addr new))
          (read-byte s))
        (tcsetattr 0 0 (addr old))))))

Simply interfacing with termios should do the trick, there is no need for external libraries. You can find on termios's man page more information (https://man7.org/linux/man-pages/man3/termios.3.html), but essentially it's when the terminal is in canonical mode (ICANON) that it needs to wait for a line delimiter before the contents of the buffer become available. I hope this helps!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 coredump
Solution 2 claj
Solution 3 Flux
Solution 4 Pedro Rodrigues