// 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 ; 2; Red ; Grn ; Blue m - Foreground 8-bit Red,Grn,Blu values (in decimal 0-255) │ └── ESC [ 48 ; 2; Red ; Grn ; Blue m - Background 8-bit Red,Grn,Blu values (in 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 --- │ ------------------------ │ │ c - (RIS) Reset term to Initial State │ D - (IND) Index: move cursor down a line, scroll if at bottom │ E - (NEL) Next Line: basically do a crlf, scroll if at bottom │ H - (HTS) Horizontal Tab Set: set a tabstop │ 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 "scroll down" escape sequences, e.g. ESC[\#T - character insert/delete operations within a line, e.g. ESC[\#@, ESC[\#P - changing the display size e.g. Fl_Terminal::resize() - changing the history size e.g. Fl_Terminal::history_lines() 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