diff --git a/src/terminal/terminal.cc b/src/terminal/terminal.cc index f2587b41c..d9ebb4c88 100644 --- a/src/terminal/terminal.cc +++ b/src/terminal/terminal.cc @@ -106,6 +106,7 @@ void Emulator::print( const Parser::Print* act ) this_cell->append( ch ); this_cell->set_wide( chwidth == 2 ); /* chwidth had better be 1 or 2 here */ fb.apply_renditions_to_cell( this_cell ); + fb.apply_hyperlink_to_cell( this_cell ); if ( chwidth == 2 && fb.ds.get_cursor_col() + 1 < fb.ds.get_width() ) { /* erase overlapped cell */ fb.reset_cell( fb.get_mutable_cell( fb.ds.get_cursor_row(), fb.ds.get_cursor_col() + 1 ) ); diff --git a/src/terminal/terminaldispatcher.h b/src/terminal/terminaldispatcher.h index 47a10c3a7..1b8c3fb7c 100644 --- a/src/terminal/terminaldispatcher.h +++ b/src/terminal/terminaldispatcher.h @@ -95,7 +95,7 @@ class Dispatcher bool parsed; std::string dispatch_chars; - std::vector OSC_string; /* only used to set the window title */ + std::vector OSC_string; void parse_params( void ); diff --git a/src/terminal/terminaldisplay.cc b/src/terminal/terminaldisplay.cc index c44952857..ca2ae8655 100644 --- a/src/terminal/terminaldisplay.cc +++ b/src/terminal/terminaldisplay.cc @@ -130,10 +130,12 @@ std::string Display::new_frame( bool initialized, const Framebuffer& last, const initialized = false; frame.cursor_x = frame.cursor_y = 0; frame.current_rendition = initial_rendition(); + frame.current_hyperlink = Hyperlink(); } else { frame.cursor_x = frame.last_frame.ds.get_cursor_col(); frame.cursor_y = frame.last_frame.ds.get_cursor_row(); frame.current_rendition = frame.last_frame.ds.get_renditions(); + frame.current_hyperlink = frame.last_frame.ds.get_hyperlink(); } /* is cursor visibility initialized? */ @@ -203,6 +205,7 @@ std::string Display::new_frame( bool initialized, const Framebuffer& last, const blank_row = std::make_shared( w, c ); } frame.update_rendition( initial_rendition(), true ); + frame.update_hyperlink( Hyperlink(), true ); int top_margin = 0; int bottom_margin = top_margin + lines_scrolled + scroll_height - 1; @@ -269,6 +272,8 @@ std::string Display::new_frame( bool initialized, const Framebuffer& last, const /* have renditions changed? */ frame.update_rendition( f.ds.get_renditions(), !initialized ); + /* has hyperlink changed? */ + frame.update_hyperlink( f.ds.get_hyperlink(), !initialized ); /* has bracketed paste mode changed? */ if ( ( !initialized ) || ( f.ds.bracketed_paste != frame.last_frame.ds.bracketed_paste ) ) { @@ -334,6 +339,7 @@ bool Display::put_row( bool initialized, if ( wrap ) { const Cell& cell = cells.at( 0 ); frame.update_rendition( cell.get_renditions() ); + frame.update_hyperlink( cell.get_hyperlink() ); frame.append_cell( cell ); frame_x += cell.get_width(); frame.cursor_x += cell.get_width(); @@ -349,6 +355,7 @@ bool Display::put_row( bool initialized, int clear_count = 0; bool wrote_last_cell = false; Renditions blank_renditions = initial_rendition(); + Hyperlink blank_hyperlink; /* iterate for every cell */ while ( frame_x < row_width ) { @@ -365,8 +372,9 @@ bool Display::put_row( bool initialized, if ( cell.empty() ) { if ( !clear_count ) { blank_renditions = cell.get_renditions(); + blank_hyperlink = cell.get_hyperlink(); } - if ( cell.get_renditions() == blank_renditions ) { + if ( cell.get_renditions() == blank_renditions && cell.get_hyperlink() == blank_hyperlink ) { /* Remember run of blank cells */ clear_count++; frame_x++; @@ -379,8 +387,8 @@ bool Display::put_row( bool initialized, /* Move to the right position. */ frame.append_silent_move( frame_y, frame_x - clear_count ); frame.update_rendition( blank_renditions ); - bool can_use_erase = has_bce || ( frame.current_rendition == initial_rendition() ); - if ( can_use_erase && has_ech && clear_count > 4 ) { + frame.update_hyperlink( blank_hyperlink ); + if ( can_use_erase( frame ) && has_ech && clear_count > 4 ) { snprintf( tmp, 64, "\033[%dX", clear_count ); frame.append( tmp ); } else { @@ -392,6 +400,7 @@ bool Display::put_row( bool initialized, // we restart counting and continue here if ( cell.empty() ) { blank_renditions = cell.get_renditions(); + blank_hyperlink = cell.get_hyperlink(); clear_count = 1; frame_x++; continue; @@ -413,6 +422,7 @@ bool Display::put_row( bool initialized, } frame.append_silent_move( frame_y, frame_x ); frame.update_rendition( cell.get_renditions() ); + frame.update_hyperlink( cell.get_hyperlink() ); frame.append_cell( cell ); frame_x += cell_width; frame.cursor_x += cell_width; @@ -428,9 +438,9 @@ bool Display::put_row( bool initialized, /* Move to the right position. */ frame.append_silent_move( frame_y, frame_x - clear_count ); frame.update_rendition( blank_renditions ); + frame.update_hyperlink( blank_hyperlink ); - bool can_use_erase = has_bce || ( frame.current_rendition == initial_rendition() ); - if ( can_use_erase && !wrap_this ) { + if ( can_use_erase( frame ) && !wrap_this ) { frame.append( "\033[K" ); } else { frame.append( clear_count, ' ' ); @@ -458,9 +468,14 @@ bool Display::put_row( bool initialized, return false; } +bool Display::can_use_erase( const FrameState& frame ) const +{ + return has_bce || ( frame.current_rendition == initial_rendition() && frame.current_hyperlink.empty() ); +} + FrameState::FrameState( const Framebuffer& s_last ) - : str(), cursor_x( 0 ), cursor_y( 0 ), current_rendition( 0 ), cursor_visible( s_last.ds.cursor_visible ), - last_frame( s_last ) + : str(), cursor_x( 0 ), cursor_y( 0 ), current_rendition( 0 ), current_hyperlink(), + cursor_visible( s_last.ds.cursor_visible ), last_frame( s_last ) { /* Preallocate for better performance. Make a guess-- doesn't matter for correctness */ str.reserve( last_frame.ds.get_width() * last_frame.ds.get_height() * 4 ); @@ -514,3 +529,12 @@ void FrameState::update_rendition( const Renditions& r, bool force ) current_rendition = r; } } + +void FrameState::update_hyperlink( const Hyperlink& h, bool force ) +{ + if ( force || current_hyperlink != h ) { + /* print hyperlink */ + append_string( h.osc8() ); + current_hyperlink = h; + } +} diff --git a/src/terminal/terminaldisplay.h b/src/terminal/terminaldisplay.h index bd900e8fa..066ef945c 100644 --- a/src/terminal/terminaldisplay.h +++ b/src/terminal/terminaldisplay.h @@ -44,6 +44,7 @@ class FrameState int cursor_x, cursor_y; Renditions current_rendition; + Hyperlink current_hyperlink; bool cursor_visible; const Framebuffer& last_frame; @@ -60,6 +61,7 @@ class FrameState void append_silent_move( int y, int x ); void append_move( int y, int x ); void update_rendition( const Renditions& r, bool force = false ); + void update_hyperlink( const Hyperlink& h, bool force = false ); }; class Display @@ -81,6 +83,8 @@ class Display const Row& old_row, bool wrap ) const; + bool can_use_erase( const FrameState& frame ) const; + public: std::string open() const; std::string close() const; diff --git a/src/terminal/terminalframebuffer.cc b/src/terminal/terminalframebuffer.cc index 5d3a1bc07..ae6b83268 100644 --- a/src/terminal/terminalframebuffer.cc +++ b/src/terminal/terminalframebuffer.cc @@ -39,13 +39,14 @@ using namespace Terminal; Cell::Cell( color_type background_color ) - : contents(), renditions( background_color ), wide( false ), fallback( false ), wrap( false ) + : contents(), renditions( background_color ), hyperlink(), wide( false ), fallback( false ), wrap( false ) {} void Cell::reset( color_type background_color ) { contents.clear(); renditions = Renditions( background_color ); + hyperlink = Hyperlink(); wide = false; fallback = false; wrap = false; @@ -62,7 +63,7 @@ void DrawState::reinitialize_tabs( unsigned int start ) DrawState::DrawState( int s_width, int s_height ) : width( s_width ), height( s_height ), cursor_col( 0 ), cursor_row( 0 ), combining_char_col( 0 ), combining_char_row( 0 ), default_tabs( true ), tabs( s_width ), scrolling_region_top_row( 0 ), - scrolling_region_bottom_row( height - 1 ), renditions( 0 ), save(), next_print_will_wrap( false ), + scrolling_region_bottom_row( height - 1 ), renditions( 0 ), hyperlink(), save(), next_print_will_wrap( false ), origin_mode( false ), auto_wrap_mode( true ), insert_mode( false ), cursor_visible( true ), reverse_video( false ), bracketed_paste( false ), mouse_reporting_mode( MOUSE_REPORTING_NONE ), mouse_focus_event( false ), mouse_alternate_scroll( false ), mouse_encoding_mode( MOUSE_ENCODING_DEFAULT ), @@ -268,6 +269,14 @@ void Framebuffer::apply_renditions_to_cell( Cell* cell ) cell->set_renditions( ds.get_renditions() ); } +void Framebuffer::apply_hyperlink_to_cell( Cell* cell ) +{ + if ( !cell ) { + cell = get_mutable_cell(); + } + cell->set_hyperlink( ds.get_hyperlink() ); +} + SavedCursor::SavedCursor() : cursor_col( 0 ), cursor_row( 0 ), renditions( 0 ), auto_wrap_mode( true ), origin_mode( false ) {} @@ -390,6 +399,7 @@ void Framebuffer::soft_reset( void ) ds.application_mode_cursor_keys = false; ds.set_scrolling_region( 0, ds.get_height() - 1 ); ds.add_rendition( 0 ); + ds.set_hyperlink( Hyperlink() ); ds.clear_saved_cursor(); } @@ -594,6 +604,34 @@ std::string Renditions::sgr( void ) const return ret; } +bool Hyperlink::operator==( const Hyperlink& x ) const +{ + if ( rep == x.rep ) { + return true; + } + if ( rep == nullptr || x.rep == nullptr ) { + return false; + } + + return rep->url == x.rep->url && rep->params == x.rep->params; +} + +std::string Hyperlink::osc8() const +{ + std::string ret; + + ret.append( "\033]8;" ); + + if ( *this ) + ret.append( rep->params ); + ret.append( ";" ); + if ( *this ) + ret.append( rep->url ); + + ret.append( "\033\\" ); + return ret; +} + void Row::reset( color_type background_color ) { gen = get_gen(); diff --git a/src/terminal/terminalframebuffer.h b/src/terminal/terminalframebuffer.h index 131ee1835..ef87ea84b 100644 --- a/src/terminal/terminalframebuffer.h +++ b/src/terminal/terminalframebuffer.h @@ -40,6 +40,7 @@ #include #include #include +#include #include /* Terminal framebuffer */ @@ -98,12 +99,39 @@ class Renditions void clear_attributes() { attributes = 0; } }; +class Hyperlink +{ +public: + Hyperlink() : rep( nullptr ) {} + Hyperlink( std::string params, std::string url ) + : rep( url.empty() ? nullptr : std::make_shared( Rep { std::move( params ), std::move( url ) } ) ) + {} + + std::string osc8() const; + + bool empty() const { return rep == nullptr; } + operator bool() const { return !empty(); } + + bool operator==( const Hyperlink& x ) const; + + bool operator!=( const Hyperlink& x ) const { return !operator==( x ); } + +private: + struct Rep + { + std::string params; + std::string url; + }; + std::shared_ptr rep; +}; + class Cell { private: typedef std::string content_type; /* can be std::string, std::vector, or __gnu_cxx::__vstring */ content_type contents; Renditions renditions; + Hyperlink hyperlink; unsigned int wide : 1; /* 0 = narrow, 1 = wide */ unsigned int fallback : 1; /* first character is combining character */ unsigned int wrap : 1; @@ -119,7 +147,7 @@ class Cell bool operator==( const Cell& x ) const { return ( ( contents == x.contents ) && ( fallback == x.fallback ) && ( wide == x.wide ) - && ( renditions == x.renditions ) && ( wrap == x.wrap ) ); + && ( renditions == x.renditions ) && ( hyperlink == x.hyperlink ) && ( wrap == x.wrap ) ); } bool operator!=( const Cell& x ) const { return !operator==( x ); } @@ -198,6 +226,8 @@ class Cell } /* Other accessors */ + const Hyperlink& get_hyperlink() const { return hyperlink; } + void set_hyperlink( Hyperlink l ) { hyperlink = std::move( l ); } const Renditions& get_renditions( void ) const { return renditions; } Renditions& get_renditions( void ) { return renditions; } void set_renditions( const Renditions& r ) { renditions = r; } @@ -271,6 +301,7 @@ class DrawState int scrolling_region_top_row, scrolling_region_bottom_row; Renditions renditions; + Hyperlink hyperlink; SavedCursor save; @@ -332,6 +363,9 @@ class DrawState int limit_top( void ) const; int limit_bottom( void ) const; + const Hyperlink& get_hyperlink() const { return hyperlink; } + void set_hyperlink( Hyperlink x ) { hyperlink = std::move( x ); } + void set_foreground_color( int x ) { renditions.set_foreground_color( x ); } void set_background_color( int x ) { renditions.set_background_color( x ); } void add_rendition( color_type x ) { renditions.set_rendition( x ); } @@ -355,7 +389,7 @@ class DrawState && ( reverse_video == x.reverse_video ) && ( renditions == x.renditions ) && ( bracketed_paste == x.bracketed_paste ) && ( mouse_reporting_mode == x.mouse_reporting_mode ) && ( mouse_focus_event == x.mouse_focus_event ) && ( mouse_alternate_scroll == x.mouse_alternate_scroll ) - && ( mouse_encoding_mode == x.mouse_encoding_mode ); + && ( mouse_encoding_mode == x.mouse_encoding_mode ) && hyperlink == x.hyperlink; } }; @@ -448,6 +482,7 @@ class Framebuffer Cell* get_combining_cell( void ); void apply_renditions_to_cell( Cell* cell ); + void apply_hyperlink_to_cell( Cell* cell ); void insert_line( int before_row, int count ); void delete_line( int row, int count ); diff --git a/src/terminal/terminalfunctions.cc b/src/terminal/terminalfunctions.cc index 40c41afc4..a823fab76 100644 --- a/src/terminal/terminalfunctions.cc +++ b/src/terminal/terminalfunctions.cc @@ -33,6 +33,8 @@ #include #include #include +#include +#include #include @@ -588,6 +590,39 @@ static void CSI_DECSTR( Framebuffer* fb, Dispatcher* dispatch __attribute( ( unu static Function func_CSI_DECSTR( CSI, "!p", CSI_DECSTR ); +static bool Parse_OSC_8( const std::vector& osc8_vector, std::string& osc8_str ) +{ + osc8_str.reserve( osc8_vector.size() ); + for ( wchar_t wide_char : osc8_vector ) { + // Valid char range is 32-126, per + // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#encodings + if ( wide_char < 32 || wide_char > 126 ) { + return false; + } + osc8_str.append( 1, static_cast( wide_char ) ); + } + return true; +} + +static void OSC_8( const std::string& OSC_string, Framebuffer* fb ) +{ + // OSC of the form "\033]8;params;url\007" + assert( OSC_string[0] == '8' ); + if ( OSC_string.size() <= 2 || OSC_string[1] != ';' ) { + // Bail early if the string is malformed. + return; + } + + size_t second_semicolon = OSC_string.find_first_of( ';', 2 ); + if ( second_semicolon == std::string::npos ) { + // Missing the second semicolon, malformed. + return; + } + + std::string url = OSC_string.substr( second_semicolon + 1 ); + fb->ds.set_hyperlink( Hyperlink( OSC_string.substr( 2, second_semicolon - 2 ), std::move( url ) ) ); +} + /* xterm uses an Operating System Command to set the window title */ void Dispatcher::OSC_dispatch( const Parser::OSC_End* act __attribute( ( unused ) ), Framebuffer* fb ) { @@ -612,6 +647,16 @@ void Dispatcher::OSC_dispatch( const Parser::OSC_End* act __attribute( ( unused cmd_num = OSC_string[0] - L'0'; offset = 2; } + if ( cmd_num == 8 ) { + // Handle OSC8 hyperlinks separately + std::string osc_8_str; + if ( !Parse_OSC_8( OSC_string, osc_8_str ) ) { + // + return; + } + OSC_8( osc_8_str, fb ); + return; + } bool set_icon = cmd_num == 0 || cmd_num == 1; bool set_title = cmd_num == 0 || cmd_num == 2; if ( set_icon || set_title ) { diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am index aacdaab69..c7d124e0e 100644 --- a/src/tests/Makefile.am +++ b/src/tests/Makefile.am @@ -21,6 +21,7 @@ displaytests = \ emulation-attributes-256color248.test \ emulation-attributes-truecolor.test \ emulation-attributes-bce.test \ + emulation-attributes-osc8.test \ emulation-back-tab.test \ emulation-cursor-motion.test \ emulation-multiline-scroll.test \ diff --git a/src/tests/emulation-attributes-osc8.test b/src/tests/emulation-attributes-osc8.test new file mode 120000 index 000000000..03c0a0163 --- /dev/null +++ b/src/tests/emulation-attributes-osc8.test @@ -0,0 +1 @@ +emulation-attributes.test \ No newline at end of file diff --git a/src/tests/emulation-attributes.test b/src/tests/emulation-attributes.test index 7e2201943..fc58e44de 100755 --- a/src/tests/emulation-attributes.test +++ b/src/tests/emulation-attributes.test @@ -35,6 +35,15 @@ if [ "$(basename "$0")" = emulation-attributes-bce.test ] && exit 77 fi +# Need 3.4 for OSC 8 support +if [ "$(basename "$0")" = emulation-attributes-osc8.test ] && + ! tmux_check 3 4; then + printf "tmux does not support OSC 8 hyperlinks\n" >&2 + exit 77 +fi + + + # Top-level wrapper. if [ $# -eq 0 ]; then e2e-test "$0" baseline direct verify @@ -139,6 +148,12 @@ baseline() printf '\033[42m\033[J16 color\n' printf '\033[0mdone\n' ;; + osc8) + printf '\033]8;;http://example.com\033\133This is a link\033]8;;\033\133\n' + printf '\033]8;id=foo;http://example.com\033\133link\033]8;;\033\133\n' + printf '\033]8;foo=bar:bar=baz;http://example.com\033\133link\033]8;;\033\133\n' + printf "\e]8;;vscode://file/home/achin/test:1:1\e\x5c\e[0m\e[35mtest\e[0m\e]8;;\e\x5c:\e[0m\e[1m\e[31mtest\e[0m\n" + ;; *) fail "unknown test name %s\n" "$1" ;;