在看到@Shawn 提到的如何使用stty.egg for Chicken 之后,我也想出了一种在 Racket 中使用的方法。这是一个完整的例子。
#lang racket/base
(require ffi/unsafe
ffi/unsafe/define)
;; If c is a control character, it is converted into a two-character
;; string that indicates its special status by preceding it with a "^".
(define (char->readable c)
(if (char-iso-control? c)
(string #\^ (integer->char (+ 64 (char->integer c))))
(string c)))
;; Return a transform of the input string with embedded control chars converted
;; to human-readable form showing a "^" prepended to it.
(define (string->readable s)
(let loop ((lst (string->list s))
(acc ""))
(if (null? lst)
acc
(loop (cdr lst) (string-append acc (char->readable (car lst)))))))
;; Display a series of arguments followed by a newline.
(define (println . args)
(for-each display args)
(newline))
;; When in raw mode, lines need to be ended with CR/LF pairs to act
;; like normal printing.
(define (println-in-raw . args)
(for-each display args)
(display (string #\return #\newline)))
;;------------------------------------------------------------------------------
;; Machinery to interface with C and handle getting in and out of "raw" mode
;; on the terminal.
(define-ffi-definer define-c (ffi-lib #f))
;; int tcgetattr(int fd, struct termios *termios_p);
;; Copy the current attributes into the buffer (termios struct)
;; pointed to. Returns 0 on success, -1 on failure in which case errno
;; will containt the error code.
(define-c tcgetattr (_fun _int _pointer -> _int))
;; int tcsetattr(inf fd, int optional_atcions,
;; const struct termios *termios_p);
;; Copy the buffer (termios struct) pointed to into the terminal
;; associated the the integer file descriptor. Returns 0 on success,
;; -1 on failure code is copied into errno.
(define-c tcsetattr (_fun _int _int _pointer -> _int))
;; void cfmakeraw(struct termios *termio_p);
(define-c cfmakeraw (_fun _pointer -> _void))
;; Explanation of `termios` and `cfmakeraw`. This is from the `cfmakeraw(3)`
;; Linux man page. Descriptions reflect the result of related flags settings.
;;
;; termios_p->c_iflag &= ~(IGNBRK | // Do not ignore a BREAK
;; BRKINT | // BREAK will produce a null byte
;; PARMRK | // Ignore parity errors
;; ISTRIP | // Do not strip the eigth bit
;; INCLCR | // Do not translate NL to CR on input
;; IGNCR | // Do not ignore carriage return on input
;; ICRNL | // Do not translate a carriage return to newline
;; IXON ); // Disable XON/XOFF flow control on output
;; termios_p->c_oflag &= ~OPOST; // Disable implementation-defined output processing
;; termios_p->c_lflag &= ~(ECHO | // Do not echo characters
;; ECHONL | // Do not echo newline characters
;; ICANON | // Disable canonical mode ("cooked") processing
;; ISIG | // Do not generate INTR, QUIT, SUSP or DSUSP signals
;; IEXTEN ); // Disable implemenation-defined input processing
;; termios_p->c_cflag &= ~(CSIZE | // No character size mask
;; PARENB ); // Turn off parity generation
;; termios_p->c_cflag |= CS8; // 8-bit characters
;;
;; NOTE: The use of 8-bit characters makes this a bit incompatible with the `read-char`
;; procedure, which works with UTF-8 characters. Needs more work.
;; The following definitions and structure are from the file
;; /Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Kernel.framework/Headers/sys/termios.h
;; on an iMac.
;; #define NCCS 20
;; typedef unsigned long tcflag_t;
;; typedef unsigned char cc_t;
;; typedef unsigned long speed_t;
(define NCCS 20)
(define-cstruct _termios
([c_iflag _ulong] ; input flags
[c_oflag _ulong] ; output flags
[c_cflag _ulong] ; control flags
[c_lflag _ulong] ; local flags
[c_cc (_array _ubyte NCCS)] ; special control chars
[c_ispeed _ulong] ; input speed
[c_ospeed _ulong] ; output speed
))
(define (malloc-termios-buffer)
(malloc 'atomic (ctype-sizeof _termios)))
(define (inner-read-line)
(define running #true)
(let ((acc ""))
(let loop ()
(let ((c (read-char)))
(cond
;; Look for characters picked to terminate a line. (Choose any
;; you want.)
[(or (eof-object? c)
(char=? c #\q)
(char=? c #\newline)
(char=? c #\return)) (begin
(println-in-raw "Finished because c = "
(char->readable c))
(set! running #f))]
[#t (set! acc (string-append acc (string c)))]))
(if (not running)
acc
(loop)))))
(define (enable-raw-mode fd termios-struct)
(cfmakeraw termios-struct)
(tcsetattr fd 0 termios-struct))
(define (disable-raw-mode fd attrs)
(tcsetattr fd 0 attrs))
;; Read a line from the standard input in raw mode and return it. The
;; existing terminal attributes are read and restored before exiting.
(define (read-raw-line)
(let* ((std-in-fd 0) ; Couldn't find in headers, but is it everywhere else.
(orig-attrs (malloc-termios-buffer))
(raw-attrs (malloc-termios-buffer)))
(tcgetattr std-in-fd orig-attrs)
(letrec ((get-raw! (lambda ()
(enable-raw-mode std-in-fd raw-attrs)))
(get-cooked! (lambda ()
(disable-raw-mode std-in-fd orig-attrs))))
(let ((a-line (dynamic-wind
get-raw!
inner-read-line
get-cooked!)))
a-line))))
(println "Enter an invisible line of text:")
(println "result of reading line in raw mode: "
(string->readable (read-raw-line)))
正如@Sylwester 所提到的,这是一个特定于实现的函数。上面的示例是使用 Racket v7.9[bc] 创建的。它不会在 DrRacket 中正确运行,因为评估窗格并不是真正的终端。在与文件在同一目录下打开的终端中,您可以在命令提示符下运行racket direct_tty.rkt 来播放程序。
大致相当于问题中提到的关于构建文本编辑器的文章中的第6步。
它还有一个更好的 (IMO) 功能,即使在出现错误或“时间旅行”继续的情况下,它也能确保终端在退出之前返回到其原始设置。请参阅read-raw-line 过程中dynamic-wind 的用法。
此实现存在(至少)一个潜在问题,即inner-read-line 中使用的read-char 过程可以处理Unicode。在原始模式的设置中,任何处理 Unicode 的功能都被禁用,输入被限制为 8 位字符。我认为这样可以使示例更简单,而不是必须投入大量的时间来重新打开它。
另外,我认为设置termios 结构的所有东西并不是真正需要的。如果您只需要cfmakeraw 来获得您想要的行为,我相信您只需将足够大的缓冲区传递给tcgetattr、tcsetattr 和cfmakeraw 即可。除非您需要进行额外的位摆弄,否则缓冲区中不需要任何底层结构。但我还没有测试过。
更新:2020 年 11 月 23 日:现在有一个 gist 显示如何使用 Chez Scheme 做到这一点。