-
Couldn't load subscription status.
- Fork 343
feat: Add image paste support and key binding #3088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
0635fbb
967b620
dbb05ae
897659f
90ba721
238bfa3
3a51b90
44c24f3
9d007b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| use clap::Args; | ||
| use crossterm::execute; | ||
| use crossterm::style::{ | ||
| self, | ||
| Color, | ||
| }; | ||
|
|
||
| use crate::cli::chat::util::clipboard::paste_image_from_clipboard; | ||
| use crate::cli::chat::{ | ||
| ChatError, | ||
| ChatSession, | ||
| ChatState, | ||
| }; | ||
| use crate::os::Os; | ||
|
|
||
| #[derive(Debug, Args, PartialEq)] | ||
| pub struct PasteArgs; | ||
|
|
||
| impl PasteArgs { | ||
| pub async fn execute(self, _os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> { | ||
| match paste_image_from_clipboard() { | ||
| Ok(path) => Ok(ChatState::HandleInput { | ||
| input: path.display().to_string(), | ||
| }), | ||
| Err(e) => { | ||
| execute!( | ||
| session.stderr, | ||
| style::SetForegroundColor(Color::Red), | ||
| style::Print("❌ Failed to paste image: "), | ||
| style::SetForegroundColor(Color::Reset), | ||
| style::Print(format!("{}\n", e)) | ||
| )?; | ||
|
|
||
| Ok(ChatState::PromptUser { | ||
| skip_printing_tools: false, | ||
| }) | ||
| }, | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| use std::borrow::Cow; | ||
| use std::cell::RefCell; | ||
| use std::path::PathBuf; | ||
| use std::sync::{ | ||
| Arc, | ||
| Mutex, | ||
| }; | ||
|
|
||
| use eyre::Result; | ||
| use rustyline::completion::{ | ||
|
|
@@ -46,6 +50,10 @@ use super::tool_manager::{ | |
| PromptQuery, | ||
| PromptQueryResult, | ||
| }; | ||
| use super::util::clipboard::{ | ||
| ClipboardError, | ||
| paste_image_from_clipboard, | ||
| }; | ||
| use crate::cli::experiment::experiment_manager::{ | ||
| ExperimentManager, | ||
| ExperimentName, | ||
|
|
@@ -54,6 +62,48 @@ use crate::database::settings::Setting; | |
| use crate::os::Os; | ||
| use crate::util::directories::chat_cli_bash_history_path; | ||
|
|
||
| /// Shared state for clipboard paste operations triggered by Ctrl+V | ||
| #[derive(Clone, Debug)] | ||
| pub struct PasteState { | ||
| inner: Arc<Mutex<PasteStateInner>>, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| struct PasteStateInner { | ||
| paths: Vec<PathBuf>, | ||
| count: usize, | ||
|
||
| } | ||
|
|
||
| impl PasteState { | ||
| pub fn new() -> Self { | ||
| Self { | ||
| inner: Arc::new(Mutex::new(PasteStateInner { | ||
| paths: Vec::new(), | ||
| count: 0, | ||
| })), | ||
| } | ||
| } | ||
|
|
||
| pub fn add(&self, path: PathBuf) -> usize { | ||
| let mut inner = self.inner.lock().unwrap(); | ||
| inner.paths.push(path); | ||
| inner.count += 1; | ||
| inner.count | ||
| } | ||
|
|
||
| pub fn take_all(&self) -> Vec<PathBuf> { | ||
| let mut inner = self.inner.lock().unwrap(); | ||
| let paths = std::mem::take(&mut inner.paths); | ||
| inner.count = 0; | ||
| paths | ||
| } | ||
|
|
||
| pub fn reset_count(&self) { | ||
| let mut inner = self.inner.lock().unwrap(); | ||
| inner.count = 0; | ||
| } | ||
| } | ||
|
|
||
| pub const COMMANDS: &[&str] = &[ | ||
| "/clear", | ||
| "/help", | ||
|
|
@@ -100,6 +150,7 @@ pub const COMMANDS: &[&str] = &[ | |
| "/changelog", | ||
| "/save", | ||
| "/load", | ||
| "/paste", | ||
| "/subscribe", | ||
| ]; | ||
|
|
||
|
|
@@ -463,10 +514,54 @@ impl Highlighter for ChatHelper { | |
| } | ||
| } | ||
|
|
||
| /// Handler for pasting images from clipboard via Ctrl+V | ||
| /// | ||
| /// This stores the pasted image path in shared state and inserts a marker. | ||
| /// The marker causes readline to return, and the chat loop handles the paste automatically. | ||
| struct PasteImageHandler { | ||
| paste_state: PasteState, | ||
| } | ||
|
|
||
| impl PasteImageHandler { | ||
| fn new(paste_state: PasteState) -> Self { | ||
| Self { paste_state } | ||
| } | ||
| } | ||
|
|
||
| impl rustyline::ConditionalEventHandler for PasteImageHandler { | ||
| fn handle( | ||
| &self, | ||
| _evt: &rustyline::Event, | ||
| _n: rustyline::RepeatCount, | ||
| _positive: bool, | ||
| _ctx: &rustyline::EventContext<'_>, | ||
| ) -> Option<Cmd> { | ||
| match paste_image_from_clipboard() { | ||
| Ok(path) => { | ||
| // Store the full path in shared state and get the count | ||
| let count = self.paste_state.add(path); | ||
|
|
||
| // Insert [Image #N] marker so user sees what they're pasting | ||
| // User presses Enter to submit | ||
| Some(Cmd::Insert(1, format!("[Image #{}]", count))) | ||
| }, | ||
| Err(ClipboardError::NoImage) => { | ||
| // Silent fail - no image to paste | ||
| Some(Cmd::Noop) | ||
| }, | ||
| Err(_) => { | ||
| // Could log error, but don't interrupt user | ||
| Some(Cmd::Noop) | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn rl( | ||
| os: &Os, | ||
| sender: PromptQuerySender, | ||
| receiver: PromptQueryResponseReceiver, | ||
| paste_state: PasteState, | ||
| ) -> Result<Editor<ChatHelper, FileHistory>> { | ||
| let edit_mode = match os.database.settings.get_string(Setting::ChatEditMode).as_deref() { | ||
| Some("vi" | "vim") => EditMode::Vi, | ||
|
|
@@ -549,6 +644,12 @@ pub fn rl( | |
| EventHandler::Simple(Cmd::Insert(1, "/tangent".to_string())), | ||
| ); | ||
|
|
||
| // Add custom keybinding for Ctrl+V to paste images from clipboard | ||
| rl.bind_sequence( | ||
| KeyEvent(KeyCode::Char('v'), Modifiers::CTRL), | ||
| EventHandler::Conditional(Box::new(PasteImageHandler::new(paste_state))), | ||
| ); | ||
|
|
||
| Ok(rl) | ||
| } | ||
|
|
||
|
|
@@ -864,7 +965,8 @@ mod tests { | |
|
|
||
| // Create a mock Os for testing | ||
| let mock_os = crate::os::Os::new().await.unwrap(); | ||
| let mut test_editor = rl(&mock_os, sender, receiver).unwrap(); | ||
| let paste_state = PasteState::new(); | ||
| let mut test_editor = rl(&mock_os, sender, receiver, paste_state).unwrap(); | ||
|
|
||
| // Reserved Emacs keybindings that should not be overridden | ||
| let reserved_keys = ['a', 'e', 'f', 'b', 'k']; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.