pic_ansi Module

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.

Quick start

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)

Width is counted in code points

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.


Uses

  • module~~pic_ansi~~UsesGraph module~pic_ansi pic_ansi module~pic_string_type pic_string_type module~pic_ansi->module~pic_string_type module~pic_strings pic_strings module~pic_ansi->module~pic_strings module~pic_types pic_types module~pic_ansi->module~pic_types module~pic_string_type->module~pic_types module~pic_ascii pic_ascii module~pic_string_type->module~pic_ascii module~pic_optional_value pic_optional_value module~pic_string_type->module~pic_optional_value module~pic_strings->module~pic_string_type module~pic_strings->module~pic_types iso_c_binding iso_c_binding module~pic_strings->iso_c_binding module~pic_strings->module~pic_ascii module~pic_strings->module~pic_optional_value module~pic_types->iso_c_binding iso_fortran_env iso_fortran_env module~pic_types->iso_fortran_env module~pic_ascii->module~pic_types module~pic_optional_value->module~pic_types

Variables

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 ansi_fg splits at 8 rather than taking two separate entry points.

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; char_code holds it.

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 key_event_t starts here.

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.


Derived Types

type, public ::  frame_t

A screen’s worth of text lines, rendered by diffing against the last.

Read more…

Components

Type Visibility Attributes Name Initial
integer(kind=default_int), private :: cols = 0_default_int

Width in code points; set_line truncates to it.

type(string_type), private, allocatable :: current(:)

Lines as set since the last render.

Read more…
logical, private :: full_redraw = .true.

Set by resize and invalidate; makes the next render emit every row regardless of the diff.

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.

Type-Bound Procedures

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

type, public ::  key_event_t

One decoded key press.

Components

Type Visibility Attributes Name Initial
integer(kind=int32), public :: char_code = 0_int32

Byte value, meaningful when code is KEY_CHAR.

integer(kind=int32), public :: code = KEY_NONE

One of the KEY_* codes.

type, public ::  pending_t

Bytes of an escape sequence that arrived without their continuation.

Read more…

Components

Type Visibility Attributes Name Initial
character(len=PENDING_CAPACITY), private :: buffer = ""

Fixed store, so a pending_t needs no allocation and can be a plain component of a caller’s state.

integer(kind=default_int), private :: length = 0_default_int

Bytes in use.

Type-Bound Procedures

procedure, public :: clear => pending_clear
procedure, public :: is_empty => pending_is_empty

Functions

public pure function ansi_alt_screen_enter() result(seq)

Switch to the alternate screen buffer.

Read more…

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_alt_screen_leave() result(seq)

Switch back to the normal screen buffer.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_bg(color) result(seq)

Set the background to one of the sixteen named colours.

Arguments

Type IntentOptional Attributes Name
integer(kind=int32), intent(in) :: color

One of the ANSI_* colour constants, 0 to 15.

Return Value character(len=:), allocatable

public pure function ansi_bg_256(index) result(seq)

Set the background from the 256-colour cube.

Arguments

Type IntentOptional Attributes Name
integer(kind=int32), intent(in) :: index

Palette index, clamped to 0 to 255.

Return Value character(len=:), allocatable

public pure function ansi_bg_rgb(r, g, b) result(seq)

Set the background to a 24-bit colour.

Arguments

Type IntentOptional 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.

Return Value character(len=:), allocatable

public pure function ansi_bold() result(seq)

Turn on bold.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_clear_line() result(seq)

Clear the line the cursor is on.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_clear_screen() result(seq)

Clear the whole screen and home the cursor.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_fg(color) result(seq)

Set the foreground to one of the sixteen named colours.

Arguments

Type IntentOptional Attributes Name
integer(kind=int32), intent(in) :: color

One of the ANSI_* colour constants, 0 to 15.

Return Value character(len=:), allocatable

public pure function ansi_fg_256(index) result(seq)

Set the foreground from the 256-colour cube.

Arguments

Type IntentOptional Attributes Name
integer(kind=int32), intent(in) :: index

Palette index, clamped to 0 to 255.

Return Value character(len=:), allocatable

public pure function ansi_fg_rgb(r, g, b) result(seq)

Set the foreground to a 24-bit colour.

Arguments

Type IntentOptional 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.

Return Value character(len=:), allocatable

public pure function ansi_hide_cursor() result(seq)

Hide the cursor.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_move_to(row, col) result(seq)

Move the cursor to a one-based row and column.

Read more…

Arguments

Type IntentOptional 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.

Return Value character(len=:), allocatable

