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:
James Peter
2025-08-26 17:06:37 +10:00
parent 0996314cc9
commit 0c6f35ce65
2 changed files with 103 additions and 0 deletions
@@ -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);
}
+85
View File
@@ -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.