Search code examples
lispcommon-lispsbcl

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

  • read-char doesn't require you to press enter. E.g.,

    CL-USER> (with-input-from-string (x "hello")
               (print (read-char x)))
    
    #\h 
    

    Similarly, if you send some input into SBCL from the command line, it will be read without a newline:

    $ echo -n hello | sbcl --eval "(print (read-char))"
    …
    #\h 
    

    After reading and printing #\h, SBCL saw the ello:

    *  
    debugger invoked on a UNBOUND-VARIABLE in thread #<THREAD
                                                       "initial thread" RUNNING
                                                        {1002979011}>:
      The variable ELLO is unbound.
    

    I think this is enough to confirm that it's not that read-char needs a newline, but rather that the buffering of the input is the problem. I think this is the same problem (or non-problem) that's described in a comp.lang.lisp thread from 2008: Re: A problem with read-char. The user asks:

    Is it possible to make read-char behave like getch in С when working with interactive stream (standard-input)? In SBCL read-char wants "enter" key to unhang from REPL, in C getchar returns immediately after user press key on keyboard. Probably is possible to run code that uses read-char with direct console access, aside REPL?

    There were four responses (see the thread index to get to all of them). These explain why this behavior is observed (viz., that the Lisp process isn't getting raw input from the terminal, but rather buffered input). Pascal Bourguignon described the problem, and a way to handle this with CLISP (but doesn't provide all that much help, aside from the usual good advice) about working around this in SBCL:

    The difference is that curses puts the terminal in raw mode to be able to receive the characters from the keyboard one at a time, instead of leaving the terminal in cooked mode, where the unix driver bufferize lines and handles backspace, amongst other niceties.

    Now, I don't know about SBCL, (check the manual of SBCL). I only have the Implementation Notes of CLISP loaded in my wetware. In CLISP you can use the EXT:WITH-KEYBOARD macro (while the basic output features of curses are provided by the SCREEN package).

    Rob Warnock's response included some workaround code for CMUCL that might or might not work for SBCL:

    I once wrote the following for CMUCL for an application that wanted to be able to type a single character response to a prompt without messing up the terminal screen:

    (defun read-char-no-echo-cbreak (&optional (stream *query-io*))
      (with-alien ((old (struct termios))
                   (new (struct termios)))
        (let ((e0 (unix-tcgetattr 0 old))
              (e1 (unix-tcgetattr 0 new))
              (bits (logior tty-icanon tty-echo tty-echoe
                            tty-echok tty-echonl)))
          (declare (ignorable e0 e1)) ;[probably should test for error here]
          (unwind-protect
               (progn
                 (setf (slot new 'c-lflag) (logandc2 (slot old 'c-lflag) bits))
                 (setf (deref (slot new 'c-cc) vmin) 1)
                 (setf (deref (slot new 'c-cc) vtime) 0)
                 (unix-tcsetattr 0 tcsadrain new)
                 (read-char stream))
            (unix-tcsetattr 0 tcsadrain old)))))
    

    SBCL has probably diverged considerably from CMUCL in this area, but something similar should be doable with SBCL. Start by looking in the SB-UNIX or maybe the SB-POSIX packages...

    User vippstar's response provided a link to what might be the most portable solution

    Since you want to do something that might not be portable to a microcontroller (but the benifit is the much more enhanced UI), use a non-standard library, such as CL-ncurses.