@@ -163,7 +163,7 @@ impl ObjectPreviewPage {
163163 self . disable_image_render( ) ;
164164 }
165165 UserEvent :: ObjectPreviewCopy => {
166- self . tx . send ( AppEventType :: NotifyWarn ( "Cannot copy image content. Copy is only available for text files." . to_string ( ) ) ) ;
166+ self . copy_image_content ( ) ;
167167 }
168168 UserEvent :: Help => {
169169 self . tx. send( AppEventType :: OpenHelp ) ;
@@ -279,6 +279,7 @@ impl ObjectPreviewPage {
279279 BuildHelpsItem :: new( UserEvent :: ObjectPreviewBack , "Close preview" ) ,
280280 BuildHelpsItem :: new( UserEvent :: ObjectPreviewDownload , "Download object" ) ,
281281 BuildHelpsItem :: new( UserEvent :: ObjectPreviewDownloadAs , "Download object as" ) ,
282+ BuildHelpsItem :: new( UserEvent :: ObjectPreviewCopy , "Copy content to clipboard" ) ,
282283 ]
283284 } ,
284285 ( ViewState :: SaveDialog ( _) , _) => {
@@ -320,6 +321,7 @@ impl ObjectPreviewPage {
320321 vec ! [
321322 BuildShortHelpsItem :: single( UserEvent :: Quit , "Quit" , 0 ) ,
322323 BuildShortHelpsItem :: group( vec![ UserEvent :: ObjectPreviewDownload , UserEvent :: ObjectPreviewDownloadAs ] , "Download" , 2 ) ,
324+ BuildShortHelpsItem :: single( UserEvent :: ObjectPreviewCopy , "Copy" , 3 ) ,
323325 BuildShortHelpsItem :: single( UserEvent :: ObjectPreviewBack , "Close" , 1 ) ,
324326 BuildShortHelpsItem :: single( UserEvent :: Help , "Help" , 0 ) ,
325327 ]
@@ -356,13 +358,24 @@ impl ObjectPreviewPage {
356358 let ( content, _, _) = encoding. decode ( & self . object . bytes ) ;
357359 let content_string = content. into_owned ( ) ;
358360
359- self . tx . send ( AppEventType :: CopyToClipboard (
361+ self . tx . send ( AppEventType :: CopyTextToClipboard (
360362 self . file_detail . name . clone ( ) ,
361363 content_string,
362364 ) ) ;
363365 }
364366 }
365367
368+ fn copy_image_content ( & mut self ) {
369+ if let PreviewType :: Image ( state) = & self . preview_type {
370+ if let Some ( ( width, height, bytes) ) = state. base_image_data ( ) {
371+ self . tx . send ( AppEventType :: CopyImageToClipboard (
372+ self . file_detail . name . clone ( ) ,
373+ ( width, height, bytes) ,
374+ ) ) ;
375+ }
376+ }
377+ }
378+
366379 fn close_save_dialog ( & mut self ) {
367380 self . view_state = ViewState :: Default ;
368381 }
@@ -447,13 +460,7 @@ mod tests {
447460
448461 use super :: * ;
449462 use chrono:: { DateTime , Local , NaiveDateTime } ;
450- use ratatui:: {
451- backend:: TestBackend ,
452- buffer:: Buffer ,
453- crossterm:: event:: { KeyCode , KeyModifiers } ,
454- style:: Color ,
455- Terminal ,
456- } ;
463+ use ratatui:: { backend:: TestBackend , buffer:: Buffer , style:: Color , Terminal } ;
457464
458465 fn object ( ss : & [ & str ] ) -> RawObject {
459466 RawObject {
@@ -616,96 +623,4 @@ mod tests {
616623 object_url : "https://bucket-1.s3.ap-northeast-1.amazonaws.com/file.txt" . to_string ( ) ,
617624 }
618625 }
619-
620- #[ tokio:: test]
621- async fn test_copy_text_content ( ) {
622- let ctx = Rc :: default ( ) ;
623- let ( tx, _rx) = tokio:: sync:: mpsc:: unbounded_channel ( ) ;
624- let tx = Sender :: new ( tx) ;
625-
626- let file_detail = file_detail ( ) ;
627- let preview = [ "Hello, world!" , "This is test content." ] ;
628- let object = object ( & preview) ;
629- let mut page = ObjectPreviewPage :: new ( file_detail, None , object, ctx, tx) ;
630-
631- page. handle_key (
632- vec ! [ UserEvent :: ObjectPreviewCopy ] ,
633- KeyEvent :: new ( KeyCode :: Char ( 'y' ) , KeyModifiers :: empty ( ) ) ,
634- ) ;
635-
636- assert ! ( matches!( page. preview_type, PreviewType :: Text ( _) ) ) ;
637- }
638-
639- #[ tokio:: test]
640- async fn test_copy_image_content_shows_warning ( ) {
641- use crate :: event:: AppEventType ;
642-
643- let ctx = Rc :: default ( ) ;
644- let ( tx, mut rx) = tokio:: sync:: mpsc:: unbounded_channel ( ) ;
645- let tx = Sender :: new ( tx) ;
646-
647- let image_bytes = vec ! [
648- 0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A , // PNG signature
649- 0x00 , 0x00 , 0x00 , 0x0D , // IHDR chunk size
650- 0x49 , 0x48 , 0x44 , 0x52 , // "IHDR"
651- 0x00 , 0x00 , 0x00 , 0x01 , // width: 1
652- 0x00 , 0x00 , 0x00 , 0x01 , // height: 1
653- 0x08 , 0x02 , // bit depth: 8, color type: 2 (RGB)
654- 0x00 , 0x00 , 0x00 , // compression, filter, interlace
655- ] ;
656- let object = RawObject { bytes : image_bytes } ;
657-
658- let mut file_detail = file_detail ( ) ;
659- file_detail. name = "image.png" . to_string ( ) ;
660- file_detail. content_type = "image/png" . to_string ( ) ;
661-
662- let mut page = ObjectPreviewPage :: new ( file_detail, None , object, ctx, tx) ;
663-
664- assert ! ( matches!( page. preview_type, PreviewType :: Image ( _) ) ) ;
665-
666- // NOTE: Clear any initial warning messages (like "Image preview is disabled")
667- while rx. try_recv ( ) . is_ok ( ) {
668- // Drain events
669- }
670-
671- page. handle_key (
672- vec ! [ UserEvent :: ObjectPreviewCopy ] ,
673- KeyEvent :: new ( KeyCode :: Char ( 'y' ) , KeyModifiers :: empty ( ) ) ,
674- ) ;
675-
676- if let Ok ( event) = rx. try_recv ( ) {
677- match event {
678- AppEventType :: NotifyWarn ( msg) => {
679- assert ! (
680- msg. contains( "Cannot copy image content" ) ,
681- "Message was: {}" ,
682- msg
683- ) ;
684- }
685- _ => panic ! ( "Expected NotifyWarn event, got: {:?}" , event) ,
686- }
687- } else {
688- panic ! ( "Expected NotifyWarn event to be sent" ) ;
689- }
690- }
691-
692- #[ test]
693- fn test_copy_respects_encoding ( ) {
694- let ctx = Rc :: default ( ) ;
695- let tx = sender ( ) ;
696-
697- let text = "Hello, 世界!" ;
698- let utf16_bytes: Vec < u8 > = text. encode_utf16 ( ) . flat_map ( |c| c. to_be_bytes ( ) ) . collect ( ) ;
699-
700- let object = RawObject { bytes : utf16_bytes } ;
701-
702- let file_detail = file_detail ( ) ;
703- let page = ObjectPreviewPage :: new ( file_detail, None , object, ctx, tx) ;
704-
705- if let PreviewType :: Text ( ref state) = page. preview_type {
706- assert ! ( matches!( state. encoding, _) ) ;
707- } else {
708- panic ! ( "Expected text preview type" ) ;
709- }
710- }
711626}
0 commit comments