Isolated algorithmic improvements.
* Fix inefficient STL use around Parser::UTF8Parser. * Reduce typeid() usage, change some of it to a virtual method * Do multiple-line scrolls as a single move
This commit is contained in:
@@ -195,9 +195,10 @@ static int vt_parser( int fd, Parser::UTF8Parser *parser )
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* feed to parser */
|
/* feed to parser */
|
||||||
|
Parser::Actions actions;
|
||||||
for ( int i = 0; i < bytes_read; i++ ) {
|
for ( int i = 0; i < bytes_read; i++ ) {
|
||||||
std::list<Parser::Action *> actions = parser->input( buf[ i ] );
|
parser->input( buf[ i ], actions );
|
||||||
for ( std::list<Parser::Action *>::iterator j = actions.begin();
|
for ( Parser::Actions::iterator j = actions.begin();
|
||||||
j != actions.end();
|
j != actions.end();
|
||||||
j++ ) {
|
j++ ) {
|
||||||
|
|
||||||
@@ -218,6 +219,7 @@ static int vt_parser( int fd, Parser::UTF8Parser *parser )
|
|||||||
|
|
||||||
fflush( stdout );
|
fflush( stdout );
|
||||||
}
|
}
|
||||||
|
actions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -666,9 +666,10 @@ void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
|
|||||||
}
|
}
|
||||||
last_byte = the_byte;
|
last_byte = the_byte;
|
||||||
|
|
||||||
list<Parser::Action *> actions( parser.input( the_byte ) );
|
Parser::Actions actions;
|
||||||
|
parser.input( the_byte, actions );
|
||||||
|
|
||||||
for ( list<Parser::Action *>::iterator it = actions.begin();
|
for ( Parser::Actions::iterator it = actions.begin();
|
||||||
it != actions.end();
|
it != actions.end();
|
||||||
it++ ) {
|
it++ ) {
|
||||||
Parser::Action *act = *it;
|
Parser::Action *act = *it;
|
||||||
@@ -678,7 +679,8 @@ void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
|
|||||||
act->name().c_str(), act->char_present ? act->ch : L'_' );
|
act->name().c_str(), act->char_present ? act->ch : L'_' );
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( typeid( *act ) == typeid( Parser::Print ) ) {
|
const std::type_info& type_act = typeid( *act );
|
||||||
|
if ( type_act == typeid( Parser::Print ) ) {
|
||||||
/* make new prediction */
|
/* make new prediction */
|
||||||
|
|
||||||
init_cursor( fb );
|
init_cursor( fb );
|
||||||
@@ -809,7 +811,7 @@ void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
|
|||||||
newline_carriage_return( fb );
|
newline_carriage_return( fb );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ( typeid( *act ) == typeid( Parser::Execute ) ) {
|
} else if ( type_act == typeid( Parser::Execute ) ) {
|
||||||
if ( act->char_present && (act->ch == 0x0d) /* CR */ ) {
|
if ( act->char_present && (act->ch == 0x0d) /* CR */ ) {
|
||||||
become_tentative();
|
become_tentative();
|
||||||
newline_carriage_return( fb );
|
newline_carriage_return( fb );
|
||||||
@@ -817,10 +819,10 @@ void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
|
|||||||
// fprintf( stderr, "Execute 0x%x\n", act->ch );
|
// fprintf( stderr, "Execute 0x%x\n", act->ch );
|
||||||
become_tentative();
|
become_tentative();
|
||||||
}
|
}
|
||||||
} else if ( typeid( *act ) == typeid( Parser::Esc_Dispatch ) ) {
|
} else if ( type_act == typeid( Parser::Esc_Dispatch ) ) {
|
||||||
// fprintf( stderr, "Escape sequence\n" );
|
// fprintf( stderr, "Escape sequence\n" );
|
||||||
become_tentative();
|
become_tentative();
|
||||||
} else if ( typeid( *act ) == typeid( Parser::CSI_Dispatch ) ) {
|
} else if ( type_act == typeid( Parser::CSI_Dispatch ) ) {
|
||||||
if ( act->char_present && (act->ch == L'C') ) { /* right arrow */
|
if ( act->char_present && (act->ch == L'C') ) { /* right arrow */
|
||||||
init_cursor( fb );
|
init_cursor( fb );
|
||||||
if ( cursor().col < fb.ds.get_width() - 1 ) {
|
if ( cursor().col < fb.ds.get_width() - 1 ) {
|
||||||
@@ -838,8 +840,6 @@ void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
|
|||||||
// fprintf( stderr, "CSI sequence %lc\n", act->ch );
|
// fprintf( stderr, "CSI sequence %lc\n", act->ch );
|
||||||
become_tentative();
|
become_tentative();
|
||||||
}
|
}
|
||||||
} else if ( typeid( *act ) == typeid( Parser::Clear ) ) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete act;
|
delete act;
|
||||||
|
|||||||
@@ -46,16 +46,17 @@ string Complete::act( const string &str )
|
|||||||
{
|
{
|
||||||
for ( unsigned int i = 0; i < str.size(); i++ ) {
|
for ( unsigned int i = 0; i < str.size(); i++ ) {
|
||||||
/* parse octet into up to three actions */
|
/* parse octet into up to three actions */
|
||||||
list<Action *> actions( parser.input( str[ i ] ) );
|
parser.input( str[ i ], actions );
|
||||||
|
|
||||||
/* apply actions to terminal and delete them */
|
/* apply actions to terminal and delete them */
|
||||||
for ( list<Action *>::iterator it = actions.begin();
|
for ( Actions::iterator it = actions.begin();
|
||||||
it != actions.end();
|
it != actions.end();
|
||||||
it++ ) {
|
it++ ) {
|
||||||
Action *act = *it;
|
Action *act = *it;
|
||||||
act->act_on_terminal( &terminal );
|
act->act_on_terminal( &terminal );
|
||||||
delete act;
|
delete act;
|
||||||
}
|
}
|
||||||
|
actions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return terminal.read_octets_to_host();
|
return terminal.read_octets_to_host();
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ namespace Terminal {
|
|||||||
Terminal::Emulator terminal;
|
Terminal::Emulator terminal;
|
||||||
Terminal::Display display;
|
Terminal::Display display;
|
||||||
|
|
||||||
|
// Only used locally by act(), but kept here as a performance optimization,
|
||||||
|
// to avoid construction/destruction. It must always be empty
|
||||||
|
// outside calls to act() to keep horrible things from happening.
|
||||||
|
Parser::Actions actions;
|
||||||
|
|
||||||
typedef std::list< std::pair<uint64_t, uint64_t> > input_history_type;
|
typedef std::list< std::pair<uint64_t, uint64_t> > input_history_type;
|
||||||
input_history_type input_history;
|
input_history_type input_history;
|
||||||
uint64_t echo_ack;
|
uint64_t echo_ack;
|
||||||
@@ -56,7 +61,7 @@ namespace Terminal {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
Complete( size_t width, size_t height ) : parser(), terminal( width, height ), display( false ),
|
Complete( size_t width, size_t height ) : parser(), terminal( width, height ), display( false ),
|
||||||
input_history(), echo_ack( 0 ) {}
|
actions(), input_history(), echo_ack( 0 ) {}
|
||||||
|
|
||||||
std::string act( const std::string &str );
|
std::string act( const std::string &str );
|
||||||
std::string act( const Parser::Action *act );
|
std::string act( const Parser::Action *act );
|
||||||
|
|||||||
+6
-14
@@ -41,21 +41,19 @@
|
|||||||
const Parser::StateFamily Parser::family;
|
const Parser::StateFamily Parser::family;
|
||||||
|
|
||||||
static void append_or_delete( Parser::Action *act,
|
static void append_or_delete( Parser::Action *act,
|
||||||
std::list<Parser::Action *> &vec )
|
Parser::Actions &vec )
|
||||||
{
|
{
|
||||||
assert( act );
|
assert( act );
|
||||||
|
|
||||||
if ( typeid( *act ) != typeid( Parser::Ignore ) ) {
|
if ( !act->ignore() ) {
|
||||||
vec.push_back( act );
|
vec.push_back( act );
|
||||||
} else {
|
} else {
|
||||||
delete act;
|
delete act;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::list<Parser::Action *> Parser::Parser::input( wchar_t ch )
|
void Parser::Parser::input( wchar_t ch, Actions &ret )
|
||||||
{
|
{
|
||||||
std::list<Action *> ret;
|
|
||||||
|
|
||||||
Transition tx = state->input( ch );
|
Transition tx = state->input( ch );
|
||||||
|
|
||||||
if ( tx.next_state != NULL ) {
|
if ( tx.next_state != NULL ) {
|
||||||
@@ -69,8 +67,6 @@ std::list<Parser::Action *> Parser::Parser::input( wchar_t ch )
|
|||||||
append_or_delete( tx.next_state->enter(), ret );
|
append_or_delete( tx.next_state->enter(), ret );
|
||||||
state = tx.next_state;
|
state = tx.next_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Parser::UTF8Parser::UTF8Parser()
|
Parser::UTF8Parser::UTF8Parser()
|
||||||
@@ -80,7 +76,7 @@ Parser::UTF8Parser::UTF8Parser()
|
|||||||
buf[0] = '\0';
|
buf[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
std::list<Parser::Action *> Parser::UTF8Parser::input( char c )
|
void Parser::UTF8Parser::input( char c, Actions &ret )
|
||||||
{
|
{
|
||||||
assert( buf_len < BUF_SIZE );
|
assert( buf_len < BUF_SIZE );
|
||||||
|
|
||||||
@@ -94,7 +90,6 @@ std::list<Parser::Action *> Parser::UTF8Parser::input( char c )
|
|||||||
|
|
||||||
size_t total_bytes_parsed = 0;
|
size_t total_bytes_parsed = 0;
|
||||||
size_t orig_buf_len = buf_len;
|
size_t orig_buf_len = buf_len;
|
||||||
std::list<Action *> ret;
|
|
||||||
|
|
||||||
/* this routine is somewhat complicated in order to comply with
|
/* this routine is somewhat complicated in order to comply with
|
||||||
Unicode 6.0, section 3.9, "Best Practices for using U+FFFD" */
|
Unicode 6.0, section 3.9, "Best Practices for using U+FFFD" */
|
||||||
@@ -138,7 +133,7 @@ std::list<Parser::Action *> Parser::UTF8Parser::input( char c )
|
|||||||
/* Cast to unsigned for checks, because some
|
/* Cast to unsigned for checks, because some
|
||||||
platforms (e.g. ARM) use uint32_t as wchar_t,
|
platforms (e.g. ARM) use uint32_t as wchar_t,
|
||||||
causing compiler warning on "pwc > 0" check. */
|
causing compiler warning on "pwc > 0" check. */
|
||||||
uint64_t pwcheck = pwc;
|
const uint32_t pwcheck = pwc;
|
||||||
|
|
||||||
if ( pwcheck > 0x10FFFF ) { /* outside Unicode range */
|
if ( pwcheck > 0x10FFFF ) { /* outside Unicode range */
|
||||||
pwc = (wchar_t) 0xFFFD;
|
pwc = (wchar_t) 0xFFFD;
|
||||||
@@ -153,13 +148,10 @@ std::list<Parser::Action *> Parser::UTF8Parser::input( char c )
|
|||||||
pwc = (wchar_t) 0xFFFD;
|
pwc = (wchar_t) 0xFFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::list<Action *> vec = parser.input( pwc );
|
parser.input( pwc, ret );
|
||||||
ret.insert( ret.end(), vec.begin(), vec.end() );
|
|
||||||
|
|
||||||
total_bytes_parsed += bytes_parsed;
|
total_bytes_parsed += bytes_parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Parser::Parser::Parser( const Parser &other )
|
Parser::Parser::Parser( const Parser &other )
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
http://www.vt100.net/emu/dec_ansi_parser */
|
http://www.vt100.net/emu/dec_ansi_parser */
|
||||||
|
|
||||||
#include <wchar.h>
|
#include <wchar.h>
|
||||||
#include <list>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "parsertransition.h"
|
#include "parsertransition.h"
|
||||||
@@ -59,7 +58,7 @@ namespace Parser {
|
|||||||
Parser & operator=( const Parser & );
|
Parser & operator=( const Parser & );
|
||||||
~Parser() {}
|
~Parser() {}
|
||||||
|
|
||||||
std::list<Action *> input( wchar_t ch );
|
void input( wchar_t ch, Actions &actions );
|
||||||
|
|
||||||
bool operator==( const Parser &x ) const
|
bool operator==( const Parser &x ) const
|
||||||
{
|
{
|
||||||
@@ -81,7 +80,7 @@ namespace Parser {
|
|||||||
public:
|
public:
|
||||||
UTF8Parser();
|
UTF8Parser();
|
||||||
|
|
||||||
std::list<Action *> input( char c );
|
void input( char c, Actions &actions );
|
||||||
|
|
||||||
bool operator==( const UTF8Parser &x ) const
|
bool operator==( const UTF8Parser &x ) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ std::string Action::str( void )
|
|||||||
char thechar[ 10 ] = { 0 };
|
char thechar[ 10 ] = { 0 };
|
||||||
if ( char_present ) {
|
if ( char_present ) {
|
||||||
if ( iswprint( ch ) )
|
if ( iswprint( ch ) )
|
||||||
snprintf( thechar, 10, "(%lc)", (wint_t)ch );
|
snprintf( thechar, 10, "(%lc)", static_cast<wint_t>(ch) );
|
||||||
else
|
else
|
||||||
snprintf( thechar, 10, "(0x%x)", (unsigned int)ch );
|
snprintf( thechar, 10, "(0x%x)", static_cast<unsigned int>(ch) );
|
||||||
}
|
}
|
||||||
|
|
||||||
return name() + std::string( thechar );
|
return name() + std::string( thechar );
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#define PARSERACTION_HPP
|
#define PARSERACTION_HPP
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Terminal {
|
namespace Terminal {
|
||||||
class Emulator;
|
class Emulator;
|
||||||
@@ -43,8 +44,8 @@ namespace Parser {
|
|||||||
class Action
|
class Action
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
bool char_present;
|
|
||||||
wchar_t ch;
|
wchar_t ch;
|
||||||
|
bool char_present;
|
||||||
mutable bool handled;
|
mutable bool handled;
|
||||||
|
|
||||||
std::string str( void );
|
std::string str( void );
|
||||||
@@ -53,14 +54,20 @@ namespace Parser {
|
|||||||
|
|
||||||
virtual void act_on_terminal( Terminal::Emulator * ) const {};
|
virtual void act_on_terminal( Terminal::Emulator * ) const {};
|
||||||
|
|
||||||
Action() : char_present( false ), ch( -1 ), handled( false ) {};
|
virtual bool ignore() const { return false; }
|
||||||
|
|
||||||
|
Action() : ch( -1 ), char_present( false ), handled( false ) {};
|
||||||
virtual ~Action() {};
|
virtual ~Action() {};
|
||||||
|
|
||||||
virtual bool operator==( const Action &other ) const;
|
virtual bool operator==( const Action &other ) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef std::vector<Action *> Actions;
|
||||||
|
|
||||||
class Ignore : public Action {
|
class Ignore : public Action {
|
||||||
public: std::string name( void ) { return std::string( "Ignore" ); }
|
public:
|
||||||
|
std::string name( void ) { return std::string( "Ignore" ); }
|
||||||
|
bool ignore() const { return true; }
|
||||||
};
|
};
|
||||||
class Print : public Action {
|
class Print : public Action {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -78,18 +78,12 @@ Framebuffer::Framebuffer( int s_width, int s_height )
|
|||||||
void Framebuffer::scroll( int N )
|
void Framebuffer::scroll( int N )
|
||||||
{
|
{
|
||||||
if ( N >= 0 ) {
|
if ( N >= 0 ) {
|
||||||
for ( int i = 0; i < N; i++ ) {
|
delete_line( ds.get_scrolling_region_top_row(), N );
|
||||||
delete_line( ds.get_scrolling_region_top_row() );
|
ds.move_row( -N, true );
|
||||||
ds.move_row( -1, true );
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
N = -N;
|
N = -N;
|
||||||
|
insert_line( ds.get_scrolling_region_top_row(), N );
|
||||||
for ( int i = 0; i < N; i++ ) {
|
ds.move_row( N, true );
|
||||||
rows.insert( rows.begin() + ds.get_scrolling_region_top_row(), newrow() );
|
|
||||||
rows.erase( rows.begin() + ds.get_scrolling_region_bottom_row() + 1 );
|
|
||||||
ds.move_row( 1, true );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,32 +252,62 @@ void DrawState::restore_cursor( void )
|
|||||||
new_grapheme();
|
new_grapheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Framebuffer::insert_line( int before_row )
|
void Framebuffer::insert_line( int before_row, int count )
|
||||||
{
|
{
|
||||||
if ( (before_row < ds.get_scrolling_region_top_row())
|
if ( (before_row < ds.get_scrolling_region_top_row())
|
||||||
|| (before_row > ds.get_scrolling_region_bottom_row() + 1) ) {
|
|| (before_row > ds.get_scrolling_region_bottom_row() + 1) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.insert( rows.begin() + before_row, newrow() );
|
int max_scroll = ds.get_scrolling_region_bottom_row() + 1 - before_row;
|
||||||
rows.erase( rows.begin() + ds.get_scrolling_region_bottom_row() + 1 );
|
if ( count > max_scroll ) {
|
||||||
|
count = max_scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Framebuffer::delete_line( int row )
|
if ( count == 0 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete old rows
|
||||||
|
rows_type::iterator start = rows.begin() + ds.get_scrolling_region_bottom_row() + 1 - count;
|
||||||
|
rows.erase( start, start + count );
|
||||||
|
// insert a block of dummy rows
|
||||||
|
start = rows.begin() + before_row;
|
||||||
|
rows.insert( start, count, Row( 0, 0 ) );
|
||||||
|
// then replace with real new rows
|
||||||
|
start = rows.begin() + before_row;
|
||||||
|
for (rows_type::iterator i = start; i < start + count; i++) {
|
||||||
|
*i = newrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Framebuffer::delete_line( int row, int count )
|
||||||
{
|
{
|
||||||
if ( (row < ds.get_scrolling_region_top_row())
|
if ( (row < ds.get_scrolling_region_top_row())
|
||||||
|| (row > ds.get_scrolling_region_bottom_row()) ) {
|
|| (row > ds.get_scrolling_region_bottom_row()) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int insertbefore = ds.get_scrolling_region_bottom_row() + 1;
|
int max_scroll = ds.get_scrolling_region_bottom_row() + 1 - row;
|
||||||
if ( insertbefore == ds.get_height() ) {
|
if ( count > max_scroll ) {
|
||||||
rows.push_back( newrow() );
|
count = max_scroll;
|
||||||
} else {
|
|
||||||
rows.insert( rows.begin() + insertbefore, newrow() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.erase( rows.begin() + row );
|
if ( count == 0 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete old rows
|
||||||
|
rows_type::iterator start = rows.begin() + row;
|
||||||
|
rows.erase( start, start + count );
|
||||||
|
// insert a block of dummy rows
|
||||||
|
start = rows.begin() + ds.get_scrolling_region_bottom_row() + 1 - count;
|
||||||
|
rows.insert( start, count, Row( 0, 0 ) );
|
||||||
|
// then replace with real new rows
|
||||||
|
start = rows.begin() + ds.get_scrolling_region_bottom_row() + 1 - count;
|
||||||
|
for (rows_type::iterator i = start; i < start + count; i++) {
|
||||||
|
*i = newrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Row::insert_cell( int col, int background_color )
|
void Row::insert_cell( int col, int background_color )
|
||||||
|
|||||||
@@ -326,8 +326,8 @@ namespace Terminal {
|
|||||||
|
|
||||||
void apply_renditions_to_current_cell( void );
|
void apply_renditions_to_current_cell( void );
|
||||||
|
|
||||||
void insert_line( int before_row );
|
void insert_line( int before_row, int count );
|
||||||
void delete_line( int row );
|
void delete_line( int row, int count );
|
||||||
|
|
||||||
void insert_cell( int row, int col );
|
void insert_cell( int row, int col );
|
||||||
void delete_cell( int row, int col );
|
void delete_cell( int row, int col );
|
||||||
|
|||||||
@@ -441,9 +441,7 @@ static void CSI_IL( Framebuffer *fb, Dispatcher *dispatch )
|
|||||||
{
|
{
|
||||||
int lines = dispatch->getparam( 0, 1 );
|
int lines = dispatch->getparam( 0, 1 );
|
||||||
|
|
||||||
for ( int i = 0; i < lines; i++ ) {
|
fb->insert_line( fb->ds.get_cursor_row(), lines );
|
||||||
fb->insert_line( fb->ds.get_cursor_row() );
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vt220 manual and Ecma-48 say to move to first column */
|
/* vt220 manual and Ecma-48 say to move to first column */
|
||||||
/* but xterm and gnome-terminal don't */
|
/* but xterm and gnome-terminal don't */
|
||||||
@@ -457,9 +455,7 @@ static void CSI_DL( Framebuffer *fb, Dispatcher *dispatch )
|
|||||||
{
|
{
|
||||||
int lines = dispatch->getparam( 0, 1 );
|
int lines = dispatch->getparam( 0, 1 );
|
||||||
|
|
||||||
for ( int i = 0; i < lines; i++ ) {
|
fb->delete_line( fb->ds.get_cursor_row(), lines );
|
||||||
fb->delete_line( fb->ds.get_cursor_row() );
|
|
||||||
}
|
|
||||||
|
|
||||||
/* same story -- xterm and gnome-terminal don't
|
/* same story -- xterm and gnome-terminal don't
|
||||||
move to first column */
|
move to first column */
|
||||||
|
|||||||
Reference in New Issue
Block a user