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 oldattr
    val 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
        NONE => (print "EOF\n"; restore ())
      | SOME #"q" => restore ()
      | SOME c =>
          ( print ("char: " ^ str c ^ " (" ^ Int.toString (ord c) ^ ")\n")
          ; 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.