【问题标题】:Reading Characters from the Terminal in Raw Mode using Scheme使用 Scheme 以原始模式从终端读取字符
【发布时间】:2020-11-05 22:18:47
【问题描述】:

我开始阅读文章“Build Your Own Text Editor”,了解如何在 C 中构建一个简单的基于终端的文本编辑器。我认为看看是否可以在 Scheme(Chez、Racket 或鸡)。

我在原始模式下捕获终端输入时遇到问题。我不知道如何在按键上立即获取字符。我根据Common Lisp at Rosetta Code 的答案在 Racket 中汇总了以下怪物(无论如何都不起作用)。

;; echo and work
(define (eaw)
  (let ((acc "")
        (ip (current-input-port))
        (done #f))
    (let loop ()
      (if (char-ready? ip)
          (let ((c (read-char ip)))
            (begin
              (if (char=? c #\q)
                  (begin
                    (set! done #t)
                    acc)
                  (begin
                    (when (not done)
                      (set! acc (string-append acc (string c))))
                    (loop)))))
          (if done
              (begin
                (display (string? acc))
                acc)
              (begin
                ;; do some work
                (display ".")
                (sleep 0.15)
                (loop)))))))

同一页面上的 Racket 示例对我不起作用——尝试打开终端时出现问题。

看起来在 REPL 中使用的 Chez 的 expediter 是用 C 编写的。在 Chicken ncurses 库中,它调用 C 运行时来实现 getch 函数。不确定 Racket 在哪里实现它。

我有没有办法在 Scheme(其中任何一个)中实现原始键盘输入,以便程序可以立即响应快捷键等?

【问题讨论】:

  • 在鸡肉中,查看stty egg
  • 谢谢@Shawn。这非常有用。

标签: scheme racket


【解决方案1】:

由于 Scheme 标准不提供这样的方法,因此无法通过列表检查输入端口是否有要读取的字符。您无法使用 Sceheme 以可移植的方式执行此操作。

大多数 Scheme 实现和相关方言都具有 Scheme 语言之外的功能。例如。 Racket has char-ready? 可以和read-char 一起使用。如果你使用这些特性,你的程序就不再是用 Scheme 编写的,而是用扩展的实现语言编写的。如果你愿意接受创建一个文本编辑器是可以实现的。

【讨论】:

  • 谢谢@Sylwester。阅读您的答案后,我有一个“哦!当然!”时刻。除了使用stay.egg 表示鸡之外,正如@Shawn 所提到的,我创建了一个特定于球拍的版本,该版本显示为另一个答案。
【解决方案2】:

在看到@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 来获得您想要的行为,我相信您只需将足够大的缓冲区传递给tcgetattrtcsetattrcfmakeraw 即可。除非您需要进行额外的位摆弄,否则缓冲区中不需要任何底层结构。但我还没有测试过。

更新:2020 年 11 月 23 日:现在有一个 gist 显示如何使用 Chez Scheme 做到这一点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-20
    相关资源
    最近更新 更多