http://xn--rpa.cc/irl/term.html
k / essays /
everything you ever wanted to know about terminals
(but were afraid to ask)
by Lexi Summer Hale
so here's a short tutorial on ansi escape codes and terminal control,
because you philistines won't stop using ncurses and oh my god WHY
ARE WE STILL USING NCURSES IT IS THE TWENTY FIRST FUCKING CENTURY
the way terminal emulators handle fancy things like color and cursor
shape aren't some mysterious opaque black box you can only access
through a library. accessing most of these capabilities is actually
extremely simple; they can even be hardcoded into a text file and
displayed by cat or less. or even curl! the way you do this is with
something called ANSI escape sequences.
almost all UI changes in a terminal are accomplished through in-band
signalling. these signals are triggered with the ASCII/UTF-8
character (0x1B or 27). it's the same character that you
send to the terminal when you press the Escape key on your keyboard
or a key sequence involving the Alt key. (typing for instance
sends the characters and in very rapid succession; this is
why you'll notice a delay in some terminal programs after you press
the escape key -- it's waiting to try and determine whether the user
hit Escape or an alt-key chord.)
the simplest thing we can do with these escapes is to make the text
bold (or "bright"). we accomplish this by sending the terminal the
character followed by [1m. [ is a character indicating to the
terminal what kind of escape we're sending, 1 indicates bold/bright
mode, and m is the control character for formatting escapes.
all text sent after this escape sequence will be bold until we
explicitly turn it off again (even if your program terminates). there
are two ways we can turn off bright mode: by clearing formatting
entirely, using the m formatting command with no arguments or the
argument 0, or more specifically clearing the bold bit with the 21m
command. (you'll notice that you can usually turn off modes by
prefixing the same number with 2.)
in a C program, this might look like the following:
#include #define szstr(str) str,sizeof(str) int main() {
write(1, szstr("plain text - \x1b[1mbold text\x1b[0m - plain text"));
}
the \x1b escape here is a C string escape that inserts hex character
0x1B () into the string. it's kind of ugly and unreadable if
you're not used to reading source with explicit escapes in it. you
can make it a lot less horrible with a handful of defines, tho:
#include #define szstr(str) str,sizeof(str) #define plain
"0" /* or "" */ #define no "2" #define bright "1" #define dim "2" #
define italic "3" #define underline "4" #define reverse "7" #define
with ";" #define ansi_esc "\x1b" #define fmt(style) ansi_esc "["
style "m" int main() { write(1, szstr( "plain text - " fmt(bright)
"bright text" fmt(no bright) " - " fmt(dim) "dim text" fmt(no dim) "
- " fmt(italic) "italic text" fmt(no italic) " - " fmt(reverse)
"reverse video" fmt(plain) " - " fmt(underline) "underlined text" fmt
(no underline) ) ); }
the beauty of this approach is that all the proper sequences are
generated at compile time, meaning the compiler turns all that into a
single string interpolated with the raw escapes. it offers much more
readability for the coder at zero cost to the end user.
but hang on, where's that semicolon coming from? it turns out, ansi
escape codes let you specify multiple formats per sequence. you can
separate each command with a ;. this would allow us to write
formatting commands like fmt(underline with bright with no italic),
which translates into \x1b[4;1;23m at compile time.
of course, being able to style text isn't nearly good enough. we also
need to be able to color it. there are two components to a color
command: what we're trying to change the color of, and what color we
want to change it to. both the foreground and background can be given
colors separately - despite what ncurses wants you to believe, you do
not have to define """color pairs""" with each foreground-background
pair you're going to use. this is a ridiculous archaism that nobody
in the 21st fucking century should be limited by.
to target the foreground, we send the character 3 for normal colors
or 9 for bright colors; to target the background, we send 4 for
normal or 10 for bright. this is then followed by a color code
selecting one of the traditional 8 terminal colors.
note that the "bright" here is both the same thing and something
different from the "bright" mode we mentioned earlier. while turning
on the "bright" mode will automatically shift text it applies to the
bright variant of its color if it is set to one of the traditional 8
colors, setting a "bright color" with 9 or 10 will not automatically
make the text bold.
#include #define szstr(str) str,sizeof(str) #define fg "3"
#define br_fg "9" #define bg "4" #define br_bg "10" #define with ";"
#define plain "" #define black "0" #define red "1" #define green "2"
#define yellow "3" #define blue "4" #define magenta "5" #define cyan
"6" #define white "7" #define ansi_esc "\x1b" #define fmt(style)
ansi_esc "[" style "m" int main() { write(1, szstr( "plain text - "
fmt(fg blue) "blue text" fmt(plain) " - " fmt(br_fg blue) "bright
blue text" fmt(plain) " - " fmt(br_bg red) "bright red background"
fmt(plain) " - " fmt(fg red with br_bg magenta) "hideous red text"
fmt(plain)) ); }
when we invoke fmt(fg red with br_bg magenta), this is translated by
the compiler into the command string \x1b[31;105m. note that we're
using fmt(plain) (\x1b[m) to clear the coloring here; this is because
if you try to reset colors with, for instance, fmt(fg black with bg
white), you'll be overriding the preferences of users who have their
terminal color schemes set to anything but that exact pair.
additionally, if the user happens to have a terminal with a
transparent background, a set background color will create ugly
colored blocks around text instead of letting whatever's behind the
window display correctly.
now, while it is more polite to use the "8+8" colors because they're
a color palette the end-user can easily configure (she might prefer
more pastel colors than the default harsh pure-color versions, or
change the saturation and lightness to better fit with her terminal
background), if you're doing anything remotely interesting UI-wise
you're going to run up against that limit very quickly. while you can
get a bit more mileage by mixing colors with styling commands, if you
want to give any configurability to the user in terms of color
schemes (as you rightly should), you'll want access to a much broader
palette of colors.
to pick from a 256-color palette, we use a slightly different sort of
escape: \x1b[38;5;(color)m to set the foreground and \x1b[48;5;
(color)m to set the background, where (color) is the palette index we
want to address. these escapes are even more unwieldy than the 8+8
color selectors, so it's even more important to have good
abstraction.
#include #define szstr(str) str,sizeof(str) #define with
";" #define plain ";" #define wfg(color) "38;5;" #color #define wbg
(color) "48;5;" #color #define ansi_esc "\x1b" #define fmt(style)
ansi_esc "[" style "m" int main() { write(1, szstr("plain text - "
fmt(wfg(198) with wbg(232)) "rose text on dark grey" fmt(plain) " - "
fmt(wfg(232) with wbg(248)) "dark grey on light grey" fmt(plain) " -
" fmt(wfg(248) with wbg(232)) "light grey on dark grey" fmt(plain))
); }
here, the stanza fmt(wfg(248) with wbg(232)) translates into \x1b[38;
5;248;48;5;232m. we're hard-coding the numbers here for simplicity
but as a rule of thumb, any time you're using 8-bit colors in a
terminal, you should always make them configurable by the user.
the opaque-seeming indexes are actually very systematic, and you can
calculate which index to use for a particular color with the formula
16 + 36r + 6g + b, where r, g, and b are integers ranging between 0
and 5. indices 232 through 255 are a grayscale ramp from dark (232)
to light (255).
of course, this is still pretty restrictive. 8-bit color may have
been enough for '90s CD-ROM games on Windows, but it's long past it's
expiration date. using true color is much more flexible. we can do
this through the escape sequence \x1b[38;2;(r);(g);(b)m where each
component is an integer between 0 and 255.
sadly, true color isn't supported on many terminals, urxvt tragically
included. for this reason, your program should never rely on it, and
abstract these settings away to be configured by the user. defaulting
to 8-bit color is a good choice, as every reasonable modern terminal
has supported it for a long time now.
but, for users of XTerm, kitty, Konsole, and libVTE-based terminal
emulators (such as gnome-terminal, mate-terminal, and termite), it's
polite to have a 24-bit color mode in place. for example:
#include #include #include struct
color { enum color_mode { trad, trad_bright, b8, b24 } mode; union {
uint8_t color; struct { uint8_t r, g, b; }; } }; struct style {
unsigned char bold : 1; unsigned char underline : 1; unsigned char
italic : 1; unsigned char dim : 1; unsigned char reverse : 1; };
struct format { struct style style; struct color fg, bg; }; struct
format fmt_menu = { {0, 0, 0, 0, 0}, {trad, 7}, {trad, 4} },
fmt_menu_hl = { {1, 0, 0, 0, 0}, {trad_bright, 7}, {trad_bright, 4},
}; void apply_color(bool bg, struct color c) { switch(c.mode) { case
trad: printf("%c%u", bg ? '4' : '3', c.color ); break; case
trad_bright: printf("%s%u", bg ? "9" : "10", c.color ); break; case
b8: printf("%c8;5;%u", bg ? '4' : '3', c.color); break; case b24:
printf("%c8;2;%u;%u;%u", bg ? '4' : '3', c.r, c.b, c.g); } } void fmt
(struct format f) { printf("\x1b["); f.bold && printf(";1");
f.underline && printf(";4"); f.italic && printf(";3"); f.reverse &&
printf(";7"); f.dim && printf(";2"); apply_color(false, f.fg);
apply_color(true, f.bg); printf("m"); } int main() { ... if (is_conf(
"style/menu/color")) { if (strcmp(conf("style/menu/color", 0), "rgb")
== 0) { fmt_menu.mode = b24; fmt_menu.r = atoi(conf("style/menu/
color", 1)); fmt_menu.g = atoi(conf("style/menu/color", 2));
fmt_menu.b = atoi(conf("style/menu/color", 3)); } else if (atoi(conf(
"style/menu/color", 0)) > 8) { fmt_menu.mode = b8; fmt_menu.color =
atoi(conf("style/menu/color", 1)); } else { fmt_menu.color = atoi
(conf("style/menu/color", 1)); } } ... }
this sort of infrastructure gives you an enormously flexible
formatting system that degrades gracefully without tying you to
massive, archaic libraries or contaminating the global namespace with
hundreds of idiot functions and macros (which is which of course
being entirely indistinguishable).
but what if you want more than formatting? what if you want an actual
TUI?
depending on the sort of TUI you want, you could actually get away
with plain old ASCII. if you're just trying to draw a progress bar,
for instance, you can (and should) use the ASCII control character
, "carriage return" (in C, \r):
#include #include #include #include
#define barwidth 25 #define szstr(str) str,sizeof(str)
typedef uint8_t bar_t; int main() { srand(time(NULL)); bar_t prmax =
-1; size_t ratio = prmax / barwidth; for(bar_t progress = 0; progress
< prmax;) { write(1,"\r", 1); size_t barlen = progress / ratio; for (
size_t i = 0; i < barwidth; ++i) { size_t barlen = progress / ratio;
if (i <= barlen) write(1,szstr("#")); else write(1,szstr("#")); }
fsync(1); // otherwise, terminal will only update on newline size_t
incr = rand() % (prmax / 10); if (prmax - progress < incr) break; //
avoid overflow progress += incr; sleep(1); } }
of course, if we want to be really fancy, we can adorn the progress
bar with ANSI colors using the escape sequences described above. this
will be left as an exercise to the reader.
this is sufficient for basic applications, but eventually, we'll get
to the point where we actually need to address and paint individual
cells of the terminal. or, what if we wanted to size the progress bar
dynamically with the size of the terminal window? it's time to break
out ANSI escape sequences again.
the first thing you should always do when writing a TUI application
is to send the TI or smcup escape. this notifies the terminal to
switch to TUI mode (the "alternate buffer"), protecting the existing
buffer so that it won't be overwritten and users can return to it
when your application closes.
in ANSI, we achieve this with the sequence [?1049h (or, as a C
string, "\x1b[?1049h").
(n.b. there's another escape, "\x1b47h", with deceptively similar
effects as 1049, but it's behavior is subtly broken on some terminals
(such as xterm) and it outright doesn't work on others (such as
kitty). "\x1b[?1049h" has the correct effects everywhere that the
alternate buffer is supported tho.)
once you've switched to the alternate buffer, the first thing you'll
want to do is clear the screen and home the cursor, to clean up any
debris previous applications might have left behind. for this, we use
the sequence [2J, which clears the screen and nukes scrollback.
(we can't use the terminal reset sequence, c, because it affects
not just the active buffer, but the entire terminal session, and will
wreck everything that's currently displayed!)
likewise, just before exit, you need to send the TE or rmcup escape.
this notifies the terminal to switch back to the previous mode. this
sequence, as a C string, is "\x1b[?1049l". to be polite, before you
send this escape, you should clean up after yourself, clearing
scrollback as before.
(h and l in these escapes seem to stand for "high" and "low," meaning
essentially "on" and "off" by reference to hardware IO lines, where
high current usually corresponds to a 1 bit and low to a 0 bit. in
the hardware terminals of the past eon, it's possible
program-configurable modes such as this were implemented with
discrete IO lines set to a particular level; it's also possible the
ANSI escape code designers just reached for a handy metaphor in an
age where booleans weren't yet in vogue. if anyone happens to find
out the actual story behind this, please do let me know)
once we're in the alternate buffer, we can safely start throwing
around escape sequences to move the cursor to arbitrary positions.
however, before we do this, we need to know how big the terminal
actually is so we can lay out the UI appropriately.
it's good form to have a function called resize() or similar that you
run on program start and later when the terminal window is resized.
while there is a horrible way to do this with ANSI escapes, it's
better to just bite the bullet and learn how to use ioctls and
termios.
termios is a POSIX interface that lets you discover and set
properties of the current terminal. it's kind of an unholy mess, but
fortunately, we only need to use a very small corner of it to get the
information we need.
we start off by importing the header. this gives us the
functions and structures we need to set ioctls. termios returns the
size of the window in a structure called struct winsize. (far more
rational than anything you'd find in ncurses, no?) this struct is
populated using the function call ioctl(1, TIOCGWINSZ, &ws) where ws
is the name of our struct (and 1 is the file descriptor for standard
output). terminal width and height can then be accessed in the fields
ws_col (for width) and ws_row (for height).
of course, we need to keep these values up to date when the terminal
size changes. this is why resize() needs to be its own function - it
needs to be called whenever our program is sent the SIGWINCH signal.
SIGWINCH is automatically sent to child processes by the controlling
terminal emulator whenever its window is reshaped.
a full example of these concepts in action:
#include ; #include ; uint16_t width; uint16_t
height; void resize(int i) { // spurious argument needed so that the
// function signature matches what signal(3) expects struct winsize
ws; ioctl(1, TIOCGWINSZ, &ws); width = ws.ws_col; height = ws.ws_row;
// from here, call a function to repaint the screen // (probably
starting with "\x1b[2J") } int main(void) { signal(SIGWINCH, resize);
resize(0); // here await user input }
throughout all of this, you may have noticed one thing: despite our
attempts to create a clean, slick TUI, the cursor itself remains
stubbornly onscreen. don't worry, tho; we can fix this.
the escape sequence to show and hide the cursor works much like the
one to switch to and from the alternate buffer, except it has the
number 25 instead of 1049. we can therefore hide the cursor by
printing the string "\x1b[?25l" and show it again with the string "\
x1b[?25h".
it's important to track how you're changing the behavior of the
terminal, though, and restore it on program exit. otherwise, the user
will have to reset the terminal herself, which many don't even know
how to do (for the record, it's $ reset or $ echo -ne "\ec"). since
you won't necessarily have control over how your program exits, it's
important to set an exit handler using the atexit(3) and signal(3)
functions. this way, even if the process is terminated with SIGTERM
or SIGINT, it will still restore the terminal to its original state.
(it won't do jack shit in case of a SIGKILL, of course, but at that
point it's the user's responsibility anyway.)
here's an example of appropriate terminal cleanup:
#include #include #include ; #define
say(str) write(1,str,sizeof(str)) void cleanup(void) { //clean up the
alternate buffer say("\x1b[2J"); //switch back to the normal buffer
say("\x1b[?1049l"); //show the cursor again say("\x1b[?25h"); } void
cleanup_die(int i) { exit(1); } int main(void) { //enter the
alternate buffer say("\x1b[?1049h"); //register our cleanup function
atexit(cleanup); signal(SIGTERM, cleanup_die); signal(SIGINT,
cleanup_die); //clean up the buffer say("\x1b[2J"); //hide the cursor
say("\x1b[?25l"); sleep(10); return 0; }
note the strategic placement of the atexit and signal functions.
depending on where the program is in its execution when it receives
SIGTERM, the cleanup function may be called before anything following
it. if these traps were placed at the top of the program, they might
be called before the alternate buffer was even opened, wiping out the
ordinary buffer and anything the user had there. this is very
impolite: we want to make sure that havoc is minimized.
of course, there is still a very small problem: if by some miracle
the program is killed after entering the alternate buffer but before
the cleanup function is registered, the user could be left stuck in
the alternate buffer. to fix this, we would have to register the
cleanup function before anything else, and start off the cleanup
function by giving the instruction to enter the alternate buffer.
this is a NOP is we're already there; if we're not, it protects the
user's terminal from the deleterious effects of the following code.
now we've set the stage for our slick ncurses-free TUI, we just need
to figure out how to put things on it.
we can move the cursor to an arbitrary position with [(r);(c)H.
(r) here is the row we want to move to (the first row being 1), and
(c) is the target column (also 1-indexed).
there's a number of other control sequences that move the cursor by
relative steps, but as a rule, you should always use absolute
addressing, as using relative addressing can lead to cumulative
errors - and if your program doesn't know the location of the cursor
at all times, something is very wrong.
if you actually try this, though, you'll quickly notice a new
problem. anything the user types will still appear onscreen, all over
your beautiful TUI, whether or not you want it to. this also moves
the cursor as a side effect. this is chaos you don't want in a
program, so we need to put an end to it. however, there's no
standardized escape code to accomplish this.
in other words, we need to use termios. the ugly side of termios.
termios, unlike libraries you might be used to, doesn't just have
functions you can call to set properties. instead, we need to
download the entire termios struct for the current terminal into a
variable of type struct termios, make our modifications, and then
upload it back to the terminal.
to do this, we need to define two of those structs: one to hold our
modifications, and one to hold the original state of the program so
it can be restored by our cleanup function at exit. to download the
struct, we use the function tcgetattr(3). this function takes two
arguments: a file descriptor representing the current terminal
(always 1, for stdout), and a pointer to a struct termios to write
into. as soon as we've populated our struct, before we've made any
modifications, we need to copy the unmodified struct into our
global-scope "backup" struct.
after that, we can turn off echo. local echo is one of a number of
flags encoded in the bitfield c_lflag, identified by the constant
ECHO. to disable it, we first invert ECHO with the ~ bitwise NOT
operator, and then bitwise-AND the field by the resulting value.
once we've made our modifications, we can send them back up with the
function tcsetattr(3). this one takes three arguments. the first is
the file descriptor to modify, as usual. the second is a constant
controlling when these modifications actually take place - for our
purposes, this is always TCSANOW. finally, we give it a pointer to
the struct we've modified.
having turned off local echo, we now need to handle it (and
line-editing) by hand, printing and deleting characters as the user
types them. the problem is, the terminal won't actually tell us the
user has typed anything until she hits , making line-editing
(or even just seeing what she's typing) impossible.
the reason this happens is something called canonical mode. canonical
mode is the normal mode of operation for dumb terminals and terminal
emulators. while in canonical mode, terminals will exhibit
traditional Unix-y behaviors, like allowing you to type anything at
any time, even if nothing is reading from stdin, and only sending
text line-by-line as is keyed. remember, unlike DOS, UNIX
uses a file/stream metaphor to interact with the terminal: it's just
another file, so you can type things in at any time (and they'll be
there as soon as a program decides to read from it again).
this doesn't suit our purposes at all, tho. we need DOS-like control
over the UI. to achieve this, we need to turn off canonical mode.
this is controlled by the ICANON flag, and with it off, we'll be able
to read characters keystroke by keystoke.
#include #include #include #include
; #define say(str) write(1, str, sizeof(str)) struct
termios initial; void restore(void) { tcsetattr(1, TCSANOW, &
initial); } void die(int i) { exit(1); } void terminit(void) { struct
termios t; tcgetattr(1, &t); initial = t; atexit(restore); signal
(SIGTERM, die); signal(SIGINT, die); t.c_lflag &= (~ECHO & ~ICANON);
tcsetattr(1, TCSANOW, &t); } int main(void) { terminit(); for(char
buf; buf != '\n' && buf != '\x1b';) { read(1, &buf, 1); say("\ryou
pressed "); write(1, &buf, 1); } return 1; }
this is the final piece we strictly need to write a TUI. however, for
extra credit:
if you're a vim user, you may have noticed that the cursor changes
shape depending on what mode you're in (i-beam for insert, block for
normal, or underline for replace). we can do this as well, with the
DECSCUSR escape sequences.
these sequences start off as usual, with [. a numeric character
then follows, indicating which cursor we want to employ. we then
finish the sequence with the command q, a literal space followed by
the letter q.
the values we can use are 0 or 1 for a blinking block cursor, 2 for a
steady block cursor, 3 for a blinking underline cursor, 4 for a
steady underline cursor, 5 for a blinking i-beam cursor, and 6 for a
steady i-beam.
now let's put it all together:
#include #include #include #include
#include #include #include
#include #include #define with ";" #
define plain "0" /* or "" */ #define no "2" #define bright "1" #
define dim "2" #define italic "3" #define underline "4" #define
reverse "7" #define fg "3" #define bg "4" #define br_fg "9" #define
br_bg "10" #define black "0" #define red "1" #define green "2" #
define yellow "3" #define blue "4" #define magenta "5" #define cyan
"6" #define white "7" #define alt_buf "?1049" #define curs "?25" #
define term_clear "2J" #define clear_line "2K" #define high "h" #
define low "l" #define jump "H" #define esc "\x1b" #define esca esc "
[" #define wfg "38;5;" #define wbg "48;5;" #define color "m" #define
fmt(f) esca f "m" #define say(s) write(1,s,sizeof(s)) #define sz(s)
(sizeof(s)/sizeof(*s)) struct termios initial; uint16_t width,
height; uint8_t meter_value = 0; uint8_t meter_size = 25; uint8_t
meter_color_on = 219; uint8_t meter_color_off = 162; bool
help_visible = true; const char* instructions[] = { "press " fmt
(reverse with bright) " i " fmt(plain) " to " fmt(underline with fg
cyan) "increase the meter value" fmt(plain), "press " fmt(reverse
with bright) " b " fmt(plain) " to " fmt(underline with fg red)
"increase the length of the meter" fmt(plain), "press " fmt(reverse
with bright) " d " fmt(plain) " to " fmt(underline with fg yellow)
"decrease the meter value" fmt(plain), "press " fmt(reverse with
bright) " s " fmt(plain) " to " fmt(underline with fg green)
"decrease the length of the meter" fmt(plain), "press " fmt(reverse
with bright) " c " fmt(plain) " to " fmt(underline with fg blue)
"change the filled color" fmt(plain), "press " fmt(reverse with
bright) " r " fmt(plain) " to " fmt(underline with br_fg red) "change
the unfilled color" fmt(plain), "", "press " fmt(reverse with bright)
" h " fmt(plain) " to " fmt(underline with fg magenta) "toggle this
text" fmt(plain), "press " fmt(reverse with bright) "ESC" fmt(plain)
" to " fmt(underline with br_fg cyan) "quit" fmt(plain) }; size_t
textsz(const char* str) { //returns size of string without formatting
characters size_t sz = 0, i = 0; count: if (str[i] == 0) return sz;
else if (str[i] == '\x1b') goto skip; else { ++i; ++sz; goto count; }
skip: if (str[i] != 'm') { ++i; goto skip; } else goto count; }; void
restore(void) { say( //enter alternate buffer if we haven't already
esca alt_buf high //clean up the buffer esca term_clear //show the
cursor esca curs high //return to the main buffer esca alt_buf low );
//restore original termios params tcsetattr(1, TCSANOW, &initial); }
void restore_die(int i) { exit(1); // since atexit has already
registered a handler, // a call to exit(3) is all we actually need }
void repaint(void); void resize(int i) { struct winsize ws; ioctl(1,
TIOCGWINSZ, &ws); width = ws.ws_col; height = ws.ws_row; say(esca
term_clear); repaint(); } void initterm(void) { // since we're using
printf here, which doesn't play nicely // with non-canonical mode, we
need to turn off buffering. setvbuf(stdout, NULL, _IONBF, 0);
termios: { struct termios t; tcgetattr(1, &t); initial = t; t.c_lflag
&= (~ECHO & ~ICANON); tcsetattr(1, TCSANOW, &t); }; atexit(restore);
signal(SIGTERM, restore_die); signal(SIGINT, restore_die); say ( esca
alt_buf high esca term_clear esca curs low ); } void repaint(void) {
const uint16_t mx = (width / 2) - (meter_size / 2), my = (height / 2)
+ 1; if (help_visible) for (size_t i = 0; i < sz(instructions); ++i)
printf(esca "%u" with "%u" jump fmt(plain) "%s", // place lines above
meter my - (1 + (sz(instructions) - i)), // center each line (width/
2) - (textsz(instructions[i])/2), // print line instructions[i]);
printf(esca "%u" with "%u" jump, my, mx); say(esca clear_line); for (
size_t i = 0; i < meter_size; ++i) printf(esca wfg "%u" color "%s", i
< meter_value ? meter_color_on : meter_color_off, i < meter_value ?
"#" : "#"); } int main() { initterm(); signal(SIGWINCH, resize);
resize(0); bool dirty = true; for (char inkey; inkey != '\x1b';) { if
(dirty) { repaint(); dirty = false; } read(1,&inkey,1); switch(inkey)
{ case 'i': // increase meter value ++meter_value; break; case 'd': /
/ decrease meter value --meter_value; break; case 'b': // increase
meter size ++meter_size; break; case 's': // decrease meter size
--meter_size; break; case 'c': // randomize meter on color
meter_color_on = rand(); break; case 'r': // randomize meter off
color meter_color_off = rand(); break; case 'h': // toggle help text
help_visible =! help_visible; say(esca term_clear); break; default:
goto end; } dirty = true; end:; } }
that's it for the tutorial. i hope you learned something and will
reconsider using fucking ncurses next time because jesus fucking
christ.
yes, ncurses supplies features like window-drawing and region
invalidation ([DEL:to avoid terminal flicker:DEL] actually justine
tunney has pointed out that this isn't an issue with modern terminal
emulators; you can generally just redraw the whole ui every frame so
region invalidation is no longer quite as useful) that are much
harder to implement yourself. no, you shouldn't have to implement it
yourself. there should be a modern library to replace curses using
the capabilities outlined here. but i swear to god developers have so
completely forgotten how terminals work that i might be one of a
handful of people left on earth who actually has the knowledge to, so
they all just layer their bullshit on top of ncurses (which should
never have survived the '90s) instead and it's maddening.
my hope is that this tutorial will curtail some of the more
egregiously trivial uses of ncurses and provide others with the
knowledge needed to implement a 21st-century terminal UI library.
because i sure as fuck don't have the energy to.
also, i have effectively zero pull in the tech community and am also
kind of a controversial character who is liable to give projects a
bad reputation, which i don't normally care about but this one is
important. point is, nothing i wrote would ever gain any traction;
any project designed to supplant ncurses needs to come from someone
who's actually known to and respected by the FOSS community. and a
maintainer who isn't a cripple.
you can find a fuller list of ANSI escapes at wikipedia.
oh, and before anyone starts up:
being compatible only with ANSI-capable terminals is a feature, not a
bug, go the fuck away. terminfo is a fucking joke. nobody needs to
target obscure dumb terminals (or smart terminals, for that matter)
from 1983 anymore.
all sample code in this document is the sole property of the author
and is released exclusively under the GNU AGPLv3.