mirror of
https://github.com/NotAShelf/stash.git
synced 2026-05-08 16:35:12 +00:00
watch: allow smarter mimetype detection
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I082de33646e0260b3113eeb4f5a77c6e6a6a6964
This commit is contained in:
parent
1f0312b2f6
commit
340d02d09a
2 changed files with 165 additions and 15 deletions
|
|
@ -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<dyn Read>`] 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<dyn std::io::Read>, String), Box<dyn std::error::Error>> {
|
||||
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<dyn std::io::Read>,
|
||||
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<dyn std::io::Read>,
|
||||
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<dyn std::io::Read>, mime_str));
|
||||
}
|
||||
}
|
||||
|
||||
if types.contains("text/plain") {
|
||||
let (reader, _) = wl_get_contents(clipboard, seat, MimeType::Text)?;
|
||||
return Ok((
|
||||
Box::new(reader) as Box<dyn std::io::Read>,
|
||||
"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<dyn std::io::Read>, 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<dyn std::io::Read>, 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<dyn std::io::Read>,
|
||||
"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<u64> = 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}");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
18
src/main.rs
18
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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn report_error<T>(
|
||||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue