diff --git a/Makefile b/Makefile index bc51d91..834c228 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ proto = userinput.proto transportinstruction.proto -source = parse.cpp parserstate.cpp parser.cpp templates.cpp terminal.cpp termemu.cpp parseraction.cpp terminalfunctions.cpp swrite.cpp terminalframebuffer.cpp terminaldispatcher.cpp terminaluserinput.cpp terminaldisplay.cpp network.cpp ntester.cpp ocb.cpp base64.cpp encrypt.cpp decrypt.cpp crypto.cpp networktransport.cpp transportfragment.cpp user.cpp userinput.pb.cc completeterminal.cpp stm-server.cpp stm.cpp transportinstruction.pb.cc transportsender.cpp stmclient.cpp -objects = parserstate.o parser.o templates.o terminal.o parseraction.o terminalfunctions.o swrite.o terminalframebuffer.o terminaldispatcher.o terminaluserinput.o terminaldisplay.o network.o ocb.o base64.o crypto.o networktransport.o transportfragment.o user.o userinput.pb.o completeterminal.o transportinstruction.pb.o transportsender.o stmclient.o +source = parse.cpp parserstate.cpp parser.cpp templates.cpp terminal.cpp termemu.cpp parseraction.cpp terminalfunctions.cpp swrite.cpp terminalframebuffer.cpp terminaldispatcher.cpp terminaluserinput.cpp terminaldisplay.cpp network.cpp ntester.cpp ocb.cpp base64.cpp encrypt.cpp decrypt.cpp crypto.cpp networktransport.cpp transportfragment.cpp user.cpp userinput.pb.cc completeterminal.cpp stm-server.cpp stm.cpp transportinstruction.pb.cc transportsender.cpp stmclient.cpp terminaloverlay.cpp +objects = parserstate.o parser.o templates.o terminal.o parseraction.o terminalfunctions.o swrite.o terminalframebuffer.o terminaldispatcher.o terminaluserinput.o terminaldisplay.o network.o ocb.o base64.o crypto.o networktransport.o transportfragment.o user.o userinput.pb.o completeterminal.o transportinstruction.pb.o transportsender.o stmclient.o terminaloverlay.o repos = templates.rpo executables = parse termemu ntester encrypt decrypt stm-server stm diff --git a/completeterminal.hpp b/completeterminal.hpp index 9af689c..44f5330 100644 --- a/completeterminal.hpp +++ b/completeterminal.hpp @@ -19,6 +19,7 @@ namespace Terminal { std::string act( const Parser::Action *act ); const Framebuffer & get_fb( void ) const { return terminal.get_fb(); } + bool parser_grounded( void ) const { return parser.is_grounded(); } /* interface for Network::Transport */ void subtract( const Complete * ) {} diff --git a/parser.hpp b/parser.hpp index 4103f62..d70f83c 100644 --- a/parser.hpp +++ b/parser.hpp @@ -18,7 +18,7 @@ #endif namespace Parser { - static StateFamily family; + static const StateFamily family; class Parser { private: @@ -37,6 +37,8 @@ namespace Parser { { return state == x.state; } + + bool is_grounded( void ) const { return state == &family.s_Ground; } }; static const size_t BUF_SIZE = 8; @@ -57,6 +59,8 @@ namespace Parser { { return parser == x.parser; } + + bool is_grounded( void ) const { return parser.is_grounded(); } }; } diff --git a/stmclient.cpp b/stmclient.cpp index 9c0cdc0..afd2a4f 100644 --- a/stmclient.cpp +++ b/stmclient.cpp @@ -11,12 +11,14 @@ #include #include #include +#include #include "stmclient.hpp" #include "swrite.hpp" #include "networktransport.hpp" #include "completeterminal.hpp" #include "user.hpp" +#include "network.hpp" void STMClient::init( void ) { @@ -100,33 +102,44 @@ void STMClient::main_init( void ) } /* local state */ - local_terminal = new Terminal::Complete( window_size.ws_col, window_size.ws_row ); + local_framebuffer = new Terminal::Framebuffer( window_size.ws_col, window_size.ws_row ); /* initialize screen */ - string init = Terminal::Display::new_frame( false, local_terminal->get_fb(), local_terminal->get_fb() ); + string init = Terminal::Display::new_frame( false, *local_framebuffer, *local_framebuffer ); swrite( STDOUT_FILENO, init.data(), init.size() ); /* open network */ Network::UserStream blank; - network = new Network::Transport< Network::UserStream, Terminal::Complete >( blank, *local_terminal, + Terminal::Complete local_terminal( window_size.ws_col, window_size.ws_row ); + network = new Network::Transport< Network::UserStream, Terminal::Complete >( blank, local_terminal, key.c_str(), ip.c_str(), port ); /* tell server the size of the terminal */ network->get_current_state().push_back( Parser::Resize( window_size.ws_col, window_size.ws_row ) ); } +void STMClient::output_new_frame( void ) +{ + /* fetch target state */ + Terminal::Framebuffer new_state( network->get_latest_remote_state().state.get_fb() ); + + /* apply local overlays */ + overlays.get_notification_engine().render_notification(); + overlays.apply( new_state ); + + /* calculate minimal difference from where we are */ + string diff = Terminal::Display::new_frame( true, + *local_framebuffer, + new_state ); + swrite( STDOUT_FILENO, diff.data(), diff.size() ); + *local_framebuffer = new_state; +} + bool STMClient::process_network_input( void ) { network->recv(); - /* is a new frame available from the terminal? */ - if ( network->get_remote_state_num() != last_remote_num ) { - string diff = Terminal::Display::new_frame( true, - local_terminal->get_fb(), - network->get_latest_remote_state().state.get_fb() ); - swrite( STDOUT_FILENO, diff.data(), diff.size() ); - *local_terminal = network->get_latest_remote_state().state; - } + overlays.get_notification_engine().server_ping( network->get_latest_remote_state().timestamp ); return true; } @@ -147,7 +160,8 @@ bool STMClient::process_user_input( int fd ) if ( !network->shutdown_in_progress() ) { for ( int i = 0; i < bytes_read; i++ ) { - network->get_current_state().push_back( Parser::UserByte( buf[ i ] ) ); + char the_byte = buf[ i ]; + network->get_current_state().push_back( Parser::UserByte( the_byte ) ); } } @@ -207,6 +221,8 @@ void STMClient::main( void ) while ( 1 ) { try { + output_new_frame(); + int active_fds = poll( pollfds, 4, network->wait_time() ); if ( active_fds < 0 ) { perror( "poll" ); @@ -240,6 +256,7 @@ void STMClient::main( void ) } if ( network->attached() && (!network->shutdown_in_progress()) ) { + overlays.get_notification_engine().set_notification_string( wstring( L"Signal received, shutting down..." ) ); network->start_shutdown(); } else { break; @@ -256,6 +273,7 @@ void STMClient::main( void ) & (POLLERR | POLLHUP | POLLNVAL) ) { /* user problem */ if ( network->attached() && (!network->shutdown_in_progress()) ) { + overlays.get_notification_engine().set_notification_string( wstring( L"Exiting..." ) ); network->start_shutdown(); } else { break; @@ -279,8 +297,14 @@ void STMClient::main( void ) network->tick(); } catch ( Network::NetworkException e ) { - fprintf( stderr, "%s: %s\r\n", e.function.c_str(), strerror( e.the_errno ) ); - sleep( 1 ); + wchar_t tmp[ 128 ]; + swprintf( tmp, 128, L"%s: %s\r\n", e.function.c_str(), strerror( e.the_errno ) ); + overlays.get_notification_engine().set_notification_string( wstring( tmp ) ); + struct timespec req; + req.tv_sec = 0; + req.tv_nsec = 200000000; /* 0.2 sec */ + nanosleep( &req, NULL ); } } } + diff --git a/stmclient.hpp b/stmclient.hpp index b96b22e..741f183 100644 --- a/stmclient.hpp +++ b/stmclient.hpp @@ -8,6 +8,7 @@ #include "completeterminal.hpp" #include "networktransport.hpp" #include "user.hpp" +#include "terminaloverlay.hpp" class STMClient { private: @@ -20,7 +21,8 @@ private: int winch_fd, shutdown_signal_fd; struct winsize window_size; - Terminal::Complete *local_terminal; + Terminal::Framebuffer *local_framebuffer; + Overlay::OverlayManager overlays; Network::Transport< Network::UserStream, Terminal::Complete > *network; uint64_t last_remote_num; @@ -29,13 +31,16 @@ private: bool process_user_input( int fd ); bool process_resize( void ); + void output_new_frame( void ); + public: STMClient( const char *s_ip, int s_port, const char *s_key ) : ip( s_ip ), port( s_port ), key( s_key ), saved_termios(), raw_termios(), winch_fd(), shutdown_signal_fd(), window_size(), - local_terminal( NULL ), + local_framebuffer( NULL ), + overlays(), network( NULL ), last_remote_num( -1 ) {} @@ -46,8 +51,8 @@ public: ~STMClient() { - if ( local_terminal != NULL ) { - delete local_terminal; + if ( local_framebuffer != NULL ) { + delete local_framebuffer; } if ( network != NULL ) { diff --git a/templates.cpp b/templates.cpp index 69aa83c..ef45af2 100644 --- a/templates.cpp +++ b/templates.cpp @@ -6,6 +6,7 @@ #include "terminal.hpp" #include "completeterminal.hpp" +#include "terminaloverlay.hpp" #include "user.hpp" #include "networktransport.cpp" @@ -19,6 +20,7 @@ namespace Parser { using namespace std; using namespace Terminal; using namespace Network; +using namespace Overlay; template class list; template class vector; @@ -38,3 +40,4 @@ template class TransportSender; template class TransportSender; template class deque; +template class list; diff --git a/terminaldisplay.cpp b/terminaldisplay.cpp index 08d980b..0cea4fe 100644 --- a/terminaldisplay.cpp +++ b/terminaldisplay.cpp @@ -84,11 +84,7 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const if ( lines_scrolled ) { if ( frame.cursor_y != f.ds.get_height() - 1 ) { - snprintf( tmp, 64, "\033[%d;%dH", f.ds.get_height(), 1 ); - frame.append( tmp ); - - frame.cursor_y = f.ds.get_height() - 1; - frame.cursor_x = 0; + frame.append_silent_move( f.ds.get_height() - 1, 0 ); } if ( frame.current_rendition_string != "\033[0m" ) { @@ -194,10 +190,7 @@ void Display::put_cell( bool initialized, FrameState &frame, const Framebuffer & } if ( (frame.x != frame.cursor_x) || (frame.y != frame.cursor_y) ) { - snprintf( tmp, 64, "\033[%d;%dH", frame.y + 1, frame.x + 1 ); - frame.append( tmp ); - frame.cursor_x = frame.x; - frame.cursor_y = frame.y; + frame.append_silent_move( frame.y, frame.x ); } std::string rendition_str = cell->renditions.sgr(); @@ -243,3 +236,19 @@ void Display::put_cell( bool initialized, FrameState &frame, const Framebuffer & frame.x += cell->width; frame.cursor_x += cell->width; } + +void FrameState::append_silent_move( int y, int x ) +{ + char tmp[ 64 ]; + + /* turn off cursor if necessary before moving cursor */ + if ( last_frame.ds.cursor_visible ) { + append( "\033[?25l" ); + last_frame.ds.cursor_visible = false; + } + + snprintf( tmp, 64, "\033[%d;%dH", y + 1, x + 1 ); + append( tmp ); + cursor_x = x; + cursor_y = y; +} diff --git a/terminaldisplay.hpp b/terminaldisplay.hpp index 97417aa..bb73c26 100644 --- a/terminaldisplay.hpp +++ b/terminaldisplay.hpp @@ -17,6 +17,8 @@ namespace Terminal { FrameState( const Framebuffer &s_last ) : x(0), y(0), str(), cursor_x(0), cursor_y(0), current_rendition_string(), last_frame( s_last ) {} void append( std::string s ) { str.append( s ); } + + void append_silent_move( int y, int x ); }; class Display { diff --git a/terminaloverlay.cpp b/terminaloverlay.cpp new file mode 100644 index 0000000..029c191 --- /dev/null +++ b/terminaloverlay.cpp @@ -0,0 +1,234 @@ +#include +#include +#include + +#include "terminaloverlay.hpp" + +using namespace Overlay; + +Validity OverlayElement::get_validity( const Framebuffer & ) const +{ + return (timestamp() < expiration_time) ? Pending : IncorrectOrExpired; +} + +void OverlayCell::apply( Framebuffer &fb ) const +{ + if ( (row >= fb.ds.get_height()) + || (col >= fb.ds.get_width()) ) { + return; + } + + *(fb.get_mutable_cell( row, col )) = replacement; +} + +Validity ConditionalOverlayCell::get_validity( const Framebuffer &fb ) const +{ + if ( (row >= fb.ds.get_height()) + || (col >= fb.ds.get_width()) ) { + return IncorrectOrExpired; + } + + const Cell ¤t = *( fb.get_cell( row, col ) ); + + if ( (timestamp() < expiration_time) && (current == original_contents) ) { + return Pending; + } + + if ( current == replacement ) { + return Correct; + } else { + return IncorrectOrExpired; + } +} + +void CursorMove::apply( Framebuffer &fb ) const +{ + assert( new_row < fb.ds.get_height() ); + assert( new_col < fb.ds.get_width() ); + assert( !fb.ds.origin_mode ); + + fb.ds.move_row( new_row, false ); + fb.ds.move_col( new_col, false, false ); +} + +Validity ConditionalCursorMove::get_validity( const Framebuffer &fb ) const +{ + if ( timestamp() < expiration_time ) { + return Pending; + } + + if ( (fb.ds.get_cursor_col() == new_col) + && (fb.ds.get_cursor_row() == new_row) ) { + return Correct; + } else { + return IncorrectOrExpired; + } +} + +void OverlayEngine::clear( void ) +{ + for_each( elements.begin(), elements.end(), []( OverlayElement *x ){ delete x; } ); + elements.clear(); +} + +OverlayEngine::~OverlayEngine() +{ + clear(); +} + +void OverlayEngine::apply( Framebuffer &fb ) const +{ + for_each( elements.begin(), elements.end(), + [&fb]( OverlayElement *x ) { x->apply( fb ); } ); +} + +void OverlayEngine::cull( const Framebuffer &fb ) +{ + elements.remove_if( [fb]( OverlayElement *x ) { return IncorrectOrExpired == x->get_validity( fb ); } ); +} + +OverlayCell::OverlayCell( uint64_t expiration_time, int s_row, int s_col, int background_color ) + : OverlayElement( expiration_time ), row( s_row ), col( s_col ), replacement( background_color ) +{} + +NotificationEngine::NotificationEngine() + : needs_render( true ), + last_word( timestamp() ), + last_render( 0 ), + message(), + message_expiration( 0 ) +{} + +void NotificationEngine::server_ping( uint64_t s_last_word ) +{ + if ( s_last_word - last_word > 4000 ) { + needs_render = true; + } + + last_word = s_last_word; +} + +void NotificationEngine::set_notification_string( const wstring s_message ) +{ + message = s_message; + message_expiration = timestamp() + 1100; + needs_render = true; +} + +void NotificationEngine::render_notification( void ) +{ + uint64_t now = timestamp(); + + if ( (now - last_render < 250) && (!needs_render) ) { + return; + } + + needs_render = false; + last_render = now; + + clear(); + + /* determine string to draw */ + if ( now >= message_expiration ) { + message.clear(); + } + + bool time_expired = now - last_word > 5000; + + wchar_t tmp[ 128 ]; + + if ( message.empty() && (!time_expired) ) { + return; + } else if ( message.empty() && time_expired ) { + swprintf( tmp, 128, L"[stm] No contact for %.0f seconds.", (double)(now - last_word) / 1000.0 ); + } else if ( (!message.empty()) && (!time_expired) ) { + swprintf( tmp, 128, L"[stm] %ls", message.c_str() ); + } else { + swprintf( tmp, 128, L"[stm] %ls (No contact for %.0f seconds.)", message.c_str(), + (double)(now - last_word) / 1000.0 ); + } + + wstring string_to_draw( tmp ); + + int overlay_col = 0; + bool dirty = false; + OverlayCell template_cell( now + 1100, 0 /* row */, -1 /* col */, 0 /* background_color */ ); + + template_cell.replacement.renditions.inverse = true; + + OverlayCell current( template_cell ); + + for ( wstring::const_iterator i = string_to_draw.begin(); i != string_to_draw.end(); i++ ) { + wchar_t ch = *i; + int chwidth = ch == L'\0' ? -1 : wcwidth( ch ); + + switch ( chwidth ) { + case 1: /* normal character */ + case 2: /* wide character */ + /* finish current cell */ + if ( dirty ) { + elements.push_back( new OverlayCell( current ) ); + dirty = false; + } + + /* initialize new cell */ + current = template_cell; + current.col = overlay_col; + current.replacement.contents.push_back( ch ); + current.replacement.width = chwidth; + overlay_col += chwidth; + dirty = true; + break; + + case 0: /* combining character */ + if ( current.replacement.contents.empty() ) { + /* string starts with combining character?? */ + /* emulate fallback rendering */ + current = template_cell; + current.col = overlay_col; + current.replacement.contents.push_back( 0xA0 ); /* no-break space */ + current.replacement.width = 1; + overlay_col++; + dirty = true; + } + + current.replacement.contents.push_back( ch ); + break; + + case -1: + break; + + default: + assert( false ); + } + } + + if ( dirty ) { + elements.push_back( new OverlayCell( current ) ); + } +} + +void NotificationEngine::apply( Framebuffer &fb ) const +{ + if ( elements.empty() ) { + return; + } + + assert( fb.ds.get_width() > 0 ); + assert( fb.ds.get_height() > 0 ); + + Cell notification_bar( 0 ); + notification_bar.renditions.inverse = true; + notification_bar.contents.push_back( 0x20 ); + + for ( int i = 0; i < fb.ds.get_width(); i++ ) { + *(fb.get_mutable_cell( 0, i )) = notification_bar; + } + + OverlayEngine::apply( fb ); +} + +void OverlayManager::apply( Framebuffer &fb ) const +{ + notifications.apply( fb ); +} diff --git a/terminaloverlay.hpp b/terminaloverlay.hpp new file mode 100644 index 0000000..13e2169 --- /dev/null +++ b/terminaloverlay.hpp @@ -0,0 +1,107 @@ +#ifndef TERMINAL_OVERLAY_HPP +#define TERMINAL_OVERLAY_HPP + +#include "terminalframebuffer.hpp" +#include "network.hpp" + +#include + +namespace Overlay { + using namespace Terminal; + using namespace Network; + using namespace std; + + enum Validity { + Pending, + Correct, + IncorrectOrExpired + }; + + /* The individual elements of an overlay -- cursor movements and replaced cells */ + class OverlayElement { + public: + uint64_t expiration_time; + + virtual void apply( Framebuffer &fb ) const = 0; + virtual Validity get_validity( const Framebuffer & ) const; + + OverlayElement( uint64_t s_expiration_time ) : expiration_time( s_expiration_time ) {} + virtual ~OverlayElement() {} + }; + + class OverlayCell : public OverlayElement { + public: + int row, col; + Cell replacement; + + OverlayCell( uint64_t expiration_time, int s_row, int s_col, int background_color ); + void apply( Framebuffer &fb ) const; + }; + + class ConditionalOverlayCell : public OverlayCell { + public: + Cell original_contents; + + Validity get_validity( const Framebuffer &fb ) const; + }; + + class CursorMove : public OverlayElement { + public: + int new_row, new_col; + + void apply( Framebuffer &fb ) const; + }; + + class ConditionalCursorMove : public CursorMove { + public: + Validity get_validity( const Framebuffer &fb ) const; + }; + + /* the various overlays -- some predictive and some for local notifications */ + class OverlayEngine { + protected: + list elements; + + public: + void cull( const Framebuffer &fb ); + virtual void apply( Framebuffer &fb ) const; + void clear( void ); + + OverlayEngine() : elements() {} + virtual ~OverlayEngine(); + }; + + class NotificationEngine : public OverlayEngine { + private: + bool needs_render; + + uint64_t last_word; + uint64_t last_render; + + wstring message; + uint64_t message_expiration; + + public: + void apply( Framebuffer &fb ) const; + void set_notification_string( const wstring s_message ); + void server_ping( uint64_t s_last_word ); + void render_notification( void ); + + NotificationEngine(); + }; + + /* the overlay manager */ + class OverlayManager { + private: + NotificationEngine notifications; + + public: + void apply( Framebuffer &fb ) const; + + NotificationEngine & get_notification_engine( void ) { return notifications; } + + OverlayManager() : notifications() {} + }; +} + +#endif diff --git a/transportsender.cpp b/transportsender.cpp index 3fb9612..5b45d14 100644 --- a/transportsender.cpp +++ b/transportsender.cpp @@ -1,4 +1,5 @@ #include +#include #include "transportsender.hpp" #include "transportfragment.hpp"