fltk/documentation/src/Fl_Terminal.dox
2023-11-14 18:14:48 +01:00

515 lines
24 KiB
Plaintext

// vim:syntax=doxygen
/**
\page Fl_Terminal_Tech_Docs Fl_Terminal Technical Documentation
This chapter covers the vt100/xterm style "escape codes" used by
Fl_Terminal for cursor positioning, text colors, and other display
screen control features such as full or partial screen clearing,
up/down scrolling, character insert/delete, etc.
\section Fl_Terminal_escape_codes The Escape Codes Fl_Terminal Supports
These are the escape codes Fl_Terminal actually supports, and is not
the 'complete' list that e.g. xterm supports. Most of the important stuff
has been implemented, but esoteric features (such as scroll regions) has not.
Features will be added as the widget matures.
\code{.unparsed}
│ --------------------------------------------------------
│ --- The CSI (Control Sequence Introducer, or "ESC[") ---
│ --------------------------------------------------------
│ ESC[#@ - (ICH) Insert blank Chars (default=1)
│ ESC[#A - (CUU) Cursor Up, no scroll/wrap
│ ESC[#B - (CUD) Cursor Down, no scroll/wrap
│ ESC[#C - (CUF) Cursor Forward, no wrap
│ ESC[#D - (CUB) Cursor Back, no wrap
│ ESC[#E - (CNL) Cursor Next Line (crlf) xterm, !gnome
│ ESC[#F - (CPL) Cursor Preceding Line: move to sol and up # lines
│ ESC[#G - (CHA) Cursor Horizontal Absolute positioning
│ │
│ ├── ESC[G - move to column 1 (start of line, sol)
│ └── ESC[#G - move to column #
│ ESC[#H - (CUP) Cursor Position (#'s are 1 based)
│ │
│ ├── ESC[H - go to row #1
│ ├── ESC[#H - go to (row #) (default=1)
│ └── ESC[#;#H - go to (row# ; col#)
│ ESC[#I - (CHT) Cursor Horizontal Tab: tab forward
│ │
│ └── ESC[#I - tab # times (default 1)
│ ESC[#J - (ED) Erase in Display
│ │
│ ├── ESC[0J - clear to end of display (default)
│ ├── ESC[1J - clear to start of display
│ ├── ESC[2J - clear all lines
│ └── ESC[3J - clear screen history
│ ESC[#K - (EL) Erase in line
│ │
│ ├── ESC[0K - clear to end of line (default)
│ ├── ESC[1K - clear to start of line
│ └── ESC[2K - clear current line
│ ESC[#L - (IL) Insert # Lines (default=1)
│ ESC[#M - (DL) Delete # Lines (default=1)
│ ESC[#P - (DCH) Delete # Chars (default=1)
│ ESC[#S - (SU) Scroll Up # lines (default=1)
│ ESC[#T - (SD) Scroll Down # lines (default=1)
│ ESC[#X - (ECH) Erase Characters (default=1)
│ ESC[#Z - (CBT) Cursor Backwards Tab
│ │
│ └── ESC[#Z - backwards tab # times (default=1)
│ ESC[#a - (HPR) move cursor relative [columns] (default=[row,col+1]) (NOT IMPLEMENTED)
│ ESC[#b - (REP) repeat prev graphics char # times (NOT IMPLEMENTED)
│ ESC[#d - (VPA) Line Position Absolute [row] (NOT IMPLEMENTED)
│ ESC[#e - (LPA) Line Position Relative [row] (NOT IMPLEMENTED)
│ ESC[#f - (CUP) cursor position (#'s 1 based), same as ESC[H
│ ESC[#g - (TBC)Tabulation Clear
│ │
│ ├── ESC[0g - Clear tabstop at cursor
│ └── ESC[3g - Clear all tabstops
│ ESC[#m - (SGR) Set Graphic Rendition
│ │
│ │ *** Attribute Enable ***
│ │
│ ├── ESC[0m - reset: normal attribs/default fg/bg color (VT100)
│ ├── ESC[1m - bold (VT100)
│ ├── ESC[2m - dim
│ ├── ESC[3m - italic
│ ├── ESC[4m - underline (VT100)
│ ├── ESC[5m - blink (NOT IMPLEMENTED) (VT100)
│ ├── ESC[6m - (unused)
│ ├── ESC[7m - inverse (VT100)
│ ├── ESC[8m - (unused)
│ ├── ESC[9m - strikeout
│ ├── ESC[21m - doubly underline (Currently this just does single underline)
│ │
│ │ *** Attribute Disable ***
│ │
│ ├── ESC[22m - disable bold/dim
│ ├── ESC[23m - disable italic
│ ├── ESC[24m - disable underline
│ ├── ESC[25m - disable blink (NOT IMPLEMENTED)
│ ├── ESC[26m - (unused)
│ ├── ESC[27m - disable inverse
│ ├── ESC[28m - disable hidden
│ ├── ESC[29m - disable strikeout
│ │
│ │ *** Foreground Text "8 Color" ***
│ │
│ ├── ESC[30m - fg Black
│ ├── ESC[31m - fg Red
│ ├── ESC[32m - fg Green
│ ├── ESC[33m - fg Yellow
│ ├── ESC[34m - fg Blue
│ ├── ESC[35m - fg Magenta
│ ├── ESC[36m - fg Cyan
│ ├── ESC[37m - fg White
│ ├── ESC[39m - fg default
│ │
│ │ *** Background Text "8 Color" ***
│ │
│ ├── ESC[40m - bg Black
│ ├── ESC[41m - bg Red
│ ├── ESC[42m - bg Green
│ ├── ESC[43m - bg Yellow
│ ├── ESC[44m - bg Blue
│ ├── ESC[45m - bg Magenta
│ ├── ESC[46m - bg Cyan
│ ├── ESC[47m - bg White
│ ├── ESC[49m - bg default
│ │
│ │ *** Special RGB Color ***
│ │
│ └── ESC [ 38 ; Red ; Grn ; Blue m - where Red,Grn,Blu are decimal (0-255)
│ ESC[s - save cursor pos (ansi.sys+xterm+gnome, but NOT vt100)
│ ESC[u - rest cursor pos (ansi.sys+xterm+gnome, but NOT vt100)
│ ESC[>#q - (DECSCA) Set Cursor style (block/line/blink..) (NOT IMPLEMENTED)
│ ESC[#;#r - (DECSTBM) Set scroll Region top;bot (NOT IMPLEMENTED)
│ ESC[#..$t - (DECRARA) (NOT IMPLEMENTED)
│ ------------------------
│ --- C1 Control Codes ---
│ ------------------------
│ <ESC>c - (RIS) Reset term to Initial State
│ <ESC>D - (IND) Index: move cursor down a line, scroll if at bottom
│ <ESC>E - (NEL) Next Line: basically do a crlf, scroll if at bottom
│ <ESC>H - (HTS) Horizontal Tab Set: set a tabstop
│ <ESC>M - (RI) Reverse Index (up w/scroll)
│ NOTE: Acronyms in parens are Digital Equipment Corporation's names these VT features.
\endcode
\section external_escape_codes Useful Terminal Escape Code Documentation
Useful links for reference:
- https://vt100.net/docs/vt100-ug/chapter3.html
- https://www.xfree86.org/current/ctlseqs.html
- https://www.x.org/docs/xterm/ctlseqs.pdf
- https://gist.github.com/justinmk/a5102f9a0c1810437885a04a07ef0a91 <-- alphabetic!
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
\section Fl_Terminal_design Fl_Terminal Design Document
When I started this project, I identified the key concepts needed to
implement Fl_Terminal:
- Draw and manage multiline Unicode text in FLTK
- Allow per-character colors and attributes
- Efficient screen buffer to handle "scrollback history"
- Efficient scrolling with vertical scrollbar for even large screen history
- Mouse selection for copy/paste
- Escape code management to implement VT100 style / ANSI escape codes.
A class was created for each character, since characters can be either ASCII
or Utf8 encoded byte sequences. This class is called Utf8Char, and handles
the character, its fg and bg color, and any attributes like dim, bold, italic, etc.
For managing the screen, after various experiments, I decided a ring buffer
was the best way to manage things, the ring split in two:
- 'screen history' which is where lines scrolled off the top are saved
- 'display screen' displayed to the user at all times, and where the cursor lives
Scrolling the display, either by scrollbar or by new text causing the display
to scroll up one line, would simply change an 'offset' index# of where in the
ring buffer the top of the screen is, automatically moving the top line
into the history, all without moving memory around.
In fact the only time screen memory is moved around is during these infrequent
operations:
- during scrolling "down"
- character insert/delete operations within a line
- changing the display size
- changing the history size
So a class "RingBuffer" is defined to manage the ring, and accessing its various
parts, either as the entire entity ring, just the history, or just the display.
These three concepts, "ring", "history" and "display" are given abbreviated
names in the RingBuffer class's API:
┌─────────────────────────────────────────┬──────────────────────────────┐
│ NOTE: Abbreviations "hist" and "disp" │ │
├─────────────────────────────────────────┘ │
│ │
│ "history" may be abbreviated as "hist", and "display" as "disp" in │
│ both this text and the source code. 4 character names are used so │
│ they line up cleanly in the source, e.g. │
│ │
│ ring_rows() ring_cols() │
│ hist_rows() hist_cols() │
│ disp_rows() disp_cols() │
│ └─┬┘ └─┬┘ └─┬┘ └─┬┘ │
│ └────┴──────────┴────┴───────── 4 characters │
│ │
└────────────────────────────────────────────────────────────────────────┘
These concepts were able to fit into C++ classes:
Utf8Char
--------
Each character on the screen is a "Utf8Char" which can manage
the UTF-8 encoding of any character as one or more bytes. Also
in that class is a byte for an attribute (underline, bold, etc),
and two integers for fg/bg color.
RingBuffer
----------
The RingBuffer class keeps track of the buffer itself, a single
array of Utf8Chars called "ring_chars" whose width is ring_cols()
and whose height is ring_rows().
The "top" part of the ring is the history, whose width is hist_cols()
and whose height is hist_rows(). hist_use_rows() is used to define
what part of the history is currently in use.
The "bottom" part of the ring is the display, whose width is disp_cols()
and whose height is disp_rows().
An index number called "offset" points to where in the ring buffer
the top of the ring currently is. This index changes each time the
screen is scrolled, and affects both where the top of the display is,
and where the top of the history is.
The memory layout of the Utf8Char character array is:
ring_chars[]:
___________________ _ _
| | ʌ
| | |
| | |
| H i s t o r y | | hist_rows
| | |
| | |
|___________________| _v_
| | ʌ
| | |
| D i s p l a y | | disp_rows
| | |
|___________________| _v_
|<----------------->|
ring_cols
hist_cols
disp_cols
So it's basically a single continuous array of Utf8Char instances
where any character can generally be accessed by index# using the formula:
ring_chars[ (row*ring_cols)+col ]
..where 'row' is the desired row, 'col' is the desired column,
and 'ring_cols' is how many columns "wide" the buffer is.
The "offset" index affects that formula as an extra row offset,
and the resulting index is then clamped within the range of the
ring buffer using modulus.
Methods are used to allow direct access to the characters
in the buffer that automatically handle the offset and modulus
formulas, namely:
u8c_ring_row(row,col) // access the entire ring by row/col
u8c_hist_row(row,col) // access just the history buffer
u8c_disp_row(row,col) // access just the display buffer
A key concept is the use of the simple 'offset' index integer
to allow the starting point of the history and display to be
moved around to implement 'text scrolling', such as when
crlf at the screen bottom causes a 'scroll up'.
This is simply an "index offset" integer applied to the
hist and disp indexes when drawing the display. So after
scrolling two lines up, the offset is just increased by 2,
redefining where the top of the history and display are, e.g.
Offset is 0: 2 Offset now 2:
┌───────────────────┐ ──┐ ┌───────────────────┐
│ │ │ │ D i s p l a y │
│ │ └─> ├───────────────────┤
│ │ │ │
│ H i s t o r y │ │ │
│ │ │ H i s t o r y │
│ │ 2 │ │
├───────────────────┤ ──┐ │ │
│ │ │ │ │
│ │ └─> ├───────────────────┤
│ D i s p l a y │ │ │
│ │ │ D i s p l a y │
│ │ │ │
└───────────────────┘ └───────────────────┘
This 'offset' trivially implements "text scrolling", avoiding having
to physically move memory around. Just the 'offset' changes, the
text remains where it is in memory.
This also makes it appear the top line in the display is 'scrolled up'
into the bottom of the scrollback 'history'.
If the offset exceeds the size of the ring buffer, it simply wraps
around back to the beginning of the buffer with a modulo.
Indexes into the display and history are also modulo their respective
rows, e.g.
act_ring_index = (hist_rows + disp_row + offset - scrollbar_pos) % ring_rows;
This way indexes for ranges can run beyond the bottom of the ring,
and automatically wrap around the ring, e.g.
┌───────────────────┐
┌─> 2 │ │
│ 3 │ D i s p l a y │
│ 4 │ │
│ ├───────────────────┤ <-- offset points here
│ │ │
disp │ │ │
index ┤ │ H i s t o r y │
wraps │ │ │
│ │ │
│ │ │
│ ├───────────────────┤
│ 0 │ D i s p l a y │
│ 1 └───────────────────┘ <- ring_rows points to end of ring
└── 2 : :
3 : :
disp_row(5) -> 4 :...................:
The dotted lines show where the display would be if not for the fact
it extends beyond the bottom of the ring buffer (due to the current offset),
and therefore wraps up to the top of the ring.
So to find a particular row in the display, in this case a 5 line display
whose lines lie between 0 and 4, some simple math calculates the row position
into the ring:
act_ring_index = (histrows // the display exists AFTER the history, so offset the hist_rows
+ offset // include the scroll 'offset'
+ disp_row // add the desired row relative to the top of the display (0..disp_rows)
) % ring_rows; // make sure the resulting index is within the ring buffer (0..ring_rows)
An additional bit of math makes sure if a negative result occurs, that
negative value works relative to the end of the ring, e.g.
if (act_ring_index < 0) act_ring_index = ring_rows + act_ring_index;
This guarantees the act_ring_index is within the ring buffer's address space,
with all offsets applied.
The math that implements this can be found in the u8c_xxxx_row() methods,
where "xxxx" is one of the concept regions "ring", "hist" or "disp":
Utf8Char *u8c;
u8c = u8c_ring_row(rrow); // address within ring, rrow can be 0..(ring_rows-1)
u8c = u8c_hist_row(hrow); // address within hist, hrow can be 0..(hist_rows-1)
u8c = u8c_disp_row(drow); // address within disp, drow can be 0..(disp_rows-1)
The small bit of math is only involved whenever a new row address is needed,
so in a display that's 80x25, to walk all the characters in the screen, the
math above would only be called 25 times, once for each row, and each column
in the row is just a simple integer offset:
for ( int row=0; row<disp_rows(); row++ ) { // walk rows: disp_rows = 25
Utf8Char *u8c = u8c_disp_row(row); // get first char in display 'row'
for ( int col=0; col<disp_cols(); col++ ) { // walk cols: disp_cols = 80
u8c[col].do_something(); // work with the char at row/col
}
}
So to recap, the concepts here are:
- The ring buffer itself, a linear array that is conceptually
split into a 2 dimensional array of rows and columns whose
height and width are:
ring_rows -- how many rows in the entire ring buffer
ring_cols -- how many columns in the ring buffer
nchars -- total chars in ring, e.g. (ring_rows * ring_cols)
- The "history" within the ring. For simplicity this is thought of
as starting relative to the top of the ring buffer, occupying
ring buffer rows:
0 .. hist_rows()-1
- The "display", or "disp", within the ring, just after the "history".
It occupies the ring buffer rows:
hist_rows() .. hist_rows()+disp_rows()-1
..or similarly:
(hist_rows)..(ring_rows-1)
The following convenience methods provide access to the
start and end indexes within the ring buffer for each entity:
// Entire ring
ring_srow() -- start row index of the ring buffer (always 0)
ring_erow() -- end row index of the ring buffer
// "history" part of ring
hist_srow() -- start row index of the screen history
hist_erow() -- end row index of the screen history
// "display" part of ring
disp_srow() -- start row index of the display
disp_erow() -- end row index of the display
The values returned by these are as described above.
For the hist_xxx() and disp_xxx() methods the 'offset' included into
the forumula. (For this reason hist_srow() won't always be zero
the way ring_srow() is, due to the 'offset')
The values returned by these methods can all be passed to the
u8c_ring_row() function to access the actual character buffer's contents.
- An "offset" used to move the "history" and "display" around within
the ring buffer to implement the "text scrolling" concept. The offset
is applied when new characters are added to the buffer, and during
drawing to find where the display actually is within the ring.
- The "scrollbar", which only is used when redrawing the screen the user sees,
and is simply an additional offset to all the above, where a scrollbar
value of zero (the scrollbar tab at the bottom) shows the display rows,
and as the scrollbar values increase as the user moves the scrollbar
tab upwards, +1 per line, this is subtracted from the normal starting
index to let the user work their way backwards into the scrollback history.
Again, negative numbers wrap around within the ring buffer automatically.
The ring buffer allows new content to simply be appended to the ring buffer,
and the index# for the start of the display and start of scrollback history are
simply incremented. So the next time the display is "drawn", it starts at
a different position in the ring.
This makes scrolling content at high speed trivial, without memory moves.
It also makes the concept of "scrolling" with the scrollbar simple as well,
simply being an extra index offset applied during drawing.
Mouse Selection
---------------
Dragging the mouse across the screen should highlight the text, allowing the user
to extend the selection either beyond or before the point started. Extending the
drag to the top of the screen should automatically 'scroll up' to select more
lines in the scrollback history, or below the bottom to do the opposite.
The mouse selection is implemented as a class to keep track of the start/end
row/col positions of the selection, and other details such as a flag indicating
if a selection has been made, what color the fg/bg text should appear when
text is selected, and methods that allow setting and extending the selection,
clearing the selection, and "scrolling" the selection, to ensure the row/col
indexes adjust correctly to track when the screen or scrollbar is scrolled.
Redraw Timer
------------
Knowing when to redraw is tricky with a terminal, because sometimes high volumes
of input will come in asynchronously, so in that case we need to determine when
to redraw the screen to show the new content; too quickly will cause the screen
to spend more time redrawing itself, preventing new input from being added. Too
slowly, the user won't see new information appear in a timely manner.
To solve this, a rate timer is used to prevent too many redraws:
- When new data comes in, a 1/10 sec timer is started and a modify flag is set.
- redraw() is NOT called yet, allowing more data to continue to arrive quickly
- When the 1/10th second timer fires, the callback checks the modify flag:
- if set, calls redraw(), resets the modify to 0, and calls
Fl::repeat_timeout() to repeat the callback in another 1/10th sec.
- if clear, no new data came in, so DISABLE the timer, done.
In this way, redraws don't happen more than 10x per second, and redraw() is called
only when there's new content to see.
The redraw rate can be set by the user application using the Fl_Terminal::redraw_rate(),
0.10 being the default.
Some terminal operations necessarily call redraw() directly, such as interactive mouse
selection, or during user scrolling the terminal's scrollbar, where it's important there's
no delay in what the user sees while interacting directly with the widget.
*/