diff --git a/FL/Fl_Terminal.H b/FL/Fl_Terminal.H new file mode 100644 index 000000000..4b7039f0c --- /dev/null +++ b/FL/Fl_Terminal.H @@ -0,0 +1,1013 @@ +// +// Fl_Terminal.cxx - 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 +// + +/** \file + Fl_Terminal widget. +*/ + +#ifndef Fl_Terminal_H +#define Fl_Terminal_H + +#include +#include +#include +#include +#include + +/** \class Fl_Terminal + + \brief Terminal widget supporting Unicode/utf-8, ANSI/xterm escape codes with full RGB color control. + + \section Fl_Terminal + + \image html Fl_Terminal-demo.png "Fl_Terminal widget showing a linux manual page" + \image latex Fl_Terminal-demo.png "Fl_Terminal widget showing a linux manual page" width=6cm + + Fl_Terminal is an output-only text widget supporting ASCII and UTF-8/Unicode. + It supports most terminal text features, such as most VT100/xterm style escape sequences + (see \ref Fl_Terminal_escape_codes), text colors/attributes, scrollback history, mouse selection, etc. + + It is recommended that accessing features such as setting text colors and cursor positioning + is best done with ANSI/XTERM escape sequences. But if one sets ansi(false) then this is + not possible. Many commonly used API functions are public, such as textfgcolor() for setting + text colors. Others, such as cursor_up() are protected to prevent common misuse, and are + available only to subclasses. + + For applications that need input support, the widget can be subclassed to provide + keyboard input, and advanced features like pseudo ttys, termio, serial port I/O, etc., + as such features are beyond the scope of FLTK. + + \subsection Fl_Terminal_Examples Examples + + \par + \code + // + // Fl_Terminal: Simple Use + // + Fl_Terminal *tty = new Fl_Terminal(...); + : + tty->append("Hello world.\n"); // simple strings + tty->append("\033[31mThis text is red.\033[0m\n"); // colored text + tty->append("\033[32mThis text is green.\033[0m\n"); + tty->printf("The value of x is %.02f\n", x); // printf() formatting + \endcode + \par + There are also public methods for doing what most "\033[" escape codes do, + so that if ansi(bool) is set to "false", one can still change text colors + or clear the screen via application control, e.g. + \par + \code + tty->home(); // home the cursor + tty->clear_screen(); // clear the screen + tty->textfgcolor(0xff000000); // change the text color to RED + tty->textbgcolor(0x0000ff00); // change the background color to BLUE + // ├┘├┘├┘ + // R G B + \endcode + \par + When creating the widget, the width/height determine the default column + and row count for the terminal's display based on the current font size. + The column width determines where text will wrap. + \par + You can specify wider column sizes than the screen using + display_columns(colwidth). When this value is larger than + the widget's width, text will wrap off-screen, and can be revealed by + resizing the widget wider. + + \subsection Fl_Terminal_Writing Writing To Terminal From Applications + + \par + An application needing terminal output as part of its user interface + can instance Fl_Terminal, and write text strings with: + \par + - append() to append strings + - printf() to append formatted strings + \par + Single character output can be done with: + \par + - print_char() to print a single ASCII/UTF-8 char at the cursor + - putchar() to put single ASCII/UTF-8 char at an x,y position + \par + + \subsection Fl_Terminal_Attributes Text Attributes + \par + The terminal's text supports these attributes: + \par + - Italic - italicized text: \\033[3m + - Bold - brighter/thicker text: \\033[1m + - Dim - lower brightness text: \\033[2m + - Underline - text that is underlined: \\033[4m + - Strikeout - text that has a line through the text: \\033[9m + - Inverse - text whose background and foreground colors are swapped: \\033[7m + - Normal - normal text: \\033[0m + \par + \image html Fl_Terminal-utf8-demo.png "Fl_Terminal screen" + \image latex Fl_Terminal-utf8-demo.png "Fl_Terminal screen" width=6cm + + \subsection Fl_Terminal_Colors Text and Background Colors + + \par + There's at least two ways to specify colors for text and background colors: + \par + - 3 bit / 8 Color Values + - Full 24 bit R/G/B colors + \par + Example of 3 bit colors: + \image html Fl_Terminal-3bit-colors.png "Fl_Terminal 3 bit colors" + \image latex Fl_Terminal-3bit-colors.png "Fl_Terminal 3 bit colors" width=6cm + \par + Example application source code using 3 bit colors: + \code + // + // Text colors + // + tty->append("\033[31m Red text.\033[0m\n"); // Print red text.. + tty->append("\033[32m Green text.\033[0m\n"); + : + tty->append("\033[36m Cyan text.\033[0m\n"); + tty->append("\033[37m White text.\033[0m\n"); + // + // Background colors + // + tty->append("\033[41m Red Background.\033[0m\n"); // background will be red + tty->append("\033[42m Green Background.\033[0m\n"); + : + tty->append("\033[46m Cyan Background.\033[0m\n"); + tty->append("\033[47m White Background.\033[0m\n"); + \endcode + \par + Example of 24 bit colors: + \image html Fl_Terminal-24bit-colors.png "Fl_Terminal 24 bit colors" + \image latex Fl_Terminal-24bit-colors.png "Fl_Terminal 24 bit colors" width=6cm + \par + Example application source code using 24 bit colors: + \code + // + // 24 bit Text Color + // + tty->append("\033[38;2;0;0;255m Text is BLUE.\033[0m\n"); // RGB: R=0, G=0, B=255 + tty->append("\033[38;2;255;0;0m Text is RED.\033[0m\n"); // RGB: R=255, G=0, B=0 + tty->append("\033[38;2;127;64;0m Text is DARK ORANGE.\033[0m\n"); // RGB: R=127, G=64, B=0 + // + // 24 bit Background Color + // + tty->append("\033[48;2;0;0;255m Background is BLUE.\033[0m\n"); // RGB: R=0, G=0, B=255 + tty->append("\033[48;2;255;0;0m Background is RED.\033[0m\n"); // RGB: R=255, G=0, B=0 + tty->append("\033[48;2;127;64;0m Background is DARK ORANGE.\033[0m\n"); // RGB: R=127, G=64, B=0 + \endcode + \par + For more on the ANSI escape codes, see \ref Fl_Terminal_escape_codes. + + \subsection Fl_Terminal_Features Features + + \par + Most standard terminal behaviors are supported, e.g. + \par + - ASCII + UTF-8/Unicode + - scrollback history management + - mouse selection + copy/paste (^C, ^A) + - autoscroll during selection + \par + Most popular ANSI/DEC VT100/Xterm escape sequences are supported (see \ref Fl_Terminal_escape_codes), including: + - per-character colors for text and background + - per-character text attributes: bold/dim, underline, strikeout + - scrolling up/down + - character insert/delete for characters/rows/screen + - clearing characters/rows/screen + \par + Does not (yet) support: + - programmable regions (scroll regions and attribute blocks) + - dynamic line wrap (where resizing display dynamically re-wraps long lines) + \par + Will likely never implement as part of this widget: + - pty/termio management (such features should be _subclassed_) + - Different per-character font family + sizes (font family/size is global only) + - variable width fonts + \par + Regarding the font family+size; the way the terminal is currently designed, + the font family and size must not vary within text; rows have to be consistent + height. Varying widths are tricky too, esp. when it comes to moving the cursor + up/down within a column; varying *widths* are supported (due to Unicode characters + sometimes being "wide", but not heights. + + \subsection Fl_Terminal_Margins Margins + + \par + The margins define the amount of space (in pixels) around the outside of the + text display area, the space between the widget's inner edge (inside the box()) + and the text display area's outer edge. The margins can be inspected and changed + with the margin_left(), margin_right(), margin_top() and margin_bottom() methods. + \par + \code +· + TERMINAL WIDGET (Fl_Terminal) + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━┓ + ┃ ↑ margin_top ┃ ┃ + ┃ TEXT DISPLAY AREA ↓ ┃ ┃ + ┃ ┌──────────────────────────────────────────────────────┐ ┃ ┃ + ┃ │ ↑ │ ┃ S ┃ + ┃ │ │ │ ┃ c ┃ + ┃ │ display_rows │ ┃ r ┃ + ┃ │ │ │ ┃ o ┃ + ┃ │ │ │ ┃ l ┃ + ┃ │ │ │ ┃ l ┃ + ┃ │◄───display_columns────────┼─────────────────────────►│ ┃ ┃ + ┃ │ │ │ ┃ B ┃ + ┃ │ │ │ ┃ a ┃ + ┃ │ │ │ ┃ r ┃ + ┃ │ │ │ ┃ ┃ + ┃ │ │ │ ┃ ┃ + ┃◄──┬──►│ ↓ │◄──┬──►┃ ┃ + ┃ │ └──────────────────────────────────────────────────────┘ │ ┃ ┃ + ┃ margin_left ↑ margin_right ┃ ┃ + ┃ ↓ margin_bottom ┃ ┃ + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━┛ + Fl_Terminal Margins +· + \endcode + + \subsection Fl_Terminal_Caveats Caveats + + \par + - This widget is not a full terminal emulator; it does not do stdio redirection, + pseudo ttys/termios/character cooking, keyboard input processing, full + curses(3) support. However, such features CAN be implemented with subclassing. + \par + - The printf() and vprintf() functions are limited to strings no longer than + 1024 characters (including NULL). For printing longer strings, use append() + which has no string limits. +**/ + +class Fl_Terminal : public Fl_Group { +public: + ////////////////////////////////////// + ////// Fl_Terminal Public Enums ////// + ////////////////////////////////////// + + /** + \enum RedrawStyle + Determines when Fl_Terminal calls redraw() if new text is added. + RATE_LIMITED is the recommended setting, using redraw_rate(float) to determine + the maximum rate of redraws. + \see redraw_style(), redraw_rate() + */ + enum RedrawStyle { + NO_REDRAW=0, ///< app must call redraw() as needed to update text to screen + RATE_LIMITED, ///< timer controlled redraws. (DEFAULT) + PER_WRITE ///< redraw triggered after *every* append() / printf() / etc. operation + }; + + /** + \enum Attrib + Bits for the per-character attributes, which control text features + such as italic, bold, underlined text, etc. + + NOTE: enum names with a leading underbar must not be used, and are + reserved for future use. + */ + enum Attrib { + NORMAL = 0x00, ///< all attributes off + BOLD = 0x01, ///< bold text: uses bold font, color brighter than normal + DIM = 0x02, ///< dim text; color slightly darker than normal + ITALIC = 0x04, ///< italic font text + UNDERLINE = 0x08, ///< underlined text + _RESERVED_1 = 0x10, ///< (reserved for internal future use) + INVERSE = 0x20, ///< inverse text; fg/bg color are swapped + _RESERVED_2 = 0x40, ///< (reserved for internal future use) + STRIKEOUT = 0x80 ///< strikeout text + }; + + /** + \enum CharFlags + Per-character 8 bit flags (uchar) used to manage special states for characters. + */ + enum CharFlags { + FG_XTERM = 0x01, ///< this char's fg color is an XTERM color; can be affected by Dim+Bold + BG_XTERM = 0x02, ///< this char's bg color is an XTERM color; can be affected by Dim+Bold + EOL = 0x04, ///< this char at end of line, used for line wrap during screen resizing + RESV_A = 0x08, + RESV_B = 0x10, + RESV_C = 0x20, + RESV_D = 0x40, + RESV_E = 0x80, + COLORMASK = (FG_XTERM | BG_XTERM) + }; + +protected: + // Margin Class //////////////////////////////////////////// + // + // Class to manage the terminal's margins + // + class Margin { + int left_, right_, top_, bottom_; + public: + Margin(void) { left_ = right_ = top_ = bottom_ = 3; } + int left(void) const { return left_; } + int right(void) const { return right_; } + int top(void) const { return top_; } + int bottom(void) const { return bottom_; } + void left(int val) { left_ = val; } + void right(int val) { right_ = val; } + void top(int val) { top_ = val; } + void bottom(int val) { bottom_ = val; } + }; + + // CharStyle Class //////////////////////////////////////////// + // + // Class to manage the terminal's character style + // This includes the font, color, and some cached internal + // info for optimized drawing speed. + // + class CharStyle { + uchar attrib_; // bold, underline.. + uchar flags_; // CharFlags + Fl_Color fgcolor_; // foreground color for text + Fl_Color bgcolor_; // background color for text + Fl_Color defaultfgcolor_; // default fg color used by ESC[0m + Fl_Color defaultbgcolor_; // default bg color used by ESC[0m + Fl_Font fontface_; // font face + Fl_Fontsize fontsize_; // font size + int fontheight_; // font height (in pixels) + int fontdescent_; // font descent (pixels below font baseline) + int charwidth_; // width of a fixed width ASCII character + public: + CharStyle(void); + Fl_Color fgcolor(void) const; + Fl_Color bgcolor(void) const; + Fl_Color defaultfgcolor(void) const { return defaultfgcolor_; } + Fl_Color defaultbgcolor(void) const { return defaultbgcolor_; } + uchar attrib(void) const { return attrib_; } + Fl_Font fontface(void) const { return fontface_; } + Fl_Fontsize fontsize(void) const { return fontsize_; } + int fontheight(void) const { return fontheight_; } + int fontdescent(void) const { return fontdescent_; } + int charwidth(void) const { return charwidth_; } + uchar colorbits_only(uchar inflags) const; + void attrib(uchar val) { attrib_ = val; } + void set_flag(uchar val) { flags_ |= val; } + void clr_flag(uchar val) { flags_ &= ~val; } + void fgcolor_uchar(uchar val); + void bgcolor_uchar(uchar val); + void fgcolor(int r,int g,int b) { fgcolor_ = (r<<24) | (g<<16) | (b<<8); clr_flag(FG_XTERM); } + void bgcolor(int r,int g,int b) { bgcolor_ = (r<<24) | (g<<16) | (b<<8); clr_flag(BG_XTERM); } + void fgcolor(Fl_Color val) { fgcolor_ = val; clr_flag(FG_XTERM); } + void bgcolor(Fl_Color val) { bgcolor_ = val; clr_flag(BG_XTERM); } + void defaultfgcolor(Fl_Color val) { defaultfgcolor_ = val; } + void defaultbgcolor(Fl_Color val) { defaultbgcolor_ = val; } + void fontface(Fl_Font val) { fontface_ = val; update(); } + void fontsize(Fl_Fontsize val) { fontsize_ = val; update(); } + void update(void); + // SGR MODES: Set Graphics Rendition + void sgr_reset(void) { // e.g. ESC[0m + attrib(Fl_Terminal::NORMAL); + fgcolor(defaultfgcolor_); + bgcolor(defaultbgcolor_); + } + int onoff(bool flag, Attrib a) { return (flag ? (attrib_ | a) : (attrib_ & ~a)); } + void sgr_bold(bool val) { attrib_ = onoff(val, Fl_Terminal::BOLD); } // e.g. ESC[1m + void sgr_dim(bool val) { attrib_ = onoff(val, Fl_Terminal::DIM); } // e.g. ESC[2m + void sgr_italic(bool val) { attrib_ = onoff(val, Fl_Terminal::ITALIC); } // e.g. ESC[3m + void sgr_underline(bool val) { attrib_ = onoff(val, Fl_Terminal::UNDERLINE); } // e.g. ESC[3m + void sgr_dbl_under(bool val) { attrib_ = onoff(val, Fl_Terminal::UNDERLINE); } // e.g. ESC[21m (TODO!) + void sgr_blink(bool val) { /* NOT IMPLEMENTED */ } // e.g. ESC[5m + void sgr_inverse(bool val) { attrib_ = onoff(val, Fl_Terminal::INVERSE); } // e.g. ESC[7m + void sgr_strike(bool val) { attrib_ = onoff(val, Fl_Terminal::STRIKEOUT); } // e.g. ESC[9m + }; + +protected: + // Cursor Class /////////////////////////////////////////////////////////// + // + // Class to manage the terminal's cursor position, color, etc. + // + class Cursor { + int col_; // cursor's current col (x) position on display + int row_; // cursor's current row (y) position on display + int h_; // cursor's height (affected by font size) + Fl_Color fgcolor_; // cursor's fg color (color of text, if any) + Fl_Color bgcolor_; // cursor's bg color + public: + Cursor(void) { + col_ = 0; + row_ = 0; + h_ = 10; + fgcolor_ = 0xfffff000; // wht + bgcolor_ = 0x00d00000; // grn + } + int col(void) const { return col_; } + int row(void) const { return row_; } + int h(void) const { return h_; } + Fl_Color fgcolor(void) const { return fgcolor_; } + Fl_Color bgcolor(void) const { return bgcolor_; } + void col(int val) { col_ = val >= 0 ? val : 0; } + void row(int val) { row_ = val >= 0 ? val : 0; } + void h(int val) { h_ = val; } + void fgcolor(Fl_Color val) { fgcolor_ = val; } + void bgcolor(Fl_Color val) { bgcolor_ = val; } + int left(void) { col_ = (col_>0) ? (col_-1) : 0; return col_; } + int right(void) { return ++col_; } + int up(void) { row_ = (row_>0) ? (row_-1) : 0; return row_; } + int down(void) { return ++row_; } + bool is_rowcol(int drow,int dcol) const; + void scroll(int nrows); + void home(void) { row_ = 0; col_ = 0; } + }; + + // Utf8Char Class /////////////////////////////////////////////////////////// + // + // Class to manage the terminal's individual UTF-8 characters. + // Includes fg/bg color, attributes (BOLD, UNDERLINE..) + // + class Utf8Char { + static const int max_utf8_ = 4; // RFC 3629 paraphrased: In UTF-8, chars are encoded with 1 to 4 octets + char text_[max_utf8_]; // memory for actual ASCII or UTF-8 byte contents + uchar len_; // length of bytes in text_[] buffer; 1 for ASCII, >1 for UTF-8 + uchar attrib_; // attribute bits for this char (bold, underline..) + uchar flags_; // CharFlags bits + Fl_Color fgcolor_; // fltk fg color (supports 8color or 24bit color set w/ESC[37;;;m) + Fl_Color bgcolor_; // fltk bg color (supports 8color or 24bit color set w/ESC[47;;;m) + // Private methods + void text_utf8_(const char *text, int len); + public: + // Public methods + Utf8Char(void); // ctor + Utf8Char(const Utf8Char& o); // copy ctor + ~Utf8Char(void); // dtor + Utf8Char& operator=(const Utf8Char& o); // assignment + inline int max_utf8() const { return max_utf8_; } + void text_utf8(const char *text, int len, const CharStyle& style); + void text_ascii(char c, const CharStyle& style); + void fl_font_set(const CharStyle& style) const; + + // Return the UTF-8 text string for this character. + // Use length() to get number of bytes in string, which will be 1 for ASCII chars. + // + const char* text_utf8(void) const { return text_; } + // Return the attribute for this char + uchar attrib(void) const { return attrib_; } + uchar flags(void) const { return flags_; } + Fl_Color fgcolor(void) const; + Fl_Color bgcolor(void) const; + // Return the length of this character in bytes (UTF-8 can be multibyte..) + int length(void) const { return int(len_); } + double pwidth(void) const; + int pwidth_int(void) const; + // Clear the character to a 'space' + void clear(const CharStyle& style) { text_ascii(' ', style); } + bool is_char(char c) const { return *text_ == c; } + void show_char(void) const { ::printf("%.*s", len_, text_); } + void show_char_info(void) const { ::fprintf(stderr, "UTF-8('%.*s', len=%d)\n", len_, text_, len_); } + + Fl_Color attr_color(Fl_Color col, const Fl_Widget *grp) const; + Fl_Color attr_fg_color(const Fl_Widget *grp) const; + Fl_Color attr_bg_color(const Fl_Widget *grp) const; + }; + + // RingBuffer Class /////////////////////////////////////////////////// + // + // Manages ring with indexed row/col and "history" vs. "display" concepts. + // + class RingBuffer { + Utf8Char *ring_chars_; // the ring UTF-8 char buffer + int ring_rows_; // #rows in ring total + int ring_cols_; // #columns in ring/hist/disp + int nchars_; // #chars in ring (ring_rows*ring_cols) + int hist_rows_; // #rows in history + int hist_use_; // #rows in use by history + int disp_rows_; // #rows in display + int offset_; // index offset (used for 'scrolling') + +private: + void new_copy(int drows, int dcols, int hrows, const CharStyle& style); + //DEBUG void write_row(FILE *fp, Utf8Char *u8c, int cols) const { + //DEBUG cols = (cols != 0) ? cols : ring_cols(); + //DEBUG for ( int col=0; collength(), u8c->text_utf8()); + //DEBUG } + //DEBUG } + public: + void clear(void); + void clear_hist(void); + RingBuffer(void); + RingBuffer(int drows, int dcols, int hrows); + ~RingBuffer(void); + + // Methods to access ring + // + // The 'offset' concept allows the 'history' and 'display' + // to be scrolled indefinitely. The 'offset' is applied + // to all the row accesses, and are clamped to within their bounds. + // + // For 'raw' access to the ring (without the offset concept), + // use the ring_chars() method, and walk from 0 - ring_rows(). + // + // _____________ + // | | <- hist_srow() <- ring_srow() + // | H i s t | + // | | + // |_____________| <- hist_erow() + // | | <- disp_srow() + // | D i s p | + // | | + // |_____________| <- disp_erow() <- ring_erow() + // + // \___________/ + // ring_cols() + // hist_cols() + // disp_cols() + // + inline int ring_rows(void) const { return ring_rows_; } + inline int ring_cols(void) const { return ring_cols_; } + inline int ring_srow(void) const { return(0); } + inline int ring_erow(void) const { return(ring_rows_ - 1); } + inline int hist_rows(void) const { return hist_rows_; } + inline int hist_cols(void) const { return ring_cols_; } + inline int hist_srow(void) const { return((offset_ + 0) % ring_rows_); } + inline int hist_erow(void) const { return((offset_ + hist_rows_ - 1) % ring_rows_); } + inline int disp_rows(void) const { return disp_rows_; } + inline int disp_cols(void) const { return ring_cols_; } + inline int disp_srow(void) const { return((offset_ + hist_rows_) % ring_rows_); } + inline int disp_erow(void) const { return((offset_ + hist_rows_ + disp_rows_ - 1) % ring_rows_); } + inline int offset(void) const { return offset_; } + // History use + inline int hist_use(void) const { return hist_use_; } + inline void hist_use(int val) { hist_use_ = val; } + inline int hist_use_srow(void) const { return((offset_ + hist_rows_ - hist_use_) % ring_rows_); } + inline Utf8Char *ring_chars(void) { return ring_chars_; } // access ring buffer directly + inline Utf8Char *ring_chars(void) const { return ring_chars_; } // access ring buffer directly + + bool is_hist_ring_row(int grow) const; + bool is_disp_ring_row(int grow) const; + //DEBUG void show_ring_info(void) const; + void move_disp_row(int src_row, int dst_row); + void clear_disp_row(int drow, const CharStyle& style); + void scroll(int rows, const CharStyle& style); + + const Utf8Char* u8c_ring_row(int row) const; + const Utf8Char* u8c_hist_row(int hrow) const; + const Utf8Char* u8c_hist_use_row(int hurow) const; + const Utf8Char* u8c_disp_row(int drow) const; + + // Non-const versions of the above methods + // Using "Effective C++" ugly-as-hell syntax technique. + // + Utf8Char* u8c_ring_row(int row); + Utf8Char* u8c_hist_row(int hrow); + Utf8Char* u8c_hist_use_row(int hurow); + Utf8Char* u8c_disp_row(int drow); + + void create(int drows, int dcols, int hrows); + void resize(int drows, int dcols, int hrows, const CharStyle& style); + + void change_disp_rows(int drows, const CharStyle& style); + void change_disp_cols(int dcols, const CharStyle& style); + }; + + // Selection Class /////////////////////////////////////////////////// + // + // Class to manage mouse selection + // + class Selection { + int srow_, scol_, erow_, ecol_; // selection start/end. NOTE: start *might* be > end + int push_row_, push_col_; // global row/col for last FL_PUSH + Fl_Color selectionbgcolor_; + Fl_Color selectionfgcolor_; + int state_ ; // 0=none, 1=started, 2=extended, 3=done + bool is_selection_; // false: no selection + public: + Selection(void); + int srow(void) const { return srow_; } + int scol(void) const { return scol_; } + int erow(void) const { return erow_; } + int ecol(void) const { return ecol_; } + void push_clear() { push_row_ = push_col_ = -1; } + void push_rowcol(int row,int col) { push_row_ = row; push_col_ = col; } + void start_push() { start(push_row_, push_col_); } + bool dragged_off(int row,int col) { return (push_row_ != row) || (push_col_ != col); } + void selectionfgcolor(Fl_Color val) { selectionfgcolor_ = val; } + void selectionbgcolor(Fl_Color val) { selectionbgcolor_ = val; } + Fl_Color selectionfgcolor(void) const { return selectionfgcolor_; } + Fl_Color selectionbgcolor(void) const { return selectionbgcolor_; } + bool is_selection(void) const { return is_selection_; } + bool get_selection(int &srow,int &scol,int &erow,int &ecol) const; // guarantees return (start < end) + bool start(int row, int col); + bool extend(int row, int col); + void end(void); + void select(int srow, int scol, int erow, int ecol); + bool clear(void); + int state(void) const { return state_; } + void scroll(int nrows); + }; + + // EscapeSeq Class /////////////////////////////////////////////////// + // + // Class to handle parsing ESC sequences + // + // Holds all state information for parsing esc sequences, + // so sequences can span multiple block read(2) operations, etc. + // Handling of parsed sequences is NOT handled in this class, + // just the parsing of the sequences and managing generic integers. + // + class EscapeSeq { + public: + // EscapeSeq Constants + // Maximums + static const int maxbuff = 80; // character buffer + static const int maxvals = 20; // integer value buffer + // Return codes + static const int success = 0; // operation succeeded + static const int fail = -1; // operation failed + static const int completed = 1; // multi-step operation completed successfully + private: + char esc_mode_; // escape parsing mode state + char csi_; // This is an ESC[.. sequence (Ctrl Seq Introducer) + char buff_[maxbuff]; // escape sequence being parsed + char *buffp_; // parsing ptr into buff[] + char *buffendp_; // end of buff[] (ptr to last valid buff char) + char *valbuffp_; // pointer to first char in buff of integer being parsed + int vals_[maxvals]; // value array for parsing #'s in ESC[#;#;#.. + int vali_; // parsing index into vals_[], 0 if none + int save_row_, save_col_; // used by ESC[s/u for save/restore + + int append_buff(char c); + int append_val(void); + + public: + EscapeSeq(void); + void reset(void); + char esc_mode(void) const; + void esc_mode(char val); + int total_vals(void) const; + int val(int i) const; + int defvalmax(int dval, int max) const; + bool parse_in_progress(void) const; + bool is_csi(void) const; + int parse(char c); + void save_cursor(int row, int col); + void restore_cursor(int &row, int &col); + }; + + // Partial UTF-8 Buffer Class //////////////////////////////////////////// + // + // Class to manage buffering partial UTF-8 characters between write calls. + // + class PartialUtf8Buf { + char buf_[10]; // buffer partial UTF-8 encoded char + int buflen_; // length of buffered UTF-8 encoded char + int clen_; // final byte length of a UTF-8 char + public: + void clear(void) { buflen_ = clen_ = 0; } // clear the buffer + PartialUtf8Buf(void) { clear(); } // Ctor + // Is byte 'c' in the middle of a UTF-8 encoded byte sequence? + bool is_continuation(char c) { + // Byte 1 Byte 2 Byte 3 ..etc.. + // ASCII: 0xxxxxxx + // UTF8(2): 110xxxxx 10xxxxxx + // UTF8(3): 1110xxxx 10xxxxxx 10xxxxxx + // UTF8(4): 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + // UTF8(5): 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + // UTF8(6): 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + // \______/ \______________________________________________/ + // Start byte Continuation bytes + // (c & 0xc0) == 0x80 + return ((c & 0xc0) == 0x80); + } + // Access buffer + const char* buf(void) const { return buf_; } + // Access buffer length + int buflen(void) const { return buflen_; } + // Append bytes of a partial UTF-8 string to the buffer. + // + // Returns: + // - true if done OK. Use is_complete() to see if a complete char received. + // - false if buffer overrun occurred, class is clear()ed. + // + // An appropriate response to 'false' would be to print the + // "unknown character" and skip all subsequent UTF-8 continuation chars. + // + bool append(const char* p, int len) { + if (len <= 0) return true; // ignore silly requests: say we did but dont + if (buflen_ + len >= (int)sizeof(buf_)) // overrun check + { clear(); return false; } // clear self, return false + if (!buflen_) clen_ = fl_utf8len(*p); // first byte? save char len for later + while (len>0) { buf_[buflen_++] = *p++; len--; } // append byte to buffer + return true; + } + bool is_complete(void) const { return (buflen_ && (buflen_ == clen_)); } + }; + +private: + // Fl_Terminal Members + Fl_Scrollbar *vscroll_; // vertical scrollbar (value: rows above disp_chars[]) + int scrollbar_size_; // local preference for scrollbar size + CharStyle current_style_; // current font, attrib, color.. + + // A ring buffer is used for the terminal's history (hist) and display (disp) buffer. + // See README-Fl_Terminal.txt, section "RING BUFFER DESCRIPTION" for diagrams/info. + // + // Ring buffer + RingBuffer ring_; // terminal history/display ring buffer + Cursor cursor_; // terminal cursor (position, color, etc) + Margin margin_; // terminal margins (top,left,bottom,right) + Selection select_; // mouse selection + EscapeSeq escseq; // Escape sequence parsing (ESC[ xterm/vt100) + bool show_unknown_; // if true, show unknown chars as '¿' (default off) + bool ansi_; // if true, parse ansi codes (default on) + char *tabstops_; // array of tab stops (0|1) \__ TODO: This should probably + int tabstops_size_; // size of tabstops[] array / be a class "TabStops". + Fl_Rect scrn_; // terminal screen xywh inside box(), margins, and vscroll + int autoscroll_dir_; // 0=autoscroll timer off, 3=scrolling up, 4=scrolling down + int autoscroll_amt_; // #pixels above or below edge, used for autoscroll speed + RedrawStyle redraw_style_; // NO_REDRAW, RATE_LIMITED, PER_WRITE + float redraw_rate_; // maximum redraw rate in seconds, default=0.10 + bool redraw_modified_; // display modified; used by update_cb() to rate limit redraws + bool redraw_timer_; // if true, redraw timer is running + PartialUtf8Buf pub_; // handles Partial Utf8 Buffer (pub) + +protected: + // Ring buffer management + const Utf8Char* u8c_ring_row(int grow) const; + const Utf8Char* u8c_hist_row(int hrow) const; + const Utf8Char* u8c_hist_use_row(int hrow) const; + const Utf8Char* u8c_disp_row(int drow) const; + // non-const versions of the above. + // "Effective C++" says: implement non-const method to cast away const + // + Utf8Char* u8c_ring_row(int grow); + Utf8Char* u8c_hist_row(int hrow); + Utf8Char* u8c_hist_use_row(int hurow); + Utf8Char* u8c_disp_row(int drow); +private: + void create_ring(int drows, int dcols, int hrows); +protected: + Utf8Char* u8c_cursor(void); +private: + int vscroll_width(void) const; + // Tabstops + void init_tabstops(int newsize); + void default_tabstops(void); + void clear_all_tabstops(void); + void set_tabstop(void); + void clear_tabstop(void); + // Updates + void update_screen_xywh(void); + void update_screen(bool font_changed); + void update_scrollbar(void); + // Resize + void resize_display_rows(int drows); + void resize_display_columns(int dcols); + void refit_disp_to_screen(void); + // Callbacks + static void scrollbar_cb(Fl_Widget*, void*); // scrollbar manipulation + static void autoscroll_timer_cb(void*); // mouse drag autoscroll + void autoscroll_timer_cb2(void); + static void redraw_timer_cb(void*); // redraw rate limiting timer + void redraw_timer_cb2(void); + + // Screen management +protected: + const CharStyle& current_style(void) const; + void current_style(const CharStyle& sty); + int x_to_glob_col(int X, int grow, int &gcol) const; + int xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol) const; + int w_to_col(int W) const; + int h_to_row(int H) const; + // API: Display clear operations + void clear_screen(bool scroll_to_hist=true); + void clear_screen_home(bool scroll_to_hist=true); + void clear_sod(void); + void clear_eod(void); + void clear_eol(void); + void clear_sol(void); + void clear_line(int row); + void clear_line(void); + const Utf8Char* walk_selection(const Utf8Char *u8c, int &row, int &col) const; + bool get_selection(int &srow,int &scol,int &erow,int &ecol) const; + bool is_selection(void) const; + bool is_inside_selection(int row,int col) const; + bool is_hist_ring_row(int grow) const; + bool is_disp_ring_row(int grow) const; + int selection_text_len(void) const; + const char* selection_text(void) const; + void clear_mouse_selection(void); + bool selection_extend(int X,int Y); + void scroll(int rows); + void insert_rows(int count); + void delete_rows(int count); + void insert_char_eol(char c, int drow, int dcol, int rep); + void insert_char(char c, int rep); + void delete_chars(int drow, int dcol, int rep); + void delete_chars(int rep); + // History + void history_use(int val, bool update=true); +public: + // API: Terminal operations + void clear_history(void); // ESC [ 3 J + void reset_terminal(void); // ESC c +protected: + // Cursor management + int cursor_h(void) const; +public: + // API: Cursor + void cursorfgcolor(Fl_Color val); + void cursorbgcolor(Fl_Color val); + Fl_Color cursorfgcolor(void) const; + Fl_Color cursorbgcolor(void) const; +protected: + void cursor_row(int row); + void cursor_col(int col); +public: + int cursor_row(void) const; + int cursor_col(void) const; +protected: + void cursor_up(int count=1, bool do_scroll=false); + void cursor_down(int count=1, bool do_scroll=false); + void cursor_left(int count=1); + void cursor_right(int count=1, bool do_scroll=false); + void cursor_home(void); + void cursor_eol(void); + void cursor_sol(void); + void cursor_cr(void); + void cursor_crlf(int count=1); + void cursor_tab_right(int count=1); + void cursor_tab_left(int count=1); + void save_cursor(void); + void restore_cursor(void); + // Printing + void handle_ctrl(char c); + bool is_printable(char c); + bool is_ctrl(char c); + void handle_SGR(void); + void handle_DECRARA(void); + void handle_escseq(char c); + // -- + void display_modified(void); + void display_modified_clear(void); + void clear_char_at_disp(int drow, int dcol); + const Utf8Char* utf8_char_at_disp(int drow, int dcol) const; + const Utf8Char* utf8_char_at_glob(int grow, int gcol) const; + void repeat_char(char c, int rep); + void utf8_cache_clear(void); + void utf8_cache_flush(void); + + // API: Character display output +public: + void putchar(const char *text, int len, int drow, int dcol); + void putchar(char c, int drow, int dcol); + void print_char(const char *text, int len=-1); + void print_char(char c); + // API: String display output + void append_utf8(const char *buf, int len=-1); + void append_ascii(const char *s); + void append(const char *s, int len=-1); +protected: + int handle_unknown_char(void); + // Drawing + void draw_row_bg(int grow, int X, int Y) const; + void draw_row(int grow, int Y) const; + void draw_buff(int Y) const; + void handle_selection_autoscroll(void); + int handle_selection(int e); +public: + // FLTK: draw(), resize(), handle() + void draw(void) FL_OVERRIDE; + void resize(int X,int Y,int W,int H) FL_OVERRIDE; + int handle(int e) FL_OVERRIDE; + +protected: + // Internal short names + // Don't make these public, but allow internals and + // derived classes to maintain brevity. + // + inline int ring_rows(void) const { return ring_.ring_rows(); } + inline int ring_cols(void) const { return ring_.ring_cols(); } + inline int ring_srow(void) const { return ring_.ring_srow(); } + inline int ring_erow(void) const { return ring_.ring_erow(); } + inline int hist_rows(void) const { return ring_.hist_rows(); } + inline int hist_cols(void) const { return ring_.hist_cols(); } + inline int hist_srow(void) const { return ring_.hist_srow(); } + inline int hist_erow(void) const { return ring_.hist_erow(); } + inline int hist_use(void) const { return ring_.hist_use(); } + inline int hist_use_srow(void) const { return ring_.hist_use_srow(); } + inline int disp_rows(void) const { return ring_.disp_rows(); } + inline int disp_cols(void) const { return ring_.disp_cols(); } + inline int disp_srow(void) const { return ring_.disp_srow(); } + inline int disp_erow(void) const { return ring_.disp_erow(); } + inline int offset(void) const { return ring_.offset(); } + + // TODO: CLEAN UP WHAT'S PUBLIC, AND WHAT SHOULD BE 'PROTECTED' AND 'PRIVATE' + // Some of the public stuff should, quite simply, "not be". + + // API: Terminal features +public: + // API: Scrollbar + int scrollbar_size(void) const; + void scrollbar_size(int val); + int scrollbar_actual_size(void) const; + // API: History + int history_rows(void) const; + void history_rows(int val); + int history_use(void) const; + // API: Display + int display_rows(void) const; + void display_rows(int val); + int display_columns(void) const; + void display_columns(int val); + // API: Box + /// Sets the box type, updates terminal margins et al. Default is FL_DOWN_FRAME. + /// + /// FL_XXX_FRAME types are handled in a special way by this widget, and guarantee + /// the background is a flat field. + /// + /// FL_XXX_BOX may draw gradients as inherited by Fl::scheme(). + /// + void box(Fl_Boxtype val) { Fl_Group::box(val); update_screen(false); } + /// Returns the current box type. + Fl_Boxtype box(void) const { return Fl_Group::box(); } + // API: Margins + /// Return the left margin; see \ref Fl_Terminal_Margins. + int margin_left(void) const { return margin_.left(); } + /// Return the right margin; see \ref Fl_Terminal_Margins. + int margin_right(void) const { return margin_.right(); } + /// Return the top margin; see \ref Fl_Terminal_Margins. + int margin_top(void) const { return margin_.top(); } + /// Return the bottom margin; see \ref Fl_Terminal_Margins. + int margin_bottom(void) const { return margin_.bottom(); } + void margin_left(int val); + void margin_right(int val); + void margin_top(int val); + void margin_bottom(int val); + // API: Text font/size/color + void textfont(Fl_Font val); + void textsize(Fl_Fontsize val); + void textfgcolor(Fl_Color val); + void textbgcolor(Fl_Color val); + void textfgcolor_default(Fl_Color val); + void textbgcolor_default(Fl_Color val); + /// Return text font used to draw all text in the terminal. + Fl_Font textfont(void) const { return current_style_.fontface(); } + /// Return text font size used to draw all text in the terminal. + Fl_Fontsize textsize(void) const { return current_style_.fontsize(); } + /// Return text's current foreground color. + Fl_Color textfgcolor(void) const { return current_style_.fgcolor(); } + /// Return text's current background color. + Fl_Color textbgcolor(void) const { return current_style_.bgcolor(); } + /// Return text's default foreground color. \see textfgcolor() + Fl_Color textfgcolor_default(void) const { return current_style_.defaultfgcolor(); } + /// Return text's default background color. \see textbgcolor() + Fl_Color textbgcolor_default(void) const { return current_style_.defaultbgcolor(); } + void textfgcolor_xterm(uchar val); + void textbgcolor_xterm(uchar val); + /// Set mouse selection foreground color. + void selectionfgcolor(Fl_Color val) { select_.selectionfgcolor(val); } + /// Set mouse selection background color. + void selectionbgcolor(Fl_Color val) { select_.selectionbgcolor(val); } + /// Get mouse selection foreground color. + Fl_Color selectionfgcolor(void) const { return select_.selectionfgcolor(); } + /// Get mouse selection background color. + Fl_Color selectionbgcolor(void) const { return select_.selectionbgcolor(); } + // API: Text attrib + void textattrib(uchar val); + // API: Redraw style/rate + RedrawStyle redraw_style(void) const; + void redraw_style(RedrawStyle val); +protected: + bool is_redraw_style(RedrawStyle val) { return redraw_style_ == val; } +public: + float redraw_rate(void) const; + void redraw_rate(float val); + // API: Show unknown/unprintable chars + bool show_unknown(void) const; + void show_unknown(bool val); + // API: ANSI sequences + bool ansi(void) const; + void ansi(bool val); + // Fl_Simple_Terminal API compatibility + int history_lines(void) const; + void history_lines(int val); + // API: printf() + void printf(const char *fmt, ...); + void vprintf(const char *fmt, va_list ap); + // Ctor + Fl_Terminal(int X, int Y, int W, int H, const char*L=0); + // Dtor + ~Fl_Terminal(void); + // Debugging features +//DEBUG void show_ring_info() const { ring_.show_ring_info(); } +//DEBUG void write_row(FILE *fp, Utf8Char *u8c, int cols) const; +//DEBUG void show_buffers(RingBuffer *a, RingBuffer *b=0) const; +}; +#endif diff --git a/documentation/Doxyfile.in b/documentation/Doxyfile.in index bf88783f4..130dd1845 100644 --- a/documentation/Doxyfile.in +++ b/documentation/Doxyfile.in @@ -789,6 +789,7 @@ INPUT = @CMAKE_CURRENT_SOURCE_DIR@/src/index.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/coordinates.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/resize.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/editor.dox \ + @CMAKE_CURRENT_SOURCE_DIR@/src/Fl_Terminal.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/drawing.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/events.dox \ @CMAKE_CURRENT_SOURCE_DIR@/src/subclassing.dox \ diff --git a/documentation/Makefile b/documentation/Makefile index c75dd567c..5643ba118 100644 --- a/documentation/Makefile +++ b/documentation/Makefile @@ -48,7 +48,8 @@ HTMLFILES = \ $(SRC_DOCDIR)/development.dox \ $(SRC_DOCDIR)/license.dox \ $(SRC_DOCDIR)/examples.dox \ - $(SRC_DOCDIR)/faq.dox + $(SRC_DOCDIR)/faq.dox \ + $(SRC_DOCDIR)/Fl_Terminal.dox MANPAGES = $(SRC_DOCDIR)/fltk.$(CAT3EXT) $(SRC_DOCDIR)/fltk-config.$(CAT1EXT) \ $(SRC_DOCDIR)/fluid.$(CAT1EXT) $(SRC_DOCDIR)/blocks.$(CAT6EXT) \ diff --git a/documentation/src/Fl_Terminal-24bit-colors.png b/documentation/src/Fl_Terminal-24bit-colors.png new file mode 100644 index 000000000..5e7f6adb1 Binary files /dev/null and b/documentation/src/Fl_Terminal-24bit-colors.png differ diff --git a/documentation/src/Fl_Terminal-3bit-colors.png b/documentation/src/Fl_Terminal-3bit-colors.png new file mode 100644 index 000000000..a0407738e Binary files /dev/null and b/documentation/src/Fl_Terminal-3bit-colors.png differ diff --git a/documentation/src/Fl_Terminal-demo.png b/documentation/src/Fl_Terminal-demo.png new file mode 100644 index 000000000..43996ba72 Binary files /dev/null and b/documentation/src/Fl_Terminal-demo.png differ diff --git a/documentation/src/Fl_Terminal-utf8-demo.png b/documentation/src/Fl_Terminal-utf8-demo.png new file mode 100644 index 000000000..a04f11bf9 Binary files /dev/null and b/documentation/src/Fl_Terminal-utf8-demo.png differ diff --git a/documentation/src/Fl_Terminal.dox b/documentation/src/Fl_Terminal.dox new file mode 100644 index 000000000..2963218da --- /dev/null +++ b/documentation/src/Fl_Terminal.dox @@ -0,0 +1,514 @@ +// vim:syntax=doxygen +/** + +\page Fl_Terminal_Tech_Docs Fl_Terminal Technical Documentation + +This chapter covers the vt100/xterm style "escape codes" used by +Fl_Terminal for cursor positioning, text colors, and other display +screen control features such as full or partial screen clearing, +up/down scrolling, character insert/delete, etc. + +\section Fl_Terminal_escape_codes The Escape Codes Fl_Terminal Supports + +These are the escape codes Fl_Terminal actually supports, and is not +the 'complete' list that e.g. xterm supports. Most of the important stuff +has been implemented, but esoteric features (such as scroll regions) has not. + +Features will be added as the widget matures. + +\code{.unparsed} +│ -------------------------------------------------------- +│ --- The CSI (Control Sequence Introducer, or "ESC[") --- +│ -------------------------------------------------------- +│ +│ ESC[#@ - (ICH) Insert blank Chars (default=1) +│ ESC[#A - (CUU) Cursor Up, no scroll/wrap +│ ESC[#B - (CUD) Cursor Down, no scroll/wrap +│ ESC[#C - (CUF) Cursor Forward, no wrap +│ ESC[#D - (CUB) Cursor Back, no wrap +│ ESC[#E - (CNL) Cursor Next Line (crlf) xterm, !gnome +│ ESC[#F - (CPL) Cursor Preceding Line: move to sol and up # lines +│ ESC[#G - (CHA) Cursor Horizontal Absolute positioning +│ │ +│ ├── ESC[G - move to column 1 (start of line, sol) +│ └── ESC[#G - move to column # +│ +│ ESC[#H - (CUP) Cursor Position (#'s are 1 based) +│ │ +│ ├── ESC[H - go to row #1 +│ ├── ESC[#H - go to (row #) (default=1) +│ └── ESC[#;#H - go to (row# ; col#) +│ +│ ESC[#I - (CHT) Cursor Horizontal Tab: tab forward +│ │ +│ └── ESC[#I - tab # times (default 1) +│ +│ ESC[#J - (ED) Erase in Display +│ │ +│ ├── ESC[0J - clear to end of display (default) +│ ├── ESC[1J - clear to start of display +│ ├── ESC[2J - clear all lines +│ └── ESC[3J - clear screen history +│ +│ ESC[#K - (EL) Erase in line +│ │ +│ ├── ESC[0K - clear to end of line (default) +│ ├── ESC[1K - clear to start of line +│ └── ESC[2K - clear current line +│ +│ ESC[#L - (IL) Insert # Lines (default=1) +│ ESC[#M - (DL) Delete # Lines (default=1) +│ ESC[#P - (DCH) Delete # Chars (default=1) +│ ESC[#S - (SU) Scroll Up # lines (default=1) +│ ESC[#T - (SD) Scroll Down # lines (default=1) +│ ESC[#X - (ECH) Erase Characters (default=1) +│ +│ ESC[#Z - (CBT) Cursor Backwards Tab +│ │ +│ └── ESC[#Z - backwards tab # times (default=1) +│ +│ ESC[#a - (HPR) move cursor relative [columns] (default=[row,col+1]) (NOT IMPLEMENTED) +│ ESC[#b - (REP) repeat prev graphics char # times (NOT IMPLEMENTED) +│ ESC[#d - (VPA) Line Position Absolute [row] (NOT IMPLEMENTED) +│ ESC[#e - (LPA) Line Position Relative [row] (NOT IMPLEMENTED) +│ ESC[#f - (CUP) cursor position (#'s 1 based), same as ESC[H +│ +│ ESC[#g - (TBC)Tabulation Clear +│ │ +│ ├── ESC[0g - Clear tabstop at cursor +│ └── ESC[3g - Clear all tabstops +│ +│ ESC[#m - (SGR) Set Graphic Rendition +│ │ +│ │ *** Attribute Enable *** +│ │ +│ ├── ESC[0m - reset: normal attribs/default fg/bg color (VT100) +│ ├── ESC[1m - bold (VT100) +│ ├── ESC[2m - dim +│ ├── ESC[3m - italic +│ ├── ESC[4m - underline (VT100) +│ ├── ESC[5m - blink (NOT IMPLEMENTED) (VT100) +│ ├── ESC[6m - (unused) +│ ├── ESC[7m - inverse (VT100) +│ ├── ESC[8m - (unused) +│ ├── ESC[9m - strikeout +│ ├── ESC[21m - doubly underline (Currently this just does single underline) +│ │ +│ │ *** Attribute Disable *** +│ │ +│ ├── ESC[22m - disable bold/dim +│ ├── ESC[23m - disable italic +│ ├── ESC[24m - disable underline +│ ├── ESC[25m - disable blink (NOT IMPLEMENTED) +│ ├── ESC[26m - (unused) +│ ├── ESC[27m - disable inverse +│ ├── ESC[28m - disable hidden +│ ├── ESC[29m - disable strikeout +│ │ +│ │ *** Foreground Text "8 Color" *** +│ │ +│ ├── ESC[30m - fg Black +│ ├── ESC[31m - fg Red +│ ├── ESC[32m - fg Green +│ ├── ESC[33m - fg Yellow +│ ├── ESC[34m - fg Blue +│ ├── ESC[35m - fg Magenta +│ ├── ESC[36m - fg Cyan +│ ├── ESC[37m - fg White +│ ├── ESC[39m - fg default +│ │ +│ │ *** Background Text "8 Color" *** +│ │ +│ ├── ESC[40m - bg Black +│ ├── ESC[41m - bg Red +│ ├── ESC[42m - bg Green +│ ├── ESC[43m - bg Yellow +│ ├── ESC[44m - bg Blue +│ ├── ESC[45m - bg Magenta +│ ├── ESC[46m - bg Cyan +│ ├── ESC[47m - bg White +│ ├── ESC[49m - bg default +│ │ +│ │ *** Special RGB Color *** +│ │ +│ └── ESC [ 38 ; Red ; Grn ; Blue m - where Red,Grn,Blu are decimal (0-255) +│ +│ ESC[s - save cursor pos (ansi.sys+xterm+gnome, but NOT vt100) +│ ESC[u - rest cursor pos (ansi.sys+xterm+gnome, but NOT vt100) +│ +│ ESC[>#q - (DECSCA) Set Cursor style (block/line/blink..) (NOT IMPLEMENTED) +│ ESC[#;#r - (DECSTBM) Set scroll Region top;bot (NOT IMPLEMENTED) +│ ESC[#..$t - (DECRARA) (NOT IMPLEMENTED) +│ +│ ------------------------ +│ --- C1 Control Codes --- +│ ------------------------ +│ +│ c - (RIS) Reset term to Initial State +│ D - (IND) Index: move cursor down a line, scroll if at bottom +│ E - (NEL) Next Line: basically do a crlf, scroll if at bottom +│ H - (HTS) Horizontal Tab Set: set a tabstop +│ M - (RI) Reverse Index (up w/scroll) +│ +│ NOTE: Acronyms in parens are Digital Equipment Corporation's names these VT features. +│ +\endcode + +\section external_escape_codes Useful Terminal Escape Code Documentation + +Useful links for reference: + + - https://vt100.net/docs/vt100-ug/chapter3.html + - https://www.xfree86.org/current/ctlseqs.html + - https://www.x.org/docs/xterm/ctlseqs.pdf + - https://gist.github.com/justinmk/a5102f9a0c1810437885a04a07ef0a91 <-- alphabetic! + - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html + +\section Fl_Terminal_design Fl_Terminal Design Document + +When I started this project, I identified the key concepts needed to +implement Fl_Terminal: + +- Draw and manage multiline Unicode text in FLTK +- Allow per-character colors and attributes +- Efficient screen buffer to handle "scrollback history" +- Efficient scrolling with vertical scrollbar for even large screen history +- Mouse selection for copy/paste +- Escape code management to implement VT100 style / ANSI escape codes. + +A class was created for each character, since characters can be either ASCII +or Utf8 encoded byte sequences. This class is called Utf8Char, and handles +the character, its fg and bg color, and any attributes like dim, bold, italic, etc. + +For managing the screen, after various experiments, I decided a ring buffer +was the best way to manage things, the ring split in two: + +- 'screen history' which is where lines scrolled off the top are saved +- 'display screen' displayed to the user at all times, and where the cursor lives + +Scrolling the display, either by scrollbar or by new text causing the display +to scroll up one line, would simply change an 'offset' index# of where in the +ring buffer the top of the screen is, automatically moving the top line +into the history, all without moving memory around. + +In fact the only time screen memory is moved around is during these infrequent +operations: + +- during scrolling "down" +- character insert/delete operations within a line +- changing the display size +- changing the history size + +So a class "RingBuffer" is defined to manage the ring, and accessing its various +parts, either as the entire entity ring, just the history, or just the display. + +These three concepts, "ring", "history" and "display" are given abbreviated +names in the RingBuffer class's API: + + ┌─────────────────────────────────────────┬──────────────────────────────┐ + │ NOTE: Abbreviations "hist" and "disp" │ │ + ├─────────────────────────────────────────┘ │ + │ │ + │ "history" may be abbreviated as "hist", and "display" as "disp" in │ + │ both this text and the source code. 4 character names are used so │ + │ they line up cleanly in the source, e.g. │ + │ │ + │ ring_rows() ring_cols() │ + │ hist_rows() hist_cols() │ + │ disp_rows() disp_cols() │ + │ └─┬┘ └─┬┘ └─┬┘ └─┬┘ │ + │ └────┴──────────┴────┴───────── 4 characters │ + │ │ + └────────────────────────────────────────────────────────────────────────┘ + +These concepts were able to fit into C++ classes: + +Utf8Char +-------- +Each character on the screen is a "Utf8Char" which can manage +the UTF-8 encoding of any character as one or more bytes. Also +in that class is a byte for an attribute (underline, bold, etc), +and two integers for fg/bg color. + +RingBuffer +---------- +The RingBuffer class keeps track of the buffer itself, a single +array of Utf8Chars called "ring_chars" whose width is ring_cols() +and whose height is ring_rows(). + +The "top" part of the ring is the history, whose width is hist_cols() +and whose height is hist_rows(). hist_use_rows() is used to define +what part of the history is currently in use. + +The "bottom" part of the ring is the display, whose width is disp_cols() +and whose height is disp_rows(). + +An index number called "offset" points to where in the ring buffer +the top of the ring currently is. This index changes each time the +screen is scrolled, and affects both where the top of the display is, +and where the top of the history is. + +The memory layout of the Utf8Char character array is: + + ring_chars[]: + ___________________ _ _ + | | ʌ + | | | + | | | + | H i s t o r y | | hist_rows + | | | + | | | + |___________________| _v_ + | | ʌ + | | | + | D i s p l a y | | disp_rows + | | | + |___________________| _v_ + + |<----------------->| + ring_cols + hist_cols + disp_cols + +So it's basically a single continuous array of Utf8Char instances +where any character can generally be accessed by index# using the formula: + + ring_chars[ (row*ring_cols)+col ] + +..where 'row' is the desired row, 'col' is the desired column, +and 'ring_cols' is how many columns "wide" the buffer is. + +The "offset" index affects that formula as an extra row offset, +and the resulting index is then clamped within the range of the +ring buffer using modulus. + +Methods are used to allow direct access to the characters +in the buffer that automatically handle the offset and modulus +formulas, namely: + + u8c_ring_row(row,col) // access the entire ring by row/col + u8c_hist_row(row,col) // access just the history buffer + u8c_disp_row(row,col) // access just the display buffer + +A key concept is the use of the simple 'offset' index integer +to allow the starting point of the history and display to be +moved around to implement 'text scrolling', such as when +crlf at the screen bottom causes a 'scroll up'. + +This is simply an "index offset" integer applied to the +hist and disp indexes when drawing the display. So after +scrolling two lines up, the offset is just increased by 2, +redefining where the top of the history and display are, e.g. + + Offset is 0: 2 Offset now 2: + ┌───────────────────┐ ──┐ ┌───────────────────┐ + │ │ │ │ D i s p l a y │ + │ │ └─> ├───────────────────┤ + │ │ │ │ + │ H i s t o r y │ │ │ + │ │ │ H i s t o r y │ + │ │ 2 │ │ + ├───────────────────┤ ──┐ │ │ + │ │ │ │ │ + │ │ └─> ├───────────────────┤ + │ D i s p l a y │ │ │ + │ │ │ D i s p l a y │ + │ │ │ │ + └───────────────────┘ └───────────────────┘ + +This 'offset' trivially implements "text scrolling", avoiding having +to physically move memory around. Just the 'offset' changes, the +text remains where it is in memory. + +This also makes it appear the top line in the display is 'scrolled up' +into the bottom of the scrollback 'history'. + +If the offset exceeds the size of the ring buffer, it simply wraps +around back to the beginning of the buffer with a modulo. + +Indexes into the display and history are also modulo their respective +rows, e.g. + + act_ring_index = (hist_rows + disp_row + offset - scrollbar_pos) % ring_rows; + +This way indexes for ranges can run beyond the bottom of the ring, +and automatically wrap around the ring, e.g. + + ┌───────────────────┐ + ┌─> 2 │ │ + │ 3 │ D i s p l a y │ + │ 4 │ │ + │ ├───────────────────┤ <-- offset points here + │ │ │ + disp │ │ │ + index ┤ │ H i s t o r y │ + wraps │ │ │ + │ │ │ + │ │ │ + │ ├───────────────────┤ + │ 0 │ D i s p l a y │ + │ 1 └───────────────────┘ <- ring_rows points to end of ring + └── 2 : : + 3 : : + disp_row(5) -> 4 :...................: + +The dotted lines show where the display would be if not for the fact +it extends beyond the bottom of the ring buffer (due to the current offset), +and therefore wraps up to the top of the ring. + +So to find a particular row in the display, in this case a 5 line display +whose lines lie between 0 and 4, some simple math calculates the row position +into the ring: + + act_ring_index = (histrows // the display exists AFTER the history, so offset the hist_rows + + offset // include the scroll 'offset' + + disp_row // add the desired row relative to the top of the display (0..disp_rows) + ) % ring_rows; // make sure the resulting index is within the ring buffer (0..ring_rows) + +An additional bit of math makes sure if a negative result occurs, that +negative value works relative to the end of the ring, e.g. + + if (act_ring_index < 0) act_ring_index = ring_rows + act_ring_index; + +This guarantees the act_ring_index is within the ring buffer's address space, +with all offsets applied. + +The math that implements this can be found in the u8c_xxxx_row() methods, +where "xxxx" is one of the concept regions "ring", "hist" or "disp": + + Utf8Char *u8c; + u8c = u8c_ring_row(rrow); // address within ring, rrow can be 0..(ring_rows-1) + u8c = u8c_hist_row(hrow); // address within hist, hrow can be 0..(hist_rows-1) + u8c = u8c_disp_row(drow); // address within disp, drow can be 0..(disp_rows-1) + +The small bit of math is only involved whenever a new row address is needed, +so in a display that's 80x25, to walk all the characters in the screen, the +math above would only be called 25 times, once for each row, and each column +in the row is just a simple integer offset: + + for ( int row=0; row //START +#include #include #include -#include +#include #define TERMINAL_HEIGHT 120 // Globals Fl_Double_Window *G_win = 0; Fl_Box *G_box = 0; -Fl_Simple_Terminal *G_tty = 0; +Fl_Terminal *G_tty = 0; // Append a date/time message to the terminal every 2 seconds void tick_cb(void *data) { @@ -43,7 +43,7 @@ int main(int argc, char **argv) { "Your app's debugging output in tty below"); // Add simple terminal to bottom of app window for scrolling history of status messages. - G_tty = new Fl_Simple_Terminal(0,200,G_win->w(),TERMINAL_HEIGHT); + G_tty = new Fl_Terminal(0,200,G_win->w(),TERMINAL_HEIGHT); G_tty->ansi(true); // enable use of "\033[32m" G_win->end(); @@ -51,4 +51,4 @@ int main(int argc, char **argv) { G_win->show(); Fl::add_timeout(0.5, tick_cb); return Fl::run(); -} //END +} 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 +#ifndef NDEBUG +#define NDEBUG // comment out to enable assert() +#endif + +#include // isdigit +#include // malloc +#include // strlen +#include // vprintf, va_list +#include + +#include +#include +#include // fl_utf8len1 +#include +#include + +///////////////////////////////// +////// 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 (valmax) ? 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' - [1m -- is_csi() will be true, val() has value(s) parsed +// 'A' - 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 == '[') { // [? 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. D, 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= 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; colclear(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 | 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= 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= 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= 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= 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(const_cast(this)->u8c_ring_row(row)); } + +Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_hist_row(int hrow) + { return const_cast(const_cast(this)->u8c_hist_row(hrow)); } + +Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_hist_use_row(int hurow) + { return const_cast(const_cast(this)->u8c_hist_use_row(hurow)); } + +Fl_Terminal::Utf8Char* Fl_Terminal::RingBuffer::u8c_disp_row(int drow) + { return const_cast(const_cast(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(const_cast(this)->u8c_ring_row(grow)); } + +Fl_Terminal::Utf8Char* Fl_Terminal::u8c_hist_row(int hrow) + { return const_cast(const_cast(this)->u8c_hist_row(hrow)); } + +Fl_Terminal::Utf8Char* Fl_Terminal::u8c_hist_use_row(int hurow) + { return const_cast(const_cast(this)->u8c_hist_use_row(hurow)); } + +Fl_Terminal::Utf8Char* Fl_Terminal::u8c_disp_row(int drow) + { return const_cast(const_cast(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; tvalue(); + // 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 "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 \[\#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 \c, \c \[0m, etc. + + The xterm color intensity values can be influenced by the Dim/Bold/Normal + modes (which can be set with e.g. \c \[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 \[\#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 \c, \c \[0m, etc. + + The xterm color intensity values can be influenced by the Dim/Bold/Normal + modes (which can be set with e.g. \c \[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 \[38;2;\;\;\m + + This setting does _not_ affect the 'default' text colors used by \[0m, + \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 \[48;2;\;\;\m + + This setting does _not_ affect the 'default' text colors used by \[0m, + \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 \c, \c \[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 \c, \c \[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; gcolfl_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 (Yscrn_.b()) return -2; // dn (off bot edge) + if (Xscrn_.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 "[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[2J[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 "[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[J[0J". +void Fl_Terminal::clear_eod(void) { + for (int drow=cursor_.row(); drow[K". +void Fl_Terminal::clear_eol(void) { + Utf8Char *u8c = u8c_disp_row(cursor_.row()) + cursor_.col(); // start at cursor + for (int col=cursor_.col(); colclear(current_style_); + //TODO: Clear mouse selection? +} + +/// Clear from cursor to Start Of Line (SOL), like \c "[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; colclear(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= cursor_.row() ) { // walk srcrow to curs line + Utf8Char *dst = u8c_disp_row(dst_drow--); + for (int dcol=0; dcolclear(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; dcolclear(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()) 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; hrowclear(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 "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; collength(), 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 0 ) + while ( --X > 0 ) // search for tabstop + if ( (X=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=, 3=, 4= + int r=0,g=0,b=0; + for (int i=0; i;;m + case 48: // bg RGB mode? e.g. ESC[48;2;;;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 [;;;;$t +// This one is fun! 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 '@': // [#@ - (ICH) Insert blank Chars (default=1) + insert_char(' ', esc.defvalmax(1,dw)); + break; + case 'A': // [#A - (CUU) cursor up, no scroll/wrap + cursor_up(esc.defvalmax(1,dh)); + break; + case 'B': // [#B - (CUD) cursor down, no scroll/wrap + cursor_down(esc.defvalmax(1,dh), no_scroll); + break; + case 'C': // [#C - (CUF) cursor right, no wrap + cursor_right(esc.defvalmax(1,dw), no_scroll); + break; + case 'D': // [#D - (CUB) cursor left, no wrap + cursor_left(esc.defvalmax(1,dw)); + break; + case 'E': // [#E - (CNL) cursor next line (crlf) xterm, !gnome + cursor_crlf(esc.defvalmax(1,dh)); + break; + case 'F': // [#F - (CPL) move to sol and up # lines + cursor_cr(); + cursor_up(esc.defvalmax(1,dh)); + break; + case 'G': // [#G - (CHA) cursor horizal absolute + switch (clamp(tot,0,1)) { // │ + case 0: // ├── [G -- move to sol + cursor_sol(); // │ default [1G + break; // │ + case 1: // └── [#G -- move to column + cursor_col(clamp(val0,1,dw)-1); + break; + } + break; + case 'H': +cup: + switch (clamp(tot,0,2)) { // [#H - (CUP) cursor position (#'s are 1 based) + case 0: // ├── [H -- no vals? + cursor_home(); // │ default [1H + break; // │ + case 1: // ├── [#H -- go to (row #) + cursor_row(clamp(val0,1,dh)-1); // │ NOTE: ESC[5H == ESC[5;1H + cursor_col(0); // │ + break; // │ + case 2: // └── [#;#H -- go to (row# ; col#) + cursor_row(clamp(val0,1,dh)-1); + cursor_col(clamp(val1,1,dw)-1); + break; + } + break; + case 'I': // [#I - (CHT) cursor forward tab (default=1) + switch (clamp(tot,0,1)) { // │ + case 0: // ├── [I -- no vals + cursor_tab_right(1); // │ default [1I + break; // │ + case 1: // └── [#I -- tab # times + cursor_tab_right(clamp(val0,1,dw)); // + break; + } + break; + case 'J': // [#J - (ED) erase in display + switch ( clamp(tot,0,1) ) { // │ + case 0: clear_eol(); break; // ├── [J -- no vals: default [0J + case 1: // │ + switch ( clamp(val0,0,3) ) { // │ + case 0: clear_eod(); break; // ├── [0J -- clear to end of display + case 1: clear_sod(); break; // ├── [1J -- clear to start of display + case 2: clear_screen(); break; // ├── [2J -- clear all lines + case 3: clear_history(); break; // └── [3J -- clear screen history + } + break; + } + break; + case 'K': + switch ( clamp(tot,0,1) ) { // [#K - (EL) Erase in Line + case 0: clear_eol(); break; // ├── [K -- no vals + case 1: switch ( clamp(val0,0,2) ) { // │ + case 0: clear_eol(); break; // ├── [0K -- clear to end of line + case 1: clear_sol(); break; // ├── [1K -- clear to start of line + case 2: clear_line(); break; // └── [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': // [#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: // ├── [Z -- no vals + cursor_tab_left(1); // │ default [1Z + break; // │ + case 1: // └── [#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': // [#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 (D, etc) + switch ( esc.esc_mode() ) { + case 'c': // c - Reset term to Initial State (RIS) + reset_terminal(); + break; + case 'D': cursor_down(1, do_scroll); break;// D - down line, scroll at bottom + case 'E': cursor_crlf(); break;// E - do a crlf + case 'H': set_tabstop(); break;// H - set a tabstop + case 'M': cursor_up(1, true); break;// 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(). + + Block I/O + + 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 append(NULL) 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; gcolattrib() != 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; gcolattrib() != 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