Skip to content

arp242/termfo

Repository files navigation

termfo is a terminfo library for Go.

It also has a little termfo commandline tool to list, search, and print various terminfo things that are not so easy to get with infocmp and/or are formatted a bit nicer.

Import at zgo.at/termfo; API docs: https://godocs.io/zgo.at/termfo

Current status: should be (mostly) usable and complete, but not widely tested yet and there are a few rough edges here and there. Also the API may change; specifically, I might rename some capability or key constants. For now I want to focus on the application I wanted to write rather than all this stuff :-)

Usage

Note: you may want to read the "Some background and concepts" section below first if you're not already familiar with the basics of terminals and/or terminfo, which explains a bit of background that may be useful.


First create a new termfo.Terminfo instance; the parameter is the terminal to load; it will use the TERM environment variable if it's empty, which is what you want >99% of the time:

ti, err := termfo.New("")

Capabilities have three types: bool, number, and string; you can get them from the Bools, Numbers, and Strings maps:

s, ok := ti.Strings[caps.EnterItalicsMode]
if ok {
    fmt.Println(s, "Italic text!", ti.GetString(caps.ExitItalicsMode))
}

n, ok := ti.Numbers[cap.MaxColors]     // "max_colors" ("colors")
if ok {
    fmt.Printf("Supports %d colours\n")
}

_, ok := ti.Bools[caps.AutoRightMargin] // "am"; note this only lists values 
                                        // present, so it's always true.
if ok {
    fmt.Println("Has am")
}

The capabilities themselves are in the termfo/caps subpackage as pointers to the caps.Cap struct, which also contains the short name (e.g. sitm for enter_italics_mode) and the description from terminfo(5). If you have an impressive unix beard and managed to memorize all the short codes then you can use the scaps package:

s, ok := ti.Strings[scaps.Sitm]
if ok {
    fmt.Println(s, "Italic text!", ti.GetString(scaps.Ritm))
}

sitm instead of enter_italics_mode is just obscure, but having the mapping is useful at times, even if only to make it easier to find out what something does from looking at constants in C code.

To add parameters use the Get() method:

ti.Get(caps.ParmDeleteLine, 5)   // Delete 5 lines

There is also Put() to write it, and Supports() to check if it's supported.

NOTE: this part of the API still sucks a bit; some of the capabilities at least indicate they accept parameters with Parm, but some don't, and omitting the argument will send nonsense (this should typed). Not yet 100% sure what a nice API would look like. Part of the problem is that terminfo files can add user-defined extended attributes.

Keys

There is some additional processing for keys; the most common way to use this is through the Terminfo.FindKeys() method; for example:

ti, _ := termfo.New("")

ch := ti.FindKeys(os.Stdin)
for e := <-ch; ; e = <-ch {
    fmt.Println("Pressed", e.Key)
}

This will keep scanning for keys in stdin. Note that you'll need to put the terminal in "raw mode" by sending the appropriate ioctls to send keys without having to press Enter. I recommend using the golang.org/x/term package for this; it's not included here as it pulls in x/sys which is ~8.5M and a somewhat large dependency. You can also use syscall. See internal/term for an example of that.

Keys are represented as a Key, which is an uint64. The lower 32 bits are used as a regular rune, and the remaining 32 for some other information like modifier keys. The upshot of this is that you can now use a single value to test for all combinations:

switch Key(0x61) {
case 'a':                           // 'a' w/o modifiers
case 'a' | keys.Ctrl:               // 'a' with control
case 'a' | keys.Ctrl | keys.Shift:  // 'a' with shift and control

case keys.Up:                       // Arrow up
case keys.Up | keys.Ctrl:           // Arrow up with control
}

Note that keys are always sent as lower-case; use 'a' | keys.Shift to test for upper-case, and control characters are always sent as 'a' | keys.Ctrl rather than 0x01.

Mouse support

There is no direct support for this (yet), mostly because I simply don't need it.

Updating

Various terminfo data in is generated from the ncurses source with term.h.zsh. This requires the ncurses source tree.

This requires zsh, awk, and gofmt.

Some background and concepts

A "terminfo" file is essentially a key/value database to tell applications about the properties of the terminal the user is using.

