The half of a terminal interface that is pure string processing.
Nothing here does any I/O, opens anything, or asks the operating system
a question. Every escape builder is a pure function returning a
string, the key decoder is a pure subroutine over a byte array, and
the frame composer hands back one string for the caller to write. The
parts that must talk to the OS – raw mode, terminal size, reading with
a timeout – live in pic_term.
That split is what makes this testable: every case below is exercised without a terminal, because there is nothing here that needs one.
use pic_ansi
type(frame_t) :: frame
type(key_event_t) :: events(16)
integer(default_int) :: n
write (*, "(a)", advance="no") ansi_alt_screen_enter()//ansi_hide_cursor()
call frame%resize(24_default_int, 80_default_int)
call frame%set_line(1_default_int, styled("ARRIVALS", fg=ANSI_CYAN, bold=.true.))
write (*, "(a)", advance="no") frame%render()
call decode_keys(pending, raw_bytes, events, n)
set_line truncates to the frame width in UTF-8 code points, not bytes,
so a row of box-drawing characters – three bytes each, one column each
– is not cut a third of the way along. East Asian wide characters, and
combining marks, still count as one column each; getting those right
needs a character-width table and is out of scope here.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer(kind=int32), | public, | parameter | :: | ANSI_BLACK | = | 0_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BLUE | = | 4_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_BLACK | = | 8_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_BLUE | = | 12_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_CYAN | = | 14_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_GREEN | = | 10_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_MAGENTA | = | 13_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_RED | = | 9_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_WHITE | = | 15_int32 |
The sixteen named colours. 0-7 are the normal set, 8-15 the bright
set, which is why |
| integer(kind=int32), | public, | parameter | :: | ANSI_BRIGHT_YELLOW | = | 11_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_CYAN | = | 6_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_GREEN | = | 2_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_MAGENTA | = | 5_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_RED | = | 1_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_WHITE | = | 7_int32 | |
| integer(kind=int32), | public, | parameter | :: | ANSI_YELLOW | = | 3_int32 | |
| character(len=*), | public, | parameter | :: | ESC | = | achar(27) |
The escape character, 0x1B. |
| integer(kind=int32), | public, | parameter | :: | KEY_BACKSPACE | = | 3_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_CHAR | = | 1_int32 |
A printable byte; |
| integer(kind=int32), | public, | parameter | :: | KEY_CTRL_C | = | 13_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_DELETE | = | 12_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_DOWN | = | 7_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_END | = | 11_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_ENTER | = | 2_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_ESC | = | 5_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_HOME | = | 10_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_LEFT | = | 9_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_NONE | = | 0_int32 |
No event. A |
| integer(kind=int32), | public, | parameter | :: | KEY_RIGHT | = | 8_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_TAB | = | 4_int32 | |
| integer(kind=int32), | public, | parameter | :: | KEY_UP | = | 6_int32 | |
| integer(kind=default_int), | public, | parameter | :: | PENDING_CAPACITY | = | 16_default_int |
Longest partial escape sequence carried between reads. The sequences recognised here are at most six bytes; the rest is slack so that a hostile or unknown sequence cannot overrun the buffer. |
| character(len=*), | private, | parameter | :: | CSI | = | ESC//"[" |
Control Sequence Introducer. |
A screen’s worth of text lines, rendered by diffing against the last.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer(kind=default_int), | private | :: | cols | = | 0_default_int |
Width in code points; |
|
| type(string_type), | private, | allocatable | :: | current(:) |
Lines as set since the last render. |
||
| logical, | private | :: | full_redraw | = | .true. |
Set by |
|
| integer(kind=default_int), | private | :: | rows | = | 0_default_int |
Number of lines. |
|
| type(string_type), | private, | allocatable | :: | shown(:) |
Lines as of the last render, for diffing. |
| procedure, public :: col_count => frame_col_count | |
| procedure, public :: invalidate => frame_invalidate | |
| procedure, public :: render => frame_render | |
| procedure, public :: resize => frame_resize | |
| procedure, public :: row_count => frame_row_count | |
| procedure, public :: set_line => frame_set_line |
One decoded key press.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer(kind=int32), | public | :: | char_code | = | 0_int32 |
Byte value, meaningful when |
|
| integer(kind=int32), | public | :: | code | = | KEY_NONE |
One of the KEY_* codes. |
Bytes of an escape sequence that arrived without their continuation.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| character(len=PENDING_CAPACITY), | private | :: | buffer | = | "" |
Fixed store, so a |
|
| integer(kind=default_int), | private | :: | length | = | 0_default_int |
Bytes in use. |
| procedure, public :: clear => pending_clear | |
| procedure, public :: is_empty => pending_is_empty |
Switch to the alternate screen buffer.
Switch back to the normal screen buffer.
Set the background to one of the sixteen named colours.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | color |
One of the ANSI_* colour constants, 0 to 15. |
Set the background from the 256-colour cube.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | index |
Palette index, clamped to 0 to 255. |
Set the background to a 24-bit colour.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | r |
Red, clamped to 0 to 255. |
||
| integer(kind=int32), | intent(in) | :: | g |
Green, clamped to 0 to 255. |
||
| integer(kind=int32), | intent(in) | :: | b |
Blue, clamped to 0 to 255. |
Turn on bold.
Clear the line the cursor is on.
Clear the whole screen and home the cursor.
Set the foreground to one of the sixteen named colours.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | color |
One of the ANSI_* colour constants, 0 to 15. |
Set the foreground from the 256-colour cube.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | index |
Palette index, clamped to 0 to 255. |
Set the foreground to a 24-bit colour.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | r |
Red, clamped to 0 to 255. |
||
| integer(kind=int32), | intent(in) | :: | g |
Green, clamped to 0 to 255. |
||
| integer(kind=int32), | intent(in) | :: | b |
Blue, clamped to 0 to 255. |
Hide the cursor.
Move the cursor to a one-based row and column.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | row |
One-based row; values below 1 are clamped to 1. |
||
| integer(kind=default_int), | intent(in) | :: | col |
One-based column; values below 1 are clamped to 1. |
Reset every attribute.
Swap foreground and background.
Show the cursor.
Number of columns text occupies.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | text |
Text to measure. |
Wrap text in the requested attributes and a reset.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | text |
Text to wrap. |
||
| integer(kind=int32), | intent(in), | optional | :: | fg |
Foreground colour, one of the ANSI_* constants. |
|
| integer(kind=int32), | intent(in), | optional | :: | bg |
Background colour, one of the ANSI_* constants. |
|
| logical, | intent(in), | optional | :: | bold |
Whether to embolden. |
Cut text to at most width columns, never mid-character and never
inside an escape sequence.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | text |
Text to truncate. |
||
| integer(kind=default_int), | intent(in) | :: | width |
Maximum width in columns. |
SGR number for a named colour: base + n, or base + 60 + n bright.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | color | |||
| integer(kind=int32), | intent(in) | :: | base |
30 for foreground, 40 for background. |
Clamp to 0 to 255, so a caller’s arithmetic slip cannot emit a sequence the terminal will misparse.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | value |
Length of the escape sequence beginning at start, or 0.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | text |
Text being scanned. |
||
| integer(kind=default_int), | intent(in) | :: | start |
Index to examine. |
Width the frame truncates to.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(frame_t), | intent(in) | :: | this |
The escape sequence that brings the screen up to date.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(frame_t), | intent(inout) | :: | this |
Number of lines the frame holds.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(frame_t), | intent(in) | :: | this |
Whether a partial sequence is being carried.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(pending_t), | intent(in) | :: | this |
Decode raw terminal bytes into key events.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(pending_t), | intent(inout) | :: | pending |
Carried partial sequence; pass the same one every call. |
||
| character(len=*), | intent(in) | :: | bytes |
Bytes just read from the terminal. May be empty, which is how a
caller flushes a held |
||
| type(key_event_t), | intent(out) | :: | events(:) |
Decoded events, filled from 1. |
||
| integer(kind=default_int), | intent(out) | :: | n_events |
How many entries of |
Decode one byte that is not part of an escape sequence.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | byte |
|
||
| type(key_event_t), | intent(out) | :: | event |
Append an event, silently dropping any beyond the caller’s array.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(key_event_t), | intent(inout) | :: | events(:) | |||
| integer(kind=default_int), | intent(inout) | :: | n_events | |||
| integer(kind=int32), | intent(in) | :: | code | |||
| integer(kind=int32), | intent(in) | :: | char_code |
Force the next render to emit every row.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(frame_t), | intent(inout) | :: | this |
Set the frame’s dimensions, discarding the diff.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(frame_t), | intent(inout) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | rows |
Number of lines; below 0 is treated as 0. |
||
| integer(kind=default_int), | intent(in) | :: | cols |
Width in code points; below 0 is treated as 0. |
Set one line, truncated to the frame width in code points.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(frame_t), | intent(inout) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | row |
One-based row. |
||
| character(len=*), | intent(in) | :: | text |
Text for the row. |
Test whether seq is a finished escape sequence.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | seq |
Bytes accumulated so far, starting with ESC. |
||
| logical, | intent(out) | :: | complete |
Whether a decision has been reached. |
||
| type(key_event_t), | intent(out) | :: | event |
The decoded key, or |
||
| integer(kind=default_int), | intent(out) | :: | consumed |
How many leading bytes of |
Forget any partial escape sequence.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(pending_t), | intent(inout) | :: | this |