summaryrefslogtreecommitdiff
path: root/src/README-Fl_Terminal.txt
diff options
context:
space:
mode:
authorerco77 <erco@seriss.com>2023-11-14 07:01:52 -0800
committerGitHub <noreply@github.com>2023-11-14 07:01:52 -0800
commit6842a43a3170c6f7a852186d5688baebdac16e2c (patch)
tree04493580bf8fc798858720c7ab7ffecedeb9ee3e /src/README-Fl_Terminal.txt
parent83f6336f3b024f4a7c55a7499d036f03d946c1b9 (diff)
Fl_Terminal widget (#800)
Pull Fl_Terminal widget from Greg's fork
Diffstat (limited to 'src/README-Fl_Terminal.txt')
-rw-r--r--src/README-Fl_Terminal.txt586
1 files changed, 586 insertions, 0 deletions
diff --git a/src/README-Fl_Terminal.txt b/src/README-Fl_Terminal.txt
new file mode 100644
index 000000000..4b1b6bc00
--- /dev/null
+++ b/src/README-Fl_Terminal.txt
@@ -0,0 +1,586 @@
+// vim: autoindent tabstop=8 shiftwidth=4 expandtab softtabstop=4
+
+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 effectively,
+ allowing per-character colors and attributes like underline,
+ strikeout, background, etc.
+
+ - An efficient screen buffer to handle the "scrollback history"
+ and "screen display" concepts; the "history" being a scrollback
+ history of text that scrolls up and off screen from the "display",
+ and the "display" being where the action is: the cursor can be
+ moved around and text scrolled up or down.
+
+ - How the vertical scrollbar should provide the user with a way to
+ scroll back into the scrollback history to allow the user to scroll back
+ to view the "scrollback history", without stopping the "screen display"
+ from operating
+
+ - How to manage mouse selection for copy/paste
+
+ - Escape code management to implement VT100 style / ANSI escape codes.
+
+
+ ┌─────────────────────────────────────────┬──────────────────────────────┐
+ │ 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_cols = 0; ring_rows = 0; │
+ │ hist_cols = 0; ring_cols = 0; │
+ │ disp_cols = 0; ring_cols = 0; │
+ │ └─┬┘ └─┬┘ │
+ │ └────┴─── 4 characters │
+ │ │
+ └────────────────────────────────────────────────────────────────────────┘
+
+So the of these concepts were able to fit into C++ class concepts well.
+Those classes being:
+
+ Utf8Char
+ ========
+ Each character on the screen is a "Utf8Char" which can manage
+ the utf8 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", and some index numbers
+ to keep track of how many rows are in the screen's history and
+ display, named "hist_rows" and "disp_rows".
+
+ The memory layout of the Utf8Char array is:
+
+ ___________________ _ _
+ | | ʌ
+ | | |
+ | | |
+ | H i s t o r y | | hist_rows
+ | | |
+ | | |
+ |___________________| _v_
+ | | ʌ
+ | | |
+ | D i s p l a y | | disp_rows
+ | | |
+ |___________________| _v_
+
+ |<----------------->|
+ ring_cols
+
+ So it's basically a single continguous array of Utf8Char instances
+ where any character can 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.
+
+ Methods are used to give access the characters in the buffer.
+
+ A key concept is 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, 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 │
+ │ │ │ │
+ └───────────────────┘ └───────────────────┘
+
+ Offset is 0: 4 Offset now 4:
+ ┌───────────────────┐ ──┐ ┌───────────────────┐
+ │ │ │ │ │
+ │ │ │ │ D i s p l a y │
+ │ │ │ │ │
+ │ H i s t o r y │ └─> ├───────────────────┤
+ │ │ │ │
+ │ │ 4 │ │
+ ├───────────────────┤ ──┐ │ H i s t o r y │
+ │ │ │ │ │
+ │ │ │ │ │
+ │ D i s p l a y │ │ │ │
+ │ │ └─> ├───────────────────┤
+ │ │ │ D i s p l a y │
+ └───────────────────┘ └───────────────────┘
+
+ The effect of applying an offset trivially implements "text scrolling",
+ so that no screen memory has to physically moved around, simply changing
+ the single integer "offset" is enough. The text remains where it was, and
+ the offset is simply incremented to scroll up. This also automatically
+ makes it appear the top line in the display is 'scrolled up' into the
+ last line of the scrollback history.
+
+ If the offset exceeds the size of the ring buffer, it is simply wrapped
+ back to the beginning of the buffer with a modulo: offset =% ring_rows;
+
+ 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.
+
+ Offset now 4:
+ ┌───────────────────┐
+ 2 │ │
+ 3 │ D i s p l a y │
+ 4 │ │ <- act_disp_row(4)
+ ├───────────────────┤
+ │ │
+ │ │
+ │ H i s t o r y │
+ │ │
+ │ │
+ │ │
+ ├───────────────────┤
+ 0 │ D i s p l a y │
+ 1 └───────────────────┘ <- ring_rows
+ 2 : :
+ 3 : :
+ disp_row(5) -> 4 :...................:
+
+ Here the "disp_row" is the desired offset into the display, but we
+ need the actual index into the ring from the top, since that's the
+ physical array.
+
+ So some simple math calculates the row position based on the "offset",
+ and the "hist" vs "disp" concepts:
+
+ 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 guaratnees 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, e.g.
+
+ 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 character 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)
+
+ - 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.
+
+ - A "scrollbar", which only is used when redrawing the screen the user sees,
+ and is simply an additional offset to all the above, where a scrollback
+ value of zero (the scrollbar tab at the bottom) shows the display rows,
+ and the values increase as the user moves the scrolltab upwards, 1 per line,
+ which is subtracted from the normal starting index to let the user work their
+ way backwards into the scrollback history.
+
+ 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.
+
+ If the display is enlarged vertically, that's easy too; the display
+ area is simply defined as being more rows, the history as less rows,
+ the history use decreased (since what was in the history before is now
+ being moved into the display), and all the math adjusts accordingly.
+
+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 at this time, allowing new data to continue to arrive
+ quickly. Once the modify flag is set, nothing changes from there.
+
+ - 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.
+
+
+
+
+OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
+ OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
+ OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
+ OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
+
+
+
+RING BUFFER DESCRIPTION
+=======================
+
+ The history and display are defined by row indexes into this buffer
+ which are adjusted whenever the text display is 'scrolled' (e.g. by crlfs)
+
+ The scrollbar is a secondary offset on top of that applied during drawing.
+ the display.
+
+ Here's what the variables managing the split ring buffer look like:
+
+
+
+ RING BUFFER
+ ─ ring_chars[] ─┬─>┌───────────────┐
+ ʌ [hist_srow] ──┘ │ │
+ ┊ │ │
+ ┊ │ H i s t o r y ┊
+ hist_rows_┊ │ │
+ ┊ │ │
+ ┊ │ │
+ ┊ │ │
+ v [hist_erow] ───>│ │
+ ─ [disp_srow] ───>├───────────────┤
+ ʌ │ │
+ ┊ │ │
+ disp_rows_┊ │ D i s p l a y │
+ ┊ │ │
+ ┊ │ │
+ v [ring_erow] ───>│ +│<── ring_bot_
+ ─ └───────────────┘ (last valid ptr address in ring)
+
+ │<──────┬──────>│
+ ring_cols
+ hist_cols
+ disp_cols
+
+ The concept here is the single ring buffer is split into two parts that can differ
+ in height (rows) but not in width (cols). For instance, typically the history is
+ many times larger than the display; a typical old school display might be 80x25,
+ but the history might be 2000 lines.
+
+ ring_row() handles the fact that 'row' might run off the end of the buffer,
+ depending on where hist_srow starts. For instance, if the display has scrolled
+ a few lines, the ring buffer arrangement might look like:
+
+ RING BUFFER
+ ring_chars[] ───>┌───────────────┐
+ │ D i s p l a y │
+ [disp_erow] ───>│ │
+ ├───────────────┤
+ [hist_srow] ───>│ │
+ │ │
+ │ H i s t o r y │
+ │ │
+ │ │
+ │ │
+ [hist_erow] ───>│ │
+ ├───────────────┤
+ [disp_srow] ───>│ │
+ │ D i s p l a y │
+ │ │
+ [ring_erow] ───>│ +│<── ring_bot_
+ └───────────────┘ (last valid ptr address in ring)
+
+
+ Note how the display 'wraps around', straddling the end of the ring buffer.
+ So trivially walking ring_chars[] from 'disp_srow' for 'disp_rows' would run
+ off the end of memory for the ring buffer. Example:
+
+ // BAD!
+ for ( int row=disp_srow; row<disp_rows; row++ ) {
+ for ( int col=0; col<disp_cols; col++ ) {
+ ring_chars[row*disp_cols+col]->do_something(); // BAD! can run off end of array
+ }
+ }
+
+ The function u8c_row() can access the Utf8Char* of each row more safely,
+ ensuring that even if the 'row' index runs off the end of the array,
+ u8c_row() handles wrapping it for you. So the safe way to walk the chars
+ of the display can be done this way:
+
+ // GOOD!
+ for ( int row=disp_srow; row<disp_rows; row++ ) {
+ Utf8Char *u8c = u8c_row(row); // safe: returns utf8 char for start of each row
+ for ( int col=0; col<disp_cols; col++ ) { // walk the columns safely
+ (u8c++)->do_something(); // get/set the utf8 char
+ }
+ }
+
+ Walking the history would be the same, just replace disp_xxxx with hist_xxxx.
+
+ One can also use ring_row_normalize() to return an index# that can be directly
+ used with ring_chars[], the value kept in range of the buffer.
+
+
+RING BUFFER "SCROLLING"
+=======================
+ A ring buffer is used to greatly simplify the act of 'scrolling', which happens a lot
+ when large amounts of data come in, each CRLF triggering a "scroll" that moves the top
+ line up into the history buffer. The history buffer can be quite large (1000's of lines),
+ so it would suck if, on each line scroll, thousands of rows of Utf8Chars had to be
+ physically moved in memory.
+
+ Much easier to just adjust the srow/erow pointers, which simply affect how drawing
+ is done, and where the display area is. This trivially handles scrolling by just
+ adjusting some integers by 1.
+
+
+ -- -- HOW SCROLLING UP ONE LINE IS DONE -- --
+
+
+ ring_chars[] ─┬─>┌───────────────┐ ─┐ ring_chars[] ──>┌─────────────────┐
+ [hist_srow] ──┘ │ │ │ │x x x x x x x x x│ <-- blanks
+ │ │ └─> [hist_srow] ──>├─────────────────┤
+ │ H i s t │ │ │
+ │ │ │ │
+ │ │ │ H i s t │
+ │ │ │ │
+ │ │ │ │
+ [hist_erow] ───>│ │ │ │
+ [disp_srow] ───>├Line 1─────────┤ ─┐ [hist_erow] ──>│Line 1 │
+ │Line 2 │ └─> [disp_srow] ──>├Line 2───────────┤
+ │Line 3 │ │Line 3 │
+ │ │ │ │
+ │ D i s p │ │ D i s p │
+ │ │ │ │
+[disp_erow][ring_erow] ───>│Line 24 +│ [ring_erow] ──>│Line 24 +│
+ └───────────────┘ └─────────────────┘
+
+ In the above, Line 1 has effectively "moved" into history because the disp_s/erow
+ and hist_s/erow variables have just been incremented.
+
+ During resize_display(), we need to preserve the display and history as much as possible
+ when the ring buffer is enlarged/shrank; the hist_rows size should be maintained, and only
+ display section changes size based on the FLTK window size.
+
+
+
+===================== OLD ====================== OLD ====================== OLD ======================
+
+Conventions used for the internals
+==================================
+This is a large widget, and these are some breadcrumbs for anyone
+working on the internals of this class.
+
+ > There is one utf8 char buffer, buff_chars[], the top part is the 'history buffer'
+ (which the user can scroll back to see), and the 'display buffer' which is the
+ 'active display'.
+ > glob or global - refers to global buffer buff_chars[]
+ > disp or display - refers to display buffer disp_chars[]
+ > Abbreviations glob/disp/buff/hist used because 4 chars line up nicely
+ > row/col variable names use a 'g' or 'd' prefix to convey 'g'lobal or 'd'isplay.
+ > The 'Cursor' class uses row/col for the display (disp_chars[]) because the
+ cursor is only ever inside the display.
+ > The 'Selection' class uses row/col for the global buffer (buff_chars[])
+ because it can be in the 'history' or the 'display'
+ > These concepts talk about the same thing:
+ > global buffer == buff_chars[] == "history buffer" == hist == grow/gcol
+ > display == disp_chars[] == "active display" == drow/dcol
+ > There is no hist_chars[] because it's just the top half of buff_chars[]
+ > There is no hist_height_ because it's the same as hist_max_
+ > There is no hist_width_ because it's the same as buff_width_.
+
+
+Fl_Terminal's Class Hierarchy
+=============================
+
+ class Fl_Terminal -- Derived from Fl_Group (to parent scrollbars, popup menus, etc)
+ We mainly use the group's background to draw over in draw().
+ Within the terminal classes are the following private/protected classes
+ that help with bookkeeping and operation of the terminal class:
+
+ class Margin -- Handles the margins around the terminal drawing area
+ class CharStyle -- The styling for the characters: single byte color + attribute (bold/inverse/etc)
+ class Cursor -- The attributes of the cursor -- position, color, etc, and some simple movement logic
+ class Utf8Char -- Visible screen buffer is an array of these, one per character
+ class RingBuffer -- The ring buffer of Utf8Char's, with the "history" and "display" concept.
+ class EscapeSeq -- A class to handle parsing Esc sequences, and keeping state info between chars
+ Single chars go in, and when a complete esc sequence is parsed, the caller
+ can find out all the integer values and command code easily to figure out
+ what op to do.
+
+ OVERALL DESIGN:
+ To handle unicode, the terminal's visible display area is a linear array of pointers to
+ instances of the 'Utf8Char' class, one instance per character. The arrangement of the array
+ is much like the IBM PC's video memory, but instead of Char/Attrib byte pairs, the Utf8Char
+ class handles the more complex per-character data and colors/attributes.
+
+ The cursor x,y value can be quickly converted to an index into this buffer.
+ Strings are printed into the buffer, again, similar to the IBM PC video memory;
+ one character at a time into the Utf8Char class instances.
+
+ When the screen redraws, it just walks this array, and calls fl_draw() to draw
+ the text, one utf8 char at a time, with the colors/fonts/attributes from the Utf8Char class.
+
+ As characters are added, Esc sequences are intercepted and parsed into the EscapeSeq class,
+ which has a single instance for the terminal.
+
+ For the scrollback history, as lines scrolls off the top of the active display area,
+ the Utf8Char's are copied to the history buffer, and the active display's top line
+ is simply rotated to the bottom line and cleared, allowing memory reuse of the Utf8Char's,
+ to prevent memory churn for the display. The goal is to allow high volume output to the
+ terminal with a minimum affect on realloc'ing memory.
+
+ OPTIMIZATIONS
+ Where possible, caching is used to prevent repeated calls to cpu expensive operations,
+ such as anything to do with calculating unicode character width/height/etc.
+
+RingBuffer
+ The ring buffer is split in two; the top part is the history, the bottom part is
+ the "display area", where new text comes in, where the cursor can be positioned,
+ and concepts like "scroll up" and "scroll down" all happen. The "history" is simply
+ a linear buffer where lines pushed up from the display are moved into.
+
+ Methods let one access the ring with index#s:
+
+ - The entire ring buffer can be accessed with:
+
+ for (int i=0; i<ring.ring_rows(); i++) {
+ Utf8Char *u8c_row = ring.u8c_ring_row(i);
+ for (int col=0; i<ring.ring_cols(); i++) {
+ u8c_row[col].xxx(); // access each Utf8Char at the row/col
+ }
+ }
+
+ Row#s can be given that are larger than the ring; these are automatically
+ wrapped around to the top. The ring, "history" and "display" can each be
+ accessed separately with index#s relative to their position
+//////////////////////////////////////////////////////////////////////////////////////
+
+//////////////////////////////////////////////////////////////////////////////////////
+ Moved this down to the bottom of the file for now -- not sure where to put this,
+ but it's useful if one wants to reuse the EscapeSeq class somewhere else. -erco Dec 2022
+
+ Typical use pattern of EscapeSeq class.
+ This is unverified code, but should give the general gist;
+
+ while ( *s ) { // walk text that may contain ESC sequences
+ if ( *s == 0x1b ) {
+ escseq.parse(*s++); // start parsing ESC seq (does a reset())
+ continue;
+ } else if ( escseq.parse_in_progress() ) { // continuing to parse an ESC seq?
+ switch (escseq.parse(*s++)) { // parse char, advance s..
+ case fail: escseq.reset(); continue; // failed? reset, continue..
+ case success: continue; // keep parsing..
+ case completed: // parsed complete esc sequence?
+ break;
+ }
+ // Handle parsed esc sequence here..
+ switch ( escseq.esc_mode() ) {
+ case 'm': // ESC[...m?
+ for ( int i=0; i<escseq.total_vals(); i++ ) {
+ int val = escseq.val(i);
+ ..handle values here..
+ }
+ break;
+ case 'J': // ESC[#J?
+ ..handle..
+ break;
+ }
+ escseq.reset(); // done handling escseq, reset()
+ continue;
+ } else {
+ ..handle non-escape chars here..
+ }
+ ++s; // advance thru string
+ }
+
+----------------------------------------------------------------------------------------
+