205: POSIX Terminal Fundamentals

by John Cowan (author) and Harold Ancell (author and editor)

Status

This SRFI is currently in draft status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-205@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

This SRFI describes procedures for command-line and terminal interface programs to safely change and reset terminal modes, for example from cooked to raw and back, and for serial-line device manipulation for interfacing with embedded hardware and the like.

It is intended to provide all the termios structure functionality a modern Scheme programmer might desire by supplying a stty procedure, and simple abstractions on top of it.

Issues

Rationale

POSIX provides a complete set of routines for manipulating terminal devices — putting them in "raw" mode, changing and querying their special characters, modifying their I/O speeds, and so forth. Now that terminal emulators have almost completely displaced terminals, very little of this is useful except for directly controlling serial-line hardware, which is still widely used for embedded programming and applications.

This SRFI provides safe high-level wrappers for raw, no echo, etc. terminal modes for use by command-line programs and textual user interfaces (TUIs), and a stty(1) style procedure to set a terminal's characteristics such as its baud rate.

These two use cases are bundled together in one SRFI because the complicated low-level infrastructure for implementing either is roughly the same, except for different sets of constants, which are presumably cheap to implement. If you first implement stty, the special terminal mode procedures can be simply and elegantly built on top of it with a few lines of code.

This SRFI depends on SRFI 198: Foreign Interface Error Handling. Various procedures or their equivalents in SRFI 170: POSIX API, from which it was forked off of, are needed or may be useful in concert with it. It is based on the 2018 edition of POSIX IEEE Std 1003.1-2017™.

Specification

Error handling

Throughout this specification, it is an error if port is not open on a terminal.

The procedures of this SRFI raise an exception when an underlying system call fails.

Safe terminal I/O primitives

The following procedures conform to an implementation model that represents how they may be implemented. A state object is a hidden object that represents the complete state of a terminal line as retrieved by tcgetattr() and set by tcsetattr(). An implementation of this SRFI maintains a stack of state objects; a new element can be pushed, the top element can be popped, and the bottom element can be retrieved without popping it. The stack is initialized with a single state object representing the state of the terminal when the program starts. In practice this may be done on the first call to any of this SRFI's procedures.

The procedures use dynamic-wind when executing their proc argument. In the before-thunk the current terminal state is fetched from a specified place and is pushed on the stack. Then specified flags are turned on or off, and the terminal is set according to the resulting state. In the after-thunk a terminal state is popped from the stack and the terminal is set accordingly. If proc's dynamic extent is escaped, the after-thunk is executed, and if control returns to the dynamic extent, the before-thunk is executed.

The general paradigm for using with-raw-mode and with-rare-mode is to set up your application, then run it in the proc provided to them, a procedure that takes the same port arguments in the same order as the containing with- or without- procedure. Inside proc, with-cooked-mode can be used for a temporary escape, for instance to a shell. The without-echo procedure is an exception in that it's generally used to enter a password or passphrase. The procedures return the values that proc returns.

(with-raw-mode port min time proc)    →    [values]       (procedure)       POSIX tcgetattr(), tcsetattr()

The terminal is set to raw mode during the dynamic execution of proc and then is restored to the previous mode. The effect of the min and time arguments is that any reads done on the terminal while raw mode is in effect will return to the caller after min bytes have been read or time deciseconds (1/10ths of a second) have elapsed, whichever comes first. Therefore, it makes no sense to use any read operation on the terminal except read-char or read-string, which read a fixed number of characters. No character is given special handling; all are passed to the application exactly as received. Echoing of input is disabled on the terminal during the execution of proc.

In terms of the implementation model, the current terminal state is retrieved and a copy of it is pushed on the stack as a state object. Then the state object is modified by disabling the following flags: ECHO ECHOE ECHOK ICANON IEXTEN ISIG BRKINT ICRNL INPCK ISTRIP IXON CSIZE PARENB OPOST. The CS8 setting is enabled, and the VMIN and VTIME settings are set using min and time respectively. Then the state object is written back to the terminal and the implementation model is followed thereafter.

