diff --git a/src/commands/watch.rs b/src/commands/watch.rs index ce2acf7..5bc4a8d 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -6,16 +6,155 @@ use std::{ }; use smol::Timer; -use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents}; +use wl_clipboard_rs::paste::{ + ClipboardType, + MimeType, + Seat, + get_contents as wl_get_contents, + get_mime_types, +}; use crate::db::{ClipboardDb, SqliteClipboardDb}; +/// Get clipboard contents with optional smart MIME type selection. +/// +/// Provides intelligent clipboard content retrieval that can +/// prioritize specific MIME types based on user preferences or built-in +/// heuristics. +/// +/// # Arguments +/// +/// * `clipboard` - The clipboard type to retrieve from (`Regular`, `Primary`, +/// etc.) +/// * `seat` - The Wayland seat identifier +/// * `preferred_types` - List of MIME types to prioritize in order. Supports +/// wildcards like `"image/*"` or `"text/*"`. Empty list enables default smart +/// detection. +/// * `smart_detection` - When true, enables intelligent MIME type selection. +/// When false, falls back to [`MimeType::Any`] behavior. +/// +/// # Returns +/// +/// Returns a tuple containing: +/// - A [`Box`] for reading the clipboard content +/// - A [`String`] representing the actual MIME type that was used +/// +/// # Errors +/// +/// Returns errors if: +/// +/// - Clipboard access fails +/// - MIME type negotiation fails +/// - Content reading fails +fn get_contents( + clipboard: ClipboardType, + seat: Seat, + types_preferred: &[String], + detection_smart: bool, +) -> Result<(Box, String), Box> { + if !types_preferred.is_empty() && detection_smart { + match get_mime_types(clipboard, seat) { + Ok(types) => { + for preferred in types_preferred { + // Handle wildcards (e.g., "image/*") + if preferred.ends_with("/*") { + let prefix = &preferred[..preferred.len() - 2]; + for mime_type in &types { + if mime_type.starts_with(prefix) { + let mime_str = mime_type.clone(); + let (reader, _) = wl_get_contents( + clipboard, + seat, + MimeType::Specific(&mime_str), + )?; + return Ok(( + Box::new(reader) as Box, + mime_str, + )); + } + } + } else { + // Exact match + if types.contains(preferred) { + let (reader, _) = wl_get_contents( + clipboard, + seat, + MimeType::Specific(preferred), + )?; + return Ok(( + Box::new(reader) as Box, + preferred.clone(), + )); + } + } + } + }, + Err(_) => { + // Fall back to regular behavior if mime type query fails + }, + } + } else if detection_smart { + // Default for "smart" detection: + // prioritize images > text/plain > other text > other + // It is as smart as I am, and to be honest, that's not very smart + match get_mime_types(clipboard, seat) { + Ok(types) => { + // Priority order: images > text/plain > other text > other + for mime_type in &types { + if mime_type.starts_with("image/") { + let mime_str = mime_type.clone(); + let (reader, _) = + wl_get_contents(clipboard, seat, MimeType::Specific(&mime_str))?; + return Ok((Box::new(reader) as Box, mime_str)); + } + } + + if types.contains("text/plain") { + let (reader, _) = wl_get_contents(clipboard, seat, MimeType::Text)?; + return Ok(( + Box::new(reader) as Box, + "text/plain".to_string(), + )); + } + + for mime_type in &types { + if mime_type.starts_with("text/") { + let mime_str = mime_type.clone(); + let (reader, _) = + wl_get_contents(clipboard, seat, MimeType::Specific(&mime_str))?; + return Ok((Box::new(reader) as Box, mime_str)); + } + } + + // Fallback to first available + if let Some(first_type) = types.iter().next() { + let mime_str = first_type.clone(); + let (reader, _) = + wl_get_contents(clipboard, seat, MimeType::Specific(&mime_str))?; + return Ok((Box::new(reader) as Box, mime_str)); + } + }, + Err(_) => { + // Fall back to regular behavior if mime type query fails + }, + } + } + + // Fallback to `Any` if smart detection is disabled or fails + let (reader, _) = wl_get_contents(clipboard, seat, MimeType::Any)?; + Ok(( + Box::new(reader) as Box, + "application/octet-stream".to_string(), + )) +} + pub trait WatchCommand { fn watch( &self, max_dedupe_search: u64, max_items: u64, excluded_apps: &[String], + preferred_types: &[String], ); } @@ -25,9 +164,10 @@ impl WatchCommand for SqliteClipboardDb { max_dedupe_search: u64, max_items: u64, excluded_apps: &[String], + preferred_types: &[String], ) { smol::block_on(async { - log::info!("Starting clipboard watch daemon"); + log::info!("starting clipboard watch daemon"); // We use hashes for comparison instead of storing full contents let mut last_hash: Option = None; @@ -44,7 +184,8 @@ impl WatchCommand for SqliteClipboardDb { if let Ok((mut reader, _)) = get_contents( ClipboardType::Regular, Seat::Unspecified, - wl_clipboard_rs::paste::MimeType::Any, + preferred_types, + true, // enable smart detection ) { buf.clear(); if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() { @@ -56,12 +197,13 @@ impl WatchCommand for SqliteClipboardDb { match get_contents( ClipboardType::Regular, Seat::Unspecified, - wl_clipboard_rs::paste::MimeType::Any, + preferred_types, + true, // enable smart detection ) { Ok((mut reader, _mime_type)) => { buf.clear(); if let Err(e) = reader.read_to_end(&mut buf) { - log::error!("Failed to read clipboard contents: {e}"); + log::error!("failed to read clipboard contents: {e}"); Timer::after(Duration::from_millis(500)).await; continue; } @@ -78,17 +220,17 @@ impl WatchCommand for SqliteClipboardDb { Some(excluded_apps), ) { Ok(_) => { - log::info!("Stored new clipboard entry (id: {id})"); + log::info!("stored new clipboard entry (id: {id})"); last_hash = Some(current_hash); }, Err(crate::db::StashError::ExcludedByApp(_)) => { - log::info!("Clipboard entry excluded by app filter"); + log::info!("clipboard entry excluded by app filter"); last_hash = Some(current_hash); }, Err(crate::db::StashError::Store(ref msg)) - if msg.contains("Excluded by app filter") => + if msg.contains("excluded by app filter") => { - log::info!("Clipboard entry excluded by app filter"); + log::info!("clipboard entry excluded by app filter"); last_hash = Some(current_hash); }, Err(e) => { @@ -102,7 +244,7 @@ impl WatchCommand for SqliteClipboardDb { Err(e) => { let error_msg = e.to_string(); if !error_msg.contains("empty") { - log::error!("Failed to get clipboard contents: {e}"); + log::error!("failed to get clipboard contents: {e}"); } }, } diff --git a/src/main.rs b/src/main.rs index d925c97..442cdf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,10 +99,11 @@ enum Command { ask: bool, }, - /// Import clipboard data from stdin (default: TSV format) + /// Import clipboard data from stdin Import { - /// Explicitly specify format: "tsv" (default) - #[arg(long, value_parser = ["tsv"])] + /// Type of the imported data. Only TSV is supported for the time being, + /// which is backwards compatible with Cliphist's export format. + #[arg(long, value_parser = ["tsv"], default_value = "tsv")] r#type: Option, /// Ask for confirmation before importing @@ -111,7 +112,12 @@ enum Command { }, /// Start a process to watch clipboard for changes and store automatically. - Watch, + Watch { + /// Comma-separated list of MIME types to prioritize (e.g., + /// "image/*,text/plain") + #[arg(long, value_delimiter = ',')] + types: Vec, + }, } fn report_error( @@ -334,7 +340,8 @@ fn main() -> color_eyre::eyre::Result<()> { } } }, - Some(Command::Watch) => { + + Some(Command::Watch { types }) => { db.watch( cli.max_dedupe_search, cli.max_items, @@ -342,6 +349,7 @@ fn main() -> color_eyre::eyre::Result<()> { &cli.excluded_apps, #[cfg(not(feature = "use-toplevel"))] &[], + &types, ); },