Noncanonical terminal input in SML
In a Unix terminal environment, when we ask for input from stdin, the
terminal will usually buffer the user’s input and not send it until
the user enters a linebreak. This is usually desirable, as it allows
line editing (backspaces and such) to occur without the programs
knowledge. However, sometimes a program does not want to wait for a
linebreak to be entered - perhaps because it wants to present a
responsive user interface. In such cases we have to ask the terminal
not to perform such buffering. On POSIX systems this is done via the
interface in termios.h
.
This small program demonstrates how to use termios.h
facilities to
perform raw terminal input in Standard ML. The Basis Library provides
the Posix.TTY
structure, which
seems to cover most of termios.h
. We use the function
Posix.TTY.TC.getattr
to retrieve the terminal attributes from stdin
(as a value of type termios
), which we can then manipulate through
various functions, and finally we replace the terminal attribute set
with our modified one. In particular, we disable “canonical mode”
(which is what provides line buffering and similar things), and we
also disable echoing, which means the user input is not echoed by the
terminal. We also return a function that restores the original
attribute set of the terminal, which must be called when our program
is done, as otherwise we will leave the terminal in an unsuitable
state for future programs.
fun noncanon () =
let
val oldattr = Posix.TTY.TC.getattr Posix.FileSys.stdin
val {iflag, oflag, cflag, lflag, cc, ispeed, ospeed} =
Posix.TTY.fieldsOf oldattrval lflag = Posix.TTY.L.clear
(lflag, Posix.TTY.L.flags [Posix.TTY.L.echo, Posix.TTY.L.icanon])val newattr = Posix.TTY.termios
{ iflag = iflag
, oflag = oflag
, cflag = cflag
, lflag = lflag
, cc = cc
, ispeed = ispeed
, ospeed = ospeed
}val () =
Posix.TTY.TC.setattr (Posix.FileSys.stdin, Posix.TTY.TC.sanow, newattr)in
fn () =>
Posix.TTY.TC.setattr (Posix.FileSys.stdin, Posix.TTY.TC.sanow, oldattr)end
We can use noncanon
as follows:
fun main () =
let
val restore = noncanon ()
fun loop () =
case TextIO.input1 TextIO.stdIn of
"EOF\n"; restore ())
NONE => (print "q" => restore ()
| SOME #
| SOME c =>"char: " ^ str c ^ " (" ^ Int.toString (ord c) ^ ")\n")
( print (
; loop ()
)in
loop ()end
val () = main ()
Run this program and note how each character is processed immediately,
without waiting for a linebreak. Note also that some input (such as
arrow keys) are sent as multiple bytes, and that interrupts such as
Ctrl-c
are also made available as characters.