205: POSIX Terminal Fundamentals

by John Cowan and Harold Ancell

Status

This SRFI is currently in withdrawn 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.

It also provides a set of miscellaneous POSIX terminal procedures that don't belong anywhere else.

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.

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 in the style of SRFI 170. It provides three procedures which will typically be shims over whatever the implementation uses to report such errors:

(posix-error? obj)    →     boolean

This procedure returns #t if obj is a condition object that describes a POSIX error, and #f otherwise.

(posix-error-name posix-error)    →     symbol

This procedure returns a symbol that is the name associated with the value of errno when the POSIX function reported an error. This can be used to provide programmatic recovery when a POSIX function can return more than one value of errno.

Because the errno codes are not standardized across different POSIX systems, but the associated names (bound by a #define in the file /usr/include/errno.h) are the same for the most part, this function returns the name rather than the code.

For example, ENOENT (a reference was made to a file or a directory that does not exist) almost always corresponds to an errno value of 2. But although ETIMEDOUT (meaning that a TCP connection has been unresponsive for too long) is standardized by POSIX, it has a errno value of 110 on Linux, 60 on FreeBSD, and 116 on Cygwin.

(posix-error-message posix-error)    →     string

This procedure returns a string that is an error message reflecting the value of errno when the POSIX function reported an error. This string may be, or may include, the output of the strerror() function, and is useful for reporting the cause of the error to the user. It may or may not be localized.

This SRFI also recommends (but does not require) that the following additional information be retrievable by other means:

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 section's with-* or without-* 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 proc min time)    →    [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 ECHO, ECHOE, ECHOK and ICANON flags, and enabling the CS8 setting. 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 current terminal state is retrieved and a copy of it is pushed on the stack as a state object. Then a copy of the state object at the bottom of the stack 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 tcgetattr(), tcsetattr()

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 ECHO, ECHOE and ECHOK flags, and enabling the CS8 setting. 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, list or state-object       (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. When raising an error, the terminal port must be set back to its original values, and the error object must report which of the values tcsetattr() was unable to set is that is the cause. At least the following arguments must be supported, although they may or may not have the expected result:

(get-possible-terminal-attributes)    →    list       (procedure)

Returns a list of symbols and two-element lists that are acceptable to the implementation of stty. A particular serial line port may not support all of them.

Miscellaneous procedures

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

(terminal-dimensions port)    →    list       (procedure)       POSIX ioctl()

Returns a list of two integers, the height and width of the terminal.

(terminal-file-name (current-output-port)) ⇒ (24 80)
(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-output) ⇒ unspecified
(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

The beginnings of a Chibi Scheme sample implementation can be found in srfi/chibi-scheme of the SRFI repository.

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 © (2022) 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