To understand why this is needed you need to understand that terminals – and applications that run inside them – are completely text-based. If you press the a key then the terminal will send exactly the letter a the the application, and nothing more. There is no such thing as a "key down" and "key up event", it's just the byte for a that's sent. Special keys like F1, arrow keys, etc. are actually multiple characters, usually starting with the 0x1b character (the escape character, which I'll write as \E henceforth); for example on my system F1 sends \EOP and the arrow up key sends \EOA.

Similarly, all output from applications to a terminal are also pure text. To do something more than just "display text" we again need to use escape sequences. For example \E[1m will make the text bold (until reset), or \E[2J will clear the screen. Some may also send back data; for example \E[6n to get the cursor position.

The reason it all works like this is because "terminals" were originally actual devices with a screen and keyboard, connected over a serial port to a computer, and this was the only way to send any data. Early version from the 60s often had a printer rather than a screen.

What you're using today is more accurately described as a terminal emulator; that is, a program that emulates one of those physical devices. Back in those days computers were very expensive (hundreds of thousands of dollars), and terminals were comparatively cheap (though still expensive, usually several thousand dollars in today's money!) I wrote a little bit more about the history at https://bestasciitable.com, which also includes some pictures.

Now, the problem is that not every terminal (or "terminal emulator", but you can use them interchangeably) may agree what the escape sequence is to make text bold, or what the "F1 key" looks like. There is nothing "special" about \E[1m; it's just that most terminals agree that this is the sequence to make text bold, but it could also have been \Ebold or even just BOLD if you wanted (but that would make it impossible to write the text "BOLD", but you could if you wanted to).

In the past there were dozens of brands and many different terminal devices, many of which had widely different escape sequences and logic, so people created databases to record all of this, and an application could work with multiple terminal devices rather than the one the author of the program was using. There are actually two solutions for this: termcap and terminfo, both more or less similar (terminfo has more features). Creating two different standards because there are too many standards ... classic. These days, systems almost exclusively use terminfo, although termcap compatibility is still provided by some systems. There have historically been a few different implementations of terminfo, but the one used almost universally today is the one that's part of ncurses, maintained by Thomas Dickey who also maintains xterm. terminfo is part of POSIX (as is curses), termcap is not.

terminfo files are usually stored in /usr/share/terminfo; the files in there are "compiled" binary files. I guess parsing text was too expensive in 1981, and the binary format stuck (including 16bit alignment for old 16bit aligned systems like the PDP-11 by the way).

Today a lot has been standardized and converged; ECMA-48 and "Xterm-style escape sequences" are what almost all (if not all) commonly used terminals use. This is why you can get away with just using printf '\x1b[1mBOLD!\x1b[0m\n' in your shell scripts and not worry too much about looking up the correct terminfo properties. The True Right Way™ to do this is still to look up the terminfo entries though, and if you get beyond some of the basics like bold text this is still needed. There are still several "styles" of doing some things for some more advanced control codes (such as RGB colours, mouse support) and recognition of "special" keys ("backspace sends delete" is a common one).

You can send this from the shell with the tput command, for example:

% printf "$(tput bold)Bold!$(tput sgr0) Not bold\n"

sgr0 is "set graphic reset" (I think?) and resets all graphical attributes. The names for these things range from "a bit obscure" or "an indecipherable set of letters with no obvious meaning" – party like it's UNIX. You also have long names (exit_attribute_mode for sgr0) but tput doesn't recognize them.

You can see a list of the terminfo entries for your current terminal with infocmp -1Lx (-L to use long names, -1 to print one per line, and -x to print extended data too). You can compare them too:

% infocmp -1Lx xterm-256color iterm2
comparing booleans.
    backspaces_with_bs: T:F.
    can_change: T:F.

comparing strings.
    clear_screen: '\E[H\E[2J', '\E[H\E[J'.
    cursor_normal: '\E[?12l\E[?25h', '\E[?25h'.
    cursor_visible: '\E[?12;25h', NULL.

There are actually many more differences between xterm-256color and iterm2, but I'm not going to show them all here. Note how clear_screen is slightly different.

Aside from a simple key→value mapping, terminfo entries can also have parameters. For example parm_delete_line (or dl) is \E[%p1%dM. The way this works is with a small stack-based language; %p1 pushes the first parameter on the stack, and %d pops a value and prints it as a number. So with dl 5 we end up with \E[5M.

There are all sorts of things you can do with this, like conditionals and arithmetic. This is useful because some may accept a RGB colour as hex in the range 0x00-0xff, whereas others may want it as a decimal in the range 0-255. Stuff like that. You don't really need to worry about this because the only people writing these files are authors or terminal applications (or people who write terminfo libraries). But it's fun to know that terminfo files are Turing-complete, no?

So this is the short version on how terminals work, and what the point of terminfo is :-) There's more to tell here (as well as another way to control terminals, with the ioctl() syscall) but I'll tell that bedtime story next week, but only if you behave and don't do anything naughty!

Others

Some other Go terminfo implementations I found:

Some of these other packages (such as termbox and tcell) also do much more than just dealing with terminfo. This package is intended to only support doing useful things with terminfo and not much more. A big advantage is that it's a lot easier to use in simpler CLI apps that are not full-blown TUIs.

About

A terminfo library for Go

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published