(with-raw-mode ((current-input-port) 2 50
               (lambda (x y) (read-char) (read-char))) ⇒ #\x03
(with-rare-mode port proc)    →    [values]       (procedure)       POSIX tcgetattr(), tcsetattr()

The terminal is set to rare (also known as cbreak) mode during the dynamic execution of proc, and then is restored to the previous mode. Just as in canonical mode, any read operation on the terminal will wait until characters are received, unlike raw mode. However, no characters are given special interpretation except the characters that send signals (by default, Ctrl-C and Ctrl-\). Echoing of input is disabled on the terminal during the execution of proc.

In terms of the implementation model, the current terminal state is retrieved and a copy of it is pushed on the stack as a state object. Then the state object is modified by disabling the following flags: ECHO ECHOE ECHOK ICANON. The CS8 setting is enabled, and the VMIN and VTIME settings are set using min and time respectively. Then the state object is written back to the terminal and the implementation model is followed thereafter.

(with-rare-mode (current-input-port)
                (lambda (x y) (read-char))) ⇒ #\newline
(with-cooked-mode port proc)    →    [values]       (procedure)       POSIX tcgetattr(), tcsetattr()

The terminal is set to cooked mode during the dynamic execution of proc and then is restored to the previous mode. Echoing of input is enabled on the terminal during the execution of proc.

In terms of the implementation model, the state object at the bottom of the stack is retrieved and a copy of it is pushed on the stack. Then another copy of the state object is modified by enabling the following flags (which are probably already enabled, but we make sure) ECHO ECHOE ECHOK ICANON IEXTEN ISIG BRKINT ICRNL INPCK ISTRIP IXON CSIZE PARENB OPOST, and the CS8 setting is enabled. Then the modified state object is written back to the terminal and the implementation model is followed thereafter.

(with-cooked-mode (current-input-port)
                  (lambda (x y z) (<spawn-shell-process> x y z))) ⇒ 0
(without-echo port proc)    →    [values]       (procedure)       POSIX -ECHO -ECHOE -ECHOK -ECHONL 

Echoing of input is disabled on the terminal during the execution of proc and then is re-enabled.

In terms of the implementation model, the current terminal state is retrieved and a copy of it is pushed on the stack as a state object. Then the state object is modified by disabling the following flags: ECHO ECHOE ECHOK. The CS8 setting is enabled, and the VMIN and VTIME settings are set using min and time respectively. Then the state object is written back to the terminal and the implementation model is followed thereafter.

(without-echo (current-input-port)
              (lambda (x) (read-string 8 (current-input-port)))) ⇒ "12345678"

Low-level terminal manipulation

(stty port . args)    →    undefined       (procedure)       POSIX tcgetattr(), tcsetattr()

Use in the manner of POSIX stty(1), as extended to allow specifying the special device file to be manipulated, here associated with a port. At least the following arguments must be supported, although they may or may not have the expected result:

(gtty port . args)    →    undefined       (procedure)       POSIX tcgetattr()
Returns a list of symbols and two-element lists that are acceptable to stty.

Miscellaneous procedures

See also the terminal? procedure in SRFI 170, which returns #t if the supplied port argument is a terminal.

(terminal-file-name port)    →    string       (procedure)       POSIX ttyname()
Returns the file name of the terminal.
(terminal-file-name port) ⇒ "/dev/ttyS0"
(terminal-flow-control port exact-integer)    →    unspecified       (procedure)       POSIX tcflow()

Controls the flow of characters in either direction, depending on which of terminal/stop-output, terminal/start-output, terminal/stop-input, and terminal/start-input is the second argument

(terminal-flow-control port terminal/start-outputunspecified
(terminal-wait port)    →    unspecified       (procedure)       POSIX tcdrain()

Waits until all characters queued to be sent on the terminal have been sent.

(terminal-wait port) ⇒ unspecified
(terminal-discard port int)    →    unspecified       (procedure)       POSIX tcflush()

Discards any input received but not yet read, or any output written but not yet sent, or both, depending on which of terminal/discard-input, terminal/discard-output, or terminal/discard-both is the second argument.

(terminal-discard port discard-input) ⇒ unspecified
(terminal-send-break port boolean)    →    undefined       (procedure)       POSIX tcsendbreak()

If boolean is false, sends a break signal (consecutive zero bits) for at least 0.25 seconds and not more than 0.5 seconds. If boolean is true, sends a break signal for an implementation-defined length of time.

(terminal-send-break port) ⇒ unspecified

Implementation

A Chibi Scheme sample implementation will be created after the API settles down.

Acknowledgments

Alex Shinn conceived the operating paradigm for the with-* and without-echo procedures, which minimizes the classic risk of leaving a terminal in an odd, often non-echoing state.

Copyright

Copyright © (2020) John Cowan and Harold Ancell.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler