ESC sequences can traverse append(), unicode support for backspace

This is basically a rewrite of the ESC handler, keeping state
in the class, so ESC sequences can continued between called
to append() (such as when reading data from a pipe in blocks).

New private class 'Fl_Escape_Seq' handles parsing and state info.
It also has careful bounds checking during parsing.

Backspace supports Unicode, and the unicode chars can straddle
across append() operations as well.

Private variables in Fl_Simple_Terminal renamed _xxx to xxx_
to improve CMP compliance.
This commit is contained in:
Greg Ercolano 2022-12-17 20:53:54 -08:00
parent 8586c257ab
commit 993b7da3b5
2 changed files with 418 additions and 130 deletions

View File

@ -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

View File

@ -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<escseq.total_vals(); i++ ) {
// int val = escseq.val(i);
// ..handle values here..
// }
// break;
// case 'J': // ESC[#J?
// ..handle..
// break;
// }
// escseq.reset(); // done handling escseq, reset()
// continue;
// } else {
// ..handle non-escape chars here..
// }
// ++s; // advance thru string
// }
//
// 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, e.g.
// 'm' - ESC[#;#..m sequence parsed, val() has value(s) parsed
// 'J' - ESC[#J sequence parsed, val() has value(s) parsed
// 'A' thru 'D' - cursor up/down/right/left movement (ESC A/B/C/D)
//
int Fl_Simple_Terminal::Fl_Escape_Seq::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_buf(c) < 0 ) goto pfail; // save ESC in buf
return success;
} else if ( c < ' ' || c >= 0x7f ) { // any other control or binary characters?
goto pfail; // reset + fail out of esc sequence parsing
}
// Whatever the character is, handle it depending on esc_mode..
if ( esc_mode() == 0x1b ) { // in ESC mode?
if ( c == '[' ) { // ESC[?
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; i<escseq.total_vals(); i++ ) {
int val = escseq.val(i);
switch (escseq.esc_mode() ) {
// ERASE IN DISPLAY (ED)
case 'J': // ESC[#J
switch (val) {
case 0: // ESC[0J -- clear to eol
// unsupported
break;
case 1: // ESC[1J -- clear to sol
// unsupported
break;
case 2: // ESC[2J -- clear visible screen
// NOTE: Currently we clear the /entire screen history/, and
// moves cursor to the top of buffer.
//
// ESC[2J should really only clear the /visible display/
// not affecting screen history or cursor position.
//
clear(); // clear text buffer
ntp_ = ntm_; // clear text contents accumulated so far
nsp_ = nsm_; // clear style contents ""
break;
}
break;
// SELECT GRAPHIC RENDITION (SGR)
case 'm':
if ( val == 0 ) { // ESC[0m? (reset color)
// Switch to "normal color"
current_style_index(normal_style_index_);
} else { // ESC[#m? (set some specific color)
// Use modulus to map into styles[] buffer
current_style_index(val % nstyles);
}
break;
// UNSUPPORTED MODES
default:
break; // unsupported
}
}
escseq.reset(); // reset after handling escseq
++sp; // advance thru string
continue;
} else if ( *sp == 8 ) { // backspace?
handle_backspace();
sp++;
} else { // Not ANSI or backspace? append to display
if ( *sp == '\n' ) ++lines_; // crlf? keep track of #lines
*ntp_++ = *sp++; // pass char thru
*nsp_++ = current_style(); // use current style
}
} // while
*ntp_ = 0;
*nsp_ = 0;
//::printf(" RESULT: ntm='%s'\n", ntm_);
//::printf(" RESULT: nsm='%s'\n", nsm_);
buf->append(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<NUL>"? 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;
}
/**