diff --git a/FL/Fl_Simple_Terminal.H b/FL/Fl_Simple_Terminal.H index bee182bc4..10ed81bdb 100644 --- a/FL/Fl_Simple_Terminal.H +++ b/FL/Fl_Simple_Terminal.H @@ -123,21 +123,64 @@ protected: Fl_Text_Buffer *sbuf; // style buffer private: - int history_lines_; // max lines allowed in screen history + // Private class to handle parsing ESC sequences + // Holds all state information for parsing esc sequences, + // so sequences can span multiple block read(2) operations, etc. + // + class Fl_Escape_Seq { + public: + static const int maxbuf = 80; + static const int maxvals = 10; + // 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 buf_[maxbuf]; // escape sequence being parsed + char *bufp_; // parsing ptr into buf[] + char *bufendp_; // end of buf[] (ptr to last valid buf char) + char *valbufp_; // pointer to first char in buf of integer being parsed + int vals_[maxvals]; // value array for parsing #'s in ESC[#;#;#.. + int vali_; // parsing index into vals_[], 0 if none + + int append_buf(char c); + int append_val(); + + public: + Fl_Escape_Seq(); + void reset(); + char esc_mode() const; + void esc_mode(char val); + int total_vals() const; + int val(int i) const; + bool parse_in_progress() const; + int parse(char c); + }; + +private: + int history_lines_; // max lines allowed in screen history bool stay_at_bottom_; // lets scroller chase last line in buffer bool ansi_; // enables ANSI sequences // scroll management - int lines; // #lines in buffer (optimization: Fl_Text_Buffer slow to calc this) - bool scrollaway; // true when user changed vscroll away from bottom - bool scrolling; // true while scroll callback active + int lines_; // #lines in buffer (optimization: Fl_Text_Buffer slow to calc this) + bool scrollaway_; // true when user changed vscroll away from bottom + bool scrolling_; // true while scroll callback active // Fl_Text_Display vscrollbar's callback+data - Fl_Callback *orig_vscroll_cb; - void *orig_vscroll_data; + Fl_Callback *orig_vscroll_cb_; + void *orig_vscroll_data_; // Style table const Fl_Text_Display::Style_Table_Entry *stable_; // the active style table - int stable_size_; // active style table size (in bytes) - int normal_style_index_; // "normal" style used by "\033[0m" reset sequence - int current_style_index_; // current style used for drawing text + int stable_size_; // active style table size (in bytes) + int normal_style_index_; // "normal" style used by "\033[0m" reset sequence + int current_style_index_; // current style used for drawing text + char current_style_; // current 'style char' (e.g. 'A' = first style entry) + Fl_Escape_Seq escseq; // escape sequence state handler + // String parsing vars initialized/used by append(), used by handle_backspace() etc. + char *ntm_; // new text memory (ntm) - malloc()ed by append() for output text + char *ntp_; // new text ptr (ntp) - points into ntm buffer + char *nsm_; // new style memory (nsm) - malloc()ed by append() for output style + char *nsp_; // new style ptr (nsp) - points into nsm buffer public: Fl_Simple_Terminal(int X,int Y,int W,int H,const char *l=0); @@ -157,6 +200,7 @@ public: int normal_style_index() const; void current_style_index(int); int current_style_index() const; + int current_style() const; // Terminal text management void append(const char *s, int len=-1); @@ -188,6 +232,8 @@ protected: void vscroll_cb2(Fl_Widget*, void*); static void vscroll_cb(Fl_Widget*, void*); void backspace_buffer(unsigned int count); + void handle_backspace(); + void append_ansi(const char *s, int len); }; #endif diff --git a/src/Fl_Simple_Terminal.cxx b/src/Fl_Simple_Terminal.cxx index 67989a5e1..680b157dd 100644 --- a/src/Fl_Simple_Terminal.cxx +++ b/src/Fl_Simple_Terminal.cxx @@ -64,12 +64,217 @@ static int strcnt(const char *s, char c) { return count; } +// --- Fl_Escape_Seq ---------------------------------------------------------- + +// Append char to buf[] safely (with bounds checking) +// Returns: +// success - ok +// fail - buffer full/overflow +// +int Fl_Simple_Terminal::Fl_Escape_Seq::append_buf(char c) { + if ( bufp_ >= bufendp_ ) return fail; // end of buffer reached? + *bufp_++ = c; + *bufp_ = 0; // keep buf[] null terminated + return success; +} + +// Append whatever integer string is at valbufp into vals_[] safely w/bounds checking +// Assumes valbufp points to a null terminated string. +// Returns: +// success - parsed ok +// fail - error occurred (non-integer, or vals_[] full) +// +int Fl_Simple_Terminal::Fl_Escape_Seq::append_val() { + if ( vali_ == maxvals ) { // too many vals_[] already? + return fail; // fail if vals_[] full + } + if ( !valbufp_ || (*valbufp_ == 0) ) { // no integer to parse? e.g. ESC[;m + vals_[vali_] = 0; // handle as if it was zero, e.g. ESC[0; + } else if ( sscanf(valbufp_, "%d", &vals_[vali_]) != 1 ) { // Parse integer into vals_[] + return fail; // fail if parsed a non-integer + } + if ( ++vali_ >= maxvals ) { // advance val index, fail if too many vals + vali_ = maxvals-1; // clamp + return fail; // fail + } + valbufp_ = 0; // parsed val ok, reset valbufp to NULL + return success; +} + +// Ctor +Fl_Simple_Terminal::Fl_Escape_Seq::Fl_Escape_Seq() { + reset(); +} + +// Reset the class +void Fl_Simple_Terminal::Fl_Escape_Seq::reset() { + esc_mode_ = 0; // disable ESC mode, so parse_in_progress() returns false + bufp_ = buf_; // point to beginning of buffer + bufendp_ = buf_ + (maxbuf - 1); // point to end of buffer + valbufp_ = 0; // disable val ptr (no vals parsed yet) + vali_ = 0; // zero val index + buf_[0] = 0; // null terminate buffer + vals_[0] = 0; // first val[] 0 +} + +// Return current escape mode. +// This is really only valid after parse() returns 'completed'. +// After a reset() this will return 0. +// +char Fl_Simple_Terminal::Fl_Escape_Seq::esc_mode() const { + return esc_mode_; +} + +// Set current escape mode. +void Fl_Simple_Terminal::Fl_Escape_Seq::esc_mode(char val) { + esc_mode_ = val; +} + +// Return the total vals parsed. +// This is really only valid after parse() returns 'completed'. +// +int Fl_Simple_Terminal::Fl_Escape_Seq::total_vals() 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_Simple_Terminal::Fl_Escape_Seq::val(int i) const { + return vals_[i]; +} + +// See if we're in the middle of parsing an ESC sequence +bool Fl_Simple_Terminal::Fl_Escape_Seq::parse_in_progress() const { + return (esc_mode_ == 0) ? false : true; +} + +// 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) +// +// Typical use pattern (this is unverified code, shown just to give a general gist): +// +// while ( *s ) { // walk text that may contain ESC sequences +// if ( *s == 0x1b ) { +// escseq.parse(*s++); // start parsing ESC seq (does a reset()) +// continue; +// } else if ( escseq.parse_in_progress() ) { // continuing to parse an ESC seq? +// switch (escseq.parse(*s++)) { // parse char, advance s.. +// case fail: escseq.reset(); continue; // failed? reset, continue.. +// case success: continue; // keep parsing.. +// case completed: // parsed complete esc sequence? +// break; +// } +// // Handle parsed esc sequence here.. +// switch ( escseq.esc_mode() ) { +// case 'm': // ESC[...m? +// for ( int i=0; i= 0x7f ) { // any other control or binary characters? + goto pfail; // reset + fail out of esc sequence parsing + } + + // Whatever the character is, handle it depending on esc_mode.. + if ( esc_mode() == 0x1b ) { // in ESC mode? + if ( c == '[' ) { // ESC[? + esc_mode(c); // switch to parsing mode for ESC[#;#;#.. + vali_ = 0; // zero vals_[] index + valbufp_ = 0; // valbufp NULL (no vals yet) + if ( append_buf(c) < 0 ) goto pfail; // save '[' in buf + return success; // success + } else if ( c >= 'A' && c <= 'D' ) { // ESC A/B/C/D? (cursor movement?) + esc_mode(c); // use as mode + vali_ = 0; + if ( append_buf(c) < 0 ) goto pfail; // save A/B/C/D in buf + return success; // success + } else { // ESCx? not supported + goto pfail; + } + } else if ( esc_mode() == '[' ) { // '[' mode? e.g. ESC[... + 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_buf(c) < 0 ) goto pfail; // save ';' in buf + return success; + } + if ( isdigit(c) ) { // parsing an integer? + if ( !valbufp_ ) // valbufp not set yet? + { valbufp_ = bufp_; } // point to first char in integer string + if ( append_buf(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 >= 'A' && 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_buf(c) < 0 ) goto pfail; // save letter in buffer + esc_mode(c); // change mode to the mode setting char + if ( vali_ == 0 ) { // no vals were specified? assume 0 (e.g. ESC[J? assume ESC[0J) + vals_[vali_++] = 0; // force vals_[0] to be 0, and vali = 1; + } + return completed; // completed/done + } + // Any other chars? reset+fail +pfail: + reset(); + return fail; +} + +// +// --- Fl_Simple_Terminal ----------------------------------------------------- +// + // Vertical scrollbar callback intercept void Fl_Simple_Terminal::vscroll_cb2(Fl_Widget *w, void*) { - scrolling = 1; - orig_vscroll_cb(w, orig_vscroll_data); - scrollaway = (mVScrollBar->value() != mVScrollBar->maximum()); - scrolling = 0; + scrolling_ = 1; + orig_vscroll_cb_(w, orig_vscroll_data_); + scrollaway_ = (mVScrollBar->value() != mVScrollBar->maximum()); + scrolling_ = 0; } void Fl_Simple_Terminal::vscroll_cb(Fl_Widget *w, void *data) { Fl_Simple_Terminal *o = (Fl_Simple_Terminal*)data; @@ -83,9 +288,9 @@ Fl_Simple_Terminal::Fl_Simple_Terminal(int X,int Y,int W,int H,const char *l) : history_lines_ = 500; // something 'reasonable' stay_at_bottom_ = true; ansi_ = false; - lines = 0; // note: lines!=mNBufferLines when lines are wrapping - scrollaway = false; - scrolling = false; + lines_ = 0; // note: lines!=mNBufferLines when lines are wrapping + scrollaway_ = false; + scrolling_ = false; // These defaults similar to typical DOS/unix terminals textfont(FL_COURIER); color(FL_BLACK); @@ -102,13 +307,14 @@ Fl_Simple_Terminal::Fl_Simple_Terminal(int X,int Y,int W,int H,const char *l) : // being present, an annoying UI bug in Fl_Text_Display. wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 0); // Style table - stable_ = &builtin_stable[0]; - stable_size_ = builtin_stable_size; + stable_ = &builtin_stable[0]; + stable_size_ = builtin_stable_size; normal_style_index_ = builtin_normal_index; current_style_index_ = builtin_normal_index; + current_style_ = 'A' + 0; // Intercept vertical scrolling - orig_vscroll_cb = mVScrollBar->callback(); - orig_vscroll_data = mVScrollBar->user_data(); + orig_vscroll_cb_ = mVScrollBar->callback(); + orig_vscroll_data_ = mVScrollBar->user_data(); mVScrollBar->callback(vscroll_cb, (void*)this); } @@ -348,6 +554,7 @@ int Fl_Simple_Terminal::normal_style_index() const { void Fl_Simple_Terminal::current_style_index(int val) { // Wrap index to ensure it's never larger than table current_style_index_ = abs(val) % (stable_size_ / STE_SIZE); + current_style_ = 'A' + current_style_index_; } /** @@ -365,6 +572,19 @@ int Fl_Simple_Terminal::current_style_index() const { return current_style_index_; } +/** + Get the current style char used for style buffer. + This character appends in parallel with any text in the text buffer + to specify the per-character styling. This is typically 'A' for the + first entry, 'B' for the second entry, etc. + + This value is changed by current_style_index(int). + \see current_style_index(int) +*/ +int Fl_Simple_Terminal::current_style() const { + return current_style_; +} + /** Set a user defined style table, which controls the font colors, faces, weights and sizes available for the terminal's text content. @@ -475,7 +695,7 @@ void Fl_Simple_Terminal::style_table(Fl_Text_Display::Style_Table_Entry *stable, should need to call this. */ void Fl_Simple_Terminal::enforce_stay_at_bottom() { - if ( stay_at_bottom_ && buffer() && !scrollaway ) { + if ( stay_at_bottom_ && buffer() && !scrollaway_ ) { scroll(mNBufferLines, 0); } } @@ -489,8 +709,8 @@ void Fl_Simple_Terminal::enforce_stay_at_bottom() { should need to call this. */ void Fl_Simple_Terminal::enforce_history_lines() { - if ( history_lines() > -1 && lines > history_lines() ) { - int trimlines = lines - history_lines(); + if ( history_lines() > -1 && lines_ > history_lines() ) { + int trimlines = lines_ - history_lines(); remove_lines(0, trimlines); // remove lines from top } } @@ -516,11 +736,132 @@ void Fl_Simple_Terminal::backspace_buffer(unsigned int count) { sbuf->remove(pos, end); } +/** + Handle a Unicode aware backspace. + This flushes the string parsed so far to Fl_Text_Display, + then lets Fl_Text_Display handle the unicode aware backspace. +*/ +void Fl_Simple_Terminal::handle_backspace() { + // FLUSH TEXT TO TEXT DISPLAY + // This prevents any Unicode multibyte char split across + // our string buffer and Fl_Text_Display, and also allows + // Fl_Text_Display to handle unicode aware backspace. + + // 1) Null temrinate buffers + *ntp_ = 0; + *nsp_ = 0; + // 2) Flush text to Fl_Text_Display + buf->append(ntm_); // flush text to FTD + sbuf->append(nsm_); // flush style to FTD + // 3) Rewind buffer and sp, restore saved chars + ntp_ = ntm_; + nsp_ = nsm_; + // 4) Let Fl_Text_Display handle unicode aware backspace + backspace_buffer(1); +} + +/** + Handle appending string with ANSI escape sequences, and other 'special' + character processing (such as backspaces). +*/ +void Fl_Simple_Terminal::append_ansi(const char *s, int len) { + int nstyles = stable_size_ / STE_SIZE; + if ( len < 0 ) len = (int)strlen(s); + // ntm/tsm - new text buffer (after ansi codes parsed+removed) + ntm_ = (char*)malloc(len+1); // new text memory + ntp_ = ntm_; // new text ptr + nsm_ = (char*)malloc(len+1); // new style memory + nsp_ = nsm_; // new style ptr + // Walk user's string looking for codes, modify new text/style text as needed + const char *sp = s; + while ( *sp ) { + if (*sp == 0x1b ) { // start of ESC sequence? + escseq.parse(*sp++); // start parsing.. + continue; + } + if ( escseq.parse_in_progress() ) { // ESC sequence in progress? + switch ( escseq.parse(*sp) ) { // parse until completed or fail + case Fl_Escape_Seq::fail: // parsing error? + escseq.reset(); // ..reset and continue + ++sp; + continue; + case Fl_Escape_Seq::success: // parsed ok / still in progress + ++sp; + continue; + case Fl_Escape_Seq::completed: // completed parsing ESC sequence? + break; + } + // Escape sequence completed ok? handle it + // Walk all values parsed from ESC[#;#;#..x + // + for ( int i=0; iappend(ntm_); // new text memory + sbuf->append(nsm_); // new style memory + free(ntm_); + free(nsm_); +} + /** Appends new string 's' to terminal. - The string can contain UTF-8, crlf's, and ANSI sequences are - also supported when ansi(bool) is set to 'true'. + The string can contain UTF-8, crlf's. + And if ansi(bool) is set to 'true', ANSI 'ESC' sequences (such as ESC[1m) + and other control characters (such as backspace) are handled. \param s string to append. @@ -532,110 +873,11 @@ void Fl_Simple_Terminal::backspace_buffer(unsigned int count) { void Fl_Simple_Terminal::append(const char *s, int len) { // Remove ansi codes and adjust style buffer accordingly. if ( ansi() ) { - int nstyles = stable_size_ / STE_SIZE; - int trim = 0; // #chars to trim (backspace into existing buffer) - if ( len < 0 ) len = (int)strlen(s); - // New text buffer (after ansi codes parsed+removed) - char *ntm = (char*)malloc(len+1); // new text memory - char *ntp = ntm; // new text ptr - char *nsm = (char*)malloc(len+1); // new style memory - char *nsp = nsm; // new style ptr - // ANSI values - char astyle = 'A'+current_style_index_; // the running style index - const char *esc = 0; - const char *sp = s; - // Walk user's string looking for codes, modify new text/style text as needed - while ( *sp ) { - if ( *sp == 033 ) { // "\033.." - esc = sp++; - switch (*sp) { - case 0: // "\033"? stop - continue; - case '[': { // "\033[.." - ++sp; - int vals[4], tv=0, seqdone=0; - while ( *sp && !seqdone && isdigit(*sp) ) { // "\033[#;#.." - char *newsp; - long a = strtol(sp, &newsp, 10); - sp = newsp; - vals[tv++] = (a<0) ? 0 : a; // prevent negative values - if ( tv >= 4 ) // too many #'s specified? abort sequence - { seqdone = 1; sp = esc+1; continue; } - switch(*sp) { - case ';': // numeric separator - ++sp; - continue; - case 'J': // erase in display - switch (vals[0]) { - case 0: // \033[0J -- clear to eol - // unsupported - break; - case 1: // \033[1J -- clear to sol - // unsupported - break; - case 2: // \033[2J -- clear entire screen - clear(); // clear text buffer - ntp = ntm; // clear text contents accumulated so far - nsp = nsm; // clear style contents "" - break; - } - ++sp; - seqdone = 1; - continue; - case 'm': // set color - if ( tv > 0 ) { // at least one value parsed? - current_style_index_ = (vals[0] == 0) // "reset"? - ? normal_style_index_ // use normal color for "reset" - : (vals[0] % nstyles); // use user's value, wrapped to ensure not larger than table - astyle = 'A' + current_style_index_; // convert index -> style buffer char - } - ++sp; - seqdone = 1; - continue; - case '\0': // EOS in middle of sequence? - *ntp = 0; // end of text - *nsp = 0; // end of style - seqdone = 1; - continue; - default: // un-supported cmd? - seqdone = 1; - sp = esc+1; // continue parsing just past esc - break; - } // switch - } // while - } // case '[' - } // switch - } // \033 - else if ( *sp == 8 ) { // backspace? - if ( --ntp < ntm ) { // dec ntp, see if beyond start? - ++trim; // flag need to trim existing buffer - ntp = ntm; // clamp - } - if ( --nsp < nsm ) { // adjust style buffer too - nsp = nsm; // clamp - } - sp++; - } - else { - // Non-ANSI character? - if ( *sp == '\n' ) ++lines; // keep track of #lines - *ntp++ = *sp++; // pass char thru - *nsp++ = astyle; // use current style - } - } // while - *ntp = 0; - *nsp = 0; - //::printf(" RESULT: ntm='%s'\n", ntm); - //::printf(" RESULT: nsm='%s'\n", nsm); - backspace_buffer(trim); // destructive backspace into buffer (if any) - buf->append(ntm); // new text memory - sbuf->append(nsm); // new style memory - free(ntm); - free(nsm); + append_ansi(s, len); } else { - // non-ansi buffer + // raw append buf->append(s); - lines += ::strcnt(s, '\n'); // count total line feeds in string added + lines_ += ::strcnt(s, '\n'); // count total line feeds in string added } enforce_history_lines(); enforce_stay_at_bottom(); @@ -729,7 +971,7 @@ void Fl_Simple_Terminal::vprintf(const char *fmt, va_list ap) { void Fl_Simple_Terminal::clear() { buf->text(""); sbuf->text(""); - lines = 0; + lines_ = 0; } /** @@ -750,8 +992,8 @@ void Fl_Simple_Terminal::remove_lines(int start, int count) { } else { buf->remove(spos, epos); } - lines -= count; - if ( lines < 0 ) lines = 0; + lines_ -= count; + if ( lines_ < 0 ) lines_ = 0; } /**