Wednesday, November 28

Linux Fu: Controlling the Terminal

A Linux terminal has a lot more features than the TeleType of yore. On a TeleType, text spews out and scrolls up and is gone forever. A real terminal can use escape characters to do navigate around and emulate most of what you like about GUIs. However, doing this at the lowest level is a chore and limits portability. Luckily, all the hard work has already been done.

First, there’s a large database of terminal capabilities available for you to use: terminfo.  And in addition, there’s a high-level library called curses or ncurses that simplifies writing programs to control the terminal display. Digging deep into every nook and cranny of ncurses could take years. Instead, I’m going to talk about using a program that comes with ncurses to control the terminal, called tput. Using these two commands, you can figure out what kind of terminal you’re dealing with, and then manipulate it nearly to your heart’s content. Let’s get started!

About terminfo

The most fundamental question you have to ask is what kind of terminal are you talking to. The answer is in the $TERM environment variable and if you ask your shell to print that variable, you’ll see what it thinks you are using: echo $TERM.
For example, a common type is vt100 or linux console. This is usually set by the system and you shouldn’t change it unless you have a good reason. In theory, of course, you could manually process this information but it would be daunting to have to figure out all the way to do something like clear the screen on the many terminals Linux understands.

That’s why there’s a terminfo database. On my system, there are 43 files in /lib/terminfo (not counting the directories) for terminals with names like dumb, cygwin, ansi, sun, and xterm. Looking at the files isn’t very useful since they are in a binary format, but the infocmp program can reverse the compilation process. Here’s part of the result of running infocmp and asking for the definition of a vt100:


