summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/Fl_Terminal.cxx3508
-rw-r--r--src/Makefile1
-rw-r--r--src/README-Fl_Terminal.txt586
-rw-r--r--src/makedepend22
5 files changed, 4118 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0db9631be..f9511f358 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -90,6 +90,7 @@ set (CPPFILES
Fl_Table.cxx
Fl_Table_Row.cxx
Fl_Tabs.cxx
+ Fl_Terminal.cxx
Fl_Text_Buffer.cxx
Fl_Text_Display.cxx
Fl_Text_Editor.cxx
diff --git a/src/Fl_Terminal.cxx b/src/Fl_Terminal.cxx
new file mode 100644
index 000000000..6b7d6dd87
--- /dev/null
+++ b/src/Fl_Terminal.cxx
@@ -0,0 +1,3508 @@
+//
+// Fl_Terminal.H - A terminal widget for Fast Light Tool Kit (FLTK).
+//
+// Copyright 2022 by Greg Ercolano.
+// Copyright 2023 by Bill Spitzak and others.
+//
+// This library is free software. Distribution and use rights are outlined in
+// the file "COPYING" which should have been included with this file. If this
+// file is missing or damaged, see the license at:
+//
+// https://www.fltk.org/COPYING.php
+//
+// Please see the following page on how to report bugs and issues:
+//
+// https://www.fltk.org/bugs.php
+//
+
+// TODO: horizontal scrollbar
+// TODO: double clicking text should make word selection,
+// and drag should word-enlarge selection
+// FIXME: While dragging a selection, hitting shift stops the selection
+
+// This must appear above #include <assert.h>
+#ifndef NDEBUG
+#define NDEBUG // comment out to enable assert()
+#endif
+
+#include <ctype.h> // isdigit
+#include <stdlib.h> // malloc
+#include <string.h> // strlen
+#include <stdarg.h> // vprintf, va_list
+#include <assert.h>
+
+#include <FL/Fl.H>
+#include <FL/Fl_Terminal.H>
+#include <FL/fl_utf8.h> // fl_utf8len1
+#include <FL/fl_draw.H>
+#include <FL/fl_string_functions.h>
+
+/////////////////////////////////
+////// Static Functions /////////
+/////////////////////////////////
+
+#define MIN(a,b) ((a)<=(b)) ? (a) : (b) // Return smaller of two values
+#define MAX(a,b) ((a)>=(b)) ? (a) : (b) // Return larger of two values
+#define ABS(a) ((a)<0) ? -(a) : (a) // Return abs value
+
+// Return val clamped between min and max
+static int clamp(int val, int min, int max)
+ { return (val<min) ? min : (val>max) ? max : val; }
+
+// Swap integer values a and b
+static void swap(int &a, int &b)
+ { int asave = a; a = b; b = asave; }
+
+static int normalize(int row, int maxrows) {
+ row = row % maxrows;
+ if (row < 0) row = maxrows + row; // negative? index relative to end
+ return row;
+}
+
+// Color channel management
+static int red(Fl_Color val) { return (val & 0xff000000) >> 24; }
+static int grn(Fl_Color val) { return (val & 0x00ff0000) >> 16; }
+static int blu(Fl_Color val) { return (val & 0x0000ff00) >> 8; }
+static Fl_Color rgb(int r,int g,int b) { return (r << 24) | (g << 16) | (b << 8); }
+
+// Return dim version of color 'val'
+static Fl_Color dim_color(Fl_Color val) {
+ int r = clamp(red(val) - 0x20, 0, 255);
+ int g = clamp(grn(val) - 0x20, 0, 255);
+ int b = clamp(blu(val) - 0x20, 0, 255);
+ //DEBUG ::printf("DIM COLOR: %08x -> %08x\n", val, rgb(r,g,b));
+ return rgb(r,g,b);
+}
+
+// Return bold version of color 'val'
+static Fl_Color bold_color(Fl_Color val) {
+ int r = clamp(red(val) + 0x20, 0, 255);
+ int g = clamp(grn(val) + 0x20, 0, 255);
+ int b = clamp(blu(val) + 0x20, 0, 255);
+ //DEBUG ::printf("BOLD COLOR: %08x -> %08x\n", val, rgb(r,g,b));
+ return rgb(r,g,b);
+}
+
+// Return an FLTK color for given foreground color index (0..7)
+static Fl_Color fltk_fg_color(uchar ci) {
+ static const Fl_Color xterm_fg_colors_[] = {
+ 0x00000000, // 0
+ 0xd0000000, // 1 - red
+ 0x00d00000, // 2 - grn
+ 0xd0d00000, // 3 - yel
+ 0x0000d000, // 4 - blu
+ 0xd000d000, // 5 - mag
+ 0x00d0d000, // 6 - cyn
+ 0xd0d0d000 // 7 - white
+ };
+ ci &= 0x07; // clamp to array size
+ return xterm_fg_colors_[ci];
+}
+
+// Return an FLTK color for the given background color index (0..7) and attribute.
+// Background colors should be just a little darker than
+// the fg colors to prevent too much brightness clashing
+// for 'normal' bg vs fg colors.
+//
+static Fl_Color fltk_bg_color(uchar ci) {
+ static const Fl_Color xterm_bg_colors_[] = {
+ 0x00000000, // 0
+ 0xc0000000, // 1 - red
+ 0x00c00000, // 2 - grn
+ 0xc0c00000, // 3 - yel
+ 0x0000c000, // 4 - blu
+ 0xc000c000, // 5 - mag
+ 0x00c0c000, // 6 - cyn
+ 0xc0c0c000 // 7 - white
+ };
+ ci &= 0x07; // clamp to array size
+ return xterm_bg_colors_[ci];
+}
+
+// See if an Fl_Boxtype is FL_XXX_FRAME
+static bool is_frame(Fl_Boxtype b) {
+ if (b == FL_UP_FRAME || b == FL_DOWN_FRAME ||
+ b == FL_THIN_UP_FRAME || b == FL_THIN_DOWN_FRAME ||
+ b == FL_ENGRAVED_FRAME || b == FL_EMBOSSED_FRAME ||
+ b == FL_BORDER_FRAME) return true;
+ return false;
+}
+
+///////////////////////////////////////
+////// Selection Class Methods ////////
+///////////////////////////////////////
+
+// Ctor
+Fl_Terminal::Selection::Selection(void) {
+ // These are used to set/get the mouse selection
+ srow_ = scol_ = erow_ = ecol_ = 0;
+ // FL_PUSH event row/col
+ push_clear();
+ selectionfgcolor_ = FL_BLACK;
+ selectionbgcolor_ = FL_WHITE;
+ state_ = 0;
+ is_selection_ = false;
+}
+
+/**
+ Return selection start/end.
+ Ensures (start < end) to allow walking 'forward' thru selection,
+ left-to-right, top-to-bottom.
+
+ Returns:
+ - true -- valid selection values returned
+ - false -- no selection was made, returned values undefined
+*/
+bool Fl_Terminal::Selection::get_selection(int &srow,int &scol,
+ int &erow,int &ecol) const {
+ srow = srow_; scol = scol_;
+ erow = erow_; ecol = ecol_;
+ if (!is_selection_) return false;
+ // Ensure (start < end) on return
+ if (srow_ == erow_ && scol_ > ecol_) swap(scol, ecol);
+ if (srow_ > erow_)
+ { swap(srow, erow); swap(scol, ecol); }
+ return true;
+}
+
+// Start new selection at specified row,col
+// Always returns true.
+//
+bool Fl_Terminal::Selection::start(int row, int col) {
+ srow_ = erow_ = row;
+ scol_ = ecol_ = col;
+ state_ = 1; // state: "started selection"
+ is_selection_ = true;
+ return true;
+}
+
+// Extend existing selection to row,col
+// Returns true if anything changed, false if not.
+//
+bool Fl_Terminal::Selection::extend(int row, int col) {
+ // no selection started yet? start and return true
+ if (!is_selection()) return start(row, col);
+ state_ = 2; // state: "extending selection"
+ if (erow_ == row && ecol_ == col) return false; // no change
+ erow_ = row;
+ ecol_ = col;
+ return true;
+}
+
+// End selection (turn dragging() off)
+void Fl_Terminal::Selection::end(void) {
+ state_ = 3; // state: "finished selection"
+ // Order selection
+ if (erow_ < srow_)
+ { swap(srow_, erow_); swap(scol_, ecol_); }
+ if (erow_ == srow_ && scol_ > ecol_) swap(scol_, ecol_);
+}
+
+// Create a complete selection
+void Fl_Terminal::Selection::select(int srow, int scol, int erow, int ecol) {
+ srow_ = srow; scol_ = scol;
+ erow_ = erow; ecol_ = ecol;
+ state_ = 3; // state: "finished selection"
+ is_selection_ = true;
+}
+
+// Clear selection
+// Returns true if there was a selection, false if there wasn't
+//
+bool Fl_Terminal::Selection::clear(void) {
+ bool was_selected = is_selection(); // save for return
+ srow_ = scol_ = erow_ = ecol_ = 0;
+ state_ = 0;
+ is_selection_ = false;
+ return was_selected;
+}
+
+// Scroll the selection up(+)/down(-) number of rows
+void Fl_Terminal::Selection::scroll(int nrows) {
+ if (is_selection()) {
+ srow_ -= nrows;
+ erow_ -= nrows;
+ // Selection scrolled off? clear selection
+ if (srow_ < 0 || erow_ < 0) clear();
+ }
+}
+
+///////////////////////////////////////
+////// EscapeSeq Class Methods ////////
+///////////////////////////////////////
+
+// Append char to buff[] safely (with bounds checking)
+// Returns:
+// success - ok
+// fail - buffer full/overflow
+//
+int Fl_Terminal::EscapeSeq::append_buff(char c) {
+ if (buffp_ >= buffendp_) return fail; // end of buffer reached?
+ *buffp_++ = c;
+ *buffp_ = 0; // keep buff[] null terminated
+ return success;
+}
+
+// Append whatever integer string is at valbuffp into vals_[] safely w/bounds checking
+// Assumes valbuffp points to a null terminated string.
+// Returns:
+// success - parsed ok
+// fail - error occurred (non-integer, or vals_[] full)
+//
+int Fl_Terminal::EscapeSeq::append_val(void) {
+ if (vali_ >= maxvals) // vals_[] full?
+ { vali_ = maxvals-1; return fail; } // clamp index, fail
+ if (!valbuffp_ || (*valbuffp_ == 0)) // no integer to parse? e.g. ESC[m, ESC[;m
+ { vals_[vali_] = 0; return success; } // zero in array, do not inc vali
+ if (sscanf(valbuffp_, "%d", &vals_[vali_]) != 1) // Parse integer into vals_[]
+ { return fail; } // fail if parsed a non-integer
+ vals_[vali_] &= 0x3ff; // sanity: enforce int in range 0 ~ 1023 (prevent DoS attack)
+ if (++vali_ >= maxvals) // advance val index, fail if too many vals
+ { vali_ = maxvals-1; return fail; } // clamp + fail
+ valbuffp_ = 0; // parsed val ok, reset valbuffp to NULL
+ return success;
+}
+
+// Ctor
+Fl_Terminal::EscapeSeq::EscapeSeq(void) {
+ reset();
+ save_row_ = -1; // only in ctor
+ save_col_ = -1;
+}
+
+// Reset the class
+// Named reset to not be confused with clear() screen/line/etc
+//
+void Fl_Terminal::EscapeSeq::reset(void) {
+ esc_mode_ = 0; // disable ESC mode, so parse_in_progress() returns false
+ csi_ = false; // CSI off until '[' received
+ buffp_ = buff_; // point to beginning of buffer
+ buffendp_ = buff_ + (maxbuff - 1); // point to end of buffer
+ valbuffp_ = 0; // disable val ptr (no vals parsed yet)
+ vali_ = 0; // zero val index
+ buff_[0] = 0; // null terminate buffer
+ vals_[0] = 0; // first val[] 0
+ memset(vals_, 0, sizeof(vals_));
+}
+
+// Return current escape mode.
+// This is really only valid after parse() returns 'completed'.
+// After a reset() this will return 0.
+//
+char Fl_Terminal::EscapeSeq::esc_mode(void) const { return esc_mode_; }
+
+// Set current escape mode.
+void Fl_Terminal::EscapeSeq::esc_mode(char val) { esc_mode_ = val; }
+
+// Return the total vals parsed.
+// This is really only valid after parse() returns 'completed'.
+//
+int Fl_Terminal::EscapeSeq::total_vals(void) const { return vali_; }
+
+// Return the value at index i.
+// i is not range checked; it's assumed 0 <= i < total_vals().
+// It is only valid to call this after parse() returns 'completed'.
+//
+int Fl_Terminal::EscapeSeq::val(int i) const { return vals_[i]; }
+
+// See if we're in the middle of parsing an ESC sequence
+bool Fl_Terminal::EscapeSeq::parse_in_progress(void) const {
+ return (esc_mode_ == 0) ? false : true;
+}
+
+// See if we're in the middle of parsing an ESC sequence
+bool Fl_Terminal::EscapeSeq::is_csi(void) const { return csi_; }
+
+// Return with default value (if none) or vals[0] (if at least one val spec'd).
+// Handles default for single values (e.g. ESC[#H vs. ESC[H)
+// vals[0] is clamped between 0 and 'max'
+//
+int Fl_Terminal::EscapeSeq::defvalmax(int dval, int max) const {
+ if (total_vals() == 0) return dval;
+ else return clamp(vals_[0], 0, max);
+}
+
+// Save row,col for later retrieval
+void Fl_Terminal::EscapeSeq::save_cursor(int row, int col) {
+ save_row_ = row;
+ save_col_ = col;
+}
+
+// Return saved position in row,col
+void Fl_Terminal::EscapeSeq::restore_cursor(int &row, int &col) {
+ row = save_row_;
+ col = save_col_;
+}
+
+// Handle parsing an escape sequence.
+// Call this only if parse_in_progress() is true.
+// Passing ESC does a reset() and sets esc_mode() to ESC.
+// When a full escape sequence has been parsed, 'completed' is returned (see below).
+//
+// Returns:
+// fail - error occurred: escape sequence invalid, class is reset()
+// success - parsing ESC sequence OK so far, still in progress/not done yet
+// completed - complete ESC sequence was parsed, esc_mode() will be the operation, e.g.
+// 'm' - <ESC>[1m -- is_csi() will be true, val() has value(s) parsed
+// 'A' - <ESC>A -- is_csi() will be false (no vals)
+//
+int Fl_Terminal::EscapeSeq::parse(char c) {
+ // NOTE: During parsing esc_mode() will be:
+ // 0 - reset/not parsing
+ // 0x1b - ESC received, expecting next one of A/B/C/D or '['
+ // '[' - actively parsing CSI sequence, e.g. ESC[
+ //
+ // At the /end/ of parsing, after 'completed' is returned,
+ // esc_mode() will be the mode setting char, e.g. 'm' for 'ESC[0m', etc.
+ //
+ if (c == 0) { // NULL? (caller should really never send us this)
+ return success; // do nothing -- leave state unchanged, return 'success'
+ } else if (c == 0x1b) { // ESC at ANY time resets class/begins new ESC sequence
+ reset();
+ esc_mode(0x1b);
+ if (append_buff(c) < 0) goto pfail; // save ESC in buf
+ return success;
+ } else if (c < ' ' || c >= 0x7f) { // any other control or binary characters?
+ goto pfail; // reset + fail out of esc sequence parsing
+ }
+ // Whatever the character is, handle it depending on esc_mode..
+ if (esc_mode() == 0x1b) { // in ESC mode?
+ if (c == '[') { // <ESC>[? CSI (Ctrl Seq Introducer)
+ esc_mode(c); // switch to parsing mode for ESC[#;#;#..
+ csi_ = true; // this is now a CSI sequence
+ vali_ = 0; // zero vals_[] index
+ valbuffp_ = 0; // valbuffp NULL (no vals yet)
+ if (append_buff(c) < 0) goto pfail; // save '[' in buf
+ return success; // success
+ } else if ( (c >= '@' && c <= 'Z') || // C1 control code (e.g. <ESC>D, <ESC>c, etc)
+ (c >= 'a' && c <= 'z') ) {
+ esc_mode(c); // save op in esc_mode() for caller to see
+ csi_ = false; // NOT a CSI sequence
+ vali_ = 0;
+ valbuffp_ = 0; // valbuffp NULL (no vals yet)
+ if (append_buff(c) < 0) goto pfail; // save op in buf
+ return completed; // completed sequence
+ } else { // ESCx?
+ goto pfail; // not supported
+ }
+ } else if (esc_mode() == '[') { // '[' mode? e.g. ESC[... aka. is_csi()
+ if (c == ';') { // ';' indicates end of a value, e.g. ESC[0;2..
+ if (append_val() < 0) goto pfail; // append value parsed so far, vali gets inc'ed
+ if (append_buff(c) < 0) goto pfail; // save ';' in buf
+ return success;
+ }
+ if (isdigit(c)) { // parsing an integer?
+ if (!valbuffp_) // valbuffp not set yet?
+ { valbuffp_ = buffp_; } // point to first char in integer string
+ if (append_buff(c) < 0) goto pfail; // add value to buffer
+ return success;
+ }
+ // Not a ; or digit? fall thru to [A-Z,a-z] check
+ } else { // all other esc_mode() chars are fail/unknown
+ goto pfail;
+ }
+ if ( ( c >= '@' && c<= 'Z') || // ESC#X or ESC[...X, where X is [A-Z,a-z]?
+ ( c >= 'a' && c<= 'z') ) {
+ if (append_val() < 0 ) goto pfail; // append any trailing vals just before letter
+ if (append_buff(c) < 0 ) goto pfail; // save letter in buffer
+ esc_mode(c); // change mode to the mode setting char
+ return completed; // completed/done
+ }
+ // Any other chars? reset+fail
+pfail:
+ reset();
+ return fail;
+}
+
+//////////////////////////////////////
+///// CharStyle Class Methods ////////
+//////////////////////////////////////
+
+// Ctor
+Fl_Terminal::CharStyle::CharStyle(void) {
+ attrib_ = 0;
+ flags_ = 0;
+ defaultfgcolor_ = 0xd0d0d000; // off white
+ defaultbgcolor_ = 0xffffffff; // special color: doesn't draw, 'shows thru' to box()
+ fgcolor_ = defaultfgcolor_;
+ bgcolor_ = defaultbgcolor_;
+ flags_ |= (FG_XTERM | BG_XTERM);
+ fontface_ = FL_COURIER;
+ fontsize_ = 14;
+ update(); // updates fontheight_, fontdescent_, charwidth_
+}
+
+// Update fontheight/descent cache whenever font changes
+void Fl_Terminal::CharStyle::update(void) {
+ // cache these values
+ fl_font(fontface_, fontsize_);
+ fontheight_ = int(fl_height() + 0.5);
+ fontdescent_ = int(fl_descent() + 0.5);
+ charwidth_ = int(fl_width("X") + 0.5);
+}
+
+// Return fg color
+Fl_Color Fl_Terminal::CharStyle::fgcolor(void) const {
+ return fgcolor_;
+}
+
+// Return bg color
+Fl_Color Fl_Terminal::CharStyle::bgcolor(void) const {
+ return bgcolor_;
+}
+
+// Return only the color bit flags
+// Only the color bits of 'inflags' are modified with our color bits.
+//
+uchar Fl_Terminal::CharStyle::colorbits_only(uchar inflags) const {
+ return (inflags & ~COLORMASK) | (flags_ & COLORMASK); // add color bits only
+}
+
+void Fl_Terminal::CharStyle::fgcolor_uchar(uchar val) {
+ fgcolor_ = fltk_fg_color(val);
+ set_flag(FG_XTERM);
+}
+
+void Fl_Terminal::CharStyle::bgcolor_uchar(uchar val) {
+ bgcolor_ = fltk_bg_color(val);
+ set_flag(BG_XTERM);
+}
+
+///////////////////////////////////
+///// Cursor Class Methods ////////
+///////////////////////////////////
+
+// Is cursor at display row,col?
+bool Fl_Terminal::Cursor::is_rowcol(int drow,int dcol) const {
+ return(drow == row_ && dcol == col_);
+}
+
+// Scroll the cursor row up(+)/down(-) number of rows
+void Fl_Terminal::Cursor::scroll(int nrows) {
+ row_ = MAX(row_ - nrows, 0); // don't let (row_<0)
+}
+
+/////////////////////////////////////
+///// Utf8Char Class Methods ////////
+/////////////////////////////////////
+
+// Ctor
+Fl_Terminal::Utf8Char::Utf8Char(void) {
+ text_[0] = ' ';
+ len_ = 1;
+ attrib_ = 0;
+ flags_ = 0;
+ fgcolor_ = 0xffffff00;
+ bgcolor_ = 0xffffffff; // special color: doesn't draw, 'shows thru' to box()
+}
+
+// copy ctor
+Fl_Terminal::Utf8Char::Utf8Char(const Utf8Char& src) {
+ // local instance not initialized yet; init first, then copy text
+ text_[0] = ' ';
+ len_ = 1;
+ attrib_ = src.attrib_;
+ flags_ = src.flags_;
+ fgcolor_ = src.fgcolor_;
+ bgcolor_ = src.bgcolor_;
+ text_utf8_(src.text_utf8(), src.length()); // copy the src text
+}
+
+// assignment
+Fl_Terminal::Utf8Char& Fl_Terminal::Utf8Char::operator=(const Utf8Char& src) {
+ // local instance is already initialized, so just change its contents
+ text_utf8_(src.text_utf8(), src.length()); // local copy src text
+ attrib_ = src.attrib_;
+ flags_ = src.flags_;
+ fgcolor_ = src.fgcolor_;
+ bgcolor_ = src.bgcolor_;
+ return *this;
+}
+
+// dtor
+Fl_Terminal::Utf8Char::~Utf8Char(void) {
+ len_ = 0;
+}
+
+// Set 'text_' to valid UTF-8 string 'text'.
+//
+// text_ must not be NULL, and len must be in range: 1 <= len <= max_utf8().
+// NOTE: Caller must handle such checks, and use handle_unknown_char()
+// for invalid chars.
+//
+void Fl_Terminal::Utf8Char::text_utf8_(const char *text, int len) {
+ memcpy(text_, text, len);
+ len_ = len; // update new length
+}
+
+// Set UTF-8 string for this char.
+//
+// text_ must not be NULL, and len must be in range: 1 <= len <= max_utf8().
+// NOTE: Caller must handle such checks, and use handle_unknown_char()
+// for invalid chars.
+//
+void Fl_Terminal::Utf8Char::text_utf8(const char *text,
+ int len,
+ const CharStyle& style) {
+ text_utf8_(text, len); // updates text_, len_
+ fl_font(style.fontface(), style.fontsize()); // need font to calc UTF-8 width
+ attrib_ = style.attrib();
+ flags_ = style.colorbits_only(flags_);
+ fgcolor_ = style.fgcolor();
+ bgcolor_ = style.bgcolor();
+}
+
+// Set char to single printable ASCII character 'c'
+// 'c' must be "printable" ASCII in the range (0x20 <= c <= 0x7e).
+// Anything outside of that is silently ignored.
+//
+void Fl_Terminal::Utf8Char::text_ascii(char c, const CharStyle& style) {
+ // Signed char vals above 0x7f are /negative/, so <0x20 check covers those
+ if (c < 0x20 || c >= 0x7e) return; // ASCII non-printable?
+ text_utf8(&c, 1, style);
+}
+
+// Set fl_font() based on specified style for this char's attribute
+void Fl_Terminal::Utf8Char::fl_font_set(const CharStyle& style) const {
+ int face = style.fontface() |
+ ((attrib_ & Fl_Terminal::BOLD) ? FL_BOLD : 0) |
+ ((attrib_ & Fl_Terminal::ITALIC) ? FL_ITALIC : 0);
+ fl_font(face, style.fontsize());
+}
+
+// Return the foreground color as an fltk color
+Fl_Color Fl_Terminal::Utf8Char::fgcolor(void) const {
+ return fgcolor_;
+}
+
+// Return the background color as an fltk color
+Fl_Color Fl_Terminal::Utf8Char::bgcolor(void) const {
+ return bgcolor_;
+}
+
+// Return the width of this character in floating point pixels
+//
+// WARNING: Uses current font, so assumes fl_font(face,size)
+// has already been set to current font!
+//
+double Fl_Terminal::Utf8Char::pwidth(void) const {
+ return fl_width(text_, len_);
+}
+
+// Return the width of this character in integer pixels
+//
+// WARNING: Uses current font, so assumes fl_font(face,size)
+// has already been set to current font!
+//
+int Fl_Terminal::Utf8Char::pwidth_int(void) const {
+ return int(fl_width(text_, len_) + 0.5);
+}
+
+// Return color \p col, possibly influenced by BOLD or DIM attributes \p attr.
+// If a \p grp widget is specified (i.e. not NULL), don't let the color \p col be
+// influenced by the attribute bits /if/ \p col matches the \p grp widget's own color().
+//
+Fl_Color Fl_Terminal::Utf8Char::attr_color(Fl_Color col, const Fl_Widget *grp) const {
+ // Don't modify color if it's the special 'see thru' color 0x0 or widget's color()
+ if (grp && ((col == 0xffffffff) || (col == grp->color()))) return grp->color();
+ switch (attrib_ & (Fl_Terminal::BOLD|Fl_Terminal::DIM)) {
+ case 0: return col; // not bold or dim? no change
+ case Fl_Terminal::BOLD: return bold_color(col); // bold? use bold_color()
+ case Fl_Terminal::DIM : return dim_color(col); // dim? use dim_color()
+ default: return col; // bold + dim? cancel out
+ }
+}
+
+// Return the fg color of char \p u8c possibly influenced by BOLD or DIM.
+// If a \p grp widget is specified (i.e. not NULL), don't let the color \p col be
+// influenced by the attribute bits /if/ \p col matches the \p grp widget's own color().
+//
+Fl_Color Fl_Terminal::Utf8Char::attr_fg_color(const Fl_Widget *grp) const {
+ if (grp && (fgcolor_ == 0xffffffff)) // see thru color?
+ { return grp->color(); } // return grp's color()
+ return (flags_ & Fl_Terminal::FG_XTERM) // fg is an xterm color?
+ ? attr_color(fgcolor(), grp) // ..use attributes
+ : fgcolor(); // ..ignore attributes.
+}
+
+Fl_Color Fl_Terminal::Utf8Char::attr_bg_color(const Fl_Widget *grp) const {
+ if (grp && (bgcolor_ == 0xffffffff)) // see thru color?
+ { return grp->color(); } // return grp's color()
+ return (flags_ & Fl_Terminal::BG_XTERM) // bg is an xterm color?
+ ? attr_color(bgcolor(), grp) // ..use attributes
+ : bgcolor(); // ..ignore attributes.
+}
+
+
+////////////////////////////////////
+///// RingBuffer Class Methods /////
+////////////////////////////////////
+
+// Create a new copy of the buffer with different row/col sizes
+// Preserves old contents of display and history in use.
+//
+// The old buffer might have an offset and the hist/disp might wrap
+// around the end of the ring. The NEW buffer's offset will be zero,
+// so the hist/disp do NOT wrap around, making the move operation easier to debug.
+//
+// The copy preservation starts at the LAST ROW in display of both old (src) and new (dst)
+// buffers, and copies rows in reverse until hist_use_srow() reached, or if we hit top
+// of the new history (index=0), which ever comes first. So in the following where the
+// display is being enlarged, the copy preservation starts at "Line 5" (bottom of display)
+// and works upwards, ending at "Line 1" (top of history use):
+//
+// OLD (SRC) NEW (DST)
+// _____________ _____________ ___
+// | x x x x x x | | x x x x x x | ʌ 'x' indicates
+// | Line 1 | ─┐ | x x x x x x | | hist_rows unused history
+// | Line 2 | └─> | Line 1 | v buffer memory.
+// |-------------| |-------------| ---
+// | Line 3 | | Line 2 | ʌ
+// | Line 4 | | Line 3 | |
+// | Line 5 | ─┐ | Line 4 | | disp_rows
+// |_____________| └─> | Line 5 | |
+// |_____________| _v_
+//
+void Fl_Terminal::RingBuffer::new_copy(int drows, int dcols, int hrows, const CharStyle& style) {
+ // Create new buffer
+ int addhist = disp_rows() - drows; // adjust history use
+ int new_ring_rows = (drows+hrows);
+ int new_hist_use = clamp(hist_use_ + addhist, 0, hrows); // clamp incase new_hist_rows smaller than old
+ int new_nchars = (new_ring_rows * dcols);
+ Utf8Char *new_ring_chars = new Utf8Char[new_nchars]; // Create new ring buffer (all blanks)
+ // Preserve old contents in new buffer
+ int dst_cols = dcols;
+ int src_stop_row = hist_use_srow();
+ int tcols = MIN(ring_cols(), dcols);
+ int src_row = hist_use_srow() + hist_use_ + disp_rows_ - 1; // use row#s relative to hist_use_srow()
+ int dst_row = new_ring_rows - 1;
+ // Copy rows: working up from bottom of disp, stop at top of hist
+ while ((src_row >= src_stop_row) && (dst_row >= 0)) {
+ Utf8Char *src = u8c_ring_row(src_row);
+ Utf8Char *dst = new_ring_chars + (dst_row*dst_cols);
+ for (int col=0; col<tcols; col++ ) *dst++ = *src++;
+ --src_row;
+ --dst_row;
+ }
+ // Install new buffer: dump old, install new, adjust internals
+ if (ring_chars_) delete[] ring_chars_;
+ ring_chars_ = new_ring_chars;
+ ring_rows_ = new_ring_rows;
+ ring_cols_ = dcols;
+ nchars_ = new_nchars;
+ hist_rows_ = hrows;
+ hist_use_ = new_hist_use;
+ disp_rows_ = drows;
+ offset_ = 0; // for new buffer, we used a zero offset
+}
+
+// Clear the class, delete previous ring if any
+void Fl_Terminal::RingBuffer::clear(void) {
+ if (ring_chars_) delete[] ring_chars_; // dump our ring
+ ring_chars_ = 0;
+ ring_rows_ = 0;
+ ring_cols_ = 0;
+ nchars_ = 0;
+ hist_rows_ = 0;
+ hist_use_ = 0;
+ disp_rows_ = 0;
+ offset_ = 0;
+}
+
+// Clear history
+void Fl_Terminal::RingBuffer::clear_hist(void) {
+ hist_use_ = 0;
+}
+
+// Default ctor
+Fl_Terminal::RingBuffer::RingBuffer(void) {
+ ring_chars_ = 0;
+ clear();
+}
+
+// Ctor with specific sizes
+Fl_Terminal::RingBuffer::RingBuffer(int drows, int dcols, int hrows) {
+ // Start with cleared buffer first..
+ ring_chars_ = 0;
+ clear();
+ // ..then create.
+ create(drows, dcols, hrows);
+}
+
+// Dtor
+Fl_Terminal::RingBuffer::~RingBuffer(void) {
+ if (ring_chars_) delete[] ring_chars_;
+ ring_chars_ = NULL;
+}
+
+// See if 'grow' is within the history buffer
+// It's assumed grow is in the range 0 .. hist_rows()-1.
+//
+bool Fl_Terminal::RingBuffer::is_hist_ring_row(int grow) const {
+ grow %= ring_rows_;
+ grow -= offset_; // move into positive space
+ if (grow < 0) { grow = (ring_rows_ + grow); }
+ int htop = 0;
+ int hbot = (hist_rows_ - 1);
+ return ((grow >= htop) && (grow <= hbot));
+}
+
+// See if 'grow' is within the display buffer
+// It's assumed grow is in the range hist_rows() .. ring_rows()-1.
+//
+bool Fl_Terminal::RingBuffer::is_disp_ring_row(int grow) const {
+ grow %= ring_rows_;
+ grow -= offset_;
+ if (grow < 0) { grow = (ring_rows_ + grow); }
+ int dtop = hist_rows_;
+ int dbot = hist_rows_ + disp_rows_ - 1;
+ return ((grow >= dtop) && (grow <= dbot));
+}
+
+// Move display row from src_row to dst_row
+void Fl_Terminal::RingBuffer::move_disp_row(int src_row, int dst_row) {
+ Utf8Char *src = u8c_disp_row(src_row);
+ Utf8Char *dst = u8c_disp_row(dst_row);
+ for (int col=0; col<disp_cols(); col++) *dst++ = *src++;
+}
+
+// Clear the display row 'drow' using specified CharStyle 'style'
+void Fl_Terminal::RingBuffer::clear_disp_row(int drow, const CharStyle& style) {
+ int row = hist_rows_ + drow + offset_;
+ Utf8Char *u8c = u8c_ring_row(row);
+ for (int col=0; col<disp_cols(); col++) u8c++->clear(style);
+}
+
+// Scroll the ring buffer up or down #rows, using 'style' for empty rows
+// > Positive rows scroll "up", moves top line(s) into history, clears bot line(s)
+// Increases hist_use (unless maxed out).
+// > Negative rows scroll "down", clears top line(s), history unaffected
+//
+void Fl_Terminal::RingBuffer::scroll(int rows, const CharStyle& style) {
+ if (rows > 0) {
+ // Scroll up into history
+ // Example: scroll(2):
+ //
+ // BEFORE AFTER
+ // --------------- ---------------
+ // | H i s t | --- | x x x x x x x | \_ blanked rows
+ // | | \ | x x x x x x x | /
+ // | | ---> |---------------| <- disp_erow()
+ // | | | H i s t |
+ // | | | |
+ // disp_srow() -> |---------------| --- | 0001 |
+ // | 0001 | \ | 0002 |
+ // | 0002 | ---> |---------------| <- disp_srow()
+ // | 0003 | | 0003 |
+ // | 0004 | | 0004 |
+ // | 0005 | --- | 0005 |
+ // | 0006 | \ | 0006 |
+ // disp_erow() -> --------------- ---> ---------------
+ //
+ // \______/
+ // Simple
+ // Offset
+ rows = clamp(rows, 1, disp_rows()); // sanity
+ // Scroll up into history
+ offset_ = (offset_ + rows) % ring_rows_;
+ // Adjust hist_use, clamp to max
+ hist_use_ = clamp(hist_use_ + rows, 0, hist_rows_);
+ // Clear exposed lines at bottom
+ int srow = (disp_rows() - rows) % disp_rows();
+ int erow = disp_rows();
+ for (int row=srow; row<erow; row++) clear_disp_row(row, style);
+ } else {
+ // Scroll down w/out affecting history
+ // To leave history unaffect, we must move memory.
+ // Start at bottom row [A] and work up to top row [B].
+ //
+ // Example: scroll(-2):
+ //
+ // BEFORE AFTER
+ // ----------------- ----------------- _
+ // | 0001 [B] | ---┐ | x x x x x x x x | \_ blanked
+ // | 0002 | | | x x x x x x x x | _/ rows
+ // | 0003 | └---> | 0001 |
+ // | 0004 [A] | ---┐ | 0002 |
+ // | 0005 | | | 0003 |
+ // | 0006 | └---> | 0004 |
+ // ----------------- -----------------
+ // \______/
+ // Memory
+ // move
+ rows = clamp(-rows, 1, disp_rows()); // make rows positive + sane
+ for (int row=disp_rows()-1; row>=0; row--) {
+ int src_row = (row - rows);
+ int dst_row = row;
+ if (src_row >= 0) move_disp_row(row-rows, dst_row); // move rows
+ else clear_disp_row(dst_row, style); // hit top? blank the rest
+ }
+ }
+}
+
+// Return UTF-8 char for 'row' in the ring
+// Scrolling offset is NOT applied; this is raw access to the ring's rows.
+//
+// Example:
+// // Walk ALL rows in the ring buffer..
+// for (int row=0; row<ring.rows(); row++) {
+// Utf8Char *u8c = ring.u8c_ring_row(row);
+// ..
+// }
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_ring_row(int row) const {
+ row = normalize(row, ring_rows());
+ assert(row >= 0 && row < ring_rows_);
+ return &ring_chars_[row * ring_cols()];
+}
+
+// Return UTF-8 char for beginning of 'row' in the history buffer.
+// Example:
+// // Walk ALL rows in history..
+// for (int hrow=0; hrow<ring.hrows(); hrow++) {
+// Utf8Char *u8c = u8c_hist_row(hrow);
+// ..
+// }
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_hist_row(int hrow) const {
+ int rowi = normalize(hrow, hist_rows());
+ rowi = (rowi + offset_) % ring_rows_;
+ assert(rowi >= 0 && rowi <= ring_rows_);
+ return &ring_chars_[rowi * ring_cols()];
+}
+
+// Special case to walk the "in use" rows of the history
+// Example:
+// // Walk the "in use" rows of history..
+// for (int hrow=0; hrow<ring.hist_use(); hrow++) {
+// Utf8Char *u8c = u8c_hist_use_row(hrow);
+// ..
+// }
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_hist_use_row(int hurow) const {
+ if (hist_use_ == 0) return 0; // history is empty! (caller is dumb to ask)
+ hurow = hurow % hist_use_; // normalize indexing within history in use
+ hurow = hist_rows_ - hist_use_ + hurow; // index hist_use rows from end history
+ hurow = (hurow + offset_) % ring_rows_; // convert to absolute index in ring_chars_[]
+ assert(hurow >= 0 && hurow <= hist_use());
+ return &ring_chars_[hurow * ring_cols()];
+}
+
+// Return UTF-8 char for beginning of 'row' in the display buffer
+// Example:
+// // Walk ALL rows in display..
+// for (int drow=0; drow<ring.drows(); drow++) {
+// Utf8Char *u8c = u8c_disp_row(drow);
+// ..
+// }
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_disp_row(int drow) const {
+ int rowi = normalize(drow, disp_rows());
+ rowi = (hist_rows_ + rowi + offset_) % ring_rows_; // display starts at end of history
+ assert(rowi >= 0 && rowi <= ring_rows_);
+ return &ring_chars_[rowi * ring_cols()];
+}
+
+// non-const versions of the above
+Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_ring_row(int row)
+ { return const_cast<Utf8Char*>(const_cast<const RingBuffer*>(this)->u8c_ring_row(row)); }
+
+Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_hist_row(int hrow)
+ { return const_cast<Utf8Char*>(const_cast<const RingBuffer*>(this)->u8c_hist_row(hrow)); }
+
+Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_hist_use_row(int hurow)
+ { return const_cast<Utf8Char*>(const_cast<const RingBuffer*>(this)->u8c_hist_use_row(hurow)); }
+
+Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_disp_row(int drow)
+ { return const_cast<Utf8Char*>(const_cast<const RingBuffer*>(this)->u8c_disp_row(drow)); }
+
+// Resize ring buffer by creating new one, dumping old (if any).
+// Input:
+// drows -- display height in lines of text (rows)
+// dcols -- display width in characters (columns)
+// hrows -- scrollback history size in lines of text (rows)
+//
+void Fl_Terminal::RingBuffer::create(int drows, int dcols, int hrows) {
+ clear();
+ // History
+ hist_rows_ = hrows;
+ hist_use_ = 0;
+ // Display
+ disp_rows_ = drows;
+ // Ring buffer
+ ring_rows_ = hist_rows_ + disp_rows_;
+ ring_cols_ = dcols;
+ nchars_ = ring_rows_ * ring_cols_;
+ ring_chars_ = new Utf8Char[nchars_];
+}
+
+// Resize the buffer, preserve previous contents as much as possible
+void Fl_Terminal::RingBuffer::resize(int drows, int dcols, int hrows, const CharStyle& style) {
+ // If dcols or (drows+hrows) changed, make a NEW buffer and copy old contents.
+ // New copy will have xxxx_rows/cols and nchars adjusted.
+ //
+ if (dcols != disp_cols() || // cols changed size?
+ (drows+hrows) != (disp_rows()+hist_rows()) ) { // total #rows changed?
+ new_copy(drows, dcols, hrows, style);
+ } else {
+ // Cols and total rows the same, probably just changed disp/hist ratio
+ int addhist = disp_rows() - drows; // adj hist_use smaller if disp enlarged
+ hist_rows_ = hrows; // adj hist rows for new value
+ disp_rows_ = drows; // adj disp rows for new value
+ hist_use_ = clamp(hist_use_ + addhist, 0, hrows);
+ }
+}
+
+// Change the display rows. Use style for new rows, if any.
+void Fl_Terminal::RingBuffer::change_disp_rows(int drows, const CharStyle& style)
+ { resize(drows, ring_cols(), hist_rows(), style); }
+
+// Change the display columns. Use style for new columns, if any.
+void Fl_Terminal::RingBuffer::change_disp_cols(int dcols, const CharStyle& style)
+ { resize(disp_rows(), dcols, hist_rows(), style); }
+
+/////////////////////////////////////
+///// Fl_Terminal Class Methods /////
+/////////////////////////////////////
+
+// Return u8c for beginning of a row inside the ring.
+// 'grow' is 'globally' indexed (relative to the beginning of the ring buffer),
+// and so can access ANY character in the entire ring buffer (hist or disp)
+// by its global index, which is to say without any scrolling offset applied.
+// Should really ONLY be used for making a complete copy of the ring.
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::u8c_ring_row(int grow) const
+ { return ring_.u8c_ring_row(grow); }
+
+// Return u8c for beginning of a row inside the history.
+// 'hrow' is indexed relative to the beginning of the history buffer.
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::u8c_hist_row(int hrow) const
+ { return ring_.u8c_hist_row(hrow); }
+
+// Return u8c for beginning of a row inside the 'in use' history.
+// 'hurow' is indexed relative to the beginning of the 'in use' buffer.
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::u8c_hist_use_row(int hurow) const
+ { return ring_.u8c_hist_use_row(hurow); }
+
+// Return u8c for beginning of a row inside the display
+// 'drow' is indexed relative to the beginning of the display buffer.
+//
+const Fl_Terminal::Utf8Char* Fl_Terminal::u8c_disp_row(int drow) const
+ { return ring_.u8c_disp_row(drow); }
+
+// non-const versions of the above
+Fl_Terminal::Utf8Char* Fl_Terminal::u8c_ring_row(int grow)
+ { return const_cast<Utf8Char*>(const_cast<const Fl_Terminal*>(this)->u8c_ring_row(grow)); }
+
+Fl_Terminal::Utf8Char* Fl_Terminal::u8c_hist_row(int hrow)
+ { return const_cast<Utf8Char*>(const_cast<const Fl_Terminal*>(this)->u8c_hist_row(hrow)); }
+
+Fl_Terminal::Utf8Char* Fl_Terminal::u8c_hist_use_row(int hurow)
+ { return const_cast<Utf8Char*>(const_cast<const Fl_Terminal*>(this)->u8c_hist_use_row(hurow)); }
+
+Fl_Terminal::Utf8Char* Fl_Terminal::u8c_disp_row(int drow)
+ { return const_cast<Utf8Char*>(const_cast<const Fl_Terminal*>(this)->u8c_disp_row(drow)); }
+
+// Create ring buffer.
+// Input:
+// drows -- display height in lines of text (rows)
+// dcols -- display width in characters (columns)
+// hrows -- scrollback history size in lines of text (rows)
+//
+// NOTE: Caller should call update_screen() at some point
+// to fix the scrollbar and other things.
+//
+void Fl_Terminal::create_ring(int drows, int dcols, int hrows) {
+ // recreate tabstops if col width being changed
+ if (dcols != ring_.ring_cols()) init_tabstops(dcols);
+ // recreate ring (dumps old)
+ ring_.create(drows, dcols, hrows);
+ // ensure cursor starts at home position
+ cursor_.home();
+}
+
+// Return the Utf8Char* for character under cursor.
+Fl_Terminal::Utf8Char* Fl_Terminal::u8c_cursor(void) {
+ return u8c_disp_row(cursor_.row()) + cursor_.col();
+}
+
+// Return scrollbar width if visible, or 0 if not visible
+int Fl_Terminal::vscroll_width(void) const {
+ return(vscroll_->visible() ? vscroll_->w() : 0);
+}
+
+// Initialize tabstops for terminal
+// NOTE: 'newsize' should always be at least 'ring_cols()'..
+//
+void Fl_Terminal::init_tabstops(int newsize) {
+ if (newsize > tabstops_size_) { // enlarge?
+ char *oldstops = tabstops_; // save old stops
+ int oldsize = tabstops_size_; // save old size
+ tabstops_ = (char*)malloc(newsize); // alloc new
+ for (int t=0; t<newsize; t++) { // init new tabstops:
+ tabstops_[t] = (oldstops && t<oldsize)
+ ? oldstops[t] // copy old
+ : ((t % 8) == 0) ? 1 : 0; // new defaults
+ }
+ if (oldstops) free((void*)oldstops); // dump old stops
+ tabstops_size_ = newsize;
+ } else {
+ // Same size or smaller? Do nothing -- just keep old tabstops
+ }
+}
+
+// Reset all tabstops to default 8th char
+void Fl_Terminal::default_tabstops(void) {
+ for (int t=1; t<tabstops_size_; t++) // t=1: skip 0
+ tabstops_[t] = ((t % 8) == 0) ? 1 : 0; // every 8th char is a tabstop
+}
+
+// Clear all tabstops
+void Fl_Terminal::clear_all_tabstops(void) {
+ memset(tabstops_, 0, tabstops_size_);
+}
+
+// Set/clear tabstop at current cursor x position
+// val: 0 clears tabstop, 1 sets tabstop
+//
+void Fl_Terminal::set_tabstop(void) {
+ int index = clamp(cursor_col(), 0, tabstops_size_-1); // clamp cursor pos
+ tabstops_[index] = 1; // set/clr tabstop
+}
+
+// Clear tabstop at current cursor x position
+void Fl_Terminal::clear_tabstop(void) {
+ int index = clamp(cursor_col(), 0, tabstops_size_-1); // clamp cursor pos
+ tabstops_[index] = 0; // clear tabstop
+}
+
+// Update the scrollbar based on screen, history buffer, etc
+// Scrollbar should range from 0 (bottom) to hist_rows (top),
+// the scrollbar's value indicating how far back into history.
+//
+void Fl_Terminal::update_scrollbar(void) {
+ int value_before = vscroll_->value();
+ // Enforce minimum tabsize of 10 or width of scrollbar
+ // The minimum vert size of tab should be scrollbar's width,
+ // but not smaller than 10 pixels, so user can grab it easily.
+ {
+ float tabsize = disp_rows() / float(disp_rows() + history_use());
+ float minpix = float(MAX(10, vscroll_->w())); // scroll width preferred, 10pix minimum
+ float minfrac = minpix/vscroll_->h(); // scroll wants a fraction, so convert
+ tabsize = MAX(minfrac, tabsize); // use the best fractional size
+ vscroll_->slider_size(tabsize);
+ }
+ vscroll_->range(hist_use(), 0); // 'minimum' is larger than 'max'
+ if (value_before == 0) vscroll_->value(0); // was at bottom? stay at bottom
+ // Ensure scrollbar in proper position
+ update_screen_xywh(); // ensure scrn_ up to date first
+ int sx = scrn_.r() + margin_.right();
+ int sy = scrn_.y() - margin_.top();
+ int sw = scrollbar_actual_size();
+ int sh = scrn_.h() + margin_.top() + margin_.bottom();
+ if (vscroll_->x() != sx ||
+ vscroll_->y() != sy ||
+ vscroll_->w() != sw ||
+ vscroll_->h() != sh) {
+ vscroll_->resize(sx, sy, sw, sh);
+ init_sizes(); // tell Fl_Group child changed size..
+ update_screen_xywh(); // ensure scrn_ is aware of sw change
+ display_modified(); // redraw Fl_Terminal since scroller changed size
+ }
+ vscroll_->redraw(); // redraw scroll always
+}
+
+// Refit the display to match screen
+void Fl_Terminal::refit_disp_to_screen(void) {
+ int dh = h_to_row(scrn_.h());
+ int dw = MAX(w_to_col(scrn_.w()), disp_cols()); // enlarge cols only
+ int drows = clamp(dh, 2, dh); // 2 rows minimum
+ int dcols = clamp(dw, 10, dw); // 10 cols minimum
+ int drow_diff = drows - ring_.disp_rows(); // change in rows?
+ ring_.resize(drows, dcols, hist_rows(), current_style_);
+ cursor_.scroll(-drow_diff);
+ clear_mouse_selection();
+ update_screen(false);
+}
+
+// Resize the display's vertical size to (drows).
+// When enlarging / shrinking, KEEP BOTTOM OF DISPLAY THE SAME, e.g.
+//
+// Display Display
+// BEFORE: SMALLER:
+// ___________ ___________
+// | Hist -3 | ʌ | Hist -3 | ʌ
+// | Hist -2 | |- 3 | Hist -2 | |
+// | Hist -1 | v | Hist -1 | |-- new hist size
+// |-----------| ---┐ | Line 1 | |
+// | Line 1 | | | Line 2 | v
+// | Line 2 | └---> |-----------|
+// | Line 3 | | Line 3 | ʌ
+// | : | | : | |-- new disp size
+// | Line 25 | -------> | Line 25 | v
+// ----------- -----------
+//
+// Display Display
+// BEFORE: LARGER
+// ___________ ___________
+// | Hist -3 | ʌ | Hist -3 | --- new hist size
+// | Hist -2 | |- 3 ┌-->|-----------|
+// | Hist -1 | v | | Hist -2 | ʌ
+// |-----------| ------┘ | Hist -1 | |
+// | Line 1 | | Line 1 | |
+// | Line 2 | | Line 2 | |-- new disp size
+// | Line 3 | | Line 3 | |
+// | : | | : | |
+// | Line 25 | --------> | Line 25 | v
+// ----------- -----------
+//
+//
+void Fl_Terminal::resize_display_rows(int drows) {
+ int drow_diff = drows - ring_.disp_rows(); // Change in rows?
+ if (drow_diff == 0) return; // No changes? early exit
+ int new_dcols = ring_cols(); // keep cols the same
+ int new_hrows = hist_rows() - drow_diff; // keep disp:hist ratio same
+ if (new_hrows<0) new_hrows = 0; // don't let hist be <0
+ ring_.resize(drows, new_dcols, new_hrows, current_style_);
+ // ..update cursor/selections to track text position
+ cursor_.scroll(-drow_diff);
+ select_.clear(); // clear any mouse selection
+ // ..update scrollbar, since disp_height relative to hist_use changed
+ update_scrollbar();
+}
+
+// Resize the display's columns
+// This affects the history and entire ring buffer too.
+// Make an effort to preserve previous content.
+// Up to caller to enforce any 'minimum' size for drows.
+//
+void Fl_Terminal::resize_display_columns(int dcols) {
+ // No changes? early exit
+ if (dcols == disp_cols()) return;
+ // Change cols, preserves previous content if possible
+ ring_.resize(disp_rows(), dcols, hist_rows(), current_style_);
+ update_scrollbar();
+}
+
+// Update only the internal terminal screen xywh and x2/y2 values
+void Fl_Terminal::update_screen_xywh(void) {
+ const Margin &m = margin_;
+ scrn_ = *this; // start with widget's current xywh
+ scrn_.inset(box()); // apply box offset
+ scrn_.inset(m.left(), m.top(), m.right(), m.bottom()); // apply margins offset
+ scrn_.inset(0, 0, scrollbar_actual_size(), 0); // apply scrollbar width
+}
+
+// Update internals when something "global" changes
+// Call this when something important is changed:
+// Resizing screen or changing font/size affect internals globally.
+// Font change affects per-character caching of char widths.
+// Display resize affects scrn_ cache, scrollbars, etc.
+//
+void Fl_Terminal::update_screen(bool font_changed) {
+ // current_style: update cursor's size for current font/size
+ if (font_changed) {
+ // Change font and current_style's font height
+ fl_font(current_style_.fontface(), current_style_.fontsize());
+ cursor_.h(current_style_.fontheight());
+ }
+ // Update the scrn_* values
+ update_screen_xywh();
+ // Recalc the scrollbar size/position/etc
+ update_scrollbar();
+}
+
+/**
+ Return terminal's scrollback history buffer size in lines of text (rows).
+*/
+int Fl_Terminal::history_rows(void) const {
+ return hist_rows();
+}
+
+/**
+ Set terminal's scrollback history buffer size in lines of text (rows).
+*/
+void Fl_Terminal::history_rows(int hrows) {
+ if (hrows == history_rows()) return; // no change? done
+ ring_.resize(disp_rows(), disp_cols(), hrows, current_style_);
+ update_screen(false); // false: no font change
+ display_modified();
+}
+
+/**
+ Returns how many lines are "in use" by the screen history buffer.
+
+ This value will be 0 if history was recently cleared with e.g.
+ clear_history() or \c "<ESC>c".
+
+ Return value will be in the range 0 .. (history_lines()-1).
+*/
+int Fl_Terminal::history_use(void) const {
+ return ring_.hist_use();
+}
+
+/**
+ Return terminal's display height in lines of text (rows).
+
+ This value is normally managed automatically by resize()
+ based on the current font size.
+*/
+int Fl_Terminal::display_rows(void) const {
+ return ring_.disp_rows();
+}
+
+/**
+ Set terminal's display height in lines of text (rows).
+
+ This value is normally managed automatically by resize()
+ based on the current font size, and should not be changed.
+
+ To change the display height, use resize() instead.
+*/
+void Fl_Terminal::display_rows(int drows) {
+ if (drows == disp_rows()) return; // no change? early exit
+ ring_.resize(drows, disp_cols(), hist_rows(), current_style_);
+ update_screen(false); // false: no font change ?NEED?
+}
+
+/**
+ Return terminal's display width in columns of text characters.
+
+ This value is normally managed automatically by resize()
+ based on the current font size.
+*/
+int Fl_Terminal::display_columns(void) const {
+ return ring_.disp_cols();
+}
+
+/**
+ Set terminal's display width in columns of text characters.
+
+ This value is normally managed automatically by resize()
+ based on the current font size, and should not be changed.
+
+ You CAN make the display_columns() larger than the width of
+ the widget; text in the terminal will simply run off the
+ screen edge and be clipped; the only way to reveal that
+ text is if the user enlarges the widget, or the font size
+ made smaller.
+
+ To change the display width, it is best to use resize() instead.
+*/
+void Fl_Terminal::display_columns(int dcols) {
+ if (dcols == disp_cols()) return; // no change? early exit
+ // Change cols, preserves previous content if possible
+ ring_.resize(disp_rows(), dcols, hist_rows(), current_style_);
+ update_screen(false); // false: no font change ?NEED?
+}
+
+/** Return current style for rendering text. */
+const Fl_Terminal::CharStyle& Fl_Terminal::current_style(void) const {
+ return current_style_;
+}
+
+/** Set current style for rendering text. */
+void Fl_Terminal::current_style(const CharStyle& sty) {
+ current_style_ = sty;
+}
+
+/**
+ Set the left margin; see \ref Fl_Terminal_Margins.
+*/
+void Fl_Terminal::margin_left(int val) {
+ val = clamp(val,0,w()-1);
+ margin_.left(val);
+ update_screen(true);
+ refit_disp_to_screen();
+}
+
+/**
+ Set the right margin; see \ref Fl_Terminal_Margins.
+*/
+void Fl_Terminal::margin_right(int val) {
+ val = clamp(val,0,w()-1);
+ margin_.right(val);
+ update_screen(true);
+ refit_disp_to_screen();
+}
+
+/**
+ Set the top margin; see \ref Fl_Terminal_Margins.
+*/
+void Fl_Terminal::margin_top(int val) {
+ val = clamp(val,0,h()-1);
+ margin_.top(val);
+ update_screen(true);
+ refit_disp_to_screen();
+}
+
+/**
+ Set the bottom margin; see \ref Fl_Terminal_Margins.
+*/
+void Fl_Terminal::margin_bottom(int val) {
+ val = clamp(val,0,h()-1);
+ margin_.bottom(val);
+ update_screen(true);
+ refit_disp_to_screen();
+}
+
+/**
+ Sets the font used for all text displayed in the terminal.
+
+ This affects all existing text (in display and history) as well
+ as any newly printed text.
+
+ Only monospace fonts are recommended, such as FL_COURIER or FL_SCREEN.
+ Custom fonts configured with Fl::set_font() will also work, as long
+ as they are monospace.
+*/
+void Fl_Terminal::textfont(Fl_Font val) {
+ current_style_.fontface(val);
+ update_screen(true);
+ display_modified();
+}
+
+/**
+ Sets the font size used for all text displayed in the terminal.
+
+ This affects all existing text (in display and history) as well
+ as any newly printed text.
+
+ Changing this will affect the display_rows() and display_columns().
+*/
+void Fl_Terminal::textsize(Fl_Fontsize val) {
+ current_style_.fontsize(val);
+ update_screen(true);
+ // Changing font size affects #lines in display, so resize it
+ refit_disp_to_screen();
+ display_modified();
+}
+
+/**
+ Sets the foreground text color as one of the 8 'xterm color' values.
+
+ This will be the foreground color used for all newly printed text,
+ similar to the \c \<ESC\>[\#m escape sequence, where \# is between 30 and 37.
+
+ This color will be reset to the default fg color if reset_terminal()
+ is called, or by \c \<ESC\>c, \c \<ESC\>[0m, etc.
+
+ The xterm color intensity values can be influenced by the Dim/Bold/Normal
+ modes (which can be set with e.g. \c \<ESC\>[1m, textattrib(), etc), so the
+ actual RGB values of these colors allow room for Dim/Bold to influence their
+ brightness. For instance, "Normal Red" is not full brightness to allow
+ "Bold Red" to be brighter. This goes for all colors except 'Black', which
+ is not influenced by Dim or Bold; Black is always Black.
+
+ The 8 color xterm values are:
+ - 0 = Black
+ - 1 = Red
+ - 2 = Green
+ - 3 = Yellow
+ - 4 = Blue
+ - 5 = Magenta
+ - 6 = Cyan
+ - 7 = White
+
+ \see textfgcolor_default(Fl_Color)
+*/
+void Fl_Terminal::textfgcolor_xterm(uchar val) {
+ current_style_.fgcolor(fltk_fg_color(val));
+}
+
+/**
+ Sets the background text color as one of the 8 'xterm color' values.
+
+ This will be the foreground color used for all newly printed text,
+ similar to the \c \<ESC\>[\#m escape sequence, where \# is between 40 and 47.
+
+ This color will be reset to the default bg color if reset_terminal()
+ is called, or by \c \<ESC\>c, \c \<ESC\>[0m, etc.
+
+ The xterm color intensity values can be influenced by the Dim/Bold/Normal
+ modes (which can be set with e.g. \c \<ESC\>[1m, textattrib(), etc), so the
+ actual RGB values of these colors allow room for Dim/Bold to influence their
+ brightness. For instance, "Normal Red" is not full brightness to allow
+ "Bold Red" to be brighter. This goes for all colors except 'Black', which
+ is not influenced by Dim or Bold; Black is always Black.
+
+ The 8 color xterm values are:
+ - 0 = Black
+ - 1 = Red
+ - 2 = Green
+ - 3 = Yellow
+ - 4 = Blue
+ - 5 = Magenta
+ - 6 = Cyan
+ - 7 = White
+
+ \see textbgcolor_default(Fl_Color)
+*/
+void Fl_Terminal::textbgcolor_xterm(uchar val) {
+ current_style_.bgcolor(fltk_bg_color(val));
+}
+
+/**
+ Set text foreground drawing color to fltk color \p val.
+ Use this for temporary color changes, similar to \<ESC\>[38;2;\<R\>;\<G\>;\<B\>m
+
+ This setting does _not_ affect the 'default' text colors used by \<ESC\>[0m,
+ \<ESC\>c, reset_terminal(), etc. To change both the current _and_
+ default fg color, also use textfgcolor_default(Fl_Color). Example:
+ \par
+ \code
+ // Set both 'current' and 'default' colors
+ Fl_Color amber = 0xd0704000;
+ tty->textfgcolor(amber); // set 'current' fg color
+ tty->textfgcolor_default(amber); // set 'default' fg color used by ESC[0m reset
+ \endcode
+ \see textfgcolor_default(Fl_Color)
+*/
+void Fl_Terminal::textfgcolor(Fl_Color val) {
+ current_style_.fgcolor(val);
+}
+
+/**
+ Set text background color to fltk color \p val.
+ Use this for temporary color changes, similar to \<ESC\>[48;2;\<R\>;\<G\>;\<B\>m
+
+ This setting does _not_ affect the 'default' text colors used by \<ESC\>[0m,
+ \<ESC\>c, reset_terminal(), etc. To change both the current _and_
+ default bg color, also use textbgcolor_default(Fl_Color). Example:
+ \par
+ \code
+ // Set both 'current' and 'default' colors
+ Fl_Color darkamber = 0x20100000;
+ tty->textbgcolor(darkamber); // set 'current' bg color
+ tty->textbgcolor_default(darkamber); // set 'default' bg color used by ESC[0m reset
+ \endcode
+
+ \see textbgcolor_default(Fl_Color)
+*/
+void Fl_Terminal::textbgcolor(Fl_Color val) {
+ current_style_.bgcolor(val);
+}
+
+/**
+ Set the default text foreground color used by \c \<ESC\>c, \c \<ESC\>[0m,
+ and reset_terminal().
+
+ Does not affect the 'current' text fg color; use textfgcolor(Fl_Color) to
+ set that.
+
+ \see textfgcolor(Fl_Color)
+*/
+void Fl_Terminal::textfgcolor_default(Fl_Color val) {
+ current_style_.defaultfgcolor(val);
+}
+
+/**
+ Set the default text background color used by \c \<ESC\>c, \c \<ESC\>[0m,
+ and reset_terminal().
+
+ Does not affect the 'current' text fg color; use textbgcolor(Fl_Color) to
+ set that.
+
+ \see textbgcolor(Fl_Color)
+*/
+void Fl_Terminal::textbgcolor_default(Fl_Color val) {
+ current_style_.defaultbgcolor(val);
+}
+
+/**
+ Set text attribute bits (underline, inverse, etc).
+ This will be the default attribute used for all newly printed text.
+
+ \see Fl_Terminal::Attrib
+*/
+void Fl_Terminal::textattrib(uchar val) {
+ current_style_.attrib(val);
+}
+
+// Convert fltk window X coord to column 'gcol' on specified global 'grow'
+// Returns 1 if 'gcol' was found, or 0 if X not within any char in 'grow'
+//
+int Fl_Terminal::x_to_glob_col(int X, int grow, int &gcol) const {
+ int cx = x() + margin_.left(); // char x position
+ const Utf8Char *u8c = utf8_char_at_glob(grow, 0);
+ for (gcol=0; gcol<ring_cols(); gcol++,u8c++) { // walk the cols looking for X
+ u8c->fl_font_set(current_style_); // pwidth_int() needs fl_font set
+ int cx2 = cx + u8c->pwidth_int(); // char x2 (right edge of char)
+ if (X >= cx && X < cx2) return 1; // found? return with gcol set
+ cx += u8c->pwidth_int(); // move cx to start x of next char
+ }
+ gcol = ring_cols()-1; // don't leave larger than #cols
+ return 0; // not found
+}
+
+// Convert fltk window X,Y coords to row + column indexing into ring_chars[]
+// Returns:
+// 1 -- found row,col
+// 0 -- not found, outside display's character area
+// -1/-2/-3/-4 -- not found, off top/bot/lt/rt edge respectively
+//
+int Fl_Terminal::xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol) const {
+ // X,Y outside terminal area? early exit
+ if (Y<scrn_.y()) return -1; // up (off top edge)
+ if (Y>scrn_.b()) return -2; // dn (off bot edge)
+ if (X<scrn_.x()) return -3; // lt (off left edge)
+ if (X>scrn_.r()) return -4; // rt (off right edge)
+ // Find toprow of what's currently drawn on screen
+ int toprow = disp_srow() - vscroll_->value();
+ // Find row the 'Y' value is in
+ grow = toprow + ( (Y-scrn_.y()) / current_style_.fontheight());
+ return x_to_glob_col(X, grow, gcol);
+}
+
+/**
+ Clear the terminal screen only; does not affect the cursor position.
+
+ Also clears the current mouse selection.
+
+ If \p 'scroll_to_hist' is true, the screen is cleared by scrolling the
+ contents into the scrollback history, where it can be retrieved with the
+ scrollbar. This is the default behavior. If false, the screen is cleared
+ and the scrollback history is unchanged.
+
+ Similar to the escape sequence \c "<ESC>[2J".
+
+ \see clear_screen_home()
+*/
+void Fl_Terminal::clear_screen(bool scroll_to_hist) {
+ if (scroll_to_hist) { scroll(disp_rows()); return; }
+ for (int drow=0; drow<disp_rows(); drow++)
+ for (int dcol=0; dcol<disp_cols(); dcol++)
+ clear_char_at_disp(drow, dcol);
+ clear_mouse_selection();
+}
+
+/**
+ Clear the terminal screen and home the cursor.
+
+ Also clears the current mouse selection.
+
+ If \p 'scroll_to_hist' is true, the screen is cleared by scrolling the
+ contents into the scrollback history, where it can be retrieved with the
+ scrollbar. This is the default behavior. If false, the screen is cleared
+ and the scrollback history is unchanged.
+
+ Similar to the escape sequence \c "<ESC>[2J<ESC>[H".
+
+ \see clear_screen()
+*/
+void Fl_Terminal::clear_screen_home(bool scroll_to_hist) {
+ cursor_home();
+ clear_screen(scroll_to_hist);
+}
+
+/// Clear from cursor to Start Of Display (EOD), like \c "<ESC>[1J".
+void Fl_Terminal::clear_sod(void) {
+ for (int drow=0; drow <= cursor_.row(); drow++)
+ if (drow == cursor_.row())
+ for (int dcol=0; dcol<=cursor_.col(); dcol++)
+ putchar(' ', drow, dcol);
+ else
+ for (int dcol=0; dcol<disp_cols(); dcol++)
+ putchar(' ', drow, dcol);
+ //TODO: Clear mouse selection?
+}
+
+/// Clear from cursor to End Of Display (EOD), like \c "<ESC>[J<ESC>[0J".
+void Fl_Terminal::clear_eod(void) {
+ for (int drow=cursor_.row(); drow<disp_rows(); drow++)
+ if (drow == cursor_.row())
+ for (int dcol=cursor_.col(); dcol<disp_cols(); dcol++)
+ putchar(' ', drow, dcol);
+ else
+ for (int dcol=0; dcol<disp_cols(); dcol++)
+ putchar(' ', drow, dcol);
+ //TODO: Clear mouse selection?
+}
+
+/// Clear from cursor to End Of Line (EOL), like \c "<ESC>[K".
+void Fl_Terminal::clear_eol(void) {
+ Utf8Char *u8c = u8c_disp_row(cursor_.row()) + cursor_.col(); // start at cursor
+ for (int col=cursor_.col(); col<disp_cols(); col++) // run from cursor to eol
+ (u8c++)->clear(current_style_);
+ //TODO: Clear mouse selection?
+}
+
+/// Clear from cursor to Start Of Line (SOL), like \c "<ESC>[1K".
+void Fl_Terminal::clear_sol(void) {
+ Utf8Char *u8c = u8c_disp_row(cursor_.row()); // start at sol
+ for (int col=0; col<=cursor_.col(); col++) // run from sol to cursor
+ (u8c++)->clear(current_style_);
+ //TODO: Clear mouse selection?
+}
+
+/// Clear entire line for specified row.
+void Fl_Terminal::clear_line(int drow) {
+ Utf8Char *u8c = u8c_disp_row(drow); // start at sol
+ for (int col=0; col<disp_cols(); col++) // run to eol
+ (u8c++)->clear(current_style_);
+ //TODO: Clear mouse selection?
+}
+
+/// Clear entire line cursor is currently on.
+void Fl_Terminal::clear_line(void) {
+ clear_line(cursor_.row());
+}
+
+/// Returns true if there's a mouse selection.
+bool Fl_Terminal::is_selection(void) const {
+ return select_.is_selection();
+}
+
+/**
+ Walk the mouse selection one character at a time from beginning to end,
+ returning a Utf8Char* to the next character in the selection, or NULL
+ if the end was reached, or if there's no selection.
+
+ This is easier to use for walking the selection than get_selection().
+
+ \p u8c should start out as NULL, rewinding to the beginning of the selection.
+ If the returned Utf8Char* is not NULL, \p row and \p col return the
+ character's row/column position in the ring buffer.
+ \par
+ \code
+ // EXAMPLE: Walk the entire mouse selection, if any
+ int row,col; // the returned row/col for each char
+ Utf8Char *u8c = NULL; // start with NULL to begin walk
+ while ((u8c = walk_selection(u8c, row, col))) { // loop until end char reached
+ ..do something with *u8c..
+ }
+ \endcode
+
+ \see get_selection(), is_selection()
+*/
+const Fl_Terminal::Utf8Char* Fl_Terminal::walk_selection(
+ const Utf8Char *u8c, ///< NULL on first iter
+ int &row, ///< returned row#
+ int &col ///< returned col#
+ ) const {
+ if (u8c==NULL) {
+ if (!is_selection()) return NULL;
+ row = select_.srow();
+ col = select_.scol();
+ u8c = u8c_ring_row(row);
+ } else {
+ // At end? done
+ if (row == select_.erow() && col == select_.ecol()) return NULL;
+ if (++col >= ring_cols()) // advance to next char
+ { col = 0; ++row; } // wrapped to next row?
+ }
+ return u8c_ring_row(row) + col;
+}
+
+/**
+ Return mouse selection's start/end position in the ring buffer, if any.
+
+ Ensures (start < end) to allow walking 'forward' thru selection,
+ left-to-right, top-to-bottom. The row/col values are indexes into
+ the entire ring buffer.
+
+ Example: walk the characters of the mouse selection:
+
+ \par
+ \code
+ // Get selection
+ int srow,scol,erow,ecol;
+ if (get_selection(srow,scol,erow,ecol)) { // mouse selection exists?
+ // Walk entire selection from start to end
+ for (int row=srow; row<=erow; row++) { // walk rows of selection
+ const Utf8Char *u8c = u8c_ring_row(row); // ptr to first character in row
+ int col_start = (row==srow) ? scol : 0; // start row? start at scol
+ int col_end = (row==erow) ? ecol : ring_cols(); // end row? end at ecol
+ u8c += col_start; // include col offset (if any)
+ for (int col=col_start; col<=col_end; col++,u8c++) { // walk columns
+ ..do something with each char at *u8c..
+ }
+ }
+ }
+ \endcode
+
+ Returns:
+ - true -- valid selection values returned
+ - false -- no selection was made, returned values undefined
+
+ \see walk_selection(), is_selection()
+*/
+bool Fl_Terminal::get_selection(int &srow, ///< starting row for selection
+ int &scol, ///< starting column for selection
+ int &erow, ///< ending row for selection
+ int &ecol ///< ending column for selection
+ ) const {
+ return select_.get_selection(srow, scol, erow, ecol);
+}
+
+// Is global row,col (relative to ring_chars[]) inside the current mouse selection?
+// Retruns:
+// true -- (row,col) inside a valid selection.
+// false -- (row,col) outside, or no valid selection.
+//
+bool Fl_Terminal::is_inside_selection(int grow, int gcol) const {
+ if (!is_selection()) return false;
+ int ncols = ring_cols();
+ // Calculate row/col magnitudes to simplify test
+ int check = (grow * ncols) + gcol;
+ int start = (select_.srow() * ncols) + select_.scol();
+ int end = (select_.erow() * ncols) + select_.ecol();
+ if (start > end) swap(start, end); // ensure (start < end)
+ return (check >= start && check <= end);
+}
+
+// See if global row (grow) is inside the 'display' area
+//
+// No wrap case: Wrap case:
+// ______________ ______________
+// ring_srow() -> | | ring_srow() -> | [C] |
+// | | : | |
+// | | : | Display | ...
+// | History | : | | :
+// | | disp_erow() -> | [C] | :
+// | | |--------------| :
+// |______________| | | : Display
+// disp_srow() -> | [A] | | History | : straddles
+// : | | | | : end of ring
+// : | Display | |--------------| :
+// : | | disp_srow() -> | [D] | ..:
+// : | | : | Display |
+// : | | : | |
+// disp_erow ┬─> | [B] | ring_erow() -> | [D] |
+// ring_erow ┘ -------------- --------------
+//
+bool Fl_Terminal::is_disp_ring_row(int grow) const {
+ return ring_.is_disp_ring_row(grow);
+}
+
+// Return byte length of all UTF-8 chars in selection, or 0 if no selection.
+// NOTE: Length includes trailing white on each line.
+//
+int Fl_Terminal::selection_text_len(void) const {
+ int row,col,len=0;
+ const Utf8Char *u8c = NULL; // start with NULL to begin walk
+ while ((u8c = walk_selection(u8c, row, col))) // loop until end char reached
+ len += u8c->length();
+ return len;
+}
+
+// Return text selection (for copy()/paste() operations)
+// Returns allocated NULL terminated string for entire selection.
+// Caller must free() this memory when done.
+// Unicode safe.
+//
+const char* Fl_Terminal::selection_text(void) const {
+ if (!is_selection()) return fl_strdup(""); // no selection? empty string
+ // Allocate buff large enough for all UTF-8 chars
+ int clen = 0; // char length
+ int buflen = selection_text_len();
+ char *buf = (char*)malloc(buflen+1); // +1 for NULL
+ char *bufp = buf;
+ char *nspc = bufp; // last 'non-space' char
+ // Loop from srow,scol .. erow,ecol
+ int row,col;
+ const Utf8Char *u8c = NULL; // start with NULL to begin walk
+ while ((u8c = walk_selection(u8c, row, col))) { // loop until end char reached
+ clen = u8c->length(); // get char length
+ memcpy(bufp, u8c->text_utf8(), clen); // append UTF-8 string to buffer
+ // Handle ignoring trailing whitespace
+ if (!u8c->is_char(' ')) nspc = bufp + clen; // save end pos of last non-spc
+ bufp += clen; // advance into buffer
+ if (col >= (ring_cols()-1)) { // eol? handle trailing white
+ if (nspc && nspc != bufp) { // trailing white space?
+ bufp = nspc; // rewind bufp, and..
+ *bufp++ = '\n'; // ..append crlf
+ nspc = bufp; // becomes new nspc for nxt row
+ }
+ }
+ }
+ *bufp = 0;
+ return buf;
+}
+
+// Clear mouse selection
+void Fl_Terminal::clear_mouse_selection(void) {
+ select_.clear();
+}
+
+// Extend selection to FLTK coords X,Y.
+// Returns true if extended, false if nothing done (X,Y offscreen)
+//
+bool Fl_Terminal::selection_extend(int X,int Y) {
+ if (is_selection()) { // selection already?
+ int grow,gcol;
+ if (xy_to_glob_rowcol(X, Y, grow, gcol) > 0) {
+ select_.extend(grow, gcol); // extend it
+ return true;
+ } else {
+ // TODO: If X,Y outside row/col area and SHIFT down,
+ // extend selection to nearest edge.
+ }
+ }
+ return false;
+}
+
+// Scroll the display up(+) or down(-) number of rows.
+// Negative row value scrolls "down", clearing top line, and history unaffected.
+// Postive row value scrolls "up", clearing bottom line, rotating top line into history.
+//
+void Fl_Terminal::scroll(int rows) {
+ // Scroll the ring
+ ring_.scroll(rows, current_style_);
+ if (rows > 0) update_scrollbar(); // scroll up? changes hist, so scrollbar affected
+ else clear_mouse_selection(); // scroll dn? clear mouse select; it might wrap ring
+}
+
+// Insert (count) rows at cursor position.
+// Causes rows below to scroll down, and empty lines created.
+// Scrolling does not involve history at all.
+//
+void Fl_Terminal::insert_rows(int count) {
+ int dst_drow = disp_rows()-1; // dst is bottom of display
+ int src_drow = clamp((dst_drow-count), 1, (disp_rows()-1)); // src is count lines up from dst
+ while ( src_drow >= cursor_.row() ) { // walk srcrow upwards to cursor row
+ Utf8Char *src = u8c_disp_row(src_drow--);
+ Utf8Char *dst = u8c_disp_row(dst_drow--);
+ for (int dcol=0; dcol<disp_cols(); dcol++) *dst++ = *src++; // move
+ }
+ // Blank remaining rows upwards to and including cursor line
+ while ( dst_drow >= cursor_.row() ) { // walk srcrow to curs line
+ Utf8Char *dst = u8c_disp_row(dst_drow--);
+ for (int dcol=0; dcol<disp_cols(); dcol++)
+ dst++->clear(current_style_);
+ }
+ clear_mouse_selection();
+}
+
+// Delete (count) rows at cursor position.
+// Causes rows to scroll up, and empty lines created at bottom of screen.
+// Scrolling does not involve history at all.
+//
+void Fl_Terminal::delete_rows(int count) {
+ int dst_drow = cursor_.row(); // dst is cursor row
+ int src_drow = clamp((dst_drow+count), 1, (disp_rows()-1)); // src is count rows below cursor
+ while ( src_drow < disp_rows() ) { // walk srcrow to EOD
+ Utf8Char *src = u8c_disp_row(src_drow++);
+ Utf8Char *dst = u8c_disp_row(dst_drow++);
+ for (int dcol=0; dcol<disp_cols(); dcol++)
+ *dst++ = *src++; // move
+ }
+ // Blank remaining rows downwards to End Of Display
+ while ( dst_drow < disp_rows() ) { // walk srcrow to EOD
+ Utf8Char *dst = u8c_disp_row(dst_drow++);
+ for (int dcol=0; dcol<disp_cols(); dcol++)
+ dst++->clear(current_style_);
+ }
+ clear_mouse_selection();
+}
+
+// Repeat printing char 'c' for 'rep' times, not to exceed end of line.
+void Fl_Terminal::repeat_char(char c, int rep) {
+ rep = clamp(rep, 1, disp_cols());
+ while ( rep-- > 0 && cursor_.col() < disp_cols() ) print_char(c);
+}
+
+// Insert char 'c' for 'rep' times at display (drow,dcol).
+void Fl_Terminal::insert_char_eol(char c, int drow, int dcol, int rep) {
+ // Walk the row from the eol backwards to the col position
+ // In this example, rep=3:
+ //
+ // dcol
+ // v
+ // BEFORE: |a|b|c|d|e|f|g|h|i|j| <- eol (disp_cols()-1)
+ // | | | | |_____
+ // src end -> |_____ | <-- src start
+ // | | | | |
+ // v v v v v
+ // AFTER: |a|b|‸|‸|‸|c|d|e|f|g|
+ // |_|_| <-- spaces added last
+ //
+ rep = clamp(rep, 0, disp_cols()); // sanity
+ if (rep == 0) return;
+ const CharStyle &style = current_style_;
+ Utf8Char *src = u8c_disp_row(drow)+disp_cols()-1-rep; // start src at 'g'
+ Utf8Char *dst = u8c_disp_row(drow)+disp_cols()-1; // start dst at 'j'
+ for (int col=(disp_cols()-1); col>=dcol; col--) { // loop col in reverse: eol -> dcol
+ if (col >= (dcol+rep)) *dst-- = *src--; // let assignment do move
+ else (dst--)->clear(style); // clear chars displaced
+ }
+}
+
+// Insert char 'c' for 'rep' times.
+// Does not wrap; characters at end of line are lost.
+//
+void Fl_Terminal::insert_char(char c, int rep) {
+ insert_char_eol(c, cursor_.row(), cursor_.col(), rep);
+}
+
+// Delete char(s) at (drow,dcol) for 'rep' times.
+void Fl_Terminal::delete_chars(int drow, int dcol, int rep) {
+ rep = clamp(rep, 0, disp_cols()); // sanity
+ if (rep == 0) return;
+ const CharStyle &style = current_style_;
+ Utf8Char *u8c = u8c_disp_row(drow);
+ for (int col=dcol; col<disp_cols(); col++) // delete left-to-right
+ if (col+rep >= disp_cols()) u8c[col].text_ascii(' ', style); // blanks
+ else u8c[col] = u8c[col+rep]; // move
+}
+
+// Delete char(s) at cursor position for 'rep' times.
+void Fl_Terminal::delete_chars(int rep) {
+ delete_chars(cursor_.row(), cursor_.col(), rep);
+}
+
+/**
+ Clears the scroll history buffer and adjusts scrollbar,
+ forcing it to redraw().
+*/
+void Fl_Terminal::clear_history(void) {
+ // Adjust history use
+ ring_.clear_hist();
+ vscroll_->value(0); // zero scroll position
+ // Clear entire history buffer
+ for (int hrow=0; hrow<hist_rows(); hrow++) {
+ Utf8Char *u8c = u8c_hist_row(hrow); // walk history rows..
+ for (int hcol=0; hcol<hist_cols(); hcol++) { // ..and history cols
+ (u8c++)->clear(current_style_);
+ }
+ }
+ // Adjust scrollbar (hist_use changed)
+ update_scrollbar();
+}
+
+/**
+ Resets terminal to default colors, clears screen, history and
+ mouse selection, homes cursor, resets tabstops. Same as \c "<ESC>c"
+*/
+void Fl_Terminal::reset_terminal(void) {
+ current_style_.sgr_reset(); // reset current style
+ clear_screen_home(); // clear screen, home cursor
+ clear_history();
+ clear_mouse_selection();
+ default_tabstops(); // reset tabstops to default 8 char
+}
+
+//DEBUG void Fl_Terminal::RingBuffer::show_ring_info(void) const {
+//DEBUG ::printf("\033[s"); // save cursor
+//DEBUG ::printf("\033[05C -- Ring Index\n");
+//DEBUG ::printf("\033[05C ring_rows_: %d\n", ring_rows_);
+//DEBUG ::printf("\033[05C ring_cols_: %d\n", ring_cols_);
+//DEBUG ::printf("\033[05C offset_: %d\n", offset_);
+//DEBUG ::printf("\033[u"); // recall cursor
+//DEBUG ::printf("\033[30C -- History Index\n");
+//DEBUG ::printf("\033[30C hist_rows_: %d srow=%d\n", hist_rows(), hist_srow());
+//DEBUG ::printf("\033[30C hist_cols_: %d erow=%d\n", hist_cols(), hist_erow());
+//DEBUG ::printf("\033[30C hist_use_: %d\n", hist_use());
+//DEBUG ::printf("\033[u"); // recall cursor
+//DEBUG ::printf("\033[60C -- Display Index\n");
+//DEBUG ::printf("\033[60C disp_rows_: %d srow=%d\n", disp_rows(), disp_srow());
+//DEBUG ::printf("\033[60C disp_cols_: %d erow=%d\n", disp_cols(), disp_erow());
+//DEBUG ::printf("\n\n");
+//DEBUG }
+
+//DEBUG // Save specified row from ring buffer 'ring' to FILE*
+//DEBUG void Fl_Terminal::write_row(FILE *fp, Utf8Char *u8c, int cols) const {
+//DEBUG cols = (cols != 0) ? cols : ring_cols();
+//DEBUG for ( int col=0; col<cols; col++, u8c++ ) {
+//DEBUG ::fprintf(fp, "%.*s", u8c->length(), u8c->text_utf8());
+//DEBUG }
+//DEBUG }
+
+//DEBUG // Show two buffers side-by-side on stdout.
+//DEBUG // Second buffer can be NULL to just show the a buffer.
+//DEBUG //
+//DEBUG void Fl_Terminal::show_buffers(RingBuffer *a, RingBuffer *b) const {
+//DEBUG int arows = a->ring_rows(), acols = a->ring_cols();
+//DEBUG int brows = b ? b->ring_rows() : 0, bcols = b ? b->ring_cols() : 0;
+//DEBUG int trows = MAX(arows,brows);
+//DEBUG // Show header
+//DEBUG ::printf("\033[H");
+//DEBUG if (a) ::printf("SRC %d x %d huse=%d off=%d", arows,acols, a->hist_use(), a->offset());
+//DEBUG if (b) ::printf(", DST %d x %d huse=%d off=%d", brows, bcols, b->hist_use(), b->offset());
+//DEBUG ::printf("\033[K\n");
+//DEBUG Utf8Char *u8c;
+//DEBUG // Show rows
+//DEBUG for (int row=0; row < trows; row++) {
+//DEBUG // 'A' buffer
+//DEBUG if (row >= arows) {
+//DEBUG ::printf(" %*s ", acols, "");
+//DEBUG } else {
+//DEBUG u8c = a->ring_chars()+(arows*acols);
+//DEBUG ::printf("%3d/%3d [", row, trows-1); write_row(stdout, u8c, acols); ::printf("] ");
+//DEBUG }
+//DEBUG if (!b) { ::printf("\033[K\n"); continue; }
+//DEBUG // 'B' buffer
+//DEBUG if (row < brows) {
+//DEBUG u8c = b->ring_chars()+(brows*bcols);
+//DEBUG ::printf("["); write_row(stdout, u8c, bcols); ::printf("]");
+//DEBUG }
+//DEBUG ::printf("\033[K\n");
+//DEBUG }
+//DEBUG ::printf("--- END\033[0J\n"); // clear eos
+//DEBUG ::printf(" HIT ENTER TO CONTINUE: "); getchar();
+//DEBUG fflush(stdout);
+//DEBUG }
+
+///////////////////////////////
+////// CURSOR MANAGEMENT //////
+///////////////////////////////
+
+/** Set the cursor's foreground color used for text under the cursor. */
+void Fl_Terminal::cursorfgcolor(Fl_Color val) { cursor_.fgcolor(val); }
+/** Set the cursor's background color used for the cursor itself. */
+void Fl_Terminal::cursorbgcolor(Fl_Color val) { cursor_.bgcolor(val); }
+/** Get the cursor's foreground color used for text under the cursor. */
+Fl_Color Fl_Terminal::cursorfgcolor(void) const { return cursor_.fgcolor(); }
+/** Get the cursor's background color used for the cursor itself. */
+Fl_Color Fl_Terminal::cursorbgcolor(void) const { return cursor_.bgcolor(); }
+
+/**
+ Move cursor to the specified row \p row.
+ This value is clamped to the range (0..display_rows()-1).
+*/
+void Fl_Terminal::cursor_row(int row) { cursor_.row( clamp(row,0,disp_rows()-1) ); }
+/** Move cursor to the specified column \p col.
+ This value is clamped to the range (0..display_columns()-1).
+*/
+void Fl_Terminal::cursor_col(int col) { cursor_.col( clamp(col,0,disp_cols()-1) ); }
+/** Return the cursor's current row position on the screen. */
+int Fl_Terminal::cursor_row(void) const { return cursor_.row(); }
+/** Return the cursor's current column position on the screen. */
+int Fl_Terminal::cursor_col(void) const { return cursor_.col(); }
+
+/**
+ Moves cursor up \p count lines.
+ If cursor hits screen top, it either stops (does not wrap) if \p do_scroll
+ is false, or scrolls down if \p do_scroll is true.
+*/
+void Fl_Terminal::cursor_up(int count, bool do_scroll) {
+ count = clamp(count, 1, disp_rows() * 2); // sanity (max 2 scrns)
+ while (count-- > 0) {
+ if (cursor_.up() <= 0) { // hit screen top?
+ cursor_.row(0); // clamp cursor to top
+ if (do_scroll) scroll(-1); // scrolling on? scroll down
+ else return; // scrolling off? stop at top
+ }
+ }
+}
+
+/**
+ Moves cursor down \p count lines.
+ If cursor hits screen bottom, it either stops (does not wrap) if \p do_scroll
+ is false, or wraps and scrolls up if \p do_scroll is true.
+*/
+void Fl_Terminal::cursor_down(int count, ///< Number of lines to move cursor down
+ bool do_scroll ///< Enable scrolling if set to true
+ ) {
+ count = clamp(count, 1, ring_rows()); // sanity
+ while (count-- > 0) {
+ if (cursor_.down() >= disp_rows()) { // hit screen bottom?
+ cursor_.row(disp_rows() - 1); // clamp
+ if (!do_scroll) break; // don't scroll? done
+ scroll(1); // scroll up 1 row to make room for new line
+ }
+ }
+}
+
+/**
+ Moves cursor left \p count columns, and cursor stops (does not wrap)
+ if it hits screen edge.
+*/
+void Fl_Terminal::cursor_left(int count) {
+ count = clamp(count, 1, disp_cols()); // sanity
+ while (count-- > 0 )
+ if (cursor_.left() < 0) // hit left edge of screen?
+ { cursor_sol(); return; } // stop, done
+}
+
+/**
+ Moves cursor right \p count columns. If cursor hits right edge of screen,
+ it either stops (does not wrap) if \p do_scroll is false, or wraps and
+ scrolls up one line if \p do_scroll is true.
+*/
+void Fl_Terminal::cursor_right(int count, bool do_scroll) {
+ while (count-- > 0 ) {
+ if (cursor_.right() >= disp_cols()) { // hit right edge?
+ if (!do_scroll) // no scroll?
+ { cursor_eol(); return; } // stop at EOL, done
+ else
+ { cursor_crlf(1); } // do scroll? crlf
+ }
+ }
+}
+
+/** Move cursor to the home position (top/left). */
+void Fl_Terminal::cursor_home(void) { cursor_.col(0); cursor_.row(0); }
+
+/** Move cursor to the last column (at the far right) on the current line. */
+void Fl_Terminal::cursor_eol(void) { cursor_.col(disp_cols()-1); }
+
+/** Move cursor to the first column (at the far left) on the current line. */
+void Fl_Terminal::cursor_sol(void) { cursor_.col(0); }
+
+/** Move cursor as if a CR (\\r) was received. Same as cursor_sol() */
+void Fl_Terminal::cursor_cr(void) { cursor_sol(); }
+
+/** Move cursor as if a CR/LF pair (\\r\\n) was received. */
+void Fl_Terminal::cursor_crlf(int count) {
+ const bool do_scroll = true;
+ count = clamp(count, 1, ring_rows()); // sanity
+ cursor_sol();
+ cursor_down(count, do_scroll);
+}
+
+// Tab right, do not wrap beyond right edge
+void Fl_Terminal::cursor_tab_right(int count) {
+ count = clamp(count, 1, disp_cols()); // sanity
+ int X = cursor_.col();
+ while ( count-- > 0 ) {
+ // Find next tabstop
+ while ( ++X < disp_cols() ) {
+ if ( (X<tabstops_size_) && tabstops_[X] ) // found?
+ { cursor_.col(X); return; } // move cur, done
+ }
+ }
+ cursor_eol();
+}
+
+// Tab left, do not wrap beyond left edge
+void Fl_Terminal::cursor_tab_left(int count) {
+ count = clamp(count, 1, disp_cols()); // sanity
+ int X = cursor_.col();
+ while ( count-- > 0 )
+ while ( --X > 0 ) // search for tabstop
+ if ( (X<tabstops_size_) && tabstops_[X] ) // found?
+ { cursor_.col(X); return; } // move cur, done
+ cursor_sol();
+}
+
+// Save current cursor position
+void Fl_Terminal::save_cursor(void) {
+ escseq.save_cursor(cursor_.row(), cursor_.col());
+}
+
+// Restore previously saved cursor position, if there was one
+void Fl_Terminal::restore_cursor(void) {
+ int row,col;
+ escseq.restore_cursor(row, col);
+ if (row != -1 && col != 1) // restore only if save first
+ { cursor_.row(row); cursor_.col(col); }
+}
+
+//////////////////////
+////// PRINTING //////
+//////////////////////
+
+void Fl_Terminal::handle_ctrl(char c) {
+ switch (c) {
+ case '\n': cursor_crlf(); return; // CRLF?
+ case '\b': cursor_left(); return; // BS?
+ case '\r': cursor_cr(); return; // CR?
+ case '\t': cursor_tab_right(); return; // TAB?
+ case 0x1b: if (ansi_) escseq.parse(c); // ESC?
+ else append_utf8("␛");
+ return;
+ default: handle_unknown_char(); return; // Unknown ctrl char?
+ }
+}
+
+// Is char printable? (0x20 thru 0x7e)
+bool Fl_Terminal::is_printable(char c) {
+ return ((c>=0x20) && (c<=0x7e));
+}
+
+// Is char a ctrl character? (0x00 thru 0x1f)
+bool Fl_Terminal::is_ctrl(char c) {
+ return ((c >= 0x00) && (c < 0x20)) ? true : false;
+}
+
+// Handle ESC[m sequences.
+// This is different from the others in that the list of vals
+// separated by ;'s can be long, to allow combining multiple mode
+// settings at once, e.g. fg and bg colors, multiple attributes, etc.
+//
+void Fl_Terminal::handle_SGR(void) { // ESC[...m?
+ // Shortcut varnames..
+ EscapeSeq &esc = escseq;
+ int tot = esc.total_vals();
+ // Handle ESC[m or ESC[;m
+ if (tot == 0)
+ { current_style_.sgr_reset(); return; }
+ // Handle ESC[#;#;#...m
+ int rgbcode = 0; // 0=none, 38=fg, 48=bg
+ int rgbmode = 0; // 0=none, 1="2", 2=<r>, 3=<g>, 4=<b>
+ int r=0,g=0,b=0;
+ for (int i=0; i<tot; i++) { // expect possibly many values
+ int val = esc.val(i); // each val one at a time
+ switch ( rgbmode ) {
+ case 0:
+ // RGB mode values?
+ switch (val) {
+ case 38: // fg RGB mode? e.g. ESC[38;2;<R>;<G>;<B>m
+ case 48: // bg RGB mode? e.g. ESC[48;2;<R>;<G>;<B>m
+ rgbmode = 1;
+ rgbcode = val;
+ continue;
+ }
+ break;
+ case 1: if (val == 2) { rgbmode++; continue; } // '2'?
+ rgbcode = rgbmode = 0; // not '2'? cancel
+ handle_unknown_char();
+ break;
+ case 2: r=clamp(val,0,255); ++rgbmode; continue; // parse red value
+ case 3: g=clamp(val,0,255); ++rgbmode; continue; // parse grn value
+ case 4: b=clamp(val,0,255); // parse blu value
+ switch (rgbcode) {
+ case 38: current_style_.fgcolor(r,g,b); // Set fg rgb
+ break;
+ case 48: current_style_.bgcolor(r,g,b); // Set bg rgb
+ break;
+ }
+ rgbcode = rgbmode = 0; // done w/rgb mode parsing
+ continue; // continue loop to parse more vals
+ }
+ if (val < 10) { // Set attribute? (bold,underline..)
+ switch ( val ) {
+ case 0: current_style_.sgr_reset(); break; // ESC[0m - reset
+ case 1: current_style_.sgr_bold(1); break; // ESC[1m - bold
+ case 2: current_style_.sgr_dim(1); break; // ESC[2m - dim
+ case 3: current_style_.sgr_italic(1); break; // ESC[3m - italic
+ case 4: current_style_.sgr_underline(1);break; // ESC[4m - underline
+ case 5: current_style_.sgr_blink(1); break; // ESC[5m - blink
+ case 6: handle_unknown_char(); break; // ESC[6m - (unused)
+ case 7: current_style_.sgr_inverse(1); break; // ESC[7m - inverse
+ case 8: handle_unknown_char(); break; // ESC[8m - (unused)
+ case 9: current_style_.sgr_strike(1); break; // ESC[9m - strikeout
+ }
+ } else if (val >= 21 && val <= 29) { // attribute extras
+ switch ( val ) {
+ case 21: current_style_.sgr_dbl_under(1);break; // ESC[21m - doubly underline
+ case 22: current_style_.sgr_dim(0); // ESC[22m - disable bold/dim
+ current_style_.sgr_bold(0); break; //
+ case 23: current_style_.sgr_italic(0); break; // ESC[23m - disable italic
+ case 24: current_style_.sgr_underline(0);break; // ESC[24m - disable underline
+ case 25: current_style_.sgr_blink(0); break; // ESC[25m - disable blink
+ case 26: handle_unknown_char(); break; // ESC[26m - (unused)
+ case 27: current_style_.sgr_inverse(0); break; // ESC[27m - disable inverse
+ case 28: handle_unknown_char(); break; // ESC[28m - disable hidden
+ case 29: current_style_.sgr_strike(0); break; // ESC[29m - disable strikeout
+ }
+ } else if (val >= 30 && val <= 37) { // Set fg color?
+ current_style_.fgcolor_uchar(val - 30);
+ } else if (val == 39) { // ESC[39m -- "normal" fg color:
+ Fl_Color fg = current_style_.defaultfgcolor(); // ..get default color
+ current_style_.fgcolor(fg); // ..set current color
+ } else if (val >= 40 && val <= 47) { // Set bg color?
+ current_style_.bgcolor_uchar(val - 40);
+ } else if (val == 49) { // ESC[49m -- "normal" bg color:
+ Fl_Color bg = current_style_.defaultbgcolor(); // ..get default bg color
+ current_style_.bgcolor(bg); // ..set current bg color
+ } else {
+ handle_unknown_char(); // does an escseq.reset() // unimplemented SGR codes
+ }
+ }
+}
+
+// Handle <ESC>[<top>;<lt>;<bot>;<rt>;<att>$t
+// This one is fun! <att> is the attrib to xor, i.e. 1(bold),4,5,7(inverse).
+// gnome-term doesn't support this, but xterm does.
+//
+void Fl_Terminal::handle_DECRARA(void) {
+ // TODO: MAYBE NEVER
+}
+
+// Handle an escape sequence character
+// If this char is the end of the sequence, do the operation
+// if possible, and then reset() to finish parsing.
+//
+void Fl_Terminal::handle_escseq(char c) {
+ // NOTE: Use xterm to test. gnome-terminal has bugs, even in 2022.
+ const bool do_scroll = true;
+ const bool no_scroll = false;
+ //UNUSED const bool do_wrap = true;
+ //UNUSED const bool no_wrap = false;
+ switch (escseq.parse(c)) { // parse char, advance s..
+ case EscapeSeq::fail: escseq.reset(); return; // failed? reset, done
+ case EscapeSeq::success: return; // keep parsing..
+ case EscapeSeq::completed: break; // parsed complete esc sequence?
+ }
+ // Shortcut varnames for escseq parsing..
+ EscapeSeq &esc = escseq;
+ char mode = esc.esc_mode();
+ int tot = esc.total_vals();
+ int val0 = (tot==0) ? 0 : esc.val(0);
+ int val1 = (tot<2) ? 0 : esc.val(1);
+ const int& dw = disp_cols();
+ const int& dh = disp_rows();
+ if (esc.is_csi()) { // Was this a CSI (ESC[..) sequence?
+ switch ( mode ) {
+ case '@': // <ESC>[#@ - (ICH) Insert blank Chars (default=1)
+ insert_char(' ', esc.defvalmax(1,dw));
+ break;
+ case 'A': // <ESC>[#A - (CUU) cursor up, no scroll/wrap
+ cursor_up(esc.defvalmax(1,dh));
+ break;
+ case 'B': // <ESC>[#B - (CUD) cursor down, no scroll/wrap
+ cursor_down(esc.defvalmax(1,dh), no_scroll);
+ break;
+ case 'C': // <ESC>[#C - (CUF) cursor right, no wrap
+ cursor_right(esc.defvalmax(1,dw), no_scroll);
+ break;
+ case 'D': // <ESC>[#D - (CUB) cursor left, no wrap
+ cursor_left(esc.defvalmax(1,dw));
+ break;
+ case 'E': // <ESC>[#E - (CNL) cursor next line (crlf) xterm, !gnome
+ cursor_crlf(esc.defvalmax(1,dh));
+ break;
+ case 'F': // <ESC>[#F - (CPL) move to sol and up # lines
+ cursor_cr();
+ cursor_up(esc.defvalmax(1,dh));
+ break;
+ case 'G': // <ESC>[#G - (CHA) cursor horizal absolute
+ switch (clamp(tot,0,1)) { // │
+ case 0: // ├── <ESC>[G -- move to sol
+ cursor_sol(); // │ default <ESC>[1G
+ break; // │
+ case 1: // └── <ESC>[#G -- move to column
+ cursor_col(clamp(val0,1,dw)-1);
+ break;
+ }
+ break;
+ case 'H':
+cup:
+ switch (clamp(tot,0,2)) { // <ESC>[#H - (CUP) cursor position (#'s are 1 based)
+ case 0: // ├── <ESC>[H -- no vals?
+ cursor_home(); // │ default <ESC>[1H
+ break; // │
+ case 1: // ├── <ESC>[#H -- go to (row #)
+ cursor_row(clamp(val0,1,dh)-1); // │ NOTE: ESC[5H == ESC[5;1H
+ cursor_col(0); // │
+ break; // │
+ case 2: // └── <ESC>[#;#H -- go to (row# ; col#)
+ cursor_row(clamp(val0,1,dh)-1);
+ cursor_col(clamp(val1,1,dw)-1);
+ break;
+ }
+ break;
+ case 'I': // <ESC>[#I - (CHT) cursor forward tab (default=1)
+ switch (clamp(tot,0,1)) { // │
+ case 0: // ├── <ESC>[I -- no vals
+ cursor_tab_right(1); // │ default <ESC>[1I
+ break; // │
+ case 1: // └── <ESC>[#I -- tab # times
+ cursor_tab_right(clamp(val0,1,dw)); //
+ break;
+ }
+ break;
+ case 'J': // <ESC>[#J - (ED) erase in display
+ switch ( clamp(tot,0,1) ) { // │
+ case 0: clear_eol(); break; // ├── <ESC>[J -- no vals: default <ESC>[0J
+ case 1: // │
+ switch ( clamp(val0,0,3) ) { // │
+ case 0: clear_eod(); break; // ├── <ESC>[0J -- clear to end of display
+ case 1: clear_sod(); break; // ├── <ESC>[1J -- clear to start of display
+ case 2: clear_screen(); break; // ├── <ESC>[2J -- clear all lines
+ case 3: clear_history(); break; // └── <ESC>[3J -- clear screen history
+ }
+ break;
+ }
+ break;
+ case 'K':
+ switch ( clamp(tot,0,1) ) { // <ESC>[#K - (EL) Erase in Line
+ case 0: clear_eol(); break; // ├── <ESC>[K -- no vals
+ case 1: switch ( clamp(val0,0,2) ) { // │
+ case 0: clear_eol(); break; // ├── <ESC>[0K -- clear to end of line
+ case 1: clear_sol(); break; // ├── <ESC>[1K -- clear to start of line
+ case 2: clear_line(); break; // └── <ESC>[2K -- clear current line
+ }
+ break;
+ }
+ break;
+ case 'L': // ESC[#L - Insert # lines (def=1)
+ insert_rows(esc.defvalmax(1,dh));
+ break;
+ case 'M': // ESC[#M - Delete # lines (def=1)
+ delete_rows(esc.defvalmax(1,dh));
+ break;
+ case 'P': // ESC[#P - Delete # chars (def=1)
+ delete_chars(esc.defvalmax(1,dh));
+ break;
+ case 'S': // ESC[#S - scroll up # lines (def=1)
+ scroll( +(esc.defvalmax(1,dh)) );
+ // ⮤ positive=scroll up
+ break;
+ case 'T': // ESC[#T - scroll dn # lines (def=1)
+ scroll( -(esc.defvalmax(1,dh)) );
+ // ⮤ negative=scroll down
+ break;
+ case 'X': // <ESC>[#X - (ECH) Erase Characters (default=1)
+ repeat_char(' ', esc.defvalmax(1,dw));
+ break;
+ case 'Z': // ESC[#Z - backtab # tabs
+ switch (clamp(tot,0,1)) { // │
+ case 0: // ├── <ESC>[Z -- no vals
+ cursor_tab_left(1); // │ default <ESC>[1Z
+ break; // │
+ case 1: // └── <ESC>[#Z -- tab # times
+ cursor_tab_left(clamp(val0,1,dw));
+ break;
+ }
+ break;
+ case 'a': // TODO // ESC[#a - (HPR) move cursor relative [columns] (default=[row,col+1])
+ case 'b': // TODO // ESC[#b - (REP) repeat prev graphics char # times
+ case 'd': // TODO // ESC[#d - (VPA) line pos absolute [row]
+ case 'e': // TODO // ESC[#e - line pos relative [rows]
+ handle_unknown_char(); // does an escseq.reset()
+ break;
+ case 'f': // <ESC>[#f - (CUP) cursor position (#'s 1 based)
+ goto cup; // (same as ESC[H)
+ case 'g': // ESC[...g? Tabulation Clear (TBC)
+ switch (val0) {
+ case 0: clear_tabstop(); break; // clears tabstop at cursor
+ case 3: clear_all_tabstops(); break; // clears all tabstops
+ default:
+ handle_unknown_char(); // does an escseq.reset()
+ break;
+ }
+ break;
+ case 'm': handle_SGR(); break; // ESC[#m - set character attributes (SGR)
+ case 's': save_cursor(); break; // ESC[s - save cur pos (xterm+gnome)
+ case 'u': restore_cursor(); break; // ESC[u - rest cur pos (xterm+gnome)
+ case 'q': // TODO? // ESC[>#q set cursor style (block/line/blink..)
+ case 'r': // TODO // ESC[#;#r set scroll region top;bot
+ // default=full window
+ handle_unknown_char(); // does an escseq.reset()
+ break;
+ case 't': handle_DECRARA(); break; // ESC[#..$t -- (DECRARA)
+ // Reverse attribs in Rect Area (row,col)
+ default:
+ handle_unknown_char(); // does an escseq.reset()
+ break;
+ }
+ } else {
+ // Not CSI? Might be C1 Control code (<ESC>D, etc)
+ switch ( esc.esc_mode() ) {
+ case 'c': // <ESC>c - Reset term to Initial State (RIS)
+ reset_terminal();
+ break;
+ case 'D': cursor_down(1, do_scroll); break;// <ESC>D - down line, scroll at bottom
+ case 'E': cursor_crlf(); break;// <ESC>E - do a crlf
+ case 'H': set_tabstop(); break;// <ESC>H - set a tabstop
+ case 'M': cursor_up(1, true); break;// <ESC>M -- (RI) Reverse Index (up w/scroll)
+ default:
+ handle_unknown_char(); // does an escseq.reset()
+ break;
+ }
+ }
+ esc.reset(); // done handling escseq, reset()
+}
+
+void Fl_Terminal::display_modified_clear(void) {
+ redraw_modified_ = false;
+}
+
+// Display modified, trigger redraw handling..
+void Fl_Terminal::display_modified(void) {
+ if (is_redraw_style(RATE_LIMITED)) {
+ if (!redraw_modified_) { // wasn't before but now is?
+ if (!redraw_timer_) {
+ Fl::add_timeout(.01, redraw_timer_cb, this); // turn on timer
+ redraw_timer_ = true;
+ }
+ redraw_modified_ = true;
+ }
+ } else if (is_redraw_style(PER_WRITE)) {
+ if (!redraw_modified_) {
+ redraw_modified_ = true;
+ redraw(); // only call redraw once
+ }
+ } else { // NO_REDRAW?
+ // do nothing
+ }
+}
+
+/**
+ Clear the character at the specified display row and column.
+
+ No range checking done on drow,dcol:
+ - \p drow must be in range 0..(disp_rows()-1)
+ - \p dcol must be in range 0..(disp_cols()-1)
+
+ - Does not trigger redraws
+*/
+void Fl_Terminal::clear_char_at_disp(int drow, int dcol) {
+ Utf8Char *u8c = u8c_disp_row(drow) + dcol;
+ u8c->clear(current_style_);
+}
+
+/**
+ Return Utf8Char* for char at specified display row and column.
+ This accesses any character in the display part of the ring buffer.
+
+ No range checking done on drow,dcol:
+ - \p drow must be in range 0..(disp_rows()-1)
+ - \p dcol must be in range 0..(disp_cols()-1)
+
+ \see u8c_disp_row()
+*/
+const Fl_Terminal::Utf8Char* Fl_Terminal::utf8_char_at_disp(int drow, int dcol) const {
+ return u8c_disp_row(drow) + dcol;
+}
+
+/**
+ Return Utf8Char* for char at specified global (grow,gcol).
+ This accesses any character in the ring buffer (history + display).
+
+ No range checking done on grow,gcol:
+ - \p grow must be in range 0..(ring_rows()-1)
+ - \p gcol must be in range 0..(ring_cols()-1)
+
+ \see u8c_ring_row()
+*/
+const Fl_Terminal::Utf8Char* Fl_Terminal::utf8_char_at_glob(int grow, int gcol) const {
+ return u8c_ring_row(grow) + gcol;
+}
+
+/**
+ Print UTF-8 character \p text of length \p len at display position \p (drow,dcol).
+ The character is displayed using the current text color/attributes.
+
+ This is a very low level method.
+
+ No range checking is done on drow,dcol:
+ - \p drow must be in range 0..(display_rows()-1)
+ - \p dcol must be in range 0..(display_columns()-1)
+
+ - Does not trigger redraws
+ - Does not handle ANSI or XTERM escape sequences
+ - Invalid UTF-8 chars show the error character (¿) depending on show_unknown(bool).
+
+ \see handle_unknown_char()
+*/
+void Fl_Terminal::putchar(const char *text, int len, int drow, int dcol) {
+ Utf8Char *u8c = u8c_disp_row(drow) + dcol;
+ // text_utf8() warns we must do invalid checks first
+ if (!text || len<1 || len>u8c->max_utf8() || len!=fl_utf8len(*text))
+ { handle_unknown_char(); return; }
+ u8c->text_utf8(text, len, current_style_);
+}
+
+/**
+ Print the ASCII character \p c at the terminal's display position \p (drow,dcol).
+
+ The character MUST be printable (in range 0x20 - 0x7e), and is displayed
+ using the current text color/attributes. Characters outside that range are either
+ ignored or print the error character (¿), depending on show_unknown(bool).
+
+ This is a very low level method.
+
+ No range checking is done on drow,dcol:
+ - \p drow must be in range 0..(display_rows()-1)
+ - \p dcol must be in range 0..(display_columns()-1)
+
+ - Does not trigger redraws
+ - Does NOT handle control codes, ANSI or XTERM escape sequences.
+
+ \see show_unknown(bool), handle_unknown_char(), is_printable()
+*/
+void Fl_Terminal::putchar(char c, int drow, int dcol) {
+ if (!is_printable(c)) { handle_unknown_char(); return; }
+ Utf8Char *u8c = u8c_disp_row(drow) + dcol;
+ u8c->text_ascii(c, current_style_);
+}
+
+/**
+ Prints single UTF-8 char \p text of optional byte length \p len
+ at current cursor position, and advances the cursor if the character
+ is printable. Handles ASCII and control codes (CR, LF, etc).
+
+ The character is displayed at the current cursor position
+ using the current text color/attributes.
+
+ Handles control codes and can be used to construct ANSI/XTERM
+ escape sequences.
+
+ - If optional \p len isn't specified or <0, strlen(text) is used.
+ - \p text must not be NULL.
+ - \p len must not be 0.
+ - \p text must be a single char only (whether UTF-8 or ASCII)
+ - \p text can be an ASCII character, though not as efficent as print_char()
+ - Invalid UTF-8 chars show the error character (¿) depending on show_unknown(bool).
+ - Does not trigger redraws
+
+ \see show_unknown(bool), handle_unknown_char()
+*/
+void Fl_Terminal::print_char(const char *text, int len/*=-1*/) {
+ len = len<0 ? fl_utf8len(*text) : len; // int(strlen(text)) : len;
+ const bool do_scroll = true;
+ if (is_ctrl(text[0])) { // Handle ctrl character
+ handle_ctrl(*text);
+ } else if (escseq.parse_in_progress()) { // ESC sequence in progress?
+ handle_escseq(*text);
+ } else { // Handle printable char..
+ putchar(text, len, cursor_row(), cursor_col());
+ cursor_right(1, do_scroll);
+ }
+}
+
+/**
+ Prints single ASCII char \p c at current cursor position, and advances the cursor.
+
+ The character is displayed at the current cursor position
+ using the current text color/attributes.
+
+ - \p c must be ASCII, not utf-8
+ - Does not trigger redraws
+*/
+void Fl_Terminal::print_char(char c) {
+ const bool do_scroll = true;
+ if (is_ctrl(c)) { // Handle ctrl character
+ handle_ctrl(c);
+ } else if (escseq.parse_in_progress()) { // ESC sequence in progress?
+ handle_escseq(c);
+ } else { // Handle printable char..
+ putchar(c, cursor_row(), cursor_col());
+ cursor_right(1, do_scroll);
+ return;
+ }
+}
+
+// Clear the Partial UTF-8 Buffer cache
+void Fl_Terminal::utf8_cache_clear(void) {
+ pub_.clear();
+}
+
+// Flush the Partial UTF-8 Buffer cache, and clear
+void Fl_Terminal::utf8_cache_flush(void) {
+ if (pub_.buflen() > 0) print_char(pub_.buf(), pub_.buflen());
+ pub_.clear();
+}
+
+/**
+ Append NULL terminated UTF-8 string to terminal.
+
+ - If buf is NULL, UTF-8 cache buffer is cleared
+ - If optional \p len isn't specified or is -1, strlen(text) is used.
+ - If \p len is 0 or <-1, no changes are made
+ - Handles UTF-8 chars split across calls (e.g. block writes from pipes, etc)
+ - Redraws are triggered automatically, depending on redraw_style()
+*/
+void Fl_Terminal::append_utf8(const char *buf, int len/*=-1*/) {
+ int mod = 0; // assume no modifications
+ if (!buf) { utf8_cache_clear(); return; } // clear cache, done
+ if (len == -1) len = int(strlen(buf)); // len optional
+ if (len<=0) return; // bad len? early exit
+
+ // Handle any partial UTF-8 from last write
+ // Try to parse up rest of incomplete buffered char from end
+ // of last block, and flush it to terminal.
+ //
+ if (pub_.buflen() > 0) { // partial UTF-8 to deal with?
+ while (len>0 && pub_.is_continuation(*buf)) { // buffer 'continuation' chars
+ if (pub_.append(buf, 1) == false) // append byte to partial UTF-8 buffer
+ { mod |= handle_unknown_char(); break; } // overrun? break loop
+ else { buf++; len--; } // shrink our buffer
+ }
+ if (pub_.is_complete()) utf8_cache_flush(); // complete UTF-8 captured? flush to tty
+ if (len <= 0) { // check len again, we may have run out
+ if (mod) display_modified();
+ return;
+ }
+ }
+
+ // For sure buf is now pointing at a valid char, so walk to end of buffer
+ int clen; // char length
+ const char *p = buf; // ptr to walk buffer
+ while (len>0) {
+ clen = fl_utf8len(*p); // how many bytes long is this char?
+ if (clen == -1) { // not expecting bad UTF-8 here
+ mod |= handle_unknown_char();
+ p += 1;
+ len -= 1;
+ } else {
+ if (len && clen>len) { // char longer than buffer?
+ if (pub_.append(p, len) == false) { // buffer it
+ mod |= handle_unknown_char();
+ utf8_cache_clear();
+ }
+ break;
+ }
+ print_char(p, clen); // write complete UTF-8 char to terminal
+ p += clen; // advance to next char
+ len -= clen; // adjust len
+ mod |= 1;
+ }
+ }
+ if (mod) display_modified();
+}
+
+/**
+ Append NULL terminated ASCII string to terminal,
+ slightly more efficient than append_utf8().
+
+ - If \p s is NULL, behavior is to do nothing
+ - Redraws are triggered automatically, depending on redraw_style()
+*/
+void Fl_Terminal::append_ascii(const char *s) {
+ if (!s) return;
+ while ( *s ) print_char(*s++); // handles display_modified()
+ display_modified();
+}
+
+/**
+ Appends string \p s to the terminal at the current cursor position
+ using the current text color/attributes.
+
+ If \p s is NULL, the UTF-8 character cache is cleared, which is
+ recommended before starting a block reading loop, and again after the
+ block loop has completed.
+
+ If \p len is not specified, it's assumed \p s is a NULL terminated
+ string. If \p len IS specified, it can be used for writing strings
+ that aren't NULL terminated, such as block reads on a pipe, network,
+ or other block oriented data source.
+
+ Redraws of the terminal widget are by default handled automatically,
+ but can be changed with redraw_rate() and redraw_style().
+
+ <B>Block I/O</B>
+
+ When reading block oriented sources (such as pipes), append() will
+ handle partial UTF-8 chars straddling the block boundaries. It does
+ this using an internal byte cache, which should be cleared before
+ and after block I/O loops by calling <TT>append(NULL)</TT> as shown
+ in the example below, to prevent the possibilities of partial UTF-8
+ characters left behind by an interrupted or incomplete block loop.
+
+ \par
+ \code
+ // Example block reading a command pipe in Unix
+
+ // Run command and read as a pipe
+ FILE *fp = popen("ls -la", "r");
+ if (!fp) { ..error_handling.. }
+
+ // Enable non-blocking I/O
+ int fd = fileno(fp);
+ fcntl(fd, F_SETFL, O_NONBLOCK);
+
+ // Clear UTF-8 character cache before starting block loop
+ G_tty->append(NULL); // prevents leftover partial UTF-8 bytes
+
+ // Block read loop
+ while (1) {
+ Fl::wait(0.05); // give fltk .05 secs of cpu to manage UI
+ ssize_t bytes = read(fd, s, sizeof(s)); // read block from pipe
+ if (bytes == -1 && errno == EAGAIN) continue; // no data yet? continue
+ if (bytes > 0) G_tty->append(s); // append output to terminal
+ else break; // end of pipe?
+ }
+
+ // Flush cache again after block loop completes
+ G_tty->append(NULL);
+
+ // Close pipe, done
+ pclose(fp);
+
+ \endcode
+
+ \note
+ - String can contain ASCII or UTF-8 chars
+ - \p len is optional; if unspecified, expects \p s to be a NULL terminated string
+ - Handles partial UTF-8 chars split between calls (e.g. block oriented writes)
+ - If \p s is NULL, this clears the "partial UTF-8" character cache
+ - Redraws are managed automatically by default; see redraw_style()
+*/
+void Fl_Terminal::append(const char *s, int len/*=-1*/) {
+ append_utf8(s, len);
+}
+
+/**
+ Handle an unknown char by either emitting an error symbol to the tty, or do nothing,
+ depending on the user configurable value of show_unknown().
+ Returns 1 if tty modified, 0 if not.
+ \see show_unknown()
+*/
+int Fl_Terminal::handle_unknown_char(void) {
+ const char *unknown = "¿";
+ if (show_unknown_) {
+ escseq.reset(); // disable any pending esc seq to prevent eating unknown char
+ print_char(unknown);
+ return 1;
+ }
+ return 0;
+}
+
+// Handle user interactive scrolling
+void Fl_Terminal::scrollbar_cb(Fl_Widget*, void* userdata) {
+ Fl_Terminal *o = (Fl_Terminal*)userdata;
+ o->redraw();
+}
+
+// Handle mouse selection autoscrolling
+void Fl_Terminal::autoscroll_timer_cb2(void) {
+ // Move scrollbar
+ // NOTE: vscroll is inverted; 0=tab at bot, so minimum() is really max
+ //
+ int amt = autoscroll_amt_; // (amt<0):above top, (amt>0):below bottom
+ int val = vscroll_->value();
+ int max = int(vscroll_->minimum()+.5); // NOTE: minimum() is really max
+ val = (amt<0) ? (val+clamp((-amt/10),1,5)) : // above top edge?
+ (amt>0) ? (val-clamp((+amt/10),1,5)) : 0; // below bot edge?
+ val = clamp(val,0,max); // limit val to scroll's range
+ int diff = ABS(val - vscroll_->value()); // how far scroll tab moved up/dn
+ // Move scrollbar
+ vscroll_->value(val);
+ // Extend selection
+ if (diff) { // >0 if up or down
+ int srow = select_.srow(), scol = select_.scol();
+ int erow = select_.erow(), ecol = select_.ecol();
+ int ltcol = 0, rtcol = ring_cols() - 1;
+ if (amt<0) { erow -= diff; ecol = ltcol; } // above top? use erow: reverse-selecting
+ if (amt>0) { erow += diff; ecol = rtcol; } // below bot? use erow: forward-selecting
+ select_.select(srow, scol, erow, ecol);
+ }
+ // Restart timeout
+ Fl::repeat_timeout(.1, autoscroll_timer_cb, this);
+ redraw();
+}
+
+// Handle mouse selection autoscrolling
+void Fl_Terminal::autoscroll_timer_cb(void *udata) {
+ Fl_Terminal *tty = (Fl_Terminal*)udata;
+ tty->autoscroll_timer_cb2();
+}
+
+// Handle triggering rate limited redraw() updates
+// When data comes in quickly, append() sets the redraw_modified_ flag
+// so our timer can trigger the redraw()s at a controlled rate.
+//
+void Fl_Terminal::redraw_timer_cb2(void) {
+ //DRAWDEBUG ::printf("--- UPDATE TICK %.02f\n", redraw_rate_); fflush(stdout);
+ if (redraw_modified_) {
+ redraw(); // Timer triggered redraw
+ redraw_modified_ = false; // acknowledge modified flag
+ Fl::repeat_timeout(redraw_rate_, redraw_timer_cb, this); // restart timer
+ } else {
+ // Timer went off and nothing to redraw? disable
+ Fl::remove_timeout(redraw_timer_cb, this);
+ redraw_timer_ = false;
+ }
+}
+
+void Fl_Terminal::redraw_timer_cb(void *udata) {
+ Fl_Terminal *tty = (Fl_Terminal*)udata;
+ tty->redraw_timer_cb2();
+}
+
+/**
+ The constructor for Fl_Terminal.
+
+ This creates an empty terminal with defaults:
+ - white on black text; see textfgcolor(Fl_Color), textbgcolor(Fl_Color)
+ - rows/cols based on the \p W and \p H values, see display_rows(), display_columns()
+ - scrollback history of 100 lines, see history_rows()
+ - redraw_style() set to RATE_LIMITED, redraw_rate() set to 0.10 seconds
+
+ Note: While Fl_Terminal derives from Fl_Group, it's not intended for user code
+ to use it as a parent for other widgets, so end() is called.
+
+ \param[in] X,Y,W,H position and size.
+ \param[in] L label string (optional), may be NULL.
+*/
+Fl_Terminal::Fl_Terminal(int X,int Y,int W,int H,const char*L) : Fl_Group(X,Y,W,H,L) {
+ // scrollbar_size must be set before scrn_
+ scrollbar_size_ = 0; // 0 uses Fl::scrollbar_size()
+ update_screen_xywh();
+ // Tabs
+ tabstops_ = 0;
+ tabstops_size_ = 0;
+ // Init ringbuffer. Also creates default tabstops
+ create_ring(h_to_row(scrn_.h()), // desired row
+ w_to_col(scrn_.w()), // desired col
+ 100); // history size: use 100 for production release
+ // Misc
+ redraw_style_ = RATE_LIMITED; // NO_REDRAW, RATE_LIMITED, PER_WRITE
+ redraw_rate_ = 0.10f; // maximum rate in seconds (1/10=10fps)
+ redraw_modified_ = false; // display 'modified' flag
+ redraw_timer_ = false;
+ autoscroll_dir_ = 0;
+ autoscroll_amt_ = 0;
+
+ // Create scrollbar
+ // Final position/size will be set by update_screen() below
+ //
+ vscroll_ = new Fl_Scrollbar(x(), y(), scrollbar_size_, h());
+ vscroll_->type(FL_VERTICAL);
+ vscroll_->linesize(1);
+ vscroll_->slider_size(1);
+ vscroll_->range(0.0, 0.0);
+ vscroll_->value(0);
+ vscroll_->callback(scrollbar_cb, (void*)this);
+ resizable(0);
+ Fl_Group::box(FL_DOWN_FRAME);
+ color(0x0); // black bg by default
+ update_screen(true); // update internal vars after setting screen size/font
+ clear_screen_home(); // clear screen, home cursor
+ clear_history(); // clear history buffer
+ show_unknown_ = false; // default "off"
+ ansi_ = true; // default "on"
+ // End group
+ end();
+}
+
+/**
+ The destructor for Fl_Terminal.
+ Destroys the terminal display, scroll history, and associated widgets.
+*/
+Fl_Terminal::~Fl_Terminal(void) {
+ // Note: RingBuffer class handles destroying itself
+ if (tabstops_)
+ { free(tabstops_); tabstops_ = 0; }
+ if (autoscroll_dir_)
+ { Fl::remove_timeout(autoscroll_timer_cb, this); autoscroll_dir_ = 0; }
+ if (redraw_timer_)
+ { Fl::remove_timeout(redraw_timer_cb, this); redraw_timer_ = false; }
+}
+
+/**
+ Returns the scrollbar's actual size; actual width for vertical scrollbars,
+ actual height for horizontal scrollbars.
+*/
+int Fl_Terminal::scrollbar_actual_size(void) const {
+ return scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
+}
+
+/**
+ Get the current size of the scrollbar's trough, in pixels.
+
+ If this value is zero (default), this widget will use the
+ Fl::scrollbar_size() value as the scrollbar's width.
+
+ \returns Scrollbar size in pixels, or 0 if the global Fl::scrollbar_size() is being used.
+ \see Fl::scrollbar_size(int)
+*/
+int Fl_Terminal::scrollbar_size(void) const {
+ return scrollbar_size_;
+}
+
+/**
+ Set the width of the scrollbar's trough to \p val, in pixels.
+
+ Only use this method if you need to override the global scrollbar size.
+
+ Setting \p val to the special value 0 causes the widget to
+ track the global Fl::scrollbar_size().
+
+ \see Fl::scrollbar_size()
+*/
+void Fl_Terminal::scrollbar_size(int val) {
+ scrollbar_size_ = val;
+ update_scrollbar();
+}
+
+////////////////////////////
+////// SCREEN DRAWING //////
+////////////////////////////
+
+// Draw the background for the specified ring_chars[] row starting at FLTK coords X,Y
+// Note we may be called to draw display, or even history if we're scrolled back.
+// If there's any change in bg color, we draw the filled rects here.
+//
+void Fl_Terminal::draw_row_bg(int grow, int X, int Y) const {
+ int bg_h = current_style_.fontheight();
+ int bg_y = Y - current_style_.fontheight() + current_style_.fontdescent();
+ Fl_Color bg_col;
+ int pwidth = 9;
+ const Utf8Char *u8c = u8c_ring_row(grow); // start of spec'd row
+ uchar lastattr = u8c->attrib();
+ for (int gcol=0; gcol<ring_cols(); gcol++,u8c++) { // walk columns
+ // Attribute changed since last char?
+ if (gcol==0 || u8c->attrib() != lastattr) {
+ u8c->fl_font_set(current_style_); // pwidth_int() needs fl_font set
+ lastattr = u8c->attrib();
+ }
+ pwidth = u8c->pwidth_int();
+ bg_col = is_inside_selection(grow, gcol) // text in mouse select?
+ ? select_.selectionbgcolor() // ..use select bg color
+ : (u8c->attrib() & Fl_Terminal::INVERSE) // Inverse mode?
+ ? u8c->attr_fg_color(this) // ..use fg color for bg
+ : u8c->attr_bg_color(this); // ..use bg color for bg
+ // Draw only if color != 0x0 ('show through' color) or widget's own color().
+ if (bg_col != 0xffffffff && bg_col != color()) {
+ fl_color(bg_col);
+ fl_rectf(X, bg_y, pwidth, bg_h);
+ }
+ X += pwidth; // advance X to next char
+ }
+}
+
+// Draw specified global row, which is the row in ring_chars[].
+// The global row includes history + display buffers.
+//
+void Fl_Terminal::draw_row(int grow, int Y) const {
+ // Draw background color spans, if any
+ int X = scrn_.x();
+ draw_row_bg(grow, X, Y);
+
+ // Draw forground text
+ int scrollval = vscroll_->value();
+ int disp_top = (disp_srow() - scrollval); // top row we need to view
+ int drow = grow - disp_top; // disp row
+ bool inside_display = is_disp_ring_row(grow); // row inside 'display'?
+ int strikeout_y = Y - (current_style_.fontheight() / 3);
+ int underline_y = Y;
+ const Utf8Char *u8c = u8c_ring_row(grow);
+ uchar lastattr = -1;
+ bool is_cursor;
+ Fl_Color fg;
+ for (int gcol=0; gcol<disp_cols(); gcol++,u8c++) { // walk the columns
+ const int &dcol = gcol; // dcol and gcol are the same
+ // Are we drawing the cursor? Only if inside display
+ is_cursor = inside_display ? cursor_.is_rowcol(drow-scrollval, dcol) : 0;
+ // Attribute changed since last char?
+ if (u8c->attrib() != lastattr) {
+ u8c->fl_font_set(current_style_); // pwidth_int() needs fl_font set
+ lastattr = u8c->attrib();
+ }
+ int pwidth = u8c->pwidth_int();
+ // DRAW CURSOR BLOCK - TODO: support other cursor types?
+ if (is_cursor) {
+ int cx = X;
+ int cy = Y - cursor_.h() + current_style_.fontdescent();
+ int cw = pwidth;
+ int ch = cursor_.h();
+ fl_color(cursorbgcolor());
+ if (Fl::focus() == this) fl_rectf(cx, cy, cw, ch);
+ else fl_rect(cx, cy, cw, ch);
+ }
+ // DRAW TEXT
+ // 1) Color for text
+ if (is_cursor) fg = cursorfgcolor(); // color for text under cursor
+ else fg = is_inside_selection(grow, gcol) // text in mouse selection?
+ ? select_.selectionfgcolor() // ..use selection FG color
+ : (u8c->attrib() & Fl_Terminal::INVERSE) // Inverse attrib?
+ ? u8c->attr_bg_color(this) // ..use char's bg color for fg
+ : u8c->attr_fg_color(this); // ..use char's fg color for fg
+ fl_color(fg);
+ // 2) Font for text - already set by u8c->fl_font_set() in the above
+ if (is_cursor) {
+ fl_font(fl_font()|FL_BOLD, fl_size()); // force text under cursor BOLD
+ lastattr = -1; // (ensure font reset on next iter)
+ }
+ // 3) Draw text for UTF-8 char. No need to draw spaces
+ if (!u8c->is_char(' ')) fl_draw(u8c->text_utf8(), u8c->length(), X, Y);
+ // 4) Strike or underline?
+ if (u8c->attrib() & Fl_Terminal::UNDERLINE) fl_line(X, underline_y, X+pwidth, underline_y);
+ if (u8c->attrib() & Fl_Terminal::STRIKEOUT) fl_line(X, strikeout_y, X+pwidth, strikeout_y);
+ // Move to next char pixel position
+ X += pwidth;
+ }
+}
+
+// Draw the part of the buffer we're scrolled to at FLTK position Y.
+//
+// This could be anywhere in the buffer, and not just the 'active diplay',
+// depending on what the scrollbar is set to.
+//
+// Handles attributes, colors, text selections, cursor.
+//
+void Fl_Terminal::draw_buff(int Y) const {
+ int srow = disp_srow() - vscroll_->value();
+ int erow = srow + disp_rows();
+ const int rowheight = current_style_.fontheight();
+ for (int grow=srow; (grow<erow) && (Y<scrn_.b()); grow++) {
+ Y += rowheight; // advance Y to bottom left corner of row
+ draw_row(grow, Y); // draw global row at Y
+ }
+}
+
+/**
+ Draws the entire Fl_Terminal.
+ Lets the group draw itself first (scrollbar should be only member),
+ followed by the terminal's screen contents.
+*/
+void Fl_Terminal::draw(void) {
+ // Draw group first, terminal last
+ Fl_Group::draw();
+
+ if (is_frame(box())) {
+ // Is box() a frame? Fill area inside frame with rectf().
+ // FL_XXX_FRAME types allow Fl_Terminal to have a /flat/ background.
+ // FL_XXX_BOX types inherit Fl::scheme() which can provide unwanted gradients.
+ //
+ fl_color(color());
+ // Draw flat field (inside border drawn by Fl_Group::draw() above)
+ int X = x() + Fl::box_dx(box());
+ int Y = y() + Fl::box_dy(box());
+ int W = w() - Fl::box_dw(box());
+ int H = h() - Fl::box_dh(box());
+ W -= vscroll_width();
+ fl_rectf(X,Y,W,H);
+ }
+
+ //DEBUG fl_color(0x80000000); // dark red box inside margins
+ //DEBUG fl_rect(scrn_);
+ fl_push_clip(scrn_.x(), scrn_.y(), scrn_.w(), scrn_.h());
+ int Y = scrn_.y();
+ draw_buff(Y);
+ fl_pop_clip();
+}
+
+// Given a width in pixels, return number of columns that "fits"
+int Fl_Terminal::w_to_col(int W) const {
+ return int((float(W) / current_style_.charwidth()) + 0.0); // +.5 overshoots
+}
+
+// Given a height in pixels, return number of rows that "fits"
+int Fl_Terminal::h_to_row(int H) const {
+ return int((float(H) / current_style_.fontheight()) + -0.5); // +.5 overshoots
+}
+
+/**
+ Handle widget resizing, such as if user resizes parent window.
+ This may increase the column width of the widget if the width
+ of the widget is made larger than it was.
+
+ \note Resizing currently does not rewrap existing text.
+ Currently enlarging makes room for longer lines, and shrinking
+ the size lets long lines run off the right edge of the display,
+ hidden from view. This behavior may change in the future to rewrap.
+*/
+void Fl_Terminal::resize(int X,int Y,int W,int H) {
+ // Let group resize itself
+ Fl_Group::resize(X,Y,W,H);
+ // Update screen stuff; margins, etc
+ update_screen(false); // no change in font, just resizing
+ // resize the display's rows+cols to match window size
+ refit_disp_to_screen();
+}
+
+// Handle autoscrolling vals + timer
+// If mouse dragged beyond top/bottom, start/continue auto-scroll select
+//
+void Fl_Terminal::handle_selection_autoscroll(void) {
+ int Y = Fl::event_y();
+ int top = scrn_.y();
+ int bot = scrn_.b();
+ int dist = (Y < top) ? Y - top : // <0 if above top
+ (Y > bot) ? Y - bot : 0; // >0 if below bottom
+ if (dist == 0) {
+ // Not off edge? stop autoscrolling, done
+ if (autoscroll_dir_) Fl::remove_timeout(autoscroll_timer_cb, this);
+ autoscroll_dir_ = 0;
+ } else {
+ // Above top/below bot? Start/continue autoscroll select
+ if (!autoscroll_dir_) Fl::add_timeout(.01, autoscroll_timer_cb, this);
+ autoscroll_amt_ = dist; // <0 if above top, >0 if below bot
+ autoscroll_dir_ = (dist < 0) ? 3 : 4; // 3=scrolling up, 4=scrolling dn
+ }
+}
+
+// Handle mouse selection on LEFT-CLICK push/drag/release
+// Returns: 1 if 'handled', 0 if not.
+//
+int Fl_Terminal::handle_selection(int e) {
+ int grow=0,gcol=0;
+ bool is_rowcol = (xy_to_glob_rowcol(Fl::event_x(), Fl::event_y(), grow, gcol) > 0)
+ ? true : false;
+ switch (e) {
+ case FL_PUSH: {
+ select_.push_rowcol(grow, gcol);
+ // SHIFT-LEFT-CLICK? Extend or start new
+ if (Fl::event_state(FL_SHIFT)) {
+ if (is_selection()) { // extend if select in progress
+ selection_extend(Fl::event_x(), Fl::event_y());
+ return 1; // express interest in FL_DRAG
+ }
+ } else { // Start a new selection
+ if (select_.clear()) redraw(); // clear prev selection
+ if (is_rowcol) return 1; // express interest in FL_DRAG
+ }
+ // Left-Click outside terminal area?
+ if (!Fl::event_state(FL_SHIFT)) {
+ select_.push_clear();
+ clear_mouse_selection();
+ redraw();
+ }
+ return 0; // event NOT handled
+ }
+ case FL_DRAG: {
+ if (is_rowcol) {
+ if (!is_selection()) { // no selection yet?
+ if (select_.dragged_off(grow, gcol)) { // dragged off FL_PUSH? enough to start
+ select_.start_push(); // ..start drag with FL_PUSH position
+ }
+ } else {
+ if (select_.extend(grow, gcol)) redraw(); // redraw if selection changed
+ }
+ }
+ // If we leave scrn area, start timer to auto-scroll+select
+ handle_selection_autoscroll();
+ return 1;
+ }
+ case FL_RELEASE: {
+ select_.push_clear();
+ select_.end();
+ // middlemouse gets immediate copy of selection
+ if (is_selection()) {
+ const char *copy = selection_text();
+ if (*copy) Fl::copy(copy, (int)strlen(copy), 0);
+ free((void*)copy);
+ }
+ return 1;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+/**
+ Handle FLTK events.
+*/
+int Fl_Terminal::handle(int e) {
+ int ret = Fl_Group::handle(e);
+ if (Fl::event_inside(vscroll_)) return ret; // early exit for scrollbar
+ switch (e) {
+ case FL_ENTER:
+ case FL_LEAVE:
+ return 1;
+ case FL_UNFOCUS:
+ case FL_FOCUS:
+ redraw();
+ return Fl::visible_focus() ? 1 : 0;
+ case FL_KEYBOARD:
+ // ^C -- Copy?
+ if ((Fl::event_state()&(FL_CTRL|FL_COMMAND)) && Fl::event_key()=='c') {
+ const char *copy = is_selection() ? selection_text() : fl_strdup(" ");
+ if (*copy) Fl::copy(copy, (int)strlen(copy), 1); // paste buffer
+ free((void*)copy);
+ return 1;
+ }
+ // ^A -- Select all?
+ if ((Fl::event_state()&(FL_CTRL|FL_COMMAND)) && Fl::event_key()=='a') {
+ // Select entire screen and history buffer
+ int srow = disp_srow() - hist_use();
+ int erow = disp_srow() + disp_rows()-1;
+ //DEBUG ::printf("CTRL-A: srow=%d erow=%d\n", srow, erow);
+ select_.select(srow, 0, erow, disp_cols()-1);
+ const char *copy = selection_text();
+ if (*copy) Fl::copy(copy, (int) strlen(copy), 0); // middle mouse buffer
+ free((void*)copy);
+ redraw();
+ return 1;
+ }
+ // Let scrollbar handle these when we have focus
+ if (Fl::focus() == this) {
+ switch (Fl::event_key()) {
+ case FL_Page_Up: case FL_Page_Down:
+ case FL_Up: case FL_Down:
+ case FL_Left: case FL_Right:
+ return vscroll_->handle(e);
+ }
+ }
+ break;
+ case FL_PUSH:
+ if (handle(FL_FOCUS)) Fl::focus(this); // Accepting focus? take it
+ if (Fl::event_button() == FL_LEFT_MOUSE) // LEFT-CLICK?
+ { ret = handle_selection(FL_PUSH); }
+ break;
+ case FL_DRAG:
+ // TODO: This logic can probably be improved to allow an FL_PUSH in margins
+ // to drag into terminal area to start a selection.
+ if (Fl::event_button() == FL_LEFT_MOUSE) // LEFT-DRAG?
+ { ret = handle_selection(FL_DRAG); }
+ break;
+ case FL_RELEASE:
+ // Selection mouse release?
+ if (Fl::event_button() == FL_LEFT_MOUSE) // LEFT-RELEASE?
+ { ret = handle_selection(FL_RELEASE); }
+ // Disable autoscroll timer, if any
+ if (autoscroll_dir_)
+ { Fl::remove_timeout(autoscroll_timer_cb, this); autoscroll_dir_ = 0; }
+ break;
+ } // switch
+ return ret;
+}
+
+/**
+ Get the redraw style.
+
+ This determines when the terminal redraws itself while text
+ is being added to it.
+
+ Value will be one of the Fl_Terminal::RedrawStyle enum values.
+
+ \see redraw_style(Fl_Terminal::RedrawStyle)
+*/
+Fl_Terminal::RedrawStyle Fl_Terminal::redraw_style() const {
+ return redraw_style_;
+}
+
+/**
+ Set how Fl_Terminal manages screen redrawing.
+
+ This setting is relevant when Fl_Terminal is used for high bandwidth
+ data; too many redraws will slow things down, too few cause redraws
+ to be 'choppy' when realtime data comes in.
+
+ Redrawing can be cpu intensive, depending on how many rows/cols are
+ being displayed; worst case: large display + small font. Speed largely
+ depends on the end user's graphics hardware and font drawing system.
+
+ \par
+ | RedrawStyle enum | Description
+ | :-------------------: | :-----------------------------------------------------------
+ | \ref NO_REDRAW | App must call redraw() as needed to update text to screen
+ | \ref RATE_LIMITED | Rate limited, timer controlled redraws. (DEFAULT) See redraw_rate()
+ | \ref PER_WRITE | Redraw triggered *every* call to append() / printf() / etc.
+
+ The default style is RATE_LIMITED, which is the easiest
+ to use, and automates redrawing to be capped at 10 redraws per second
+ max. See redraw_rate(float) to control this automated redraw speed.
+
+ \see redraw_rate(), RedrawStyle
+*/
+void Fl_Terminal::redraw_style(RedrawStyle val) {
+ redraw_style_ = val;
+ // Disable rate limit timer if it's being turned off
+ if (redraw_style_ != RATE_LIMITED && redraw_timer_)
+ { Fl::remove_timeout(redraw_timer_cb, this); redraw_timer_ = false; }
+}
+
+/**
+ Get max rate redraw speed in floating point seconds.
+*/
+float Fl_Terminal::redraw_rate(void) const {
+ return redraw_rate_;
+}
+
+/**
+ Set the maximum rate redraw speed in floating point seconds
+ if redraw_style() is set to RATE_LIMITED.
+
+ When output is sent to the terminal, rather than calling redraw()
+ right away, a timer is started with this value indicating how long
+ to wait before calling redraw(), causing the output to be shown.
+ 0.10 is recommended (1/10th of a second), to limit redraws to no
+ more than 10 redraws per second.
+
+ The value that works best depends on how fast data arrives, and
+ how fast the font system can draw text at runtime.
+
+ Values too small cause too many redraws to occur, causing the
+ terminal to get backlogged if large bursts of data arrive quickly.
+ Values too large cause realtime output to be too "choppy".
+*/
+void Fl_Terminal::redraw_rate(float val) {
+ redraw_rate_ = val;
+}
+
+/**
+ Return the "show unknown" flag.
+ See show_unknown(bool) for more info.
+*/
+bool Fl_Terminal::show_unknown(void) const {
+ return show_unknown_;
+}
+
+/**
+ Set the "show unknown" flag.
+
+ If true, unknown escape sequences and unprintable control characters
+ will be shown with the error character "¿".
+
+ If false, those sequences and characters will be ignored.
+
+ \see handle_unknown_char()
+*/
+void Fl_Terminal::show_unknown(bool val) {
+ show_unknown_ = val;
+}
+
+/**
+ Return the state of the ANSI flag.
+ \see ansi(bool)
+*/
+bool Fl_Terminal::ansi(void) const {
+ return ansi_;
+}
+
+/**
+ Enable/disable the ANSI mode flag.
+ If true, ANSI and VT100/xterm codes will be processed.
+ If false, these codes won't be processed and will either
+ be ignored or print the error character "¿", depending on
+ the value of show_unknown().
+ \see show_unknown(), \ref Fl_Terminal_escape_codes
+*/
+void Fl_Terminal::ansi(bool val) {
+ ansi_ = val;
+ // If disabled, reset the class to clear old state information
+ if (!ansi_) escseq.reset();
+}
+
+/**
+ Return the number of lines of screen history.
+*/
+int Fl_Terminal::history_lines(void) const { return history_rows(); }
+
+/**
+ Set the number of lines of screen history.
+ Large values can be briefly heavy on cpu and memory usage.
+*/
+void Fl_Terminal::history_lines(int val) { history_rows(val); }
+
+/**
+ Appends printf formatted messages to the terminal.
+
+ The string can contain UTF-8, crlf's, and ANSI sequences are
+ also supported. Example:
+ \par
+ \code
+ #include <FL/Fl_Terminal.H>
+ int main(..) {
+ :
+ // Create a terminal, and append some messages to it
+ Fl_Terminal *tty = new Fl_Terminal(..);
+ :
+ // Append three lines of formatted text to the buffer
+ tty->printf("The current date is: %s.\nThe time is: %s\n", date_str, time_str);
+ tty->printf("The current PID is %ld.\n", (long)getpid());
+ :
+ \endcode
+ \note The expanded string is currently limited to 1024 characters (including NULL).
+ For longer strings use append() which has no string limits.
+*/
+void Fl_Terminal::printf(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ Fl_Terminal::vprintf(fmt, ap);
+ va_end(ap);
+}
+
+/**
+ Appends printf formatted messages to the terminal.
+
+ Subclasses can use this to implement their own printf()
+ functionality.
+
+ The string can contain UTF-8, crlf's, and ANSI sequences are
+ also supported when ansi(bool) is set to 'true'.
+
+ \note The expanded string is currently limited to 1024 characters (including NULL).
+ For longer strings use append() which has no string limits.
+ \param fmt is a printf format string for the message text.
+ \param ap is a va_list created by va_start() and closed with va_end(),
+ which the caller is responsible for handling.
+*/
+void Fl_Terminal::vprintf(const char *fmt, va_list ap) {
+ char buffer[1024]; // XXX: should be user configurable..
+ ::vsnprintf(buffer, 1024, fmt, ap);
+ buffer[1024-1] = 0; // XXX: MICROSOFT
+ append(buffer);
+}
+
diff --git a/src/Makefile b/src/Makefile
index a8468e3b3..39a77d29c 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -94,6 +94,7 @@ CPPFILES = \
Fl_Table.cxx \
Fl_Table_Row.cxx \
Fl_Tabs.cxx \
+ Fl_Terminal.cxx \
Fl_Text_Buffer.cxx \
Fl_Text_Display.cxx \
Fl_Text_Editor.cxx \
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
+ }
+
+----------------------------------------------------------------------------------------
+
diff --git a/src/makedepend b/src/makedepend
index bbb4fe04d..0199f9d96 100644
--- a/src/makedepend
+++ b/src/makedepend
@@ -3878,6 +3878,28 @@ Fl_Tabs.o: ../FL/fl_utf8.h
Fl_Tabs.o: ../FL/Fl_Widget.H
Fl_Tabs.o: ../FL/Fl_Window.H
Fl_Tabs.o: ../FL/platform_types.h
+Fl_Terminal.o: ../FL/Enumerations.H
+Fl_Terminal.o: ../FL/Fl.H
+Fl_Terminal.o: ../FL/fl_attr.h
+Fl_Terminal.o: ../FL/Fl_Bitmap.H
+Fl_Terminal.o: ../FL/Fl_Cairo.H
+Fl_Terminal.o: ../FL/fl_casts.H
+Fl_Terminal.o: ../FL/fl_config.h
+Fl_Terminal.o: ../FL/fl_draw.H
+Fl_Terminal.o: ../FL/Fl_Export.H
+Fl_Terminal.o: ../FL/Fl_Group.H
+Fl_Terminal.o: ../FL/Fl_Image.H
+Fl_Terminal.o: ../FL/Fl_Rect.H
+Fl_Terminal.o: ../FL/Fl_Scrollbar.H
+Fl_Terminal.o: ../FL/Fl_Slider.H
+Fl_Terminal.o: ../FL/fl_string_functions.h
+Fl_Terminal.o: ../FL/Fl_Terminal.H
+Fl_Terminal.o: ../FL/fl_types.h
+Fl_Terminal.o: ../FL/fl_utf8.h
+Fl_Terminal.o: ../FL/Fl_Valuator.H
+Fl_Terminal.o: ../FL/Fl_Widget.H
+Fl_Terminal.o: ../FL/Fl_Window.H
+Fl_Terminal.o: ../FL/platform_types.h
Fl_Text_Buffer.o: ../config.h
Fl_Text_Buffer.o: ../FL/Enumerations.H
Fl_Text_Buffer.o: ../FL/Fl.H