std/term
import std/term to use it. Source: src/std/term.iyi at v0.18.0.
import std/term::{Term} rows, columns = Term.size || {24, 80}
key = Term.raw { STDIN.read_bytes(1) }.or_panic
secret = Term.read_password("Password: ")Term.tty?(io) (or a descriptor) asks the question the prelude's __iyi_isatty already answers for std/colorize, and is that function rather than a second copy of it: TCGETS refused or not on Linux, libSystem's isatty on darwin, GetConsoleMode answering at all on Windows. Only Windows' standard handles go through the prelude there — its probe is cached per standard stream — and any other handle, a CONIN$ opened by hand, is asked directly.
Term.size answers {rows, columns}, in that order because it is the order the kernel's struct winsize and stty size give them: TIOCGWINSZ on Linux and darwin, and on Windows the visible window of the console screen buffer (srWindow, as Go's x/term reads it — the buffer itself is thousands of rows of scrollback). With no stream given it asks standard output, then standard error, then standard input, and the first terminal answers: prog | less still learns the width of the terminal it is running in. Nil is no terminal on any of them, and also a terminal that says 0 by 0 — a pseudo-terminal nobody set a size on answers exactly that, and a program dividing by the width should be told it does not know rather than handed a zero.
Term.raw(io = STDIN) { } puts the terminal in raw mode, runs the block and answers its value; Term.no_echo(io = STDIN) { } turns echo off in the same way. The mode the terminal had before is saved and written back by a defer, so it is restored when the block returns, when a ! in it returns early and when it panics — the panic path runs pending defers before the task dies (SPEC.md III.1.4), which is what Crystal's begin/ensure around the same calls gives IO#raw. A stream that is not a terminal is a TermError and the block does not run: standard input redirected from a file is a condition a program meets, not a mistake in it. A closed stream is a mistake and panics. A signal that ends the process runs no cleanup, here as in Go; inside raw that does not arise from the keyboard, because Ctrl-C is a byte there.
The flag sets are the two references' own, cited so a reader can check each bit:
* raw: the termios(3) manpage's `cfmakeraw`, which Go's x/term
`makeRaw` (term_unix.go) and Crystal's `FileDescriptor.cfmakeraw`
(src/crystal/system/unix/file_descriptor.cr) both write out by hand —
input IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON off, output
OPOST off, local ECHO ECHONL ICANON ISIG IEXTEN off, CSIZE and PARENB
cleared and CS8 set, VMIN 1 and VTIME 0: a read answers as soon as one
byte has arrived, Ctrl-C and Ctrl-Z are bytes, CR is not turned into
NL and NL is not turned into CR NL on the way out. Python's
`tty.cfmakeraw` clears more (IXANY, IXOFF, NOFLSH, TOSTOP, …) and is
not followed. Windows: Go's and Crystal's console mode,
ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT and ENABLE_PROCESSED_INPUT off and
ENABLE_VIRTUAL_TERMINAL_INPUT on, so arrow keys arrive as the escape
sequences a POSIX terminal sends.
* echo off: Crystal's `IO#noecho` set — ECHO, ECHOE, ECHOK and ECHONL
off and nothing else, so the terminal still reads a line and still
answers Ctrl-C with SIGINT. ECHONL matters: left on, a canonical
terminal echoes the newline it was told not to echo the line before.
Windows: ENABLE_ECHO_INPUT off.
* a password: echo off as above, plus what Go's `readPassword` forces on
— ICANON, ISIG, ICRNL — so a password is read as a line even from a
terminal a caller left in raw mode, and the Enter key's CR arrives as
the NL a line ends at. Windows: ENABLE_ECHO_INPUT off,
ENABLE_LINE_INPUT and ENABLE_PROCESSED_INPUT on. Go turns line input
off there and edits the line itself; here the console's own line
editing is kept, because `STDIN` reads a Windows console a line at a
time through `ReadConsoleW` already.Every change is made at once (TCSANOW: TCSETS on Linux, tcsetattr with TCSANOW on darwin), as Go and the other library make it, so input typed ahead of the block is not thrown away; TCSAFLUSH, Python's getpass choice, would discard it.
Term.read_password(prompt) writes the prompt to standard error — not standard output, which is the program's data, and the stream Python's getpass falls back to — reads a line from standard input with echo off, writes the newline the terminal did not echo, and answers the line without its line ending; nil at the end of the input (Ctrl-D on an empty line). Standard input that is not a terminal is read as it is: a pipe echoes nothing, so echo secret | prog is how a script hands a program a password, as Python's getpass allows. A terminal that answers TCGETS and then refuses the change panics with the reason, because reading the password anyway would show it on the screen.
How the platform is reached, in the neighbouring modules' way and with no C library added: on Linux the ioctl system call by hand (16 on x86_64, 29 on aarch64), TCGETS 0x5401, TCSETS 0x5402 and TIOCGWINSZ 0x5413 from <asm-generic/ioctls.h> — x86_64 and aarch64 share the asm-generic numbers, flag values and the 36-byte struct termios (four 32-bit flag words, c_line, nineteen control characters, so VTIME is byte 22 and VMIN byte 23). On darwin libSystem's tcgetattr and tcsetattr, because Apple documents no stable system call interface, and ioctl for TIOCGWINSZ 0x40087468 (_IOR('t', 104, struct winsize), Go's x/sys zerrors_darwin_arm64.go); darwin's struct termios is 72 bytes — four 64-bit flag words, twenty control characters, VMIN at 16 and VTIME at 17, then two speeds — and its flag values are BSD's, not Linux's (ECHONL is 0x10, ICANON 0x100, CS8 0x300). On Windows kernel32's GetConsoleMode, SetConsoleMode and GetConsoleScreenBufferInfo.
Termstruct
pub struct Term
def initialize
# Runs the block with echo off on *io*'s terminal and answers its value;
# lines are still read as lines and Ctrl-C is still a signal. The
# previous mode is restored when the block ends, however it ends. A
# `TermError`, and the block not run, when *io* is not a terminal.
def self.no_echo(io : IO = STDIN, &block : (-> T)) : T | TermError forall T
# Runs the block with *io*'s terminal in raw mode and answers its value:
# every byte is the program's as it is typed — no line to wait for, no
# echo, no signal from Ctrl-C, no CR to NL — and output is written
# without NL becoming CR NL. The previous mode is restored when the
# block ends, however it ends. A `TermError`, and the block not run,
# when *io* is not a terminal or the terminal refuses the change.
def self.raw(io : IO = STDIN, &block : (-> T)) : T | TermError forall T
# Writes *prompt* to standard error and reads a line from standard input
# with echo off, then writes the newline the terminal did not echo.
# Answers the line without its line ending, or nil at the end of the
# input. Standard input that is not a terminal is read as it is.
def self.read_password(prompt : String = "") : String | ::Nil
# `{rows, columns}` of the terminal *io* is, or nil.
def self.size(io : IO) : ::Tuple(Int32, Int32) | ::Nil
def self.size(fd : Int32) : ::Tuple(Int32, Int32) | ::Nil
def self.size(fd : Int64) : ::Tuple(Int32, Int32) | ::Nil
# `{rows, columns}` of the terminal on standard output, standard error
# or standard input, the first that is one; nil when none is, or when
# the terminal does not know its size (0 by 0).
def self.size : ::Tuple(Int32, Int32) | ::Nil
# Whether *io* is a terminal.
def self.tty?(io : IO) : Bool
# Whether descriptor *fd* is a terminal — on Windows, a standard
# stream's number or a handle's value, as `IO#handle` answers it.
def self.tty?(fd : Int32) : Bool
def self.tty?(fd : Int64) : Bool
endTermErrorstruct
A terminal that could not be put in the mode asked for, and the platform's error number for why: errno on Linux and darwin, the GetLastError code on Windows.
pub struct TermError
def code : Int32
def fd : Int64
def initialize(operation : String, fd : Int64, code : Int32, terminal : Bool)
# The stream is not a terminal: ENOTTY (25 on Linux and darwin) from
# TCGETS or `tcgetattr`; on Windows `GetConsoleMode` refusing it at all,
# which is the test for a console whatever code it leaves.
def not_a_terminal? : Bool
def operation : String
def reason : String
def stream : String
endError for TermErrorimpl
impl Error for TermError
def message : String
end
Terminals: whether a stream is one, how big it is, and the two modes a program asks a terminal for — raw, and echo off — for the length of a block, with the previous mode put back however the block ends.