diff options
Diffstat (limited to 'documentation')
| -rw-r--r-- | documentation/Doxyfile.in | 1 | ||||
| -rw-r--r-- | documentation/Makefile | 3 | ||||
| -rw-r--r-- | documentation/src/Fl_Terminal-24bit-colors.png | bin | 0 -> 63524 bytes | |||
| -rw-r--r-- | documentation/src/Fl_Terminal-3bit-colors.png | bin | 0 -> 59419 bytes | |||
| -rw-r--r-- | documentation/src/Fl_Terminal-demo.png | bin | 0 -> 48352 bytes | |||
| -rw-r--r-- | documentation/src/Fl_Terminal-utf8-demo.png | bin | 0 -> 37477 bytes | |||
| -rw-r--r-- | documentation/src/Fl_Terminal.dox | 514 |
7 files changed, 517 insertions, 1 deletions
diff --git a/documentation/Doxyfile.in b/documentation/Doxyfile.in index bf88783f4..130dd1845 100644 --- a/documentation/Doxyfile.in +++ b/documentation/Doxyfile.in @@ -789,6 +789,7 @@ INPUT = @CMAKE_CURRENT_SOURCE_DIR@/src/index.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/coordinates.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/resize.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/editor.dox \ + @CMAKE_CURRENT_SOURCE_DIR@/src/Fl_Terminal.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/drawing.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/events.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/subclassing.dox \ diff --git a/documentation/Makefile b/documentation/Makefile index c75dd567c..5643ba118 100644 --- a/documentation/Makefile +++ b/documentation/Makefile @@ -48,7 +48,8 @@ HTMLFILES = \ $(SRC_DOCDIR)/development.dox \ $(SRC_DOCDIR)/license.dox \ $(SRC_DOCDIR)/examples.dox \ - $(SRC_DOCDIR)/faq.dox + $(SRC_DOCDIR)/faq.dox \ + $(SRC_DOCDIR)/Fl_Terminal.dox MANPAGES = $(SRC_DOCDIR)/fltk.$(CAT3EXT) $(SRC_DOCDIR)/fltk-config.$(CAT1EXT) \ $(SRC_DOCDIR)/fluid.$(CAT1EXT) $(SRC_DOCDIR)/blocks.$(CAT6EXT) \ diff --git a/documentation/src/Fl_Terminal-24bit-colors.png b/documentation/src/Fl_Terminal-24bit-colors.png Binary files differnew file mode 100644 index 000000000..5e7f6adb1 --- /dev/null +++ b/documentation/src/Fl_Terminal-24bit-colors.png diff --git a/documentation/src/Fl_Terminal-3bit-colors.png b/documentation/src/Fl_Terminal-3bit-colors.png Binary files differnew file mode 100644 index 000000000..a0407738e --- /dev/null +++ b/documentation/src/Fl_Terminal-3bit-colors.png diff --git a/documentation/src/Fl_Terminal-demo.png b/documentation/src/Fl_Terminal-demo.png Binary files differnew file mode 100644 index 000000000..43996ba72 --- /dev/null +++ b/documentation/src/Fl_Terminal-demo.png diff --git a/documentation/src/Fl_Terminal-utf8-demo.png b/documentation/src/Fl_Terminal-utf8-demo.png Binary files differnew file mode 100644 index 000000000..a04f11bf9 --- /dev/null +++ b/documentation/src/Fl_Terminal-utf8-demo.png diff --git a/documentation/src/Fl_Terminal.dox b/documentation/src/Fl_Terminal.dox new file mode 100644 index 000000000..2963218da --- /dev/null +++ b/documentation/src/Fl_Terminal.dox @@ -0,0 +1,514 @@ +// 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. + +*/ |