vt100|vt100-am|dec vt100 (w/advanced video),

       am, mc5i, msgr, xenl, xon,

        cols#80, it#8, lines#24, vt#3,

        acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz~~,

        bel=^G, blink=\E[5m$<2>, bold=\E[1m$<2>,

        clear=\E[H\E[J$<50>, cr=\r, csr=\E[%i%p1%d;%p2%dr,
 

This might not look much better than the binary version. But you can probably guess that it is telling you, in part, that the bell character (bel) is Control-G (^G). You can also probably guess that \E is an escape character and puzzle out how to make characters bold or clear the screen. Some of the data is structural such as 24 lines and 80 columns of output. The capabilities (like xon for using XON/XOFF protocol) are a bit harder to puzzle out without looking it up.

The exact meanings, though, aren’t important. Because we are going to use higher-level constructs to tell tput what we want and it — or at least the ncurses library — will look up the data in terminfo and then do the right thing.

Using tput for Information

I’m not sure where the name tput came from — you have to assume it is “terminal put.” But you can use tput to get information, too. For example, instead of looking at $TERM, you can say tput longname. You can learn other things, too. For example, how wide is the terminal (in characters)? tput cols.

This is especially handy for terminals under X Windows that might change size. In a script you can trap the WINCH event to run some code when the terminal resizes, which can be very handy. Other things you can read is the number of lines and the number of colors.

For example, assuming you have fortune installed, you might want a little fortune window that updates every 30 seconds. It would be nice, though to have the fortune center even if the screen changes size.

#!/bin/bash
# Simple centered fortune
TIMEOUT=30


do_print() {
  w=$(tput cols)
  h=$(tput lines)
  n=${#text}
  spaces=$(( ($w-$n)/2 ))
  lines=$(( $h/2 ))
  clear
  for I in `seq 1 $lines`
  do
   echo 
  done
  for I in `seq 1 $spaces`
  do
    echo -n ' '
  done
  echo $text
}

while true
do
  text=$(fortune -n $(tput cols) -s )
  do_print
  sleep ${TIMEOUT:-30}
done

This works pretty well. When a fortune appears it will be in the center of the window. I do all the drawing in do_print because I have future plans for that function.

The fortune command tells it to only give us short fortunes and the definition of short is the width of the screen according to tput. I use a few bash-specific techniques here including expression evaluation, string length(${#text}), and default parameters

You’ll notice, though, if you resize the window, the fortune won’t be in the right place until a new one appears. How can we fix that?

Use a WINCH

Turns out, the program should get a SIGWINCH signal when the terminal resizes. I say “should” because there are cases where this doesn’t occur, for example in some emacs shells. (In fact, a lot of this breaks in emacs.) There are probably other setups that could have problems, too. However, assuming the code gets SIGWINCH, it is easy to handle it. This line of code should take care of it: trap do_print WINCH

The only problem is, it doesn’t work right for this script. The script spends most of its time sleeping and it will only get the signal when it wakes up. But by that time, you’ll also get a new fortune, so while it does the right thing, it either does it with the new fortune or it does it with the old fortune and is immediately overwritten.

The answer is to sleep less. However, it is rude to the rest of the system to simply sit and spin, so I change the sleep to this:

# Can't just sleep for timeout
# because we won't get SIGWINCH when sleeping
# If you want faster response, sleep less
TO=${TIMEOUT:-30}
TO=$(( $TO * 2 ))
for I in `seq 1 ${TIMEOUT:-30}`
do
   sleep 0.5
done

This is a little over complex because I wanted to sleep for 500 milliseconds instead of a full second. The less you sleep the more responsive the script is to changes. However, the more system resources you waste, so it is a tradeoff.

Formatting

Learning the size of the screen is pretty useful. But you can also use tput to format text:

  • blink – Blinking text
  • bold – Bold text
  • invis – Invisible text
  • rev – Reverse video
  • rms0 – End standout mode
  • rmul – End underlined text
  • setab – Background color
  • setaf -Foreground color
  • sgr0 – Turn off all attributes
  • smso – Start standout mode
  • smul – Start underlined mode

The colors take a number ranging from 0 black to 7 white. A value of 9 resets the default color. Try changing the echo $text line to look like this:

tput setab 7   # white background
tput setaf 4   # blue text
tput bold
echo $text
tput sgr0

Unfortunately, you can’t use color names unless you define them yourself. If you don’t like using separate tput calls for each item, you can use the -S option and a here document:

tput -S >> tputEOF
  setab  7  # white
  setaf  3  # blue
  bold
tputEOF

You still have to put one item per line, though. I can’t say why standout and underline mode have a specific end code and the rest you just have to use sgr0 to turn everything off.

Erasing and Saving

I used clear to clear the screen but I could have used tput clear. In fact, on some systems clear is an alias to tput and the program is smart enough to know if you call it as clear it should clear the screen and exit.

There are a few commands that let you either clear parts of the screen or save and recall the screen:

  • clear – Clear the whole screen
  • ed – Clear to end of screen
  • el – Clear to end of line
  • el1 – Clear to start of the line
  • rmcup – Restore saved screen
  • smcup – Save screen and clear

You can try tput smcup from the command line if you want to see it work. Just issue the command, do some things, and then issue tput rmcup. If you want to color the whole screen, try this in the fortune script, replacing the clear command:

tput setab 2
tput clear

Then take out the setab command further down.

Cursor Control

The last item is to be able to move the cursor under program control:

  • civis – Set cursor invisible
  • cnorm – Set normal cursor
  • cud1 – Move down one line
  • cup – Set row and column
  • cuu1 – Move cursor up one line
  • home – Move cursor to top left
  • rc – Restore saved cursor position
  • sc – Save cursor position

You could do a better job on the script now, to position the print location to the right place by replacing both for loops in do_print with: tput cup $lines $spaces.

Other Ideas

Using tput you can easily create things like progress bars or display data by pages without relying on external pager programs. Of course, if you need something too fancy, you probably ought to go with a GUI program or a dialog-box library. Of course, there are many of these for Linux: dialog, cdialog, zenity, kdialog, and easybashgui come to mind.

But sometimes you can’t, don’t want to, or just don’t need to get involved with more complex libraries. For simply putting text somewhere up on the screen, it’s hard to beat tput.

No comments:

Post a Comment