Pergunta

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)))
Foi útil?

Solução

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.

Outras dicas

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.

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.

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.

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!

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top