mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
tui: add raw clipboard image paste support
- Reintroduce clipboard_image helpers (arboard + image + tempfile) - Integrate with composer: if pasted text is empty, save PNG from clipboard after reading image and insert temp file path; show footer confirmation - Keep existing path normalization intact and prioritized
This commit is contained in:
@@ -28,6 +28,7 @@ use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::textarea::TextArea;
|
||||
use crate::bottom_pane::textarea::TextAreaState;
|
||||
use crate::clipboard_paste::normalize_pasted_path;
|
||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||
use codex_file_search::FileMatch;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
@@ -409,6 +410,23 @@ impl ChatComposer {
|
||||
self.pending_pastes.push((placeholder, pasted));
|
||||
} else if self.handle_paste_image_path(pasted.clone()) {
|
||||
self.textarea.insert_str(" ");
|
||||
} else if pasted.trim().is_empty() {
|
||||
// No textual content pasted — try reading an image directly from the OS clipboard.
|
||||
match paste_image_to_temp_png() {
|
||||
Ok((path, info)) => {
|
||||
let path_str = path.to_string_lossy();
|
||||
self.textarea.insert_str(&path_str);
|
||||
self.textarea.insert_str(" ");
|
||||
// Give a small visual confirmation in the footer.
|
||||
self.flash_footer_notice(format!(
|
||||
"Added image {}x{} (PNG)",
|
||||
info.width, info.height
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall back to doing nothing special; keep composer unchanged.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.textarea.insert_str(&pasted);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,89 @@
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PasteImageError {
|
||||
ClipboardUnavailable(String),
|
||||
NoImage(String),
|
||||
EncodeFailed(String),
|
||||
IoError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PasteImageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PasteImageError::ClipboardUnavailable(msg) => write!(f, "clipboard unavailable: {msg}"),
|
||||
PasteImageError::NoImage(msg) => write!(f, "no image on clipboard: {msg}"),
|
||||
PasteImageError::EncodeFailed(msg) => write!(f, "could not encode image: {msg}"),
|
||||
PasteImageError::IoError(msg) => write!(f, "io error: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for PasteImageError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodedImageFormat {
|
||||
Png,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PastedImageInfo {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
#[allow(dead_code)]
|
||||
pub encoded_format: EncodedImageFormat, // Always PNG for now.
|
||||
}
|
||||
|
||||
/// Capture image from system clipboard, encode to PNG, and return bytes + info.
|
||||
pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError> {
|
||||
tracing::debug!("attempting clipboard image read");
|
||||
let mut cb = arboard::Clipboard::new()
|
||||
.map_err(|e| PasteImageError::ClipboardUnavailable(e.to_string()))?;
|
||||
let img = cb
|
||||
.get_image()
|
||||
.map_err(|e| PasteImageError::NoImage(e.to_string()))?;
|
||||
let w = img.width as u32;
|
||||
let h = img.height as u32;
|
||||
|
||||
let mut png: Vec<u8> = Vec::new();
|
||||
let Some(rgba_img) = image::RgbaImage::from_raw(w, h, img.bytes.into_owned()) else {
|
||||
return Err(PasteImageError::EncodeFailed("invalid RGBA buffer".into()));
|
||||
};
|
||||
let dyn_img = image::DynamicImage::ImageRgba8(rgba_img);
|
||||
tracing::debug!("clipboard image decoded RGBA {w}x{h}");
|
||||
{
|
||||
let mut cursor = std::io::Cursor::new(&mut png);
|
||||
dyn_img
|
||||
.write_to(&mut cursor, image::ImageFormat::Png)
|
||||
.map_err(|e| PasteImageError::EncodeFailed(e.to_string()))?;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"clipboard image encoded to PNG ({len} bytes)",
|
||||
len = png.len()
|
||||
);
|
||||
Ok((
|
||||
png,
|
||||
PastedImageInfo { width: w, height: h, encoded_format: EncodedImageFormat::Png },
|
||||
))
|
||||
}
|
||||
|
||||
/// Convenience: write to a temp file and return its path + info.
|
||||
pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
|
||||
let (png, info) = paste_image_as_png()?;
|
||||
// Create a unique temporary file with a .png suffix to avoid collisions.
|
||||
let tmp = Builder::new()
|
||||
.prefix("codex-clipboard-")
|
||||
.suffix(".png")
|
||||
.tempfile()
|
||||
.map_err(|e| PasteImageError::IoError(e.to_string()))?;
|
||||
std::fs::write(tmp.path(), &png).map_err(|e| PasteImageError::IoError(e.to_string()))?;
|
||||
// Persist the file (so it remains after the handle is dropped) and return its PathBuf.
|
||||
let (_file, path) = tmp
|
||||
.keep()
|
||||
.map_err(|e| PasteImageError::IoError(e.error.to_string()))?;
|
||||
Ok((path, info))
|
||||
}
|
||||
|
||||
// Clipboard image helpers removed from default build to keep dependencies and warnings minimal.
|
||||
// If clipboard image pasting is needed, reintroduce using arboard + image crates.
|
||||
|
||||
Reference in New Issue
Block a user