Version 10 of Reading a single character from the keyboard using Tcl

Updated 2005-09-16 10:55:46 by lwv

DKF, 8-Sep-2005

How to read a single character from the keyboard using just Tcl? (It's pretty easy using a Tk GUI, of course!) Why doesn't just doing [read stdin 1] work?

Well, in fact it does work, but only if the terminal isn't working in line-editing mode. In line-editing mode, the OS terminal engine only sends the text to the applications once the user presses the return key. Guess what mode the terminal is in by default? :^/ In line-editing mode, the operating system is doing things like buffering up the data (to make I/O more efficient, providing for functionality like backspacing (perhaps ^H/^?), interruption signaling (perhaps ^C/^?), word deletion, line deletion, line refresh, literal quoting (perhaps ^V), and more. Normally, you want this - these are the things that, at a command line, you want to take place. However, often in a text user interface, you want more control than that.

(A little terminology. We're trying to switch things to raw mode here so we can read the "raw" keystrokes, with the default line-editing mode often called cooked mode by contrast.)


Raw Mode on Unix

On Unix platforms (e.g. Linux, Solaris, MacOS X, AIX, etc.) you can use the stty program to turn raw mode on and off, like this:

  exec /bin/stty raw <@stdin
  set c [read stdin 1]
  exec /bin/stty -raw <@stdin

(We use the <@stdin because stty works out what terminal to work with using standard input on some platforms. On others it prefers /dev/tty instead, but putting in the redirection makes the code more portable.)

However, it is usually a good idea to turn off echoing of characters in raw mode. It means that you're responsible for everything, but that's often what you want anyway. Wrapping things up in some procedures, we get this:

  proc enableRaw {{channel stdin}} {
     exec /bin/stty raw -echo <@stdin
  }
  proc disableRaw {{channel stdin}} {
     exec /bin/stty -raw echo <@stdin
  }

  enableRaw
  set c [read stdin 1]
  puts -nonewline $c
  disableRaw

Raw Mode on Windows

A different approach is needed on Microsoft Windows NT-based systems. This requires the twapi extension.

  package require twapi
  proc enableRaw {{channel stdin}} {
     set console_handle [twapi::GetStdHandle -10]
     set oldmode [twapi::GetConsoleMode $console_handle]
     set newmode [expr {$oldmode & ~6}] ;# Turn off the echo and line-editing bits
     twapi::SetConsoleMode $console_handle $newmode
  }
  proc disableRaw {{channel stdin}} {
     set console_handle [twapi::GetStdHandle -10]
     set oldmode [twapi::GetConsoleMode $console_handle]
     set newmode [expr {$oldmode | 6}] ;# Turn on the echo and line-editing bits
     twapi::SetConsoleMode $console_handle $newmode
  }

  enableRaw
  set c [read stdin 1]
  puts -nonewline $c
  disableRaw

This code was adapted from the Echo-free password entry page with the help of the appropriate page on MSDN[L1 ].

If you are using TWAPI 0.7 or above, you can use modify_console_input_mode[L2 ] to simplify the above code.

(Apparently, you're stuck on 95/98/ME.)


schlenk 8/Sep/2005 The same API exists on Win9x according to the MSDN article referred to above, so TWAPI may work, but it is untested and unsupported on windows 9x.

PWQ 10 Sept 05

Whats wrong with using fileevents:

 proc readsingle {} {puts "I read '[read stdin 1]' char}

 fileevent stdin read readsingle

This should work on all platforms

dzach In Windows it doesn't. It waits for a newline. As it says at the begining of this page, it does work, but only if the terminal isn't working in line-editing mode. In line-editing mode, the OS terminal engine only sends the text to the applications once the user presses the return key. Which unfortunately is true.

DKF: Once you're in raw mode, using fileevent is exactly the way to do it. In cooked mode, the data only ever comes to Tcl in the first place by whole lines, so fileevent works, but isn't too useful for when you want to read less than a line (e.g. a single char).


Category Example | Category Device Control