// ***************** START OF ANSITERM.CPP ***************** // // // This file contains all the code to support the AnsiTerm // class, which is a terminal emulation class that supports // the IBM PC ANSI.SYS control sequences. #include #include #include #include "terminal.h" #include "ansiterm.h" // These two defines can be used to help debug the terminal // emulation class. DEBUG is used to split the screen and // provide a bottom window that displays escape sequences as // they are parsed. KEYBOARD_FAKE lets you input escape // sequences from the keyboard. //#define KEYBOARD_FAKE #define DEBUG // // The debug macro in this file is only meant to be used // when debugging in MS-DOS. A bit of additional code would // need to be written under Windows to create a debug window. // So we turn off the debug macro if we are building a // Windows program. // #if defined( _WINDOWS ) && defined( DEBUG ) #undef DEBUG #endif #if defined( DEBUG ) #include "textwind.h" BaseWindow *debug_window; #endif // This is a list of keyboard mappings that are defined when the // emulator first starts. These key mappings can be remapped // dynamically, although this class does not support the feature. // These values are loaded into the mapping arrays when the // constructor executes. struct key_strings { int key_value; char *translation; } initial_key_translations[] = { { LEFT, "\x1b[D" }, { RIGHT, "\x1b[C" }, { UP, "\x1b[A" }, { DOWN, "\x1b[B" }, { HOME, "\x1b[H" }, { END, "\x1b[F" }, { PGUP, "\x1b[I" }, { PGDN, "\x1b[G" }, { INSERT, "\x1b[L" }, { F1, "\x1b[M" }, { F2, "\x1b[N" }, { F3, "\x1b[O" }, { F4, "\x1b[P" }, { F5, "\x1b[Q" }, { F6, "\x1b[R" }, { F7, "\x1b[S" }, { F8, "\x1b[T" }, { F9, "\x1b[U" }, { F10, "\x1b[V" }, { SHIFT_F1, "\x1b[Y" }, { SHIFT_F2, "\x1b[Z" }, { SHIFT_F3, "\x1b[a" }, { SHIFT_F4, "\x1b[b" }, { SHIFT_F5, "\x1b[c" }, { SHIFT_F6, "\x1b[d" }, { SHIFT_F7, "\x1b[e" }, { SHIFT_F8, "\x1b[f" }, { SHIFT_F9, "\x1b[g" }, { SHIFT_F10, "\x1b[h" }, { CONTROL_F1, "\x1b[k" }, { CONTROL_F2, "\x1b[l" }, { CONTROL_F3, "\x1b[m" }, { CONTROL_F4, "\x1b[n" }, { CONTROL_F5, "\x1b[o" }, { CONTROL_F6, "\x1b[p" }, { CONTROL_F7, "\x1b[q" }, { CONTROL_F8, "\x1b[r" }, { CONTROL_F9, "\x1b[s" }, { CONTROL_F10, "\x1b[t" }, { ALT_F1, "\x1b[w" }, { ALT_F2, "\x1b[x" }, { ALT_F3, "\x1b[y" }, { ALT_F4, "\x1b[z" }, { ALT_F5, "\x1b[@" }, { ALT_F6, "\x1b[[" }, { ALT_F7, "\x1b[\\" }, { ALT_F8, "\x1b[]" }, { ALT_F9, "\x1b[^" }, { ALT_F10, "\x1b[_" }, { 0, 0 } }; // // The constructor sets up the pointers to the port and the // TextWindow objects. It puts the window in a predefined // state, allocates the memory for the ansi string parsing // storage, then initializes the key maps. If the DEBUG macro // is turned on, the screen is split and a second debug text // window is opened. // AnsiTerminal::AnsiTerminal( RS232 &p, BaseWindow &w ) : Terminal( p, w ) { int key; char *translation; window->Clear(); window->Goto(); window->SetWrap( 1 ); saved_row = 0; saved_col = 0; for ( int i = 0 ; i < 15 ; i++ ) ansi_parms[ i ] = new char[ 81 ]; ansi_parms[ 15 ] = 0; keys = new char *[ 256 ]; extended_keys = new char *[ 256 ]; for ( i = 0 ; i < 256 ; i++ ) { if ( keys ) keys[ i ] = 0; if ( extended_keys ) extended_keys[ i ] = 0; } for ( i = 0; initial_key_translations[ i ].translation != 0; i++ ) { key = initial_key_translations[ i ].key_value; translation = initial_key_translations[ i ].translation; if ( extended_keys && key > 256 ) extended_keys[ ( key >> 8 ) & 0xff ] = translation; else if ( keys ) keys[ key ] = translation; } #if defined( DEBUG ) Set43LineMode( 1 ); debug_window = new TextWindow( 25, 0, 80, 18 ); debug_window->SetWrap( 1 ); #endif } // // ReadPort filters port input for the end application. If // it sees the first character of an escape sequence, it gets // parsed, and the application never sees it. Normal characters // get passed straight back to the application. Note that if // the KEYBOARD_FAKE macro is defined, input comes from the // keyboard instead of the port. // int AnsiTerminal::ReadPort( void ) { int c; #ifdef KEYBOARD_FAKE while ( ( c = window->ReadKey() ) == 0 ) ; if ( c == F10 ) return RS232_ERROR; #else c = port->Read(); #endif if ( c == ESC ) { parse(); return RS232_TIMEOUT; } return c; } // // The destructor for an AnsiTerminal objects just has to free // up the memory allocated for the ANSI parser. If DEBUG is // turned on, the screen is restored to 25 line mode. AnsiTerminal::~AnsiTerminal( void ) { for ( int i = 0 ; i < 15 ; i++ ) delete[] ansi_parms[ i ]; delete[] keys; delete[] extended_keys; #if defined( DEBUG ) && !defined( _WINDOWS ) Set43LineMode( 0 ); #endif } // This is the actual parser that reads in ANSI strings. It // is just a fairly simple state machine, that sits in a loop // reading in characters until it detects the end of an ANSI // sequence. When it is done, the ansi_parms[] array holds a // list of numeric and quoted strings, and parm_count is the // index to the last valid string. Error handling is // non-existent, if anything odd happens the routine just returns // with a failure. int AnsiTerminal::parse_ansi_string( void ) { int index; enum { READY_TO_READ, READING_DIGITS, READING_STRING, DONE_WITH_STRING } scan_state; int c; parm_count = 0; index = 0; for ( int i = 0 ; i < 15 ; i++ ) if ( ansi_parms[ i ] != 0 ) memset( ansi_parms[ i ], 0, 81 ); #ifdef KEYBOARD_FAKE while ( ( c = window->ReadKey() ) == 0 ) ; #else c = port->Read( 200 ); #endif if ( c != '[' ) return 0; scan_state = READY_TO_READ; for ( ; ; ) { if ( index >= 80 || ansi_parms[ parm_count ] == 0 ) return 0; #ifdef KEYBOARD_FAKE while ( ( c = window->ReadKey() ) == 0 ) ; #else c = port->Read( 1000 ); #endif if ( c < 0 ) return 0; switch ( scan_state ) { case READY_TO_READ: if ( parm_count == 0 && ( c == '=' || c == '?' ) ) ansi_parms[ parm_count++ ][ 0 ] = (char) c; else if ( c == '"' ) { scan_state = READING_STRING; ansi_parms[ parm_count ][ index++ ] = (char) c; } else if ( c >= '0' && c <= '9' ) { ansi_parms[ parm_count ][ index++ ] = (char) c; scan_state = READING_DIGITS; } else if ( c == ';' ) { parm_count++; index = 0; } else { ansi_parms[ parm_count ][ index ] = (char) c; return 1; } break; case READING_DIGITS : if ( c == ';' ) { parm_count++; index = 0; scan_state = READY_TO_READ; } else if ( c >= '0' && c <='9' ) ansi_parms[ parm_count ][ index++ ] = (char) c; else { ansi_parms[ ++parm_count ][ 0 ] = (char) c; return 1; } break; case READING_STRING : if ( c == '"' ) scan_state = DONE_WITH_STRING; ansi_parms[ parm_count ][ index++ ] = (char) c; break; case DONE_WITH_STRING : if ( c == ';' ) { parm_count++; index = 0; scan_state = READY_TO_READ; } else { ansi_parms[ ++parm_count ][ 0 ] = (char) c; return 1; } break; } } } // This routine is the high level controller for the terminal // emulation class. It calls parse_ansi_string() to break the // escape sequence down into usable components, then dispatches // the appropriate member function to do the work. If DEBUG is // switched on, the escape sequence is dumped out to the debug // window. void AnsiTerminal::parse( void ) { if ( parse_ansi_string() ) { #if defined( DEBUG ) debug_window->Goto(); DisplayAttribute att = debug_window->GetAttribute(); debug_window->SetAttribute( att ^ 0x77 ); *debug_window << "ESC [ "; for ( int i = 0 ; i <= parm_count; i++ ) *debug_window << "<" << ansi_parms[ i ] << ">"; *debug_window << " "; window->Goto(); #endif switch( ansi_parms[ parm_count ][ 0 ] ) { case 'A' : cursor_move( -1, 0 ); break; case 'B' : cursor_move( 1, 0 ); break; case 'C' : cursor_move( 0, 1 ); break; case 'D' : cursor_move( 0, -1 ); break; case 'H' : position_cursor(); break; case 'J' : erase_in_display(); break; case 'K' : erase_in_line(); break; case 'f' : position_cursor(); break; case 'l' : set_mode(); break; case 'h' : set_mode(); break; case 'm' : set_color(); break; case 'n' : cursor_position_report(); break; case 's' : save_position(); break; case 'u' : restore_position(); break; } } } // ESC[#;#f and ESC[#;#H // // These two commands have the same effect, which is to position // the cursor at a location specified by the two numbers, which // are a row and column sequence. One or both parameters can be // omitted, in which case the default value of 1 is used. Note // that row and column numbers in ANSI are 1 based, while the // BaseWindow class numbers are 0 based. void AnsiTerminal::position_cursor() { int row; int col; if ( parm_count > 0 ) row = atoi( ansi_parms[ 0 ] ); else row = 1; if ( parm_count > 1 ) col = atoi( ansi_parms[ 1 ] ); else col = 1; window->SetPosition( row - 1, col - 1 ); } // ESC[#A Cursor up // ESC[#B Cursor down // ESC[#C Cursor right // ESC[#D Cursor left // // These four commands are all handled with this member function. // The single numeric parameter defaults to 1 if it is omitted. // Any movement outside the screen bounds is ignored by the // BaseWindow functions, so this routine doesn't have to worry // about it. void AnsiTerminal::cursor_move( int row_dir, int col_dir ) { int offset; int row; int col; if ( parm_count > 0 ) offset = atoi( ansi_parms[ 0 ] ); else offset = 1; window->GetPosition( row, col ); row += offset * row_dir; col += offset * col_dir; window->SetPosition( row, col ); } // ESC[6n Device Status Report // // This command is handled by issuing a Cursor Position Report // sequence, ESC[#;#R, with the two numeric parameters being the // row and column number. Note that handling the command this way // is somewhat idiosynchratic to the PC. void AnsiTerminal::cursor_position_report( void ) { int row; int col; char temp[ 40 ]; if ( parm_count != 1 ) return; if ( strcmp( ansi_parms[ 0 ], "6" ) != 0 ) return; window->GetPosition( row, col ); sprintf( temp, "%c[%d;%dR", ESC, row + 1, col + 1 ); port->Write( temp ); } // ESC[2J Erase in display // // The official ANSI version of this command will erase some or // all of the display, depending on the value of the numeric // parameters. The IBM PC version only support parameter 2, which // erases the entire display. The cursor is homed as part of this // command. void AnsiTerminal::erase_in_display( void ) { if ( parm_count != 1 ) return; if ( strcmp( ansi_parms[ 0 ], "2" ) != 0 ) return; window->Clear(); window->SetPosition( 0, 0 ); } // ESC[K Erase in line // // This is another ANSI command that is only partially supported // by IBM ANSI. When no numeric parameter is given, the line is // erased from the cursor position to the end of the line. void AnsiTerminal::erase_in_line( void ) { int row; int col; int width; int height; int i; if ( parm_count != 0 ) return; window->GetPosition( row, col ); window->GetDimensions( width, height ); for ( i = col ; i < width; i++ ) *window << ' '; window->SetPosition( row, col ); } // ESC[s Save Cursor Position // // This command saves off the current cursor position for later // restoration. This is an IBM extension to the ANSI standard. void AnsiTerminal::save_position( void ) { if ( parm_count != 0 ) return; window->GetPosition( saved_row, saved_col ); } // ESC[u Restore Cursor Position // // Another IBM extension to the ANSI standard. This command // restores the previously saved cursor position. void AnsiTerminal::restore_position( void ) { if ( parm_count != 0 ) return; window->SetPosition( saved_row, saved_col ); } // ESC[#;#;...;#m Set Graphics Rendition // // This command sets the current display attributes to various // attributes. Multiple command parameters can be strung // together in unlimited combinations. This implementation is // limited to 14 parameters. void AnsiTerminal::set_color( void ) { int command; int att; for ( int i = 0 ; i < parm_count ; i++ ) { command = atoi( ansi_parms[ i ] ); att = window->GetAttribute(); switch( command ) { case 0 : att = NORMAL_ATTRIBUTE; break; case 1 : att = att | 8; break; case 5 : att = att | 0x80; break; case 7 : att = REVERSE_ATTRIBUTE; break; case 8 : att = INVISIBLE_ATTRIBUTE; break; case 30 : att = ( att & 0xf0 ) | 0x00; break; case 31 : att = ( att & 0xf0 ) | 0x04; break; case 32 : att = ( att & 0xf0 ) | 0x02; break; case 33 : att = ( att & 0xf0 ) | 0x0e; break; case 34 : att = ( att & 0xf0 ) | 0x01; break; case 35 : att = ( att & 0xf0 ) | 0x05; break; case 36 : att = ( att & 0xf0 ) | 0x03; break; case 37 : att = ( att & 0xf0 ) | 0x07; break; case 40 : att = ( att & 0x0f ) | 0x00; break; case 41 : att = ( att & 0x0f ) | 0x40; break; case 42 : att = ( att & 0x0f ) | 0x20; break; case 43 : att = ( att & 0x0f ) | 0x60; break; case 44 : att = ( att & 0x0f ) | 0x10; break; case 45 : att = ( att & 0x0f ) | 0x50; break; case 46 : att = ( att & 0x0f ) | 0x30; break; case 47 : att = ( att & 0x0f ) | 0x70; break; } window->SetAttribute( att ); } } // ESC[=#h ESC[=#l Set/Reset Mode // ESC[=h ESC[=l // ESC[=0h ESC[=0l // ESC[?7h ESC[?7l // // This command is used to change the current video mode. The // TextWindow class used here doesn't support changing modes, so // most versions of this command aren't supported.d The single // exception is the last version, which turns on/off line wrap. void AnsiTerminal::set_mode( void ) { if ( parm_count != 2 ) return; if ( strcmp( ansi_parms[ 0 ], "?" ) != 0 && strcmp( ansi_parms[ 0 ], "=" ) != 0 ) return; if ( strcmp( ansi_parms[ 1 ], "7" ) != 0 ) return; switch( ansi_parms[ 2 ][ 0 ] ) { case 'h' : window->SetWrap( 1 ); break; case 'l' : window->SetWrap( 0 ); break; } } // Writing a key to the serial port is done by checking to see if // a translation is defined. If not, the key itself is sent out, // otherwise the translation is sent. void AnsiTerminal::WriteKey( int key ) { char *translation; if ( extended_keys && key > 256 ) { translation = extended_keys[ ( key >> 8 ) & 0xff ]; if ( translation != 0 ) port->Write( translation ); } else { if ( keys ) translation = keys[ key & 0xff ]; else translation = 0; if ( translation != 0 ) port->Write( translation ); else port->Write( key ); } } // ******************* END OF ANSITERM.CPP *******************