@@ -94,6 +94,10 @@ pub struct Selection {
9494///
9595/// Various fields are made public so that their values can be accessed while implementing the
9696/// trait.
97+ #[ cfg( feature = "clipboard" ) ]
98+ #[ cfg_attr( docsrs, cfg( feature = "clipboard" ) ) ]
99+ pub type ClipboardHandler = Box < dyn Fn ( & str ) + Send + Sync + ' static > ;
100+
97101#[ derive( Clone , Debug ) ]
98102pub ( crate ) struct HelpState {
99103 pub ( crate ) screen : Screen ,
@@ -153,6 +157,10 @@ pub struct PagerState {
153157 pub ( crate ) prompt : String ,
154158 /// The input classifier to be called when a input is detected
155159 pub ( crate ) input_classifier : Box < dyn input:: InputClassifier + Sync + Send > ,
160+ /// Callback that writes selected text to the clipboard when set; without
161+ /// it, `CopySelection` creates its own `arboard::Clipboard` handle.
162+ #[ cfg( feature = "clipboard" ) ]
163+ pub ( crate ) clipboard_handler : Option < ClipboardHandler > ,
156164 /// Functions to run when the pager quits
157165 pub ( crate ) exit_callbacks : Vec < Box < dyn FnMut ( ) + Send + Sync + ' static > > ,
158166 /// Callbacks for hooks
@@ -216,6 +224,8 @@ impl PagerState {
216224 running : & minus_core:: RUNMODE ,
217225 left_mark : 0 ,
218226 input_classifier : Box :: < HashedEventRegister < RandomState > > :: default ( ) ,
227+ #[ cfg( feature = "clipboard" ) ]
228+ clipboard_handler : None ,
219229 exit_callbacks : Vec :: with_capacity ( 5 ) ,
220230 hooks : Hooks :: new ( ) ,
221231 message : None ,
@@ -490,23 +500,24 @@ impl PagerState {
490500
491501 let mut selected = Vec :: with_capacity ( end_line. saturating_sub ( start_line) + 1 ) ;
492502 for line_idx in start_line..=end_line {
493- let line = * lines. get ( line_idx) ?;
503+ let raw_line = * lines. get ( line_idx) ?;
504+ let line = strip_ansi ( raw_line) ;
494505 let line_len = line. chars ( ) . count ( ) ;
495506 let start_col = if line_idx == start_line {
496- self . selection_col_in_line ( start, line_idx, line)
507+ self . selection_col_in_line ( start, line_idx, & line)
497508 . min ( line_len)
498509 } else {
499510 0
500511 } ;
501512 let end_col = if line_idx == end_line {
502- self . selection_col_in_line ( end, line_idx, line)
513+ self . selection_col_in_line ( end, line_idx, & line)
503514 . saturating_add ( 1 )
504515 . min ( line_len)
505516 } else {
506517 line_len
507518 } ;
508519
509- selected. push ( slice_chars ( line, start_col, end_col) . to_string ( ) ) ;
520+ selected. push ( slice_chars ( & line, start_col, end_col) . to_string ( ) ) ;
510521 }
511522
512523 Some ( selected. join ( "\n " ) )
@@ -701,29 +712,34 @@ fn highlight_visible_range(line: Cow<str>, start: usize, end: usize) -> Cow<str>
701712 let mut highlighted = false ;
702713
703714 while byte_idx < bytes. len ( ) {
715+ if highlighted && visible_idx == end {
716+ out. push_str ( RESET ) ;
717+ highlighted = false ;
718+ }
719+ if !highlighted && visible_idx == start {
720+ out. push_str ( REVERSE ) ;
721+ highlighted = true ;
722+ }
723+
704724 if bytes[ byte_idx] == b'\x1b' && bytes. get ( byte_idx + 1 ) == Some ( & b'[' ) {
705725 let esc_start = byte_idx;
706726 byte_idx += 2 ;
727+ let mut final_byte = 0 ;
707728 while byte_idx < bytes. len ( ) {
708729 let byte = bytes[ byte_idx] ;
709730 byte_idx += 1 ;
710731 if ( 0x40 ..=0x7e ) . contains ( & byte) {
732+ final_byte = byte;
711733 break ;
712734 }
713735 }
714736 out. push_str ( & line[ esc_start..byte_idx] ) ;
737+ if highlighted && final_byte == b'm' {
738+ out. push_str ( REVERSE ) ;
739+ }
715740 continue ;
716741 }
717742
718- if !highlighted && visible_idx == start {
719- out. push_str ( REVERSE ) ;
720- highlighted = true ;
721- }
722- if highlighted && visible_idx == end {
723- out. push_str ( RESET ) ;
724- highlighted = false ;
725- }
726-
727743 let ch = line[ byte_idx..] . chars ( ) . next ( ) . unwrap ( ) ;
728744 out. push ( ch) ;
729745 visible_idx += 1 ;
@@ -737,9 +753,66 @@ fn highlight_visible_range(line: Cow<str>, start: usize, end: usize) -> Cow<str>
737753 out. into ( )
738754}
739755
756+ pub ( crate ) fn strip_ansi ( s : & str ) -> String {
757+ let mut out = String :: with_capacity ( s. len ( ) ) ;
758+ let bytes = s. as_bytes ( ) ;
759+ let mut i = 0 ;
760+ while i < bytes. len ( ) {
761+ if bytes[ i] == b'\x1b' {
762+ if i + 1 < bytes. len ( ) {
763+ match bytes[ i + 1 ] {
764+ b'[' => {
765+ i += 2 ;
766+ while i < bytes. len ( ) {
767+ let b = bytes[ i] ;
768+ i += 1 ;
769+ if ( 0x40 ..=0x7e ) . contains ( & b) {
770+ break ;
771+ }
772+ }
773+ }
774+ b']' => {
775+ i += 2 ;
776+ while i < bytes. len ( ) {
777+ if bytes[ i] == 0x07 {
778+ i += 1 ;
779+ break ;
780+ }
781+ if bytes[ i] == b'\x1b' && i + 1 < bytes. len ( ) && bytes[ i + 1 ] == b'\\' {
782+ i += 2 ;
783+ break ;
784+ }
785+ i += 1 ;
786+ }
787+ }
788+ _ => {
789+ i += 2 ;
790+ }
791+ }
792+ } else {
793+ i += 1 ;
794+ }
795+ } else if bytes[ i] == 0x9b {
796+ i += 1 ;
797+ while i < bytes. len ( ) {
798+ let b = bytes[ i] ;
799+ i += 1 ;
800+ if ( 0x40 ..=0x7e ) . contains ( & b) {
801+ break ;
802+ }
803+ }
804+ } else {
805+ let ch = s[ i..] . chars ( ) . next ( ) . unwrap ( ) ;
806+ out. push ( ch) ;
807+ i += ch. len_utf8 ( ) ;
808+ }
809+ }
810+ out
811+ }
812+
740813#[ cfg( test) ]
741814mod tests {
742- use super :: { PagerState , Selection } ;
815+ use super :: { PagerState , Selection , highlight_visible_range , strip_ansi } ;
743816 use crate :: LineNumbers ;
744817
745818 #[ test]
@@ -762,6 +835,49 @@ mod tests {
762835 ) ;
763836 }
764837
838+ #[ test]
839+ fn extract_selection_with_ansi_styles ( ) {
840+ let mut ps = PagerState :: new ( ) . unwrap ( ) ;
841+ ps. line_numbers = LineNumbers :: Disabled ;
842+ ps. screen . line_wrapping = false ;
843+ ps. screen . orig_text =
844+ "\x1b [31mhello\x1b [0m \x1b [1;32mworld\x1b [0m\n \x1b [34msecond\x1b [0m line\n " . to_string ( ) ;
845+ ps. reformat_display ( ) ;
846+
847+ // Select "hello world" from line 0
848+ ps. selection_anchor = ps. selection_from_coordinates ( 0 , 0 ) ;
849+ ps. selection = ps. selection_from_coordinates ( 10 , 0 ) ;
850+ assert_eq ! ( ps. extract_selection( ) . as_deref( ) , Some ( "hello world" ) ) ;
851+
852+ // Select "world" from line 0
853+ ps. selection_anchor = ps. selection_from_coordinates ( 6 , 0 ) ;
854+ ps. selection = ps. selection_from_coordinates ( 10 , 0 ) ;
855+ assert_eq ! ( ps. extract_selection( ) . as_deref( ) , Some ( "world" ) ) ;
856+ }
857+
858+ #[ test]
859+ fn test_strip_ansi ( ) {
860+ assert_eq ! ( strip_ansi( "" ) , "" ) ;
861+ assert_eq ! ( strip_ansi( "plain text" ) , "plain text" ) ;
862+ assert_eq ! ( strip_ansi( "\x1b [31mhello\x1b [0m" ) , "hello" ) ;
863+ assert_eq ! ( strip_ansi( "\x1b [1;38;2;255;0;0mRGB\x1b [0m text" ) , "RGB text" ) ;
864+ assert_eq ! ( strip_ansi( "\x1b ]8;;https://example.com\x07 link\x1b ]8;;\x07 " ) , "link" ) ;
865+ assert_eq ! ( strip_ansi( "\x1b ]8;;https://example.com\x1b \\ link\x1b ]8;;\x1b \\ " ) , "link" ) ;
866+ }
867+
868+ #[ test]
869+ fn test_highlight_visible_range_with_ansi_styles ( ) {
870+ use std:: borrow:: Cow ;
871+ // Selection across style reset and color change: "hello world"
872+ let line = Cow :: Borrowed ( "\x1b [31mhello\x1b [0m \x1b [32mworld\x1b [0m" ) ;
873+ let highlighted = highlight_visible_range ( line, 0 , 11 ) ;
874+ // Highlight should be active for "hello", re-asserted after \x1b[0m and \x1b[32m, and reset after 11
875+ assert_eq ! (
876+ highlighted. as_ref( ) ,
877+ "\x1b [7m\x1b [31m\x1b [7mhello\x1b [0m\x1b [7m \x1b [32m\x1b [7mworld\x1b [27m\x1b [0m"
878+ ) ;
879+ }
880+
765881 #[ test]
766882 fn extract_selection_across_wrapped_rows ( ) {
767883 let mut ps = PagerState :: new ( ) . unwrap ( ) ;
0 commit comments