11use std:: borrow:: Cow ;
22use std:: cell:: RefCell ;
33use std:: path:: PathBuf ;
4+ use std:: sync:: {
5+ Arc ,
6+ Mutex ,
7+ } ;
48
59use eyre:: Result ;
610use rustyline:: completion:: {
@@ -46,6 +50,10 @@ use super::tool_manager::{
4650 PromptQuery ,
4751 PromptQueryResult ,
4852} ;
53+ use super :: util:: clipboard:: {
54+ ClipboardError ,
55+ paste_image_from_clipboard,
56+ } ;
4957use crate :: cli:: experiment:: experiment_manager:: {
5058 ExperimentManager ,
5159 ExperimentName ,
@@ -54,6 +62,41 @@ use crate::database::settings::Setting;
5462use crate :: os:: Os ;
5563use crate :: util:: directories:: chat_cli_bash_history_path;
5664
65+ /// Shared state for clipboard paste operations triggered by Ctrl+V
66+ #[ derive( Clone , Debug ) ]
67+ pub struct PasteState {
68+ inner : Arc < Mutex < PasteStateInner > > ,
69+ }
70+
71+ #[ derive( Debug ) ]
72+ struct PasteStateInner {
73+ paths : Vec < PathBuf > ,
74+ }
75+
76+ impl PasteState {
77+ pub fn new ( ) -> Self {
78+ Self {
79+ inner : Arc :: new ( Mutex :: new ( PasteStateInner { paths : Vec :: new ( ) } ) ) ,
80+ }
81+ }
82+
83+ pub fn add ( & self , path : PathBuf ) -> usize {
84+ let mut inner = self . inner . lock ( ) . unwrap ( ) ;
85+ inner. paths . push ( path) ;
86+ inner. paths . len ( )
87+ }
88+
89+ pub fn take_all ( & self ) -> Vec < PathBuf > {
90+ let mut inner = self . inner . lock ( ) . unwrap ( ) ;
91+ std:: mem:: take ( & mut inner. paths )
92+ }
93+
94+ pub fn reset_count ( & self ) {
95+ let mut inner = self . inner . lock ( ) . unwrap ( ) ;
96+ inner. paths . clear ( ) ;
97+ }
98+ }
99+
57100pub const COMMANDS : & [ & str ] = & [
58101 "/clear" ,
59102 "/help" ,
@@ -100,6 +143,7 @@ pub const COMMANDS: &[&str] = &[
100143 "/changelog" ,
101144 "/save" ,
102145 "/load" ,
146+ "/paste" ,
103147 "/subscribe" ,
104148] ;
105149
@@ -463,10 +507,54 @@ impl Highlighter for ChatHelper {
463507 }
464508}
465509
510+ /// Handler for pasting images from clipboard via Ctrl+V
511+ ///
512+ /// This stores the pasted image path in shared state and inserts a marker.
513+ /// The marker causes readline to return, and the chat loop handles the paste automatically.
514+ struct PasteImageHandler {
515+ paste_state : PasteState ,
516+ }
517+
518+ impl PasteImageHandler {
519+ fn new ( paste_state : PasteState ) -> Self {
520+ Self { paste_state }
521+ }
522+ }
523+
524+ impl rustyline:: ConditionalEventHandler for PasteImageHandler {
525+ fn handle (
526+ & self ,
527+ _evt : & rustyline:: Event ,
528+ _n : rustyline:: RepeatCount ,
529+ _positive : bool ,
530+ _ctx : & rustyline:: EventContext < ' _ > ,
531+ ) -> Option < Cmd > {
532+ match paste_image_from_clipboard ( ) {
533+ Ok ( path) => {
534+ // Store the full path in shared state and get the count
535+ let count = self . paste_state . add ( path) ;
536+
537+ // Insert [Image #N] marker so user sees what they're pasting
538+ // User presses Enter to submit
539+ Some ( Cmd :: Insert ( 1 , format ! ( "[Image #{}]" , count) ) )
540+ } ,
541+ Err ( ClipboardError :: NoImage ) => {
542+ // Silent fail - no image to paste
543+ Some ( Cmd :: Noop )
544+ } ,
545+ Err ( _) => {
546+ // Could log error, but don't interrupt user
547+ Some ( Cmd :: Noop )
548+ } ,
549+ }
550+ }
551+ }
552+
466553pub fn rl (
467554 os : & Os ,
468555 sender : PromptQuerySender ,
469556 receiver : PromptQueryResponseReceiver ,
557+ paste_state : PasteState ,
470558) -> Result < Editor < ChatHelper , FileHistory > > {
471559 let edit_mode = match os. database . settings . get_string ( Setting :: ChatEditMode ) . as_deref ( ) {
472560 Some ( "vi" | "vim" ) => EditMode :: Vi ,
@@ -549,6 +637,12 @@ pub fn rl(
549637 EventHandler :: Simple ( Cmd :: Insert ( 1 , "/tangent" . to_string ( ) ) ) ,
550638 ) ;
551639
640+ // Add custom keybinding for Ctrl+V to paste images from clipboard
641+ rl. bind_sequence (
642+ KeyEvent ( KeyCode :: Char ( 'v' ) , Modifiers :: CTRL ) ,
643+ EventHandler :: Conditional ( Box :: new ( PasteImageHandler :: new ( paste_state) ) ) ,
644+ ) ;
645+
552646 Ok ( rl)
553647}
554648
@@ -891,7 +985,8 @@ mod tests {
891985
892986 // Create a mock Os for testing
893987 let mock_os = crate :: os:: Os :: new ( ) . await . unwrap ( ) ;
894- let mut test_editor = rl ( & mock_os, sender, receiver) . unwrap ( ) ;
988+ let paste_state = PasteState :: new ( ) ;
989+ let mut test_editor = rl ( & mock_os, sender, receiver, paste_state) . unwrap ( ) ;
895990
896991 // Reserved Emacs keybindings that should not be overridden
897992 let reserved_keys = [ 'a' , 'e' , 'f' , 'b' , 'k' ] ;
0 commit comments