diff --git a/Cargo.lock b/Cargo.lock index 113cb91..b8677cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2033,9 +2033,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "rand_core", ] diff --git a/README.md b/README.md index 775f618..d29b4f4 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ releases are made when a version gets tagged, and are available under - Build and install from source with Cargo: ```bash - cargo install stash-clipboard --locked + cargo install stash --locked ``` Additionally, you may get Stash from source via `cargo install` using diff --git a/flake.lock b/flake.lock index 8a9f105..e50ffba 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1777830388, - "narHash": "sha256-2uoQAqUk2H0ijQtGiWAyNeQYGYc6yfAcRRLlJAz4Gp8=", + "lastModified": 1775839657, + "narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=", "owner": "ipetkov", "repo": "crane", - "rev": "d459c1350e96ce1a7e3859c513ef5e9869d67d6f", + "rev": "7cf72d978629469c4bd4206b95c402514c1f6000", "type": "github" }, "original": { @@ -17,11 +17,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", "type": "github" }, "original": { diff --git a/src/clipboard/persist.rs b/src/clipboard/persist.rs index f5312a7..a677f50 100644 --- a/src/clipboard/persist.rs +++ b/src/clipboard/persist.rs @@ -196,7 +196,8 @@ fn serve_clipboard_child(prepared: PreparedCopy) { }, Err(e) => { - log::debug!("clipboard persistence: serve ended: {e}"); + log::error!("clipboard persistence: serve failed: {e}"); + exit(1); }, } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 369949c..b3041e5 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -698,7 +698,7 @@ impl SqliteClipboardDb { let mime_type = match mime { Some(ref m) if m == "text/plain" => MimeType::Text, Some(ref m) => MimeType::Specific(m.clone().clone()), - None => MimeType::Autodetect, + None => MimeType::Text, }; let copy_result = opts .copy(Source::Bytes(contents.clone().into()), mime_type); diff --git a/src/commands/watch.rs b/src/commands/watch.rs index 111a330..71cdc17 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -435,14 +435,6 @@ impl WatchCommand for SqliteClipboardDb { log::info!("clipboard entry excluded by app filter"); last_hash = Some(current_hash); }, - Err(crate::db::StashError::AllWhitespace) => { - log::debug!("clipboard entry is all whitespace, skipping"); - last_hash = Some(current_hash); - }, - Err(crate::db::StashError::TooSmall(_)) => { - log::debug!("clipboard entry below minimum size, skipping"); - last_hash = Some(current_hash); - }, Err(e) => { log::error!("failed to store clipboard entry: {e}"); last_hash = Some(current_hash); @@ -526,8 +518,8 @@ mod tests { #[test] fn test_pick_image_preference_falls_back() { - // No image types in offer set; first type is used as fallback. let offered = vec!["text/html".to_string(), "text/plain".to_string()]; + // No image types offered — falls back to first assert_eq!(pick_mime(&offered, "image").unwrap(), "text/html"); } @@ -558,14 +550,14 @@ mod tests { #[test] fn test_pick_html_fallback_when_only_html() { - // text/html is used when it is the only offered type. + // When text/html is the only type, pick it let offered = vec!["text/html".to_string()]; assert_eq!(pick_mime(&offered, "any").unwrap(), "text/html"); } #[test] fn test_pick_text_over_html_when_no_image() { - // html + plain with no image type; plain text wins over html. + // Rich text copy: html + plain, no image — prefer plain text let offered = vec!["text/html".to_string(), "text/plain".to_string()]; assert_eq!(pick_mime(&offered, "any").unwrap(), "text/plain"); } diff --git a/src/db/mod.rs b/src/db/mod.rs index 8ff5f8d..65eb097 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -338,87 +338,215 @@ impl SqliteClipboardDb { if schema_version == 0 { tx.execute_batch( "CREATE TABLE IF NOT EXISTS clipboard ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - contents BLOB NOT NULL, - mime TEXT - );", + id INTEGER PRIMARY KEY AUTOINCREMENT, + contents BLOB NOT NULL, + mime TEXT + );", ) - .map_err(migration_err)?; - tx.pragma_update(None, "user_version", 1i64) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to create clipboard table: {e}").into(), + ) + })?; + + tx.execute("PRAGMA user_version = 1", []).map_err(|e| { + StashError::Store(format!("Failed to set schema version: {e}").into()) + })?; } + // Add content_hash column if it doesn't exist. Migration MUST be done to + // avoid breaking existing installations. if schema_version < 2 { - if !column_exists(&tx, "content_hash") { + let has_content_hash: bool = tx + .query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND \ + name='clipboard'", + [], + |row| { + let sql: String = row.get(0)?; + Ok(sql.to_lowercase().contains("content_hash")) + }, + ) + .unwrap_or(false); + + if !has_content_hash { tx.execute("ALTER TABLE clipboard ADD COLUMN content_hash INTEGER", []) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to add content_hash column: {e}").into(), + ) + })?; } + + // Create index for content_hash if it doesn't exist tx.execute( "CREATE INDEX IF NOT EXISTS idx_content_hash ON \ clipboard(content_hash)", [], ) - .map_err(migration_err)?; - tx.pragma_update(None, "user_version", 2i64) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to create content_hash index: {e}").into(), + ) + })?; + + tx.execute("PRAGMA user_version = 2", []).map_err(|e| { + StashError::Store(format!("Failed to set schema version: {e}").into()) + })?; } + // Add last_accessed column if it doesn't exist if schema_version < 3 { - if !column_exists(&tx, "last_accessed") { + let has_last_accessed: bool = tx + .query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND \ + name='clipboard'", + [], + |row| { + let sql: String = row.get(0)?; + Ok(sql.to_lowercase().contains("last_accessed")) + }, + ) + .unwrap_or(false); + + if !has_last_accessed { tx.execute("ALTER TABLE clipboard ADD COLUMN last_accessed INTEGER", [ ]) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to add last_accessed column: {e}").into(), + ) + })?; } + + // Create index for last_accessed if it doesn't exist tx.execute( "CREATE INDEX IF NOT EXISTS idx_last_accessed ON \ clipboard(last_accessed)", [], ) - .map_err(migration_err)?; - tx.pragma_update(None, "user_version", 3i64) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to create last_accessed index: {e}").into(), + ) + })?; + + tx.execute("PRAGMA user_version = 3", []).map_err(|e| { + StashError::Store(format!("Failed to set schema version: {e}").into()) + })?; } + // Add expires_at column if it doesn't exist (v4) if schema_version < 4 { - if !column_exists(&tx, "expires_at") { + let has_expires_at: bool = tx + .query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND \ + name='clipboard'", + [], + |row| { + let sql: String = row.get(0)?; + Ok(sql.to_lowercase().contains("expires_at")) + }, + ) + .unwrap_or(false); + + if !has_expires_at { tx.execute("ALTER TABLE clipboard ADD COLUMN expires_at REAL", []) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to add expires_at column: {e}").into(), + ) + })?; } + + // Create partial index for expires_at (only index non-NULL values) tx.execute( "CREATE INDEX IF NOT EXISTS idx_expires_at ON clipboard(expires_at) \ WHERE expires_at IS NOT NULL", [], ) - .map_err(migration_err)?; - tx.pragma_update(None, "user_version", 4i64) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to create expires_at index: {e}").into(), + ) + })?; + + tx.execute("PRAGMA user_version = 4", []).map_err(|e| { + StashError::Store(format!("Failed to set schema version: {e}").into()) + })?; } + // Add is_expired column if it doesn't exist (v5) if schema_version < 5 { - if !column_exists(&tx, "is_expired") { + let has_is_expired: bool = tx + .query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND \ + name='clipboard'", + [], + |row| { + let sql: String = row.get(0)?; + Ok(sql.to_lowercase().contains("is_expired")) + }, + ) + .unwrap_or(false); + + if !has_is_expired { tx.execute( "ALTER TABLE clipboard ADD COLUMN is_expired INTEGER DEFAULT 0", [], ) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to add is_expired column: {e}").into(), + ) + })?; } + + // Create index for is_expired (for filtering) tx.execute( "CREATE INDEX IF NOT EXISTS idx_is_expired ON clipboard(is_expired) \ WHERE is_expired = 1", [], ) - .map_err(migration_err)?; - tx.pragma_update(None, "user_version", 5i64) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to create is_expired index: {e}").into(), + ) + })?; + + tx.execute("PRAGMA user_version = 5", []).map_err(|e| { + StashError::Store(format!("Failed to set schema version: {e}").into()) + })?; } + // Add mime_types column if it doesn't exist (v6) + // Stores all MIME types offered by the source application as JSON array. + // Needed for clipboard persistence to re-offer the same types. if schema_version < 6 { - if !column_exists(&tx, "mime_types") { + let has_mime_types: bool = tx + .query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND \ + name='clipboard'", + [], + |row| { + let sql: String = row.get(0)?; + Ok(sql.to_lowercase().contains("mime_types")) + }, + ) + .unwrap_or(false); + + if !has_mime_types { tx.execute("ALTER TABLE clipboard ADD COLUMN mime_types TEXT", []) - .map_err(migration_err)?; + .map_err(|e| { + StashError::Store( + format!("Failed to add mime_types column: {e}").into(), + ) + })?; } - tx.pragma_update(None, "user_version", 6i64) - .map_err(migration_err)?; + + tx.execute("PRAGMA user_version = 6", []).map_err(|e| { + StashError::Store(format!("Failed to set schema version: {e}").into()) + })?; } tx.commit().map_err(|e| { @@ -427,29 +555,14 @@ impl SqliteClipboardDb { ) })?; + // Initialize Wayland state in background thread. This will be used to track + // focused window state. #[cfg(feature = "use-toplevel")] crate::wayland::init_wayland_state(); Ok(Self { conn, db_path }) } } -/// Check whether `column` exists in the `clipboard` table. -fn column_exists(conn: &Connection, column: &str) -> bool { - conn - .prepare("PRAGMA table_info(clipboard)") - .and_then(|mut stmt| { - stmt - .query_map([], |row| row.get::<_, String>(1)) - .map(|rows| rows.filter_map(Result::ok).any(|c| c == column)) - }) - .unwrap_or(false) -} - -/// Convert a rusqlite error into [`StashError::Store`]. -fn migration_err(e: rusqlite::Error) -> StashError { - StashError::Store(e.to_string().into()) -} - impl SqliteClipboardDb { pub fn list_json( &self, @@ -652,7 +765,7 @@ impl ClipboardDb for SqliteClipboardDb { .conn .execute( "DELETE FROM clipboard WHERE id IN (SELECT id FROM clipboard ORDER \ - BY COALESCE(last_accessed, 0) ASC, id ASC LIMIT ?1)", + BY id ASC LIMIT ?1)", params![i64::try_from(to_delete).unwrap_or(i64::MAX)], ) .map_err(|e| StashError::Trim(e.to_string().into()))?; @@ -815,23 +928,40 @@ impl ClipboardDb for SqliteClipboardDb { &self, id: i64, ) -> Result<(i64, Vec, Option), StashError> { - let (contents, mime): (Vec, Option) = self - .conn - .query_row( - "SELECT contents, mime FROM clipboard WHERE id = ?1", - params![id], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .map_err(|e| StashError::DecodeGet(e.to_string().into()))?; + let (contents, mime, content_hash): (Vec, Option, Option) = + self + .conn + .query_row( + "SELECT contents, mime, content_hash FROM clipboard WHERE id = ?1", + params![id], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .map_err(|e| StashError::DecodeGet(e.to_string().into()))?; - self - .conn - .execute( - "UPDATE clipboard SET last_accessed = CAST(strftime('%s', 'now') AS \ - INTEGER) WHERE id = ?1", - params![id], - ) - .map_err(|e| StashError::Store(e.to_string().into()))?; + if let Some(hash) = content_hash { + let most_recent_id: Option = self + .conn + .query_row( + "SELECT id FROM clipboard WHERE content_hash = ?1 AND last_accessed \ + = (SELECT MAX(last_accessed) FROM clipboard WHERE content_hash = \ + ?1)", + params![hash], + |row| row.get(0), + ) + .optional() + .map_err(|e| StashError::DecodeGet(e.to_string().into()))?; + + if most_recent_id != Some(id) { + self + .conn + .execute( + "UPDATE clipboard SET last_accessed = CAST(strftime('%s', 'now') \ + AS INTEGER) WHERE id = ?1", + params![id], + ) + .map_err(|e| StashError::Store(e.to_string().into()))?; + } + } Ok((id, contents, mime)) } @@ -1123,10 +1253,17 @@ pub fn size_str(size: usize) -> String { /// Check if clipboard should be excluded based on excluded apps configuration. /// Uses timing correlation and focused window detection to identify source app. fn should_exclude_by_app(excluded_apps: Option<&[String]>) -> bool { - match excluded_apps { - Some(apps) if !apps.is_empty() => detect_excluded_app_activity(apps), - _ => false, + let excluded = match excluded_apps { + Some(apps) if !apps.is_empty() => apps, + _ => return false, + }; + + // Try multiple detection strategies + if detect_excluded_app_activity(excluded) { + return true; } + + false } /// Detect if clipboard likely came from an excluded app using multiple @@ -2101,231 +2238,4 @@ mod tests { "Regex loading should be deterministic" ); } - - #[test] - fn test_migration_from_v3() { - let temp_dir = tempfile::tempdir().expect("temp dir"); - let db_path = temp_dir.path().join("test_v3.db"); - let conn = Connection::open(&db_path).expect("open"); - conn - .execute_batch( - "CREATE TABLE clipboard ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - contents BLOB NOT NULL, - mime TEXT, - content_hash INTEGER, - last_accessed INTEGER - ); - INSERT INTO clipboard (contents, mime, content_hash) VALUES \ - (x'010203', 'text/plain', 12345);", - ) - .expect("create v3 schema"); - conn - .pragma_update(None, "user_version", 3i64) - .expect("set version"); - - let db = SqliteClipboardDb::new(conn, db_path).expect("migrate"); - assert_eq!(get_schema_version(&db.conn).expect("version"), 6); - assert!(table_column_exists(&db.conn, "clipboard", "expires_at")); - assert!(table_column_exists(&db.conn, "clipboard", "is_expired")); - assert!(table_column_exists(&db.conn, "clipboard", "mime_types")); - let count: i64 = db - .conn - .query_row("SELECT COUNT(*) FROM clipboard", [], |r| r.get(0)) - .expect("count"); - assert_eq!(count, 1, "existing data must survive migration"); - } - - #[test] - fn test_migration_from_v4() { - let temp_dir = tempfile::tempdir().expect("temp dir"); - let db_path = temp_dir.path().join("test_v4.db"); - let conn = Connection::open(&db_path).expect("open"); - conn - .execute_batch( - "CREATE TABLE clipboard ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - contents BLOB NOT NULL, - mime TEXT, - content_hash INTEGER, - last_accessed INTEGER, - expires_at REAL - ); - INSERT INTO clipboard (contents, mime) VALUES (x'aabbcc', \ - 'image/png');", - ) - .expect("create v4 schema"); - conn - .pragma_update(None, "user_version", 4i64) - .expect("set version"); - - let db = SqliteClipboardDb::new(conn, db_path).expect("migrate"); - assert_eq!(get_schema_version(&db.conn).expect("version"), 6); - assert!(table_column_exists(&db.conn, "clipboard", "is_expired")); - assert!(table_column_exists(&db.conn, "clipboard", "mime_types")); - let count: i64 = db - .conn - .query_row("SELECT COUNT(*) FROM clipboard", [], |r| r.get(0)) - .expect("count"); - assert_eq!(count, 1, "existing data must survive migration"); - } - - #[test] - fn test_migration_from_v5() { - let temp_dir = tempfile::tempdir().expect("temp dir"); - let db_path = temp_dir.path().join("test_v5.db"); - let conn = Connection::open(&db_path).expect("open"); - conn - .execute_batch( - "CREATE TABLE clipboard ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - contents BLOB NOT NULL, - mime TEXT, - content_hash INTEGER, - last_accessed INTEGER, - expires_at REAL, - is_expired INTEGER DEFAULT 0 - ); - INSERT INTO clipboard (contents, mime) VALUES (x'deadbeef', \ - 'application/octet-stream');", - ) - .expect("create v5 schema"); - conn - .pragma_update(None, "user_version", 5i64) - .expect("set version"); - - let db = SqliteClipboardDb::new(conn, db_path).expect("migrate"); - assert_eq!(get_schema_version(&db.conn).expect("version"), 6); - assert!(table_column_exists(&db.conn, "clipboard", "mime_types")); - } - - /// Pre-migration entries (NULL content_hash) must have last_accessed - /// updated when accessed via copy_entry. - #[test] - fn test_copy_entry_updates_last_accessed_null_hash() { - let db = test_db(); - db.conn - .execute( - "INSERT INTO clipboard (contents, mime, content_hash, last_accessed) \ - VALUES (?1, 'text/plain', NULL, 0)", - rusqlite::params![b"legacy data".as_ref()], - ) - .expect("insert null-hash entry"); - let id: i64 = db - .conn - .query_row("SELECT last_insert_rowid()", [], |r| r.get(0)) - .expect("id"); - - db.copy_entry(id).expect("copy"); - - let last_accessed: i64 = db - .conn - .query_row( - "SELECT last_accessed FROM clipboard WHERE id = ?1", - [id], - |r| r.get(0), - ) - .expect("last_accessed"); - assert!( - last_accessed > 0, - "last_accessed must be updated for null-hash entries" - ); - } - - /// trim_db must evict the least-recently-accessed entries, not the - /// lowest-id entries. - #[test] - fn test_trim_db_evicts_lru_not_oldest() { - let db = test_db(); - let mut ids = Vec::new(); - for i in 0..5u8 { - let id = db - .store_entry( - std::io::Cursor::new(vec![i; 4]), - 0, - 100, - None, - None, - DEFAULT_MAX_ENTRY_SIZE, - None, - None, - ) - .expect("store"); - ids.push(id); - } - - // Zero out all timestamps so copy_entry produces a strictly higher value. - db.conn - .execute("UPDATE clipboard SET last_accessed = 0", []) - .expect("reset timestamps"); - - // Touch the first (oldest by id) entry to make it most-recently-used. - db.copy_entry(ids[0]).expect("copy"); - - // Trim to 4; ids[0] was just accessed and must survive. - db.trim_db(4).expect("trim"); - - let still_there: i64 = db - .conn - .query_row( - "SELECT COUNT(*) FROM clipboard WHERE id = ?1", - [ids[0]], - |r| r.get(0), - ) - .expect("count"); - assert_eq!( - still_there, 1, - "recently accessed entry must not be evicted" - ); - - let total: i64 = db - .conn - .query_row("SELECT COUNT(*) FROM clipboard", [], |r| r.get(0)) - .expect("total"); - assert_eq!(total, 4); - } - - /// All new columns must be NULL for entries created before their respective - /// schema versions. - #[test] - fn test_migration_null_columns_for_legacy_entries() { - let temp_dir = tempfile::tempdir().expect("temp dir"); - let db_path = temp_dir.path().join("test_legacy.db"); - { - let conn = Connection::open(&db_path).expect("open"); - conn - .execute_batch( - "CREATE TABLE clipboard ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - contents BLOB NOT NULL, - mime TEXT - ); - INSERT INTO clipboard (contents, mime) VALUES (x'68656c6c6f', \ - 'text/plain');", - ) - .expect("create v0 schema"); - } - - let conn = Connection::open(&db_path).expect("open"); - let db = SqliteClipboardDb::new(conn, db_path).expect("migrate"); - - let (hash, accessed, expires): (Option, Option, Option) = db - .conn - .query_row( - "SELECT content_hash, last_accessed, expires_at FROM clipboard WHERE \ - id = 1", - [], - |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), - ) - .expect("query"); - assert!(hash.is_none(), "content_hash must be NULL for pre-v2 entry"); - assert!( - accessed.is_none(), - "last_accessed must be NULL for pre-v3 entry" - ); - assert!( - expires.is_none(), - "expires_at must be NULL for pre-v4 entry" - ); - } } diff --git a/src/db/nonblocking.rs b/src/db/nonblocking.rs index d6a00cd..d62e0dd 100644 --- a/src/db/nonblocking.rs +++ b/src/db/nonblocking.rs @@ -8,7 +8,6 @@ use crate::db::{ClipboardDb, SqliteClipboardDb, StashError}; /// on a thread pool to avoid blocking the async runtime. Since /// [`rusqlite::Connection`] is not Send, we store the database path and open a /// new connection for each operation. -#[derive(Clone)] pub struct AsyncClipboardDb { db_path: PathBuf, } @@ -73,11 +72,25 @@ impl AsyncClipboardDb { AND (is_expired IS NULL OR is_expired = 0) ORDER BY expires_at ASC", ) .map_err(|e| StashError::ListDecode(e.to_string().into()))?; - stmt - .query_map([], |row| Ok((row.get::<_, f64>(0)?, row.get::<_, i64>(1)?))) + + let mut rows = stmt + .query([]) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + let mut expirations = Vec::new(); + + while let Some(row) = rows + .next() .map_err(|e| StashError::ListDecode(e.to_string().into()))? - .collect::, _>>() - .map_err(|e| StashError::ListDecode(e.to_string().into())) + { + let exp = row + .get::<_, f64>(0) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + let id = row + .get::<_, i64>(1) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + expirations.push((exp, id)); + } + Ok(expirations) }) .await } @@ -123,6 +136,14 @@ impl AsyncClipboardDb { } } +impl Clone for AsyncClipboardDb { + fn clone(&self) -> Self { + Self { + db_path: self.db_path.clone(), + } + } +} + #[cfg(test)] mod tests { use std::{collections::HashSet, hash::Hasher}; diff --git a/src/multicall/wl_copy.rs b/src/multicall/wl_copy.rs index 7948c68..3794420 100644 --- a/src/multicall/wl_copy.rs +++ b/src/multicall/wl_copy.rs @@ -222,7 +222,8 @@ fn fork_and_serve(prepared_copy: wl_clipboard_rs::copy::PreparedCopy) { 0 => { // Child process - serve clipboard content if let Err(e) = prepared_copy.serve() { - log::debug!("background clipboard service ended: {e}"); + log::error!("background clipboard service failed: {e}"); + std::process::exit(1); } std::process::exit(0); }, diff --git a/src/multicall/wl_paste.rs b/src/multicall/wl_paste.rs index 5a893d6..5daa1fd 100644 --- a/src/multicall/wl_paste.rs +++ b/src/multicall/wl_paste.rs @@ -456,9 +456,6 @@ fn handle_regular_paste( bail!("no content available and --no-newline specified"); } if let Err(e) = out.write_all(&buf) { - if e.kind() == io::ErrorKind::BrokenPipe { - return Ok(()); - } bail!("failed to write to stdout: {e}"); } @@ -474,12 +471,12 @@ fn handle_regular_paste( || types == "application/x-sh" }; - if !args.no_newline && is_text_content && !buf.ends_with(b"\n") { - if let Err(e) = out.write_all(b"\n") { - if e.kind() != io::ErrorKind::BrokenPipe { - bail!("failed to write newline to stdout: {e}"); - } - } + if !args.no_newline + && is_text_content + && !buf.ends_with(b"\n") + && let Err(e) = out.write_all(b"\n") + { + bail!("failed to write newline to stdout: {e}"); } }, Err(PasteError::NoSeats) => {