public pure function ansi_reset() result(seq)

Reset every attribute.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_reverse() result(seq)

Swap foreground and background.

Arguments

None

Return Value character(len=:), allocatable

public pure function ansi_show_cursor() result(seq)

Show the cursor.

Arguments

None

Return Value character(len=:), allocatable

public pure function display_width(text) result(width)

Number of columns text occupies.

Read more…

Arguments

Type IntentOptional Attributes Name
character(len=*), intent(in) :: text

Text to measure.

Return Value integer(kind=default_int)

public pure function styled(text, fg, bg, bold) result(seq)

Wrap text in the requested attributes and a reset.

Read more…

Arguments

Type IntentOptional 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.

Return Value character(len=:), allocatable

public pure function truncate_to_width(text, width) result(cut)

Cut text to at most width columns, never mid-character and never inside an escape sequence.

Read more…

Arguments

Type IntentOptional Attributes Name
character(len=*), intent(in) :: text

Text to truncate.

integer(kind=default_int), intent(in) :: width

Maximum width in columns.

Return Value character(len=:), allocatable

private pure function base_code(color, base) result(code)

SGR number for a named colour: base + n, or base + 60 + n bright.

Arguments

Type IntentOptional Attributes Name
integer(kind=int32), intent(in) :: color
integer(kind=int32), intent(in) :: base

30 for foreground, 40 for background.

Return Value integer(kind=int32)

private pure function clamp_byte(value) result(r)

Clamp to 0 to 255, so a caller’s arithmetic slip cannot emit a sequence the terminal will misparse.

Arguments

Type IntentOptional Attributes Name
integer(kind=int32), intent(in) :: value

Return Value integer(kind=int32)

private pure function escape_length(text, start) result(n)

Length of the escape sequence beginning at start, or 0.

Read more…

Arguments

Type IntentOptional Attributes Name
character(len=*), intent(in) :: text

Text being scanned.

integer(kind=default_int), intent(in) :: start

Index to examine.

Return Value integer(kind=default_int)

private pure function frame_col_count(this) result(c)

Width the frame truncates to.

Arguments

Type IntentOptional Attributes Name
class(frame_t), intent(in) :: this

Return Value integer(kind=default_int)

private function frame_render(this) result(output)

The escape sequence that brings the screen up to date.

Read more…

Arguments

Type IntentOptional Attributes Name
class(frame_t), intent(inout) :: this

Return Value character(len=:), allocatable

private pure function frame_row_count(this) result(r)

Number of lines the frame holds.

Arguments

Type IntentOptional Attributes Name
class(frame_t), intent(in) :: this

Return Value integer(kind=default_int)

private pure function pending_is_empty(this) result(r)

Whether a partial sequence is being carried.

Arguments

Type IntentOptional Attributes Name
class(pending_t), intent(in) :: this

Return Value logical


Subroutines

public pure subroutine decode_keys(pending, bytes, events, n_events)

Decode raw terminal bytes into key events.

Read more…

Arguments

Type IntentOptional 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 ESC.

type(key_event_t), intent(out) :: events(:)

Decoded events, filled from 1.

integer(kind=default_int), intent(out) :: n_events

How many entries of events were filled.

private pure subroutine decode_single(byte, event)

Decode one byte that is not part of an escape sequence.

Arguments

Type IntentOptional Attributes Name
integer(kind=default_int), intent(in) :: byte

iachar value of the byte.

type(key_event_t), intent(out) :: event

private pure subroutine emit(events, n_events, code, char_code)

Append an event, silently dropping any beyond the caller’s array.

Arguments

Type IntentOptional 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

private subroutine frame_invalidate(this)

Force the next render to emit every row.

Read more…

Arguments

Type IntentOptional Attributes Name
class(frame_t), intent(inout) :: this

private subroutine frame_resize(this, rows, cols)

Set the frame’s dimensions, discarding the diff.

Read more…

Arguments

Type IntentOptional 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.

private subroutine frame_set_line(this, row, text)

Set one line, truncated to the frame width in code points.

Read more…

Arguments

Type IntentOptional 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.

private pure subroutine match_sequence(seq, complete, event, consumed)

Test whether seq is a finished escape sequence.

Read more…

Arguments

Type IntentOptional 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 KEY_NONE if the sequence is unknown.

integer(kind=default_int), intent(out) :: consumed

How many leading bytes of seq the decision used.

private pure subroutine pending_clear(this)

Forget any partial escape sequence.

Arguments

Type IntentOptional Attributes Name
class(pending_t), intent(inout) :: this