From f79c3bb8b7da1feac8c5b17c6c943bf4ffe8e675 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 04:35:22 +0100 Subject: [PATCH 01/13] init --- .cargo/config.toml | 2 +- src/clipboard.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 1611eb7..769bdbf 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,4 @@ [env] -RUST_LOG = "warn,cosmic_ext_applet_clipboard_manager=debug" +RUST_LOG = "warn,cosmic_ext_applet_clipboard_manager=info" # RUST_BACKTRACE = "full" diff --git a/src/clipboard.rs b/src/clipboard.rs index 2841402..c2440e8 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -47,7 +47,7 @@ pub fn sub() -> impl Stream { // 1.the main one // optional 2. metadata let mime_type_filter = |mut mime_types: HashSet| { - debug!("mime type {:?}", mime_types); + info!("mime type {:#?}", mime_types); let mut request = Vec::new(); @@ -141,7 +141,7 @@ pub fn sub() -> impl Stream { let data = Entry::new_now(mime_type, contents, metadata, false); - info!("sending data to database: {:?}", data); + debug!("sending data to database: {:?}", data); output.send(ClipboardMessage::Data(data)).await.unwrap(); } From d1b5094cf5929e00fd73d06a7d75d6846681cfeb Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 04:35:44 +0100 Subject: [PATCH 02/13] init --- migrations/20240723123147_init.sql | 25 ++++++++++++------------ migrations/20240929151638_favorite.sql | 7 ------- src/db/mod.rs | 27 ++++++++++++-------------- 3 files changed, 25 insertions(+), 34 deletions(-) delete mode 100644 migrations/20240929151638_favorite.sql diff --git a/migrations/20240723123147_init.sql b/migrations/20240723123147_init.sql index 029895d..4132b7f 100644 --- a/migrations/20240723123147_init.sql +++ b/migrations/20240723123147_init.sql @@ -1,17 +1,18 @@ CREATE TABLE IF NOT EXISTS ClipboardEntries ( creation INTEGER PRIMARY KEY, +); + +CREATE TABLE IF NOT EXISTS FavoriteClipboardEntries ( + id INTEGER PRIMARY KEY, + position INTEGER NOT NULL, + FOREIGN KEY (id) REFERENCES ClipboardEntries(creation) ON DELETE CASCADE, + -- UNIQUE (position), + CHECK (position >= 0) +); + +CREATE TABLE IF NOT EXISTS ClipboardContents ( + id INTEGER PRIMARY KEY, mime TEXT NOT NULL, content BLOB NOT NULL, - metadataMime TEXT, - metadata TEXT, - CHECK ( - ( - metadataMime IS NULL - AND metadata IS NULL - ) - OR ( - metadataMime IS NOT NULL - AND metadata IS NOT NULL - ) - ) + FOREIGN KEY (id) REFERENCES ClipboardEntries(creation) ON DELETE CASCADE, ); \ No newline at end of file diff --git a/migrations/20240929151638_favorite.sql b/migrations/20240929151638_favorite.sql deleted file mode 100644 index 8034ea9..0000000 --- a/migrations/20240929151638_favorite.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE IF NOT EXISTS FavoriteClipboardEntries ( - id INTEGER PRIMARY KEY, - position INTEGER NOT NULL, - FOREIGN KEY (id) REFERENCES ClipboardEntries(creation) ON DELETE CASCADE, - -- UNIQUE (position), - CHECK (position >= 0) -); \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index d88699d..ccf010c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -27,23 +27,23 @@ pub mod test; type TimeId = i64; -const DB_VERSION: &str = "4"; +const DB_VERSION: &str = "5"; const DB_PATH: &str = constcat::concat!(APPID, "-db-", DB_VERSION, ".sqlite"); #[derive(Clone, Eq, Derivative)] pub struct Entry { pub creation: TimeId, - pub mime: String, // todo: lazelly load image in memory, since we can't search them anyways - pub content: Vec, /// (Mime, Content) - pub metadata: Option, + pub content: HashMap>, pub is_favorite: bool, } impl Hash for Entry { fn hash(&self, state: &mut H) { - self.content.hash(state); + for e in self.content.values() { + e.hash(state); + } } } @@ -68,31 +68,23 @@ impl Entry { pub fn new( creation: i64, - mime: String, - content: Vec, - metadata: Option, + content: HashMap>, is_favorite: bool, ) -> Self { Self { creation, - mime, content, - metadata, is_favorite, } } pub fn new_now( - mime: String, - content: Vec, - metadata: Option, + content: HashMap>, is_favorite: bool, ) -> Self { Self::new( Utc::now().timestamp_millis(), - mime, content, - metadata, is_favorite, ) } @@ -394,12 +386,17 @@ impl Db { let query_load_table = r#" SELECT creation, mime, content, metadataMime, metadata FROM ClipboardEntries + JOIN ClipboardContents ON (id = creation) + GROUP BY "#; let rows = sqlx::query(query_load_table) .fetch_all(&mut self.conn) .await?; + sqlx::query(query_load_table) + .fetch(executor) + for row in &rows { let data = Entry::from_row(row, &self.favorites)?; From f33e5a96028f4f2690b24c845c03365986661db2 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:37:25 +0100 Subject: [PATCH 03/13] init --- Cargo.lock | 10 + Cargo.toml | 2 + migrations/20240723123147_init.sql | 6 +- src/db/mod.rs | 696 +++------------------------ src/db/sqlite_db.rs | 728 +++++++++++++++++++++++++++++ 5 files changed, 796 insertions(+), 646 deletions(-) create mode 100644 src/db/sqlite_db.rs diff --git a/Cargo.lock b/Cargo.lock index aa06b1d..6fb9ef6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alive_lock_file" +version = "0.2.0" +dependencies = [ + "anyhow", + "dirs", + "log", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -1128,6 +1137,7 @@ dependencies = [ name = "cosmic-ext-applet-clipboard-manager" version = "0.1.0" dependencies = [ + "alive_lock_file", "anyhow", "chrono", "configurator_schema", diff --git a/Cargo.toml b/Cargo.toml index 1b02440..b3a6c92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ nucleo = "0.5" futures = "0.3" include_dir = "0.7" itertools = "0.13.0" +alive_lock_file = { path = "../alive_lock_file" } + [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" default-features = false diff --git a/migrations/20240723123147_init.sql b/migrations/20240723123147_init.sql index 4132b7f..bb1a0fc 100644 --- a/migrations/20240723123147_init.sql +++ b/migrations/20240723123147_init.sql @@ -1,11 +1,13 @@ CREATE TABLE IF NOT EXISTS ClipboardEntries ( + id INTEGER PRIMARY KEY, creation INTEGER PRIMARY KEY, + CREATE INDEX index_creation ON ClipboardEntries (creation) ); CREATE TABLE IF NOT EXISTS FavoriteClipboardEntries ( id INTEGER PRIMARY KEY, position INTEGER NOT NULL, - FOREIGN KEY (id) REFERENCES ClipboardEntries(creation) ON DELETE CASCADE, + FOREIGN KEY (id) REFERENCES ClipboardEntries(id) ON DELETE CASCADE, -- UNIQUE (position), CHECK (position >= 0) ); @@ -14,5 +16,5 @@ CREATE TABLE IF NOT EXISTS ClipboardContents ( id INTEGER PRIMARY KEY, mime TEXT NOT NULL, content BLOB NOT NULL, - FOREIGN KEY (id) REFERENCES ClipboardEntries(creation) ON DELETE CASCADE, + FOREIGN KEY (id) REFERENCES ClipboardEntries(id) ON DELETE CASCADE ); \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index ccf010c..cfec2dc 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,4 @@ +use alive_lock_file::LockResult; use derivative::Derivative; use futures::{future::BoxFuture, FutureExt}; use sqlx::{migrate::MigrateDatabase, prelude::*, sqlite::SqliteRow, Sqlite, SqliteConnection}; @@ -22,103 +23,16 @@ use crate::{ utils::{self}, }; -#[cfg(test)] -pub mod test; +// #[cfg(test)] +// pub mod test; -type TimeId = i64; +mod sqlite_db; -const DB_VERSION: &str = "5"; -const DB_PATH: &str = constcat::concat!(APPID, "-db-", DB_VERSION, ".sqlite"); -#[derive(Clone, Eq, Derivative)] -pub struct Entry { - pub creation: TimeId, - // todo: lazelly load image in memory, since we can't search them anyways - /// (Mime, Content) - pub content: HashMap>, - pub is_favorite: bool, +fn now() -> i64 { + Utc::now().timestamp_millis() } -impl Hash for Entry { - fn hash(&self, state: &mut H) { - for e in self.content.values() { - e.hash(state); - } - } -} - -impl PartialEq for Entry { - fn eq(&self, other: &Self) -> bool { - self.content == other.content - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct EntryMetadata { - pub mime: String, - pub value: String, -} - -impl Entry { - fn get_hash(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.hash(&mut hasher); - hasher.finish() - } - - pub fn new( - creation: i64, - content: HashMap>, - is_favorite: bool, - ) -> Self { - Self { - creation, - content, - is_favorite, - } - } - - pub fn new_now( - content: HashMap>, - is_favorite: bool, - ) -> Self { - Self::new( - Utc::now().timestamp_millis(), - content, - is_favorite, - ) - } - - /// SELECT creation, mime, content, metadataMime, metadata - fn from_row(row: &SqliteRow, favorites: &Favorites) -> Result { - let id = row.get("creation"); - let is_fav = favorites.contains(&id); - - Ok(Entry::new( - id, - row.get("mime"), - row.get("content"), - row.try_get("metadataMime") - .ok() - .map(|metadata_mime| EntryMetadata { - mime: metadata_mime, - value: row.get("metadata"), - }), - is_fav, - )) - } -} - -impl Debug for Entry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Data") - .field("creation", &self.creation) - .field("mime", &self.mime) - .field("content", &self.get_content()) - .field("metadata", &self.metadata) - .finish() - } -} pub enum Content<'a> { Text(&'a str), @@ -136,8 +50,33 @@ impl Debug for Content<'_> { } } -impl Entry { - pub fn get_content(&self) -> Result> { + + +// currently best effort +fn find_alt(html: &str) -> Option<&str> { + const DEB: &str = "alt=\""; + + if let Some(pos) = html.find(DEB) { + const OFFSET: usize = DEB.as_bytes().len(); + + if let Some(pos_end) = html[pos + OFFSET..].find('"') { + return Some(&html[pos + OFFSET..pos + pos_end + OFFSET]); + } + } + + None +} + + + +trait EntryTrait { + + fn is_favorite(&self) -> bool; + + fn content(&self) -> HashMap>; + + + fn get_content(&self) -> Result> { if self.mime == "text/uri-list" { let text = if let Some(metadata) = &self.metadata { &metadata.value @@ -180,577 +119,46 @@ impl Entry { None } -} - -// currently best effort -fn find_alt(html: &str) -> Option<&str> { - const DEB: &str = "alt=\""; - - if let Some(pos) = html.find(DEB) { - const OFFSET: usize = DEB.as_bytes().len(); - - if let Some(pos_end) = html[pos + OFFSET..].find('"') { - return Some(&html[pos + OFFSET..pos + pos_end + OFFSET]); - } - } - - None -} - -pub struct Db { - conn: SqliteConnection, - hashs: HashMap, - state: BTreeMap, - filtered: Vec<(TimeId, Vec)>, - query: String, - needle: Option, - matcher: Matcher, - data_version: i64, - favorites: Favorites, -} - -#[derive(Default)] -struct Favorites { - favorites: Vec, - favorites_hash_set: HashSet, -} - -impl Favorites { - fn contains(&self, id: &TimeId) -> bool { - self.favorites_hash_set.contains(id) - } - fn clear(&mut self) { - self.favorites.clear(); - self.favorites_hash_set.clear(); - } - - fn insert_at(&mut self, id: TimeId, pos: Option) { - match pos { - Some(pos) => self.favorites.insert(pos, id), - None => self.favorites.push(id), - } - self.favorites_hash_set.insert(id); - } - - fn remove(&mut self, id: &TimeId) -> Option { - self.favorites_hash_set.remove(id); - self.favorites.iter().position(|e| e == id).inspect(|i| { - self.favorites.remove(*i); - }) - } - - fn fav(&self) -> &Vec { - &self.favorites - } - fn change(&mut self, prev: &TimeId, new: TimeId) { - let pos = self.favorites.iter().position(|e| e == prev).unwrap(); - self.favorites[pos] = new; - self.favorites_hash_set.remove(prev); - self.favorites_hash_set.insert(new); - } } -impl Db { - pub async fn new(config: &Config) -> Result { - let directories = directories::ProjectDirs::from(QUALIFIER, ORG, APP).unwrap(); +trait DbTrait: Sized { - std::fs::create_dir_all(directories.cache_dir())?; + type Entry: EntryTrait + Debug; - Self::inner_new(config, directories.cache_dir()).await - } + async fn new(config: &Config) -> Result; - async fn inner_new(config: &Config, db_dir: &Path) -> Result { - let db_path = db_dir.join(DB_PATH); + async fn with_path(config: &Config, db_dir: &Path) -> Result; - let db_path = db_path - .to_str() - .ok_or(anyhow!("can't convert path to str"))?; + async fn reload(&mut self) -> Result<()>; - if !Sqlite::database_exists(db_path).await? { - info!("Creating database {}", db_path); - Sqlite::create_database(db_path).await?; - } + fn insert<'a: 'b, 'b>(&'a mut self, data: Self::Entry) -> BoxFuture<'b, Result<()>>; - let mut conn = SqliteConnection::connect(db_path).await?; + async fn delete(&mut self, data: &Self::Entry) -> Result<()>; - let migration_path = db_dir.join("migrations"); - std::fs::create_dir_all(&migration_path)?; - include_dir::include_dir!("migrations") - .extract(&migration_path) - .unwrap(); + async fn clear(&mut self) -> Result<()>; - match sqlx::migrate::Migrator::new(migration_path).await { - Ok(migrator) => migrator, - Err(e) => { - warn!("migrator error {e}, fall back to relative path"); - sqlx::migrate::Migrator::new(Path::new("./migrations")).await? - } - } - .run(&mut conn) - .await?; - - if let Some(max_duration) = config.maximum_entries_lifetime() { - let now_millis = utils::now_millis(); - let max_millis = max_duration.as_millis().try_into().unwrap_or(u64::MAX); - - let query_delete_old_one = r#" - DELETE FROM ClipboardEntries - WHERE (? - creation) >= ? AND creation NOT IN( - SELECT id - FROM FavoriteClipboardEntries - ); - "#; - - sqlx::query(query_delete_old_one) - .bind(now_millis) - .bind(max_millis as i64) - .execute(&mut conn) - .await - .unwrap(); - } + async fn add_favorite(&mut self, entry: &Self::Entry, index: Option) -> Result<()>; - if let Some(max_number_of_entries) = &config.maximum_entries_number { - let query_delete_old_one = r#" - WITH MostRecentEntries AS ( - SELECT creation - FROM ClipboardEntries - ORDER BY creation DESC - LIMIT ? - ) - DELETE FROM ClipboardEntries - WHERE creation NOT IN ( - SELECT creation - FROM MostRecentEntries - FULL JOIN FavoriteClipboardEntries ON (creation = id)); - "#; - - sqlx::query(query_delete_old_one) - .bind(max_number_of_entries) - .execute(&mut conn) - .await - .unwrap(); - } - - let mut db = Db { - data_version: fetch_data_version(&mut conn).await?, - conn, - hashs: HashMap::default(), - state: BTreeMap::default(), - filtered: Vec::default(), - query: String::default(), - needle: None, - matcher: Matcher::new(nucleo::Config::DEFAULT), - favorites: Favorites::default(), - }; - - db.reload().await?; - - Ok(db) - } + async fn remove_favorite(&mut self, entry: &Self::Entry) -> Result<()>; - async fn reload(&mut self) -> Result<()> { - self.hashs.clear(); - self.state.clear(); - self.favorites.clear(); + fn favorite_len(&self) -> usize; - { - let query_load_favs = r#" - SELECT id, position - FROM FavoriteClipboardEntries - "#; + fn search(&mut self); - let rows = sqlx::query(query_load_favs) - .fetch_all(&mut self.conn) - .await?; + fn set_query_and_search(&mut self, query: String); - let mut rows = rows - .iter() - .map(|row| { - let id: i64 = row.get("id"); - let index: i32 = row.get("position"); - (id, index as usize) - }) - .collect::>(); + fn query(&self) -> &str; - rows.sort_by(|e1, e2| e1.1.cmp(&e2.1)); + fn get(&self, index: usize) -> Option<&Self::Entry>; - debug_assert_eq!(rows.last().map(|e| e.1 + 1).unwrap_or(0), rows.len()); - - for (id, pos) in rows { - self.favorites.insert_at(id, Some(pos)); - } - } + fn iter(&self) -> impl Iterator; - { - let query_load_table = r#" - SELECT creation, mime, content, metadataMime, metadata - FROM ClipboardEntries - JOIN ClipboardContents ON (id = creation) - GROUP BY - "#; - - let rows = sqlx::query(query_load_table) - .fetch_all(&mut self.conn) - .await?; - - sqlx::query(query_load_table) - .fetch(executor) - - for row in &rows { - let data = Entry::from_row(row, &self.favorites)?; - - self.hashs.insert(data.get_hash(), data.creation); - self.state.insert(data.creation, data); - } - } - - self.search(); - - Ok(()) - } - - async fn get_last_row(&mut self) -> Result> { - let query_get_last_row = r#" - SELECT creation, mime, content, metadataMime, metadata - FROM ClipboardEntries - ORDER BY creation DESC - LIMIT 1 - "#; - - let entry = sqlx::query(query_get_last_row) - .fetch_optional(&mut self.conn) - .await? - .map(|e| Entry::from_row(&e, &self.favorites).unwrap()); - - Ok(entry) - } - - // the <= 200 condition, is to unsure we reuse the same timestamp - // of the first process that inserted the data. - pub fn insert<'a: 'b, 'b>(&'a mut self, mut data: Entry) -> BoxFuture<'b, Result<()>> { - async move { - // insert a new data, only if the last row is not the same AND was not created recently - let query_insert_if_not_exist = r#" - WITH last_row AS ( - SELECT creation, mime, content, metadataMime, metadata - FROM ClipboardEntries - ORDER BY creation DESC - LIMIT 1 - ) - INSERT INTO ClipboardEntries (creation, mime, content, metadataMime, metadata) - SELECT $1, $2, $3, $4, $5 - WHERE NOT EXISTS ( - SELECT 1 - FROM last_row AS lr - WHERE lr.content = $3 AND ($6 - lr.creation) <= 1000 - ); - "#; - - if let Err(e) = sqlx::query(query_insert_if_not_exist) - .bind(data.creation) - .bind(&data.mime) - .bind(&data.content) - .bind(data.metadata.as_ref().map(|m| &m.mime)) - .bind(data.metadata.as_ref().map(|m| &m.value)) - .bind(utils::now_millis()) - .execute(&mut self.conn) - .await - { - if let sqlx::Error::Database(e) = &e { - if e.is_unique_violation() { - warn!("a different value with the same id was already inserted"); - data.creation += 1; - return self.insert(data).await; - } - } - - return Err(e.into()); - } - - // safe to unwrap since we insert before - let last_row = self.get_last_row().await?.unwrap(); - - let new_id = last_row.creation; - - let data_hash = data.get_hash(); - - if let Some(old_id) = self.hashs.remove(&data_hash) { - self.state.remove(&old_id); - - if self.favorites.contains(&old_id) { - data.is_favorite = true; - let query_delete_old_id = r#" - UPDATE FavoriteClipboardEntries - SET id = $1 - WHERE id = $2; - "#; - - sqlx::query(query_delete_old_id) - .bind(new_id) - .bind(old_id) - .execute(&mut self.conn) - .await?; - - self.favorites.change(&old_id, new_id); - } else { - data.is_favorite = false; - } - - // in case 2 same data were inserted in a short period - // we don't want to remove the old_id - if new_id != old_id { - let query_delete_old_id = r#" - DELETE FROM ClipboardEntries - WHERE creation = ?; - "#; - - sqlx::query(query_delete_old_id) - .bind(old_id) - .execute(&mut self.conn) - .await?; - } - } - - data.creation = new_id; - - self.hashs.insert(data_hash, data.creation); - self.state.insert(data.creation, data); - - self.search(); - Ok(()) - } - .boxed() - } - - pub async fn delete(&mut self, data: &Entry) -> Result<()> { - let query = r#" - DELETE FROM ClipboardEntries - WHERE creation = ?; - "#; - - sqlx::query(query) - .bind(data.creation) - .execute(&mut self.conn) - .await?; - - self.hashs.remove(&data.get_hash()); - self.state.remove(&data.creation); - - if data.is_favorite { - self.favorites.remove(&data.creation); - } - - self.search(); - Ok(()) - } - - pub async fn clear(&mut self) -> Result<()> { - let query_delete = r#" - DELETE FROM ClipboardEntries - WHERE creation NOT IN( - SELECT id - FROM FavoriteClipboardEntries - ); - "#; - - sqlx::query(query_delete).execute(&mut self.conn).await?; - - self.reload().await?; - - Ok(()) - } - - pub async fn add_favorite(&mut self, entry: &Entry, index: Option) -> Result<()> { - debug_assert!(!self.favorites.fav().contains(&entry.creation)); - - self.favorites.insert_at(entry.creation, index); - - if let Some(pos) = index { - let query = r#" - UPDATE FavoriteClipboardEntries - SET position = position + 1 - WHERE position >= ?; - "#; - sqlx::query(query) - .bind(pos as i32) - .execute(&mut self.conn) - .await - .unwrap(); - } - - let index = index.unwrap_or(self.favorite_len() - 1); - - { - let query = r#" - INSERT INTO FavoriteClipboardEntries (id, position) - VALUES ($1, $2); - "#; - - sqlx::query(query) - .bind(entry.creation) - .bind(index as i32) - .execute(&mut self.conn) - .await?; - } - - if let Some(e) = self.state.get_mut(&entry.creation) { - e.is_favorite = true; - } - - Ok(()) - } - - pub async fn remove_favorite(&mut self, entry: &Entry) -> Result<()> { - debug_assert!(self.favorites.fav().contains(&entry.creation)); - - { - let query = r#" - DELETE FROM FavoriteClipboardEntries - WHERE id = ?; - "#; - - sqlx::query(query) - .bind(entry.creation) - .execute(&mut self.conn) - .await?; - } - - if let Some(pos) = self.favorites.remove(&entry.creation) { - let query = r#" - UPDATE FavoriteClipboardEntries - SET position = position - 1 - WHERE position >= ?; - "#; - sqlx::query(query) - .bind(pos as i32) - .execute(&mut self.conn) - .await?; - } - - if let Some(e) = self.state.get_mut(&entry.creation) { - e.is_favorite = false; - } - Ok(()) - } - - pub fn favorite_len(&self) -> usize { - self.favorites.favorites.len() - } - - pub fn search(&mut self) { - if self.query.is_empty() { - self.filtered.clear(); - } else if let Some(atom) = &self.needle { - self.filtered = Self::iter_inner(&self.state, &self.favorites) - .filter_map(|data| { - data.get_searchable_text().and_then(|text| { - let mut buf = Vec::new(); - - let haystack = Utf32Str::new(text, &mut buf); - - let mut indices = Vec::new(); - - let _res = atom.indices(haystack, &mut self.matcher, &mut indices); - - if !indices.is_empty() { - Some((data.creation, indices)) - } else { - None - } - }) - }) - .collect::>(); - } - } - - pub fn set_query_and_search(&mut self, query: String) { - if query.is_empty() { - self.needle.take(); - } else { - let atom = Atom::new( - &query, - CaseMatching::Smart, - Normalization::Smart, - AtomKind::Substring, - true, - ); - - self.needle.replace(atom); - } - - self.query = query; - - self.search(); - } - - pub fn query(&self) -> &str { - &self.query - } - - pub fn get(&self, index: usize) -> Option<&Entry> { - if self.query.is_empty() { - self.iter().nth(index) - } else { - self.filtered - .get(index) - .map(|(id, _indices)| &self.state[id]) - } - } - - pub fn iter(&self) -> impl Iterator { - debug_assert!(self.query.is_empty()); - Self::iter_inner(&self.state, &self.favorites) - } - - fn iter_inner<'a>( - state: &'a BTreeMap, - favorites: &'a Favorites, - ) -> impl Iterator + 'a { - favorites - .fav() - .iter() - .filter_map(|id| state.get(id)) - .chain(state.values().filter(|e| !e.is_favorite).rev()) - } - - pub fn search_iter(&self) -> impl Iterator)> { - debug_assert!(!self.query.is_empty()); - - self.filtered - .iter() - .map(|(id, indices)| (&self.state[id], indices)) - } - - pub fn len(&self) -> usize { - if self.query.is_empty() { - self.state.len() - } else { - self.filtered.len() - } - } - - pub async fn handle_message(&mut self, _message: DbMessage) -> Result<()> { - let data_version = fetch_data_version(&mut self.conn).await?; - - if self.data_version != data_version { - self.reload().await?; - } - - self.data_version = data_version; - - Ok(()) - } -} + fn search_iter(&self) -> impl Iterator)>; -/// https://www.sqlite.org/pragma.html#pragma_data_version -async fn fetch_data_version(conn: &mut SqliteConnection) -> Result { - let data_version: i64 = sqlx::query("PRAGMA data_version") - .fetch_one(conn) - .await? - .get("data_version"); + fn len(&self) -> usize; - Ok(data_version) + async fn handle_message(&mut self, message: DbMessage) -> Result<()>; } #[derive(Clone, Debug)] diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs new file mode 100644 index 0000000..1bc5520 --- /dev/null +++ b/src/db/sqlite_db.rs @@ -0,0 +1,728 @@ +use alive_lock_file::LockResult; +use derivative::Derivative; +use futures::{future::BoxFuture, FutureExt}; +use sqlx::{migrate::MigrateDatabase, prelude::*, sqlite::SqliteRow, Sqlite, SqliteConnection}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + fmt::Debug, + hash::{DefaultHasher, Hash, Hasher}, + path::Path, +}; + +use anyhow::{anyhow, bail, Result}; +use nucleo::{ + pattern::{Atom, AtomKind, CaseMatching, Normalization}, + Matcher, Utf32Str, +}; + +use chrono::Utc; + +use crate::{ + app::{APP, APPID, ORG, QUALIFIER}, + config::Config, + utils::{self}, +}; + +use super::{DbMessage, DbTrait, EntryTrait}; + + +type Time = i64; +type EntryId = i64; + +const DB_VERSION: &str = "5"; +const DB_PATH: &str = constcat::concat!(APPID, "-db-", DB_VERSION, ".sqlite"); + +const LOCK_FILE: &str = constcat::concat!(APPID, "-db", ".lock"); + + +#[derive(Default)] +struct Favorites { + favorites: Vec, + favorites_hash_set: HashSet, +} + +impl Favorites { + fn contains(&self, id: &EntryId) -> bool { + self.favorites_hash_set.contains(id) + } + fn clear(&mut self) { + self.favorites.clear(); + self.favorites_hash_set.clear(); + } + + fn insert_at(&mut self, id: EntryId, pos: Option) { + match pos { + Some(pos) => self.favorites.insert(pos, id), + None => self.favorites.push(id), + } + self.favorites_hash_set.insert(id); + } + + fn remove(&mut self, id: &EntryId) -> Option { + self.favorites_hash_set.remove(id); + self.favorites.iter().position(|e| e == id).inspect(|i| { + self.favorites.remove(*i); + }) + } + + fn fav(&self) -> &Vec { + &self.favorites + } + + fn change(&mut self, prev: &EntryId, new: EntryId) { + let pos = self.favorites.iter().position(|e| e == prev).unwrap(); + self.favorites[pos] = new; + self.favorites_hash_set.remove(prev); + self.favorites_hash_set.insert(new); + } +} + + + +#[derive(Clone, Eq, Derivative)] +pub struct Entry { + pub id: EntryId, + pub creation: Time, + // todo: lazelly load image in memory, since we can't search them anyways + /// (Mime, Content) + pub content: HashMap>, + pub is_favorite: bool, +} + +impl Hash for Entry { + fn hash(&self, state: &mut H) { + for e in self.content.values() { + e.hash(state); + } + } +} + +impl PartialEq for Entry { + fn eq(&self, other: &Self) -> bool { + self.content == other.content + } +} + +impl EntryTrait for Entry { + fn is_favorite(&self) -> bool { + todo!() + } + + fn content(&self) -> HashMap> { + todo!() + } +} + +impl Entry { + fn get_hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } + + pub fn new(id: i64, creation: i64, content: HashMap>, is_favorite: bool) -> Self { + Self { + creation, + content, + is_favorite, + id, + } + } + + +} + +impl Debug for Entry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Data") + .field("id", &self.id) + .field("creation", &self.creation) + .field("content", &self.get_content()) + .finish() + } +} + + + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct EntryMetadata { + pub mime: String, + pub value: String, +} + +// impl Entry { + + +// /// SELECT creation, mime, content, metadataMime, metadata +// fn from_row(row: &SqliteRow, favorites: &Favorites) -> Result { +// let id = row.get("creation"); +// let is_fav = favorites.contains(&id); + +// Ok(Entry::new( +// id, +// row.get("mime"), +// row.get("content"), +// row.try_get("metadataMime") +// .ok() +// .map(|metadata_mime| EntryMetadata { +// mime: metadata_mime, +// value: row.get("metadata"), +// }), +// is_fav, +// )) +// } +// } + + + + + +pub struct Db { + conn: SqliteConnection, + /// Hash -> Id + hashs: HashMap, + /// time -> Id + times: BTreeMap, + /// Id -> Entry + entries: HashMap, + filtered: Vec<(EntryId, Vec)>, + query: String, + needle: Option, + matcher: Matcher, + data_version: i64, + favorites: Favorites, +} + + +impl DbTrait for Db { + type Entry = Entry; + + async fn new(config: &Config) -> Result { + let directories = directories::ProjectDirs::from(QUALIFIER, ORG, APP).unwrap(); + + std::fs::create_dir_all(directories.cache_dir())?; + + Self::with_path(config, directories.cache_dir()).await + } + + async fn with_path(config: &Config, db_dir: &Path) -> Result { + + if let Err(e) = alive_lock_file::remove_lock(LOCK_FILE) { + error!("can't remove lock {e}"); + } + + + let db_path = db_dir.join(DB_PATH); + + let db_path = db_path + .to_str() + .ok_or(anyhow!("can't convert path to str"))?; + + if !Sqlite::database_exists(db_path).await? { + info!("Creating database {}", db_path); + Sqlite::create_database(db_path).await?; + } + + let mut conn = SqliteConnection::connect(db_path).await?; + + let migration_path = db_dir.join("migrations"); + std::fs::create_dir_all(&migration_path)?; + include_dir::include_dir!("migrations") + .extract(&migration_path) + .unwrap(); + + match sqlx::migrate::Migrator::new(migration_path).await { + Ok(migrator) => migrator, + Err(e) => { + warn!("migrator error {e}, fall back to relative path"); + sqlx::migrate::Migrator::new(Path::new("./migrations")).await? + } + } + .run(&mut conn) + .await?; + + if let Some(max_duration) = config.maximum_entries_lifetime() { + let now_millis = utils::now_millis(); + let max_millis = max_duration.as_millis().try_into().unwrap_or(u64::MAX); + + let query_delete_old_one = r#" + DELETE FROM ClipboardEntries + WHERE (? - creation) >= ? AND id NOT IN( + SELECT id + FROM FavoriteClipboardEntries + ); + "#; + + sqlx::query(query_delete_old_one) + .bind(now_millis) + .bind(max_millis as i64) + .execute(&mut conn) + .await + .unwrap(); + } + + if let Some(max_number_of_entries) = &config.maximum_entries_number { + + let query_get_most_older = r#" + SELECT creation + FROM ClipboardEntries + ORDER BY creation DESC + LIMIT 1 OFFSET ? + "#; + + match sqlx::query(query_get_most_older) + .bind(max_number_of_entries) + .fetch_optional(&mut conn) + .await + .unwrap() { + + + Some(r) => { + let creation: Time = r.get("creation"); + + let query_delete_old_one = r#" + + DELETE FROM ClipboardEntries + WHERE creation < ? AND id NOT IN ( + SELECT id + FROM FavoriteClipboardEntries); + "#; + + sqlx::query(query_delete_old_one) + .bind(creation) + .execute(&mut conn) + .await + .unwrap(); + + }, + None => { + // nothing to do + }, + } + + + + } + + let mut db = Db { + data_version: fetch_data_version(&mut conn).await?, + conn, + hashs: HashMap::default(), + times: BTreeMap::default(), + entries: HashMap::default(), + filtered: Vec::default(), + query: String::default(), + needle: None, + matcher: Matcher::new(nucleo::Config::DEFAULT), + favorites: Favorites::default(), + }; + + db.reload().await?; + + Ok(db) + } + + async fn reload(&mut self) -> Result<()> { + self.hashs.clear(); + self.entries.clear(); + self.times.clear(); + self.favorites.clear(); + + { + let query_load_favs = r#" + SELECT id, position + FROM FavoriteClipboardEntries + "#; + + let rows = sqlx::query(query_load_favs) + .fetch_all(&mut self.conn) + .await?; + + let mut rows = rows + .iter() + .map(|row| { + let id: i64 = row.get("id"); + let index: i32 = row.get("position"); + (id, index as usize) + }) + .collect::>(); + + rows.sort_by(|e1, e2| e1.1.cmp(&e2.1)); + + debug_assert_eq!(rows.last().map(|e| e.1 + 1).unwrap_or(0), rows.len()); + + for (id, pos) in rows { + self.favorites.insert_at(id, Some(pos)); + } + } + + { + let query_load_table = r#" + SELECT creation, mime, content, metadataMime, metadata + FROM ClipboardEntries + JOIN ClipboardContents ON (id = creation) + GROUP BY + "#; + + let rows = sqlx::query(query_load_table) + .fetch_all(&mut self.conn) + .await?; + + sqlx::query(query_load_table) + .fetch(executor) + + for row in &rows { + let data = Entry::from_row(row, &self.favorites)?; + + self.hashs.insert(data.get_hash(), data.creation); + self.state.insert(data.creation, data); + } + } + + self.search(); + + Ok(()) + } + + + + // the <= 200 condition, is to unsure we reuse the same timestamp + // of the first process that inserted the data. + fn insert<'a: 'b, 'b>(&'a mut self, mut data: Entry) -> BoxFuture<'b, Result<()>> { + async move { + + match alive_lock_file::try_lock(LOCK_FILE)? { + LockResult::Success => {} + LockResult::AlreadyLocked => { + info!("db already locked"); + return Ok(()); + } + } + + // insert a new data, only if the last row is not the same AND was not created recently + let query_insert_if_not_exist = r#" + WITH last_row AS ( + SELECT creation, mime, content, metadataMime, metadata + FROM ClipboardEntries + ORDER BY creation DESC + LIMIT 1 + ) + INSERT INTO ClipboardEntries (creation, mime, content, metadataMime, metadata) + SELECT $1, $2, $3, $4, $5 + WHERE NOT EXISTS ( + SELECT 1 + FROM last_row AS lr + WHERE lr.content = $3 AND ($6 - lr.creation) <= 1000 + ); + "#; + + if let Err(e) = sqlx::query(query_insert_if_not_exist) + .bind(data.creation) + .bind(&data.mime) + .bind(&data.content) + .bind(data.metadata.as_ref().map(|m| &m.mime)) + .bind(data.metadata.as_ref().map(|m| &m.value)) + .bind(utils::now_millis()) + .execute(&mut self.conn) + .await + { + if let sqlx::Error::Database(e) = &e { + if e.is_unique_violation() { + warn!("a different value with the same id was already inserted"); + data.creation += 1; + return self.insert(data).await; + } + } + + return Err(e.into()); + } + + // safe to unwrap since we insert before + let last_row = self.get_last_row().await?.unwrap(); + + let new_id = last_row.creation; + + let data_hash = data.get_hash(); + + if let Some(old_id) = self.hashs.remove(&data_hash) { + self.state.remove(&old_id); + + if self.favorites.contains(&old_id) { + data.is_favorite = true; + let query_delete_old_id = r#" + UPDATE FavoriteClipboardEntries + SET id = $1 + WHERE id = $2; + "#; + + sqlx::query(query_delete_old_id) + .bind(new_id) + .bind(old_id) + .execute(&mut self.conn) + .await?; + + self.favorites.change(&old_id, new_id); + } else { + data.is_favorite = false; + } + + // in case 2 same data were inserted in a short period + // we don't want to remove the old_id + if new_id != old_id { + let query_delete_old_id = r#" + DELETE FROM ClipboardEntries + WHERE creation = ?; + "#; + + sqlx::query(query_delete_old_id) + .bind(old_id) + .execute(&mut self.conn) + .await?; + } + } + + data.creation = new_id; + + self.hashs.insert(data_hash, data.creation); + self.state.insert(data.creation, data); + + self.search(); + Ok(()) + } + .boxed() + } + + async fn delete(&mut self, data: &Entry) -> Result<()> { + let query = r#" + DELETE FROM ClipboardEntries + WHERE creation = ?; + "#; + + sqlx::query(query) + .bind(data.creation) + .execute(&mut self.conn) + .await?; + + self.hashs.remove(&data.get_hash()); + self.state.remove(&data.creation); + + if data.is_favorite { + self.favorites.remove(&data.creation); + } + + self.search(); + Ok(()) + } + + async fn clear(&mut self) -> Result<()> { + let query_delete = r#" + DELETE FROM ClipboardEntries + WHERE creation NOT IN( + SELECT id + FROM FavoriteClipboardEntries + ); + "#; + + sqlx::query(query_delete).execute(&mut self.conn).await?; + + self.reload().await?; + + Ok(()) + } + + async fn add_favorite(&mut self, entry: &Entry, index: Option) -> Result<()> { + debug_assert!(!self.favorites.fav().contains(&entry.creation)); + + self.favorites.insert_at(entry.creation, index); + + if let Some(pos) = index { + let query = r#" + UPDATE FavoriteClipboardEntries + SET position = position + 1 + WHERE position >= ?; + "#; + sqlx::query(query) + .bind(pos as i32) + .execute(&mut self.conn) + .await + .unwrap(); + } + + let index = index.unwrap_or(self.favorite_len() - 1); + + { + let query = r#" + INSERT INTO FavoriteClipboardEntries (id, position) + VALUES ($1, $2); + "#; + + sqlx::query(query) + .bind(entry.creation) + .bind(index as i32) + .execute(&mut self.conn) + .await?; + } + + if let Some(e) = self.state.get_mut(&entry.creation) { + e.is_favorite = true; + } + + Ok(()) + } + + async fn remove_favorite(&mut self, entry: &Entry) -> Result<()> { + debug_assert!(self.favorites.fav().contains(&entry.creation)); + + { + let query = r#" + DELETE FROM FavoriteClipboardEntries + WHERE id = ?; + "#; + + sqlx::query(query) + .bind(entry.creation) + .execute(&mut self.conn) + .await?; + } + + if let Some(pos) = self.favorites.remove(&entry.creation) { + let query = r#" + UPDATE FavoriteClipboardEntries + SET position = position - 1 + WHERE position >= ?; + "#; + sqlx::query(query) + .bind(pos as i32) + .execute(&mut self.conn) + .await?; + } + + if let Some(e) = self.state.get_mut(&entry.creation) { + e.is_favorite = false; + } + Ok(()) + } + + fn favorite_len(&self) -> usize { + self.favorites.favorites.len() + } + + fn search(&mut self) { + if self.query.is_empty() { + self.filtered.clear(); + } else if let Some(atom) = &self.needle { + self.filtered = Self::iter_inner(&self.state, &self.favorites) + .filter_map(|data| { + data.get_searchable_text().and_then(|text| { + let mut buf = Vec::new(); + + let haystack = Utf32Str::new(text, &mut buf); + + let mut indices = Vec::new(); + + let _res = atom.indices(haystack, &mut self.matcher, &mut indices); + + if !indices.is_empty() { + Some((data.creation, indices)) + } else { + None + } + }) + }) + .collect::>(); + } + } + + fn set_query_and_search(&mut self, query: String) { + if query.is_empty() { + self.needle.take(); + } else { + let atom = Atom::new( + &query, + CaseMatching::Smart, + Normalization::Smart, + AtomKind::Substring, + true, + ); + + self.needle.replace(atom); + } + + self.query = query; + + self.search(); + } + + fn query(&self) -> &str { + &self.query + } + + fn get(&self, index: usize) -> Option<&Entry> { + if self.query.is_empty() { + self.iter().nth(index) + } else { + self.filtered + .get(index) + .map(|(id, _indices)| &self.state[id]) + } + } + + fn iter(&self) -> impl Iterator { + debug_assert!(self.query.is_empty()); + Self::iter_inner(&self.state, &self.favorites) + } + + + + fn search_iter(&self) -> impl Iterator)> { + debug_assert!(!self.query.is_empty()); + + self.filtered + .iter() + .map(|(id, indices)| (&self.state[id], indices)) + } + + fn len(&self) -> usize { + if self.query.is_empty() { + self.state.len() + } else { + self.filtered.len() + } + } + + async fn handle_message(&mut self, _message: DbMessage) -> Result<()> { + let data_version = fetch_data_version(&mut self.conn).await?; + + if self.data_version != data_version { + self.reload().await?; + } + + self.data_version = data_version; + + Ok(()) + } +} + +impl Db { + + fn iter_inner<'a>( + state: &'a BTreeMap, + favorites: &'a Favorites, + ) -> impl Iterator + 'a { + favorites + .fav() + .iter() + .filter_map(|id| state.get(id)) + .chain(state.values().filter(|e| !e.is_favorite).rev()) + } +} +/// https://www.sqlite.org/pragma.html#pragma_data_version +async fn fetch_data_version(conn: &mut SqliteConnection) -> Result { + let data_version: i64 = sqlx::query("PRAGMA data_version") + .fetch_one(conn) + .await? + .get("data_version"); + + Ok(data_version) +} + From fe6326fcd854968ccd3d5400aab27196083a065d Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:21:53 +0100 Subject: [PATCH 04/13] ff --- src/app.rs | 93 ++++++++++++++++++++++------------------ src/clipboard.rs | 101 ++++++++------------------------------------ src/db/mod.rs | 25 +++++++---- src/db/sqlite_db.rs | 10 ++--- src/main.rs | 2 +- src/message.rs | 12 +++--- src/view.rs | 26 ++++++------ 7 files changed, 111 insertions(+), 158 deletions(-) diff --git a/src/app.rs b/src/app.rs index be7d2b0..4e8f361 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,7 +20,7 @@ use futures::executor::block_on; use futures::StreamExt; use crate::config::{Config, PRIVATE_MODE}; -use crate::db::{self, Db, DbMessage}; +use crate::db::{self, DbMessage, DbTrait, EntryTrait}; use crate::message::{AppMsg, ConfigMsg}; use crate::navigation::EventMsg; use crate::utils::task_message; @@ -35,7 +35,7 @@ pub const ORG: &str = "wiiznokes"; pub const APP: &str = "cosmic-ext-applet-clipboard-manager"; pub const APPID: &str = constcat::concat!(QUALIFIER, ".", ORG, ".", APP); -pub struct AppState { +pub struct AppState { core: Core, config_handler: cosmic_config::Config, popup: Option, @@ -48,20 +48,6 @@ pub struct AppState { last_quit: Option<(i64, PopupKind)>, } -impl AppState { - fn focus_next(&mut self) { - if self.db.len() > 0 { - self.focused = (self.focused + 1) % self.db.len(); - } - } - - fn focus_previous(&mut self) { - if self.db.len() > 0 { - self.focused = (self.focused + self.db.len() - 1) % self.db.len(); - } - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum ClipboardState { Init, @@ -93,7 +79,19 @@ enum PopupKind { QuickSettings, } -impl AppState { +impl AppState { + fn focus_next(&mut self) { + if self.db.len() > 0 { + self.focused = (self.focused + 1) % self.db.len(); + } + } + + fn focus_previous(&mut self) { + if self.db.len() > 0 { + self.focused = (self.focused + self.db.len() - 1) % self.db.len(); + } + } + fn toggle_popup(&mut self, kind: PopupKind) -> Task { self.qr_code.take(); match &self.popup { @@ -196,7 +194,7 @@ impl AppState { } } -impl cosmic::Application for AppState { +impl cosmic::Application for AppState { type Executor = cosmic::executor::Default; type Flags = Flags; type Message = AppMsg; @@ -214,9 +212,9 @@ impl cosmic::Application for AppState { let config = flags.config; PRIVATE_MODE.store(config.private_mode, atomic::Ordering::Relaxed); - let db = block_on(async { db::Db::new(&config).await.unwrap() }); + let db = block_on(async { Db::new(&config).await.unwrap() }); - let window = AppState { + let state = AppState { core, config_handler: flags.config_handler, popup: None, @@ -235,7 +233,7 @@ impl cosmic::Application for AppState { #[cfg(not(debug_assertions))] let command = Task::none(); - (window, command) + (state, command) } fn on_close_requested(&self, id: window::Id) -> Option { @@ -300,15 +298,21 @@ impl cosmic::Application for AppState { } } }, - AppMsg::Copy(data) => { - if let Err(e) = clipboard::copy(data) { - error!("can't copy: {e}"); + AppMsg::Copy(id) => { + match self.db.get_from_id(id) { + Some(data) => { + if let Err(e) = clipboard::copy(data.clone()) { + error!("can't copy: {e}"); + } + } + None => error!("id not found"), } + return self.close_popup(); } - AppMsg::Delete(data) => { - if let Err(e) = block_on(self.db.delete(&data)) { - error!("can't delete {:?}: {}", data.get_content(), e); + AppMsg::Delete(id) => { + if let Err(e) = block_on(self.db.delete(id)) { + error!("can't delete {}: {}", id, e); } } AppMsg::Clear => { @@ -357,21 +361,28 @@ impl cosmic::Application for AppState { error!("{err}"); } } - AppMsg::ShowQrCode(e) => { - // todo: handle better this error - if e.content.len() < 700 { - match qr_code::Data::new(&e.content) { - Ok(s) => { - self.qr_code.replace(Ok(s)); - } - Err(e) => { - error!("{e}"); + AppMsg::ShowQrCode(id) => { + match self.db.get_from_id(id) { + Some(entry) => { + let content = entry.qr_code_content(); + + // todo: handle better this error + if content.len() < 700 { + match qr_code::Data::new(content) { + Ok(s) => { + self.qr_code.replace(Ok(s)); + } + Err(e) => { + error!("{e}"); + self.qr_code.replace(Err(())); + } + } + } else { + error!("qr code to long: {}", content.len()); self.qr_code.replace(Err(())); } } - } else { - error!("qr code to long: {}", e.content.len()); - self.qr_code.replace(Err(())); + None => error!("id not found"), } } AppMsg::ReturnToClipboard => { @@ -390,12 +401,12 @@ impl cosmic::Application for AppState { } }, AppMsg::AddFavorite(entry) => { - if let Err(err) = block_on(self.db.add_favorite(&entry, None)) { + if let Err(err) = block_on(self.db.add_favorite(entry, None)) { error!("{err}"); } } AppMsg::RemoveFavorite(entry) => { - if let Err(err) = block_on(self.db.remove_favorite(&entry)) { + if let Err(err) = block_on(self.db.remove_favorite(entry)) { error!("{err}"); } } diff --git a/src/clipboard.rs b/src/clipboard.rs index c2440e8..c8e9a3b 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, io::Read, sync::atomic::{self}, }; @@ -12,8 +12,7 @@ use wl_clipboard_rs::{ paste_watch, }; -use crate::db::Entry; -use crate::{config::PRIVATE_MODE, db::EntryMetadata}; +use crate::{config::PRIVATE_MODE, db::{EntryTrait, MimeDataMap}}; use os_pipe::PipeReader; // prefer popular formats @@ -27,7 +26,7 @@ const TEXT_MIME_TYPES: [&str; 3] = ["text/plain;charset=utf-8", "UTF8_STRING", " #[derive(Debug, Clone)] pub enum ClipboardMessage { Connected, - Data(Entry), + Data(MimeDataMap), /// Means that the source was closed, or the compurer just started /// This means the clipboard manager must become the source, by providing the last entry EmptyKeyboard, @@ -46,59 +45,15 @@ pub fn sub() -> impl Stream { // return a vec of maximum 2 mimetypes // 1.the main one // optional 2. metadata - let mime_type_filter = |mut mime_types: HashSet| { + let mime_type_filter = |mime_types: HashSet| { info!("mime type {:#?}", mime_types); - - let mut request = Vec::new(); - - if mime_types.iter().any(|m| m.starts_with("image/")) { - for prefered_image_format in IMAGE_MIME_TYPES { - if let Some(mime) = mime_types.take(prefered_image_format) { - request.push(mime); - break; - } - } - - if request.is_empty() { - return request; - } - - // can be useful for metadata (alt) - if let Some(mime) = mime_types.take("text/html") { - request.push(mime); - } - return request; - } - - if let Some(mime) = mime_types.take("text/uri-list") { - request.push(mime); - } - - if mime_types.iter().any(|m| m.starts_with("text/")) { - for prefered_text_format in TEXT_MIME_TYPES { - if let Some(mime) = mime_types.take(prefered_text_format) { - request.push(mime); - return request; - } - } - - for mime in mime_types { - if mime.starts_with("text/") { - request.push(mime); - return request; - } - } - } - - request + mime_types.into_iter().collect() }; match clipboard_watcher .start_watching(paste_watch::Seat::Unspecified, mime_type_filter) { Ok(res) => { - debug_assert!(res.len() == 1 || res.len() == 2); - if !PRIVATE_MODE.load(atomic::Ordering::Relaxed) { tx.blocking_send(Some(res)).expect("can't send"); } else { @@ -119,29 +74,15 @@ pub fn sub() -> impl Stream { loop { match rx.recv().await { - Some(Some(mut res)) => { - let (mut pipe, mime_type) = res.next().unwrap(); - - let mut contents = Vec::new(); - pipe.read_to_end(&mut contents).unwrap(); - - let metadata = if let Some((mut pipe, mimitype)) = res.next() { - let mut metadata = String::new(); - pipe.read_to_string(&mut metadata).unwrap(); - - debug!("metadata = {}", metadata); - - Some(EntryMetadata { - mime: mimitype, - value: metadata, + Some(Some(res)) => { + let data = res + .map(|(mut pipe, mime_type)| { + let mut contents = Vec::new(); + pipe.read_to_end(&mut contents).unwrap(); + (mime_type, contents) }) - } else { - None - }; - - let data = Entry::new_now(mime_type, contents, metadata, false); + .collect(); - debug!("sending data to database: {:?}", data); output.send(ClipboardMessage::Data(data)).await.unwrap(); } @@ -173,23 +114,17 @@ pub fn sub() -> impl Stream { }) } -pub fn copy(data: Entry) -> Result<(), copy::Error> { +pub fn copy(data: Entry) -> Result<(), copy::Error> { debug!("copy {:?}", data); - let mut sources = Vec::with_capacity(if data.metadata.is_some() { 2 } else { 1 }); + let mut sources = Vec::with_capacity(data.content().len()); - let source = MimeSource { - source: copy::Source::Bytes(data.content.into_boxed_slice()), - mime_type: copy::MimeType::Specific(data.mime), - }; - - sources.push(source); - - if let Some(metadata) = data.metadata { + for (mime, content) in data.content() { let source = MimeSource { - source: copy::Source::Bytes(metadata.value.into_boxed_str().into_boxed_bytes()), - mime_type: copy::MimeType::Specific(metadata.mime), + source: copy::Source::Bytes(content.into_boxed_slice()), + mime_type: copy::MimeType::Specific(mime), }; + sources.push(source); } diff --git a/src/db/mod.rs b/src/db/mod.rs index cfec2dc..05457a7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -27,12 +27,15 @@ use crate::{ // pub mod test; mod sqlite_db; +pub use sqlite_db::DbSqlite; fn now() -> i64 { Utc::now().timestamp_millis() } +pub type EntryId = i64; +pub type MimeDataMap = HashMap>; pub enum Content<'a> { Text(&'a str), @@ -69,11 +72,15 @@ fn find_alt(html: &str) -> Option<&str> { -trait EntryTrait { +pub trait EntryTrait: Debug + Clone + Send { fn is_favorite(&self) -> bool; - fn content(&self) -> HashMap>; + fn content(&self) -> MimeDataMap; + + fn id(&self) -> EntryId; + + fn qr_code_content(&self) -> &[u8]; fn get_content(&self) -> Result> { @@ -122,9 +129,9 @@ trait EntryTrait { } -trait DbTrait: Sized { +pub trait DbTrait: Sized { - type Entry: EntryTrait + Debug; + type Entry: EntryTrait; async fn new(config: &Config) -> Result; @@ -132,15 +139,15 @@ trait DbTrait: Sized { async fn reload(&mut self) -> Result<()>; - fn insert<'a: 'b, 'b>(&'a mut self, data: Self::Entry) -> BoxFuture<'b, Result<()>>; + fn insert<'a: 'b, 'b>(&'a mut self, data: MimeDataMap) -> BoxFuture<'b, Result<()>>; - async fn delete(&mut self, data: &Self::Entry) -> Result<()>; + async fn delete(&mut self, data: EntryId) -> Result<()>; async fn clear(&mut self) -> Result<()>; - async fn add_favorite(&mut self, entry: &Self::Entry, index: Option) -> Result<()>; + async fn add_favorite(&mut self, entry: EntryId, index: Option) -> Result<()>; - async fn remove_favorite(&mut self, entry: &Self::Entry) -> Result<()>; + async fn remove_favorite(&mut self, entry: EntryId) -> Result<()>; fn favorite_len(&self) -> usize; @@ -152,6 +159,8 @@ trait DbTrait: Sized { fn get(&self, index: usize) -> Option<&Self::Entry>; + fn get_from_id(&self, id: EntryId) -> Option<&Self::Entry>; + fn iter(&self) -> impl Iterator; fn search_iter(&self) -> impl Iterator)>; diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 1bc5520..5ae520a 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -27,7 +27,6 @@ use super::{DbMessage, DbTrait, EntryTrait}; type Time = i64; -type EntryId = i64; const DB_VERSION: &str = "5"; const DB_PATH: &str = constcat::concat!(APPID, "-db-", DB_VERSION, ".sqlite"); @@ -86,7 +85,6 @@ pub struct Entry { // todo: lazelly load image in memory, since we can't search them anyways /// (Mime, Content) pub content: HashMap>, - pub is_favorite: bool, } impl Hash for Entry { @@ -177,7 +175,7 @@ pub struct EntryMetadata { -pub struct Db { +pub struct DbSqlite { conn: SqliteConnection, /// Hash -> Id hashs: HashMap, @@ -194,7 +192,7 @@ pub struct Db { } -impl DbTrait for Db { +impl DbTrait for DbSqlite { type Entry = Entry; async fn new(config: &Config) -> Result { @@ -304,7 +302,7 @@ impl DbTrait for Db { } - let mut db = Db { + let mut db = DbSqlite { data_version: fetch_data_version(&mut conn).await?, conn, hashs: HashMap::default(), @@ -703,7 +701,7 @@ impl DbTrait for Db { } } -impl Db { +impl DbSqlite { fn iter_inner<'a>( state: &'a BTreeMap, diff --git a/src/main.rs b/src/main.rs index 729b958..1d55817 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,5 +70,5 @@ fn main() -> cosmic::iced::Result { config_handler, config, }; - cosmic::applet::run::(flags) + cosmic::applet::run::>(flags) } diff --git a/src/message.rs b/src/message.rs index a9afd9b..06b10b9 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,7 +1,7 @@ use crate::{ clipboard::ClipboardMessage, config::Config, - db::{DbMessage, Entry}, + db::{DbMessage, EntryId, EntryTrait}, navigation::EventMsg, }; @@ -15,16 +15,16 @@ pub enum AppMsg { ClipboardEvent(ClipboardMessage), #[allow(dead_code)] RetryConnectingClipboard, - Copy(Entry), - Delete(Entry), + Copy(EntryId), + Delete(EntryId), Clear, Navigation(EventMsg), Db(DbMessage), - ShowQrCode(Entry), + ShowQrCode(EntryId), ReturnToClipboard, Config(ConfigMsg), - AddFavorite(Entry), - RemoveFavorite(Entry), + AddFavorite(EntryId), + RemoveFavorite(EntryId), NextPage, PreviousPage, } diff --git a/src/view.rs b/src/view.rs index 97496f5..9af6989 100644 --- a/src/view.rs +++ b/src/view.rs @@ -16,13 +16,13 @@ use itertools::Itertools; use crate::{ app::AppState, - db::{Content, Entry}, + db::{Content, DbTrait, EntryTrait}, fl, icon_button, message::{AppMsg, ConfigMsg}, utils::formatted_value, }; -impl AppState { +impl AppState { pub fn quick_settings_view(&self) -> Element<'_, AppMsg> { fn toggle_settings<'a>( info: impl Into> + 'a, @@ -215,7 +215,7 @@ impl AppState { fn image_entry<'a>( &'a self, - entry: &'a Entry, + entry: &'a Db::Entry, is_focused: bool, image_data: &'a [u8], ) -> Option> { @@ -226,7 +226,7 @@ impl AppState { fn uris_entry<'a>( &'a self, - entry: &'a Entry, + entry: &'a Db::Entry, is_focused: bool, uris: &[&'a str], ) -> Option> { @@ -255,7 +255,7 @@ impl AppState { fn text_entry_with_indices<'a>( &'a self, - entry: &'a Entry, + entry: &'a Db::Entry, is_focused: bool, content: &'a str, _indices: &'a [u32], @@ -265,7 +265,7 @@ impl AppState { fn text_entry<'a>( &'a self, - entry: &'a Entry, + entry: &'a Db::Entry, is_focused: bool, content: &'a str, ) -> Option> { @@ -282,12 +282,12 @@ impl AppState { fn base_entry<'a>( &'a self, - entry: &'a Entry, + entry: &'a Db::Entry, is_focused: bool, content: impl Into>, ) -> Element<'a, AppMsg> { let btn = button::custom(content) - .on_press(AppMsg::Copy(entry.clone())) + .on_press(AppMsg::Copy(entry.id())) .padding([8, 16]) .class(Button::Custom { active: Box::new(move |focused, theme| { @@ -343,25 +343,25 @@ impl AppState { Some(vec![ menu::Tree::new( button::text(fl!("delete_entry")) - .on_press(AppMsg::Delete(entry.clone())) + .on_press(AppMsg::Delete(entry.id())) .width(Length::Fill) .class(Button::Destructive), ), menu::Tree::new( button::text(fl!("show_qr_code")) - .on_press(AppMsg::ShowQrCode(entry.clone())) + .on_press(AppMsg::ShowQrCode(entry.id())) .width(Length::Fill), ), - if entry.is_favorite { + if entry.is_favorite() { menu::Tree::new( button::text(fl!("remove_favorite")) - .on_press(AppMsg::RemoveFavorite(entry.clone())) + .on_press(AppMsg::RemoveFavorite(entry.id())) .width(Length::Fill), ) } else { menu::Tree::new( button::text(fl!("add_favorite")) - .on_press(AppMsg::AddFavorite(entry.clone())) + .on_press(AppMsg::AddFavorite(entry.id())) .width(Length::Fill), ) }, From 11d91a8d51958a7565e146ea5c4660c187797e7c Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:28:16 +0100 Subject: [PATCH 05/13] ok --- migrations/20240723123147_init.sql | 14 +- src/app.rs | 2 +- src/clipboard.rs | 19 +- src/db/mod.rs | 146 ++++---- src/db/sqlite_db.rs | 527 +++++++++++++---------------- src/message.rs | 2 +- src/view.rs | 76 ++--- 7 files changed, 344 insertions(+), 442 deletions(-) diff --git a/migrations/20240723123147_init.sql b/migrations/20240723123147_init.sql index bb1a0fc..4c4e7dd 100644 --- a/migrations/20240723123147_init.sql +++ b/migrations/20240723123147_init.sql @@ -4,6 +4,13 @@ CREATE TABLE IF NOT EXISTS ClipboardEntries ( CREATE INDEX index_creation ON ClipboardEntries (creation) ); +CREATE TABLE IF NOT EXISTS ClipboardContents ( + id INTEGER PRIMARY KEY, + mime TEXT NOT NULL, + content BLOB NOT NULL, + FOREIGN KEY (id) REFERENCES ClipboardEntries(id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS FavoriteClipboardEntries ( id INTEGER PRIMARY KEY, position INTEGER NOT NULL, @@ -11,10 +18,3 @@ CREATE TABLE IF NOT EXISTS FavoriteClipboardEntries ( -- UNIQUE (position), CHECK (position >= 0) ); - -CREATE TABLE IF NOT EXISTS ClipboardContents ( - id INTEGER PRIMARY KEY, - mime TEXT NOT NULL, - content BLOB NOT NULL, - FOREIGN KEY (id) REFERENCES ClipboardEntries(id) ON DELETE CASCADE -); \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 4e8f361..92286d7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,7 +20,7 @@ use futures::executor::block_on; use futures::StreamExt; use crate::config::{Config, PRIVATE_MODE}; -use crate::db::{self, DbMessage, DbTrait, EntryTrait}; +use crate::db::{DbMessage, DbTrait, EntryTrait}; use crate::message::{AppMsg, ConfigMsg}; use crate::navigation::EventMsg; use crate::utils::task_message; diff --git a/src/clipboard.rs b/src/clipboard.rs index c8e9a3b..30c01a6 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::HashSet, io::Read, sync::atomic::{self}, }; @@ -12,17 +12,12 @@ use wl_clipboard_rs::{ paste_watch, }; -use crate::{config::PRIVATE_MODE, db::{EntryTrait, MimeDataMap}}; +use crate::{ + config::PRIVATE_MODE, + db::{EntryTrait, MimeDataMap}, +}; use os_pipe::PipeReader; -// prefer popular formats -// orderer by priority -const IMAGE_MIME_TYPES: [&str; 3] = ["image/png", "image/jpeg", "image/ico"]; - -// prefer popular formats -// orderer by priority -const TEXT_MIME_TYPES: [&str; 3] = ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"]; - #[derive(Debug, Clone)] pub enum ClipboardMessage { Connected, @@ -117,9 +112,9 @@ pub fn sub() -> impl Stream { pub fn copy(data: Entry) -> Result<(), copy::Error> { debug!("copy {:?}", data); - let mut sources = Vec::with_capacity(data.content().len()); + let mut sources = Vec::with_capacity(data.raw_content().len()); - for (mime, content) in data.content() { + for (mime, content) in data.into_raw_content() { let source = MimeSource { source: copy::Source::Bytes(content.into_boxed_slice()), mime_type: copy::MimeType::Specific(mime), diff --git a/src/db/mod.rs b/src/db/mod.rs index 05457a7..0338e5d 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,27 +1,11 @@ -use alive_lock_file::LockResult; -use derivative::Derivative; -use futures::{future::BoxFuture, FutureExt}; -use sqlx::{migrate::MigrateDatabase, prelude::*, sqlite::SqliteRow, Sqlite, SqliteConnection}; -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - fmt::Debug, - hash::{DefaultHasher, Hash, Hasher}, - path::Path, -}; - -use anyhow::{anyhow, bail, Result}; -use nucleo::{ - pattern::{Atom, AtomKind, CaseMatching, Normalization}, - Matcher, Utf32Str, -}; +use futures::future::BoxFuture; +use std::{collections::HashMap, fmt::Debug, path::Path}; + +use anyhow::{bail, Result}; use chrono::Utc; -use crate::{ - app::{APP, APPID, ORG, QUALIFIER}, - config::Config, - utils::{self}, -}; +use crate::config::Config; // #[cfg(test)] // pub mod test; @@ -29,7 +13,6 @@ use crate::{ mod sqlite_db; pub use sqlite_db::DbSqlite; - fn now() -> i64 { Utc::now().timestamp_millis() } @@ -53,84 +36,69 @@ impl Debug for Content<'_> { } } - - -// currently best effort -fn find_alt(html: &str) -> Option<&str> { - const DEB: &str = "alt=\""; - - if let Some(pos) = html.find(DEB) { - const OFFSET: usize = DEB.as_bytes().len(); - - if let Some(pos_end) = html[pos + OFFSET..].find('"') { - return Some(&html[pos + OFFSET..pos + pos_end + OFFSET]); - } - } - - None -} - - - pub trait EntryTrait: Debug + Clone + Send { - fn is_favorite(&self) -> bool; - fn content(&self) -> MimeDataMap; + fn raw_content(&self) -> &MimeDataMap; + + fn into_raw_content(self) -> MimeDataMap; fn id(&self) -> EntryId; - fn qr_code_content(&self) -> &[u8]; + // todo: prioritize certain mime types + fn qr_code_content(&self) -> &[u8] { + self.raw_content().iter().next().unwrap().1 + } + // todo: prioritize certain mime types + fn viewable_content(&self) -> Result> { + for (mime, content) in self.raw_content() { + if mime == "text/uri-list" { + let text = core::str::from_utf8(content)?; - fn get_content(&self) -> Result> { - if self.mime == "text/uri-list" { - let text = if let Some(metadata) = &self.metadata { - &metadata.value - } else { - core::str::from_utf8(&self.content)? - }; + let uris = text + .lines() + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .collect(); - let uris = text - .lines() - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .collect(); + return Ok(Content::UriList(uris)); + } - return Ok(Content::UriList(uris)); - } - if self.mime.starts_with("text/") { - return Ok(Content::Text(core::str::from_utf8(&self.content)?)); - } + if mime.starts_with("text/") { + return Ok(Content::Text(core::str::from_utf8(content)?)); + } - if self.mime.starts_with("image/") { - return Ok(Content::Image(&self.content)); + if mime.starts_with("image/") { + return Ok(Content::Image(content)); + } } - bail!("unsupported mime type {}", self.mime) + bail!( + "unsupported mime types {:#?}", + self.raw_content().keys().collect::>() + ) } - fn get_searchable_text(&self) -> Option<&str> { - if self.mime.starts_with("text/") { - return core::str::from_utf8(&self.content).ok(); - } + fn searchable_content(&self) -> impl Iterator { + self.raw_content().iter().filter_map(|(mime, content)| { + if mime.starts_with("text/") { + let text = core::str::from_utf8(content).ok()?; - if let Some(metadata) = &self.metadata { - #[allow(clippy::assigning_clones)] - if metadata.mime == "text/html" { - if let Some(alt) = find_alt(&metadata.value) { - return Some(alt); + if mime == "text/html" { + if let Some(alt) = find_alt(text) { + return Some(alt); + } } + + return Some(text); } - return Some(&metadata.value); - } - None + None + }) } - } pub trait DbTrait: Sized { - type Entry: EntryTrait; async fn new(config: &Config) -> Result; @@ -155,22 +123,38 @@ pub trait DbTrait: Sized { fn set_query_and_search(&mut self, query: String); - fn query(&self) -> &str; + fn get_query(&self) -> &str; fn get(&self, index: usize) -> Option<&Self::Entry>; fn get_from_id(&self, id: EntryId) -> Option<&Self::Entry>; - fn iter(&self) -> impl Iterator; - - fn search_iter(&self) -> impl Iterator)>; + fn iter(&self) -> Box + '_>; fn len(&self) -> usize; async fn handle_message(&mut self, message: DbMessage) -> Result<()>; + + fn is_search_active(&self) -> bool { + !self.get_query().is_empty() + } } #[derive(Clone, Debug)] pub enum DbMessage { CheckUpdate, } +// currently best effort +fn find_alt(html: &str) -> Option<&str> { + const DEB: &str = "alt=\""; + + if let Some(pos) = html.find(DEB) { + const OFFSET: usize = DEB.as_bytes().len(); + + if let Some(pos_end) = html[pos + OFFSET..].find('"') { + return Some(&html[pos + OFFSET..pos + pos_end + OFFSET]); + } + } + + None +} diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 5ae520a..193249b 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -1,30 +1,28 @@ use alive_lock_file::LockResult; use derivative::Derivative; -use futures::{future::BoxFuture, FutureExt}; -use sqlx::{migrate::MigrateDatabase, prelude::*, sqlite::SqliteRow, Sqlite, SqliteConnection}; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use sqlx::{migrate::MigrateDatabase, prelude::*, Sqlite, SqliteConnection}; use std::{ + cell::RefCell, collections::{BTreeMap, HashMap, HashSet}, fmt::Debug, hash::{DefaultHasher, Hash, Hasher}, path::Path, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; use nucleo::{ pattern::{Atom, AtomKind, CaseMatching, Normalization}, Matcher, Utf32Str, }; -use chrono::Utc; - use crate::{ app::{APP, APPID, ORG, QUALIFIER}, config::Config, utils::{self}, }; -use super::{DbMessage, DbTrait, EntryTrait}; - +use super::{now, DbMessage, DbTrait, EntryId, EntryTrait, MimeDataMap}; type Time = i64; @@ -33,6 +31,31 @@ const DB_PATH: &str = constcat::concat!(APPID, "-db-", DB_VERSION, ".sqlite"); const LOCK_FILE: &str = constcat::concat!(APPID, "-db", ".lock"); +pub struct DbSqlite { + conn: SqliteConnection, + /// Hash -> Id + hashs: HashMap, + /// time -> Id + times: BTreeMap, + /// Id -> Entry + entries: HashMap, + filtered: Vec, + query: String, + needle: Option, + matcher: RefCell, + data_version: i64, + favorites: Favorites, +} + +#[derive(Clone, Eq, Derivative)] +pub struct Entry { + id: EntryId, + creation: Time, + // todo: lazelly load image in memory, since we can't search them anyways + /// (Mime, Content) + raw_content: MimeDataMap, + is_favorite: bool, +} #[derive(Default)] struct Favorites { @@ -68,6 +91,7 @@ impl Favorites { &self.favorites } + #[allow(dead_code)] fn change(&mut self, prev: &EntryId, new: EntryId) { let pos = self.favorites.iter().position(|e| e == prev).unwrap(); self.favorites[pos] = new; @@ -76,38 +100,45 @@ impl Favorites { } } +fn hash_entry_content(data: &MimeDataMap, state: &mut H) { + for e in data.values() { + e.hash(state); + } +} - -#[derive(Clone, Eq, Derivative)] -pub struct Entry { - pub id: EntryId, - pub creation: Time, - // todo: lazelly load image in memory, since we can't search them anyways - /// (Mime, Content) - pub content: HashMap>, +fn get_hash_entry_content(data: &MimeDataMap) -> u64 { + let mut hasher = DefaultHasher::new(); + hash_entry_content(data, &mut hasher); + hasher.finish() } impl Hash for Entry { fn hash(&self, state: &mut H) { - for e in self.content.values() { - e.hash(state); - } + hash_entry_content(&self.raw_content, state); } } impl PartialEq for Entry { fn eq(&self, other: &Self) -> bool { - self.content == other.content + self.id == other.id } } impl EntryTrait for Entry { fn is_favorite(&self) -> bool { - todo!() + self.is_favorite + } + + fn raw_content(&self) -> &MimeDataMap { + &self.raw_content } - fn content(&self) -> HashMap> { - todo!() + fn id(&self) -> EntryId { + self.id + } + + fn into_raw_content(self) -> MimeDataMap { + self.raw_content } } @@ -117,85 +148,22 @@ impl Entry { self.hash(&mut hasher); hasher.finish() } - - pub fn new(id: i64, creation: i64, content: HashMap>, is_favorite: bool) -> Self { - Self { - creation, - content, - is_favorite, - id, - } - } - - } impl Debug for Entry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Data") - .field("id", &self.id) + .field("id", &self.id) .field("creation", &self.creation) - .field("content", &self.get_content()) + .field("content", &self.viewable_content()) .finish() } } - - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct EntryMetadata { - pub mime: String, - pub value: String, -} - -// impl Entry { - - -// /// SELECT creation, mime, content, metadataMime, metadata -// fn from_row(row: &SqliteRow, favorites: &Favorites) -> Result { -// let id = row.get("creation"); -// let is_fav = favorites.contains(&id); - -// Ok(Entry::new( -// id, -// row.get("mime"), -// row.get("content"), -// row.try_get("metadataMime") -// .ok() -// .map(|metadata_mime| EntryMetadata { -// mime: metadata_mime, -// value: row.get("metadata"), -// }), -// is_fav, -// )) -// } -// } - - - - - -pub struct DbSqlite { - conn: SqliteConnection, - /// Hash -> Id - hashs: HashMap, - /// time -> Id - times: BTreeMap, - /// Id -> Entry - entries: HashMap, - filtered: Vec<(EntryId, Vec)>, - query: String, - needle: Option, - matcher: Matcher, - data_version: i64, - favorites: Favorites, -} - - impl DbTrait for DbSqlite { type Entry = Entry; - async fn new(config: &Config) -> Result { + async fn new(config: &Config) -> Result { let directories = directories::ProjectDirs::from(QUALIFIER, ORG, APP).unwrap(); std::fs::create_dir_all(directories.cache_dir())?; @@ -204,11 +172,9 @@ impl DbTrait for DbSqlite { } async fn with_path(config: &Config, db_dir: &Path) -> Result { - if let Err(e) = alive_lock_file::remove_lock(LOCK_FILE) { error!("can't remove lock {e}"); } - let db_path = db_dir.join(DB_PATH); @@ -260,7 +226,6 @@ impl DbTrait for DbSqlite { } if let Some(max_number_of_entries) = &config.maximum_entries_number { - let query_get_most_older = r#" SELECT creation FROM ClipboardEntries @@ -272,13 +237,12 @@ impl DbTrait for DbSqlite { .bind(max_number_of_entries) .fetch_optional(&mut conn) .await - .unwrap() { - - - Some(r) => { - let creation: Time = r.get("creation"); + .unwrap() + { + Some(r) => { + let creation: Time = r.get("creation"); - let query_delete_old_one = r#" + let query_delete_old_one = r#" DELETE FROM ClipboardEntries WHERE creation < ? AND id NOT IN ( @@ -286,20 +250,16 @@ impl DbTrait for DbSqlite { FROM FavoriteClipboardEntries); "#; - sqlx::query(query_delete_old_one) - .bind(creation) - .execute(&mut conn) - .await - .unwrap(); - - }, - None => { - // nothing to do - }, - } - - - + sqlx::query(query_delete_old_one) + .bind(creation) + .execute(&mut conn) + .await + .unwrap(); + } + None => { + // nothing to do + } + } } let mut db = DbSqlite { @@ -311,7 +271,7 @@ impl DbTrait for DbSqlite { filtered: Vec::default(), query: String::default(), needle: None, - matcher: Matcher::new(nucleo::Config::DEFAULT), + matcher: Matcher::new(nucleo::Config::DEFAULT).into(), favorites: Favorites::default(), }; @@ -326,6 +286,7 @@ impl DbTrait for DbSqlite { self.times.clear(); self.favorites.clear(); + // init favorite { let query_load_favs = r#" SELECT id, position @@ -354,26 +315,61 @@ impl DbTrait for DbSqlite { } } + // init entries and times { let query_load_table = r#" - SELECT creation, mime, content, metadataMime, metadata + SELECT id, creation FROM ClipboardEntries - JOIN ClipboardContents ON (id = creation) - GROUP BY "#; - let rows = sqlx::query(query_load_table) - .fetch_all(&mut self.conn) - .await?; + let mut stream = sqlx::query(query_load_table).fetch(&mut self.conn); + + while let Some(res) = stream.next().await { + let row = res?; - sqlx::query(query_load_table) - .fetch(executor) + let id = row.get("id"); + let creation = row.get("creation"); - for row in &rows { - let data = Entry::from_row(row, &self.favorites)?; + let entry = Entry { + id, + creation, + raw_content: MimeDataMap::default(), + is_favorite: self.favorites.contains(&id), + }; - self.hashs.insert(data.get_hash(), data.creation); - self.state.insert(data.creation, data); + self.entries.insert(id, entry); + + self.times.insert(creation, id); + } + } + + // init contents + { + // todo: we can probably optimize by sorting with id + + let query_load_table = r#" + SELECT id, mime, content + FROM ClipboardContents + "#; + + let mut stream = sqlx::query(query_load_table).fetch(&mut self.conn); + + while let Some(res) = stream.next().await { + let row = res?; + + let id = row.get("id"); + let mime: String = row.get("mime"); + let content: Vec = row.get("content"); + + let entry = self.entries.get_mut(&id).expect("entry should exist"); + entry.raw_content.insert(mime, content); + } + } + + // init hashs + { + for entry in self.entries.values() { + self.hashs.insert(entry.get_hash(), entry.id()); } } @@ -382,13 +378,14 @@ impl DbTrait for DbSqlite { Ok(()) } - + fn get_from_id(&self, id: EntryId) -> Option<&Self::Entry> { + self.entries.get(&id) + } // the <= 200 condition, is to unsure we reuse the same timestamp // of the first process that inserted the data. - fn insert<'a: 'b, 'b>(&'a mut self, mut data: Entry) -> BoxFuture<'b, Result<()>> { + fn insert<'a: 'b, 'b>(&'a mut self, data: MimeDataMap) -> BoxFuture<'b, Result<()>> { async move { - match alive_lock_file::try_lock(LOCK_FILE)? { LockResult::Success => {} LockResult::AlreadyLocked => { @@ -396,126 +393,103 @@ impl DbTrait for DbSqlite { return Ok(()); } } - - // insert a new data, only if the last row is not the same AND was not created recently - let query_insert_if_not_exist = r#" - WITH last_row AS ( - SELECT creation, mime, content, metadataMime, metadata - FROM ClipboardEntries - ORDER BY creation DESC - LIMIT 1 - ) - INSERT INTO ClipboardEntries (creation, mime, content, metadataMime, metadata) - SELECT $1, $2, $3, $4, $5 - WHERE NOT EXISTS ( - SELECT 1 - FROM last_row AS lr - WHERE lr.content = $3 AND ($6 - lr.creation) <= 1000 - ); - "#; - - if let Err(e) = sqlx::query(query_insert_if_not_exist) - .bind(data.creation) - .bind(&data.mime) - .bind(&data.content) - .bind(data.metadata.as_ref().map(|m| &m.mime)) - .bind(data.metadata.as_ref().map(|m| &m.value)) - .bind(utils::now_millis()) - .execute(&mut self.conn) - .await - { - if let sqlx::Error::Database(e) = &e { - if e.is_unique_violation() { - warn!("a different value with the same id was already inserted"); - data.creation += 1; - return self.insert(data).await; - } - } - - return Err(e.into()); - } - // safe to unwrap since we insert before - let last_row = self.get_last_row().await?.unwrap(); - - let new_id = last_row.creation; - - let data_hash = data.get_hash(); - - if let Some(old_id) = self.hashs.remove(&data_hash) { - self.state.remove(&old_id); - - if self.favorites.contains(&old_id) { - data.is_favorite = true; - let query_delete_old_id = r#" - UPDATE FavoriteClipboardEntries - SET id = $1 - WHERE id = $2; + let hash = get_hash_entry_content(&data); + let now = now(); + + if let Some(id) = self.hashs.get(&hash) { + let entry = self.entries.get_mut(id).unwrap(); + entry.creation = now; + self.times.remove(&entry.creation); + self.times.insert(now, *id); + + let query_update_creation = r#" + UPDATE ClipboardEntries + SET creation = $1 + WHERE id = $2; + "#; + + sqlx::query(query_update_creation) + .bind(now) + .bind(id) + .execute(&mut self.conn) + .await?; + } else { + let id = now; + + let query_insert_new_entry = r#" + INSERT INTO ClipboardEntries (id, creation) + SELECT $1, $2 + "#; + + sqlx::query(query_insert_new_entry) + .bind(id) + .bind(now) + .execute(&mut self.conn) + .await?; + + for (mime, content) in &data { + let query_insert_content = r#" + INSERT INTO ClipboardContents (id, mime, content) + SELECT $1, $2, $3 "#; - sqlx::query(query_delete_old_id) - .bind(new_id) - .bind(old_id) + sqlx::query(query_insert_content) + .bind(id) + .bind(mime) + .bind(content) .execute(&mut self.conn) .await?; - - self.favorites.change(&old_id, new_id); - } else { - data.is_favorite = false; } - // in case 2 same data were inserted in a short period - // we don't want to remove the old_id - if new_id != old_id { - let query_delete_old_id = r#" - DELETE FROM ClipboardEntries - WHERE creation = ?; - "#; + let entry = Entry { + id, + creation: now, + raw_content: data, + is_favorite: false, + }; - sqlx::query(query_delete_old_id) - .bind(old_id) - .execute(&mut self.conn) - .await?; - } + self.times.insert(now, id); + self.hashs.insert(hash, id); + self.entries.insert(id, entry); } - data.creation = new_id; - - self.hashs.insert(data_hash, data.creation); - self.state.insert(data.creation, data); - self.search(); Ok(()) } .boxed() } - async fn delete(&mut self, data: &Entry) -> Result<()> { + async fn delete(&mut self, id: EntryId) -> Result<()> { let query = r#" DELETE FROM ClipboardEntries - WHERE creation = ?; + WHERE id = ?; "#; - sqlx::query(query) - .bind(data.creation) - .execute(&mut self.conn) - .await?; + sqlx::query(query).bind(id).execute(&mut self.conn).await?; - self.hashs.remove(&data.get_hash()); - self.state.remove(&data.creation); + match self.entries.remove(&id) { + Some(entry) => { + self.hashs.remove(&entry.get_hash()); + self.times.remove(&entry.creation); - if data.is_favorite { - self.favorites.remove(&data.creation); + if entry.is_favorite() { + self.favorites.remove(&entry.creation); + } + } + None => { + warn!("no entry to remove") + } } self.search(); Ok(()) } - async fn clear(&mut self) -> Result<()> { + async fn clear(&mut self) -> Result<()> { let query_delete = r#" DELETE FROM ClipboardEntries - WHERE creation NOT IN( + WHERE id NOT IN( SELECT id FROM FavoriteClipboardEntries ); @@ -528,18 +502,18 @@ impl DbTrait for DbSqlite { Ok(()) } - async fn add_favorite(&mut self, entry: &Entry, index: Option) -> Result<()> { - debug_assert!(!self.favorites.fav().contains(&entry.creation)); + async fn add_favorite(&mut self, id: EntryId, index: Option) -> Result<()> { + debug_assert!(!self.favorites.fav().contains(&id)); - self.favorites.insert_at(entry.creation, index); + self.favorites.insert_at(id, index); if let Some(pos) = index { - let query = r#" + let query_bump_positions = r#" UPDATE FavoriteClipboardEntries SET position = position + 1 WHERE position >= ?; "#; - sqlx::query(query) + sqlx::query(query_bump_positions) .bind(pos as i32) .execute(&mut self.conn) .await @@ -555,21 +529,21 @@ impl DbTrait for DbSqlite { "#; sqlx::query(query) - .bind(entry.creation) + .bind(id) .bind(index as i32) .execute(&mut self.conn) .await?; } - if let Some(e) = self.state.get_mut(&entry.creation) { + if let Some(e) = self.entries.get_mut(&id) { e.is_favorite = true; } Ok(()) } - async fn remove_favorite(&mut self, entry: &Entry) -> Result<()> { - debug_assert!(self.favorites.fav().contains(&entry.creation)); + async fn remove_favorite(&mut self, id: EntryId) -> Result<()> { + debug_assert!(self.favorites.fav().contains(&id)); { let query = r#" @@ -577,13 +551,10 @@ impl DbTrait for DbSqlite { WHERE id = ?; "#; - sqlx::query(query) - .bind(entry.creation) - .execute(&mut self.conn) - .await?; + sqlx::query(query).bind(id).execute(&mut self.conn).await?; } - if let Some(pos) = self.favorites.remove(&entry.creation) { + if let Some(pos) = self.favorites.remove(&id) { let query = r#" UPDATE FavoriteClipboardEntries SET position = position - 1 @@ -595,43 +566,46 @@ impl DbTrait for DbSqlite { .await?; } - if let Some(e) = self.state.get_mut(&entry.creation) { + if let Some(e) = self.entries.get_mut(&id) { e.is_favorite = false; } + Ok(()) } - fn favorite_len(&self) -> usize { + fn favorite_len(&self) -> usize { self.favorites.favorites.len() } - fn search(&mut self) { + fn search(&mut self) { if self.query.is_empty() { self.filtered.clear(); } else if let Some(atom) = &self.needle { - self.filtered = Self::iter_inner(&self.state, &self.favorites) - .filter_map(|data| { - data.get_searchable_text().and_then(|text| { + self.filtered = self + .iter() + .filter_map(|entry| { + if entry.searchable_content().any(|text| { let mut buf = Vec::new(); let haystack = Utf32Str::new(text, &mut buf); let mut indices = Vec::new(); - let _res = atom.indices(haystack, &mut self.matcher, &mut indices); + let _res = + atom.indices(haystack, &mut self.matcher.borrow_mut(), &mut indices); - if !indices.is_empty() { - Some((data.creation, indices)) - } else { - None - } - }) + !indices.is_empty() + }) { + Some(entry.id) + } else { + None + } }) .collect::>(); } } - fn set_query_and_search(&mut self, query: String) { + fn set_query_and_search(&mut self, query: String) { if query.is_empty() { self.needle.take(); } else { @@ -651,44 +625,43 @@ impl DbTrait for DbSqlite { self.search(); } - fn query(&self) -> &str { + fn get_query(&self) -> &str { &self.query } - fn get(&self, index: usize) -> Option<&Entry> { - if self.query.is_empty() { - self.iter().nth(index) - } else { - self.filtered - .get(index) - .map(|(id, _indices)| &self.state[id]) - } + fn get(&self, index: usize) -> Option<&Self::Entry> { + self.iter().nth(index) } - fn iter(&self) -> impl Iterator { - debug_assert!(self.query.is_empty()); - Self::iter_inner(&self.state, &self.favorites) - } - - - - fn search_iter(&self) -> impl Iterator)> { - debug_assert!(!self.query.is_empty()); - - self.filtered - .iter() - .map(|(id, indices)| (&self.state[id], indices)) + fn iter(&self) -> Box + '_> { + if self.is_search_active() { + Box::new(self.filtered.iter().filter_map(|id| self.entries.get(id))) + } else { + Box::new( + self.favorites + .fav() + .iter() + .filter_map(|id| self.entries.get(id)) + .chain( + self.times + .values() + .filter_map(|id| self.entries.get(id)) + .filter(|e| !e.is_favorite) + .rev(), + ), + ) + } } - fn len(&self) -> usize { + fn len(&self) -> usize { if self.query.is_empty() { - self.state.len() + self.entries.len() } else { self.filtered.len() } } - async fn handle_message(&mut self, _message: DbMessage) -> Result<()> { + async fn handle_message(&mut self, _message: DbMessage) -> Result<()> { let data_version = fetch_data_version(&mut self.conn).await?; if self.data_version != data_version { @@ -701,19 +674,6 @@ impl DbTrait for DbSqlite { } } -impl DbSqlite { - - fn iter_inner<'a>( - state: &'a BTreeMap, - favorites: &'a Favorites, - ) -> impl Iterator + 'a { - favorites - .fav() - .iter() - .filter_map(|id| state.get(id)) - .chain(state.values().filter(|e| !e.is_favorite).rev()) - } -} /// https://www.sqlite.org/pragma.html#pragma_data_version async fn fetch_data_version(conn: &mut SqliteConnection) -> Result { let data_version: i64 = sqlx::query("PRAGMA data_version") @@ -723,4 +683,3 @@ async fn fetch_data_version(conn: &mut SqliteConnection) -> Result { Ok(data_version) } - diff --git a/src/message.rs b/src/message.rs index 06b10b9..212b2b3 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,7 +1,7 @@ use crate::{ clipboard::ClipboardMessage, config::Config, - db::{DbMessage, EntryId, EntryTrait}, + db::{DbMessage, EntryId}, navigation::EventMsg, }; diff --git a/src/view.rs b/src/view.rs index 9af6989..55a3d7b 100644 --- a/src/view.rs +++ b/src/view.rs @@ -22,7 +22,7 @@ use crate::{ utils::formatted_value, }; -impl AppState { +impl AppState { pub fn quick_settings_view(&self) -> Element<'_, AppMsg> { fn toggle_settings<'a>( info: impl Into> + 'a, @@ -89,7 +89,7 @@ impl AppState { let content: Element<_> = match self.qr_code.is_none() { true => row() .push( - text_input::search_input(fl!("search_entries"), self.db.query()) + text_input::search_input(fl!("search_entries"), self.db.get_query()) .always_active() .on_input(AppMsg::Search) .on_paste(AppMsg::Search) @@ -144,50 +144,24 @@ impl AppState { let range = self.page * maximum_entries_by_page..(self.page + 1) * maximum_entries_by_page; - let entries_view: Vec<_> = if self.db.query().is_empty() { - self.db - .iter() - .enumerate() - .get(range) - .filter_map(|(pos, data)| match data.get_content() { - Ok(c) => match c { - Content::Text(text) => { - self.text_entry(data, pos == self.focused, text) - } - Content::Image(image) => { - self.image_entry(data, pos == self.focused, image) - } - Content::UriList(uris) => { - self.uris_entry(data, pos == self.focused, &uris) - } - }, - Err(_) => None, - }) - .collect() - } else { - self.db - .search_iter() - .enumerate() - .get(range) - .filter_map(|(pos, (data, indices))| match data.get_content() { - Ok(c) => match c { - Content::Text(text) => self.text_entry_with_indices( - data, - pos == self.focused, - text, - indices, - ), - Content::Image(image) => { - self.image_entry(data, pos == self.focused, image) - } - Content::UriList(uris) => { - self.uris_entry(data, pos == self.focused, &uris) - } - }, - Err(_) => None, - }) - .collect() - }; + let entries_view: Vec<_> = self + .db + .iter() + .enumerate() + .get(range) + .filter_map(|(pos, data)| match data.viewable_content() { + Ok(c) => match c { + Content::Text(text) => self.text_entry(data, pos == self.focused, text), + Content::Image(image) => { + self.image_entry(data, pos == self.focused, image) + } + Content::UriList(uris) => { + self.uris_entry(data, pos == self.focused, &uris) + } + }, + Err(_) => None, + }) + .collect(); if self.config.horizontal { let column = row::with_children(entries_view) @@ -253,16 +227,6 @@ impl AppState { )) } - fn text_entry_with_indices<'a>( - &'a self, - entry: &'a Db::Entry, - is_focused: bool, - content: &'a str, - _indices: &'a [u32], - ) -> Option> { - self.text_entry(entry, is_focused, content) - } - fn text_entry<'a>( &'a self, entry: &'a Db::Entry, From b395e0c710b8072cd66c3a9ec011e74f4ee8fe55 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:02:08 +0100 Subject: [PATCH 06/13] ff --- migrations/20240723123147_init.sql | 2 +- src/db/mod.rs | 11 +- src/db/sqlite_db.rs | 147 +++++++++++---------- src/db/test.rs | 198 ++++++++--------------------- 4 files changed, 134 insertions(+), 224 deletions(-) diff --git a/migrations/20240723123147_init.sql b/migrations/20240723123147_init.sql index 4c4e7dd..cbe6db4 100644 --- a/migrations/20240723123147_init.sql +++ b/migrations/20240723123147_init.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS ClipboardEntries ( id INTEGER PRIMARY KEY, - creation INTEGER PRIMARY KEY, + creation INTEGER, CREATE INDEX index_creation ON ClipboardEntries (creation) ); diff --git a/src/db/mod.rs b/src/db/mod.rs index 0338e5d..0e5d726 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,3 @@ -use futures::future::BoxFuture; use std::{collections::HashMap, fmt::Debug, path::Path}; use anyhow::{bail, Result}; @@ -7,8 +6,8 @@ use chrono::Utc; use crate::config::Config; -// #[cfg(test)] -// pub mod test; +#[cfg(test)] +pub mod test; mod sqlite_db; pub use sqlite_db::DbSqlite; @@ -107,7 +106,9 @@ pub trait DbTrait: Sized { async fn reload(&mut self) -> Result<()>; - fn insert<'a: 'b, 'b>(&'a mut self, data: MimeDataMap) -> BoxFuture<'b, Result<()>>; + async fn insert(&mut self, data: MimeDataMap) -> Result<()>; + + async fn insert_with_time(&mut self, data: MimeDataMap, time: i64) -> Result<()>; async fn delete(&mut self, data: EntryId) -> Result<()>; @@ -117,8 +118,6 @@ pub trait DbTrait: Sized { async fn remove_favorite(&mut self, entry: EntryId) -> Result<()>; - fn favorite_len(&self) -> usize; - fn search(&mut self); fn set_query_and_search(&mut self, query: String); diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 193249b..ef519e6 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -1,6 +1,6 @@ use alive_lock_file::LockResult; use derivative::Derivative; -use futures::{future::BoxFuture, FutureExt, StreamExt}; +use futures::StreamExt; use sqlx::{migrate::MigrateDatabase, prelude::*, Sqlite, SqliteConnection}; use std::{ cell::RefCell, @@ -44,21 +44,21 @@ pub struct DbSqlite { needle: Option, matcher: RefCell, data_version: i64, - favorites: Favorites, + pub(super) favorites: Favorites, } #[derive(Clone, Eq, Derivative)] pub struct Entry { - id: EntryId, - creation: Time, + pub id: EntryId, + pub creation: Time, // todo: lazelly load image in memory, since we can't search them anyways /// (Mime, Content) - raw_content: MimeDataMap, - is_favorite: bool, + pub raw_content: MimeDataMap, + pub is_favorite: bool, } #[derive(Default)] -struct Favorites { +pub(super) struct Favorites { favorites: Vec, favorites_hash_set: HashSet, } @@ -87,7 +87,7 @@ impl Favorites { }) } - fn fav(&self) -> &Vec { + pub(super) fn fav(&self) -> &Vec { &self.favorites } @@ -98,6 +98,10 @@ impl Favorites { self.favorites_hash_set.remove(prev); self.favorites_hash_set.insert(new); } + + pub(super) fn len(&self) -> usize { + self.favorites.len() + } } fn hash_entry_content(data: &MimeDataMap, state: &mut H) { @@ -382,82 +386,79 @@ impl DbTrait for DbSqlite { self.entries.get(&id) } - // the <= 200 condition, is to unsure we reuse the same timestamp - // of the first process that inserted the data. - fn insert<'a: 'b, 'b>(&'a mut self, data: MimeDataMap) -> BoxFuture<'b, Result<()>> { - async move { - match alive_lock_file::try_lock(LOCK_FILE)? { - LockResult::Success => {} - LockResult::AlreadyLocked => { - info!("db already locked"); - return Ok(()); - } + async fn insert(&mut self, data: MimeDataMap) -> Result<()> { + self.insert_with_time(data, now()).await + } + async fn insert_with_time(&mut self, data: MimeDataMap, now: i64) -> Result<()> { + match alive_lock_file::try_lock(LOCK_FILE)? { + LockResult::Success => {} + LockResult::AlreadyLocked => { + info!("db already locked"); + return Ok(()); } + } - let hash = get_hash_entry_content(&data); - let now = now(); + let hash = get_hash_entry_content(&data); - if let Some(id) = self.hashs.get(&hash) { - let entry = self.entries.get_mut(id).unwrap(); - entry.creation = now; - self.times.remove(&entry.creation); - self.times.insert(now, *id); + if let Some(id) = self.hashs.get(&hash) { + let entry = self.entries.get_mut(id).unwrap(); + entry.creation = now; + self.times.remove(&entry.creation); + self.times.insert(now, *id); - let query_update_creation = r#" - UPDATE ClipboardEntries - SET creation = $1 - WHERE id = $2; - "#; + let query_update_creation = r#" + UPDATE ClipboardEntries + SET creation = $1 + WHERE id = $2; + "#; - sqlx::query(query_update_creation) - .bind(now) - .bind(id) - .execute(&mut self.conn) - .await?; - } else { - let id = now; + sqlx::query(query_update_creation) + .bind(now) + .bind(id) + .execute(&mut self.conn) + .await?; + } else { + let id = now; + + let query_insert_new_entry = r#" + INSERT INTO ClipboardEntries (id, creation) + SELECT $1, $2 + "#; - let query_insert_new_entry = r#" - INSERT INTO ClipboardEntries (id, creation) - SELECT $1, $2 + sqlx::query(query_insert_new_entry) + .bind(id) + .bind(now) + .execute(&mut self.conn) + .await?; + + for (mime, content) in &data { + let query_insert_content = r#" + INSERT INTO ClipboardContents (id, mime, content) + SELECT $1, $2, $3 "#; - sqlx::query(query_insert_new_entry) + sqlx::query(query_insert_content) .bind(id) - .bind(now) + .bind(mime) + .bind(content) .execute(&mut self.conn) .await?; - - for (mime, content) in &data { - let query_insert_content = r#" - INSERT INTO ClipboardContents (id, mime, content) - SELECT $1, $2, $3 - "#; - - sqlx::query(query_insert_content) - .bind(id) - .bind(mime) - .bind(content) - .execute(&mut self.conn) - .await?; - } - - let entry = Entry { - id, - creation: now, - raw_content: data, - is_favorite: false, - }; - - self.times.insert(now, id); - self.hashs.insert(hash, id); - self.entries.insert(id, entry); } - self.search(); - Ok(()) + let entry = Entry { + id, + creation: now, + raw_content: data, + is_favorite: false, + }; + + self.times.insert(now, id); + self.hashs.insert(hash, id); + self.entries.insert(id, entry); } - .boxed() + + self.search(); + Ok(()) } async fn delete(&mut self, id: EntryId) -> Result<()> { @@ -520,7 +521,7 @@ impl DbTrait for DbSqlite { .unwrap(); } - let index = index.unwrap_or(self.favorite_len() - 1); + let index = index.unwrap_or(self.favorites.len() - 1); { let query = r#" @@ -573,10 +574,6 @@ impl DbTrait for DbSqlite { Ok(()) } - fn favorite_len(&self) -> usize { - self.favorites.favorites.len() - } - fn search(&mut self) { if self.query.is_empty() { self.filtered.clear(); diff --git a/src/db/test.rs b/src/db/test.rs index 51fc29e..0632c9c 100644 --- a/src/db/test.rs +++ b/src/db/test.rs @@ -12,10 +12,10 @@ use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, Env use crate::{ config::Config, - utils::{self}, + db::{DbSqlite, DbTrait}, }; -use crate::db::{Db, Entry}; +use super::MimeDataMap; fn prepare_db_dir() -> PathBuf { let fmt_layer = fmt::layer().with_target(false); @@ -39,7 +39,7 @@ fn prepare_db_dir() -> PathBuf { async fn test() -> Result<()> { let db_dir = prepare_db_dir(); - let mut db = Db::inner_new(&Config::default(), &db_dir).await?; + let mut db = DbSqlite::with_path(&Config::default(), &db_dir).await?; test_db(&mut db).await.unwrap(); @@ -50,15 +50,17 @@ async fn test() -> Result<()> { Ok(()) } -async fn test_db(db: &mut Db) -> Result<()> { +fn build_content(content: &[(&str, &str)]) -> MimeDataMap { + content + .iter() + .map(|(mime, content)| (mime.to_string(), content.as_bytes().into())) + .collect() +} + +async fn test_db(db: &mut DbSqlite) -> Result<()> { assert!(db.len() == 0); - let data = Entry::new_now( - "text/plain".into(), - "content".as_bytes().into(), - None, - false, - ); + let data = build_content(&[("text/plain", "content")]); db.insert(data).await.unwrap(); @@ -66,12 +68,7 @@ async fn test_db(db: &mut Db) -> Result<()> { sleep(Duration::from_millis(1000)); - let data = Entry::new_now( - "text/plain".into(), - "content".as_bytes().into(), - None, - false, - ); + let data = build_content(&[("text/plain", "content")]); db.insert(data).await.unwrap(); @@ -79,12 +76,7 @@ async fn test_db(db: &mut Db) -> Result<()> { sleep(Duration::from_millis(1000)); - let data = Entry::new_now( - "text/plain".into(), - "content2".as_bytes().into(), - None, - false, - ); + let data = build_content(&[("text/plain", "content2")]); db.insert(data.clone()).await.unwrap(); @@ -92,8 +84,7 @@ async fn test_db(db: &mut Db) -> Result<()> { let next = db.iter().next().unwrap(); - assert!(next.creation == data.creation); - assert!(next.content == data.content); + assert!(next.raw_content == data); Ok(()) } @@ -103,29 +94,25 @@ async fn test_db(db: &mut Db) -> Result<()> { async fn test_delete_old_one() { let db_path = prepare_db_dir(); - let mut db = Db::inner_new(&Config::default(), &db_path).await.unwrap(); + let mut db = DbSqlite::with_path(&Config::default(), &db_path) + .await + .unwrap(); + + let data = build_content(&[("text/plain", "content")]); - let data = Entry::new_now( - "text/plain".into(), - "content".as_bytes().into(), - None, - false, - ); db.insert(data).await.unwrap(); sleep(Duration::from_millis(100)); - let data = Entry::new_now( - "text/plain".into(), - "content2".as_bytes().into(), - None, - false, - ); + let data = build_content(&[("text/plain", "content2")]); + db.insert(data).await.unwrap(); assert!(db.len() == 2); - let db = Db::inner_new(&Config::default(), &db_path).await.unwrap(); + let db = DbSqlite::with_path(&Config::default(), &db_path) + .await + .unwrap(); assert!(db.len() == 2); @@ -133,7 +120,7 @@ async fn test_delete_old_one() { maximum_entries_lifetime: Some(0), ..Default::default() }; - let db = Db::inner_new(&config, &db_path).await.unwrap(); + let db = DbSqlite::with_path(&config, &db_path).await.unwrap(); assert!(db.len() == 0); } @@ -143,137 +130,64 @@ async fn test_delete_old_one() { async fn same() { let db_path = prepare_db_dir(); - let mut db = Db::inner_new(&Config::default(), &db_path).await.unwrap(); - - let now = utils::now_millis(); - - let data = Entry::new( - now, - "text/plain".into(), - "content".as_bytes().into(), - None, - false, - ); - - db.insert(data).await.unwrap(); + let mut db = DbSqlite::with_path(&Config::default(), &db_path) + .await + .unwrap(); - let data = Entry::new( - now, - "text/plain".into(), - "content".as_bytes().into(), - None, - false, - ); + let data = build_content(&[("text/plain", "content")]); - db.insert(data).await.unwrap(); + db.insert(data.clone()).await.unwrap(); + db.insert(data.clone()).await.unwrap(); assert!(db.len() == 1); } -#[tokio::test] -#[serial] -async fn different_content_same_time() { - let db_path = prepare_db_dir(); - - let mut db = Db::inner_new(&Config::default(), &db_path).await.unwrap(); - - let now = utils::now_millis(); - - let data = Entry::new( - now, - "text/plain".into(), - "content".as_bytes().into(), - None, - false, - ); - - db.insert(data).await.unwrap(); - - let data = Entry::new( - now, - "text/plain".into(), - "content2".as_bytes().into(), - None, - false, - ); - - db.insert(data).await.unwrap(); - assert!(db.len() == 2); -} - #[tokio::test] #[serial] async fn favorites() { let db_path = prepare_db_dir(); - let mut db = Db::inner_new(&Config::default(), &db_path).await.unwrap(); + let mut db = DbSqlite::with_path(&Config::default(), &db_path) + .await + .unwrap(); let now1 = 1000; - - let data1 = Entry::new( - now1, - "text/plain".into(), - "content1".as_bytes().into(), - None, - false, - ); - - db.insert(data1).await.unwrap(); + let data1 = build_content(&[("text/plain", "content1")]); + db.insert_with_time(data1, now1).await.unwrap(); let now2 = 2000; - - let data2 = Entry::new( - now2, - "text/plain".into(), - "content2".as_bytes().into(), - None, - false, - ); - - db.insert(data2).await.unwrap(); + let data2 = build_content(&[("text/plain", "content2")]); + db.insert_with_time(data2, now2).await.unwrap(); let now3 = 3000; + let data3 = build_content(&[("text/plain", "content3")]); + db.insert_with_time(data3.clone(), now3).await.unwrap(); - let data3 = Entry::new( - now3, - "text/plain".into(), - "content3".as_bytes().into(), - None, - false, - ); + db.add_favorite(now3, None).await.unwrap(); - db.insert(data3.clone()).await.unwrap(); + assert!(db.get_from_id(now3).unwrap().is_favorite); + assert_eq!(db.favorites.len(), 1); - db.add_favorite(&db.state.get(&now3).unwrap().clone(), None) - .await - .unwrap(); - - db.delete(&db.state.get(&now3).unwrap().clone()) - .await - .unwrap(); + db.delete(now3).await.unwrap(); - assert_eq!(db.favorite_len(), 0); + assert_eq!(db.favorites.len(), 0); db.insert(data3).await.unwrap(); - db.add_favorite(&db.state.get(&now1).unwrap().clone(), None) - .await - .unwrap(); + db.add_favorite(now1, None).await.unwrap(); - db.add_favorite(&db.state.get(&now3).unwrap().clone(), None) - .await - .unwrap(); + db.add_favorite(now3, None).await.unwrap(); - db.add_favorite(&db.state.get(&now2).unwrap().clone(), Some(1)) - .await - .unwrap(); + db.add_favorite(now2, Some(1)).await.unwrap(); - assert_eq!(db.favorite_len(), 3); + assert_eq!(db.favorites.len(), 3); assert_eq!(db.favorites.fav(), &vec![now1, now2, now3]); - let db = Db::inner_new( + db.remove_favorite(now2).await.unwrap(); + + let db = DbSqlite::with_path( &Config { - maximum_entries_lifetime: None, + maximum_entries_lifetime: Some(0), ..Default::default() }, &db_path, @@ -283,8 +197,8 @@ async fn favorites() { assert_eq!(db.len(), 3); - assert_eq!(db.favorite_len(), 3); - assert_eq!(db.favorites.fav(), &vec![now1, now2, now3]); + assert_eq!(db.favorites.len(), 2); + assert_eq!(db.favorites.fav(), &vec![now1, now3]); } fn remove_dir_contents(dir: &Path) { From 754da08939ead8b28f7c2c42ebdb953344a9fc6c Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:36:48 +0100 Subject: [PATCH 07/13] fixes --- Cargo.lock | 2 ++ Cargo.toml | 9 ++------- migrations/20240723123147_init.sql | 5 +++-- src/db/sqlite_db.rs | 25 +++++++++++++++++++------ src/db/test.rs | 26 +++++++++++++------------- 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fb9ef6..931566e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,8 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "alive_lock_file" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5639bdca4a334142d2efc8a192cff12ae003e42cf19e8a45f24246eb244f3cc" dependencies = [ "anyhow", "dirs", diff --git a/Cargo.toml b/Cargo.toml index b3a6c92..93330c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,17 +40,12 @@ nucleo = "0.5" futures = "0.3" include_dir = "0.7" itertools = "0.13.0" -alive_lock_file = { path = "../alive_lock_file" } +alive_lock_file = "0.2" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" default-features = false -features = [ - "applet", - "tokio", - "wayland", - "qr_code", -] +features = ["applet", "tokio", "wayland", "qr_code"] [dependencies.wl-clipboard-rs] git = "https://github.com/wiiznokes/wl-clipboard-rs.git" diff --git a/migrations/20240723123147_init.sql b/migrations/20240723123147_init.sql index cbe6db4..f4e92a6 100644 --- a/migrations/20240723123147_init.sql +++ b/migrations/20240723123147_init.sql @@ -1,9 +1,10 @@ CREATE TABLE IF NOT EXISTS ClipboardEntries ( id INTEGER PRIMARY KEY, - creation INTEGER, - CREATE INDEX index_creation ON ClipboardEntries (creation) + creation INTEGER ); +CREATE INDEX IF NOT EXISTS index_creation ON ClipboardEntries (creation); + CREATE TABLE IF NOT EXISTS ClipboardContents ( id INTEGER PRIMARY KEY, mime TEXT NOT NULL, diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index ef519e6..0a34852 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -1,4 +1,4 @@ -use alive_lock_file::LockResult; +use alive_lock_file::LockResultWithDrop; use derivative::Derivative; use futures::StreamExt; use sqlx::{migrate::MigrateDatabase, prelude::*, Sqlite, SqliteConnection}; @@ -45,6 +45,7 @@ pub struct DbSqlite { matcher: RefCell, data_version: i64, pub(super) favorites: Favorites, + lock: Option, } #[derive(Clone, Eq, Derivative)] @@ -277,6 +278,7 @@ impl DbTrait for DbSqlite { needle: None, matcher: Matcher::new(nucleo::Config::DEFAULT).into(), favorites: Favorites::default(), + lock: None, }; db.reload().await?; @@ -390,12 +392,23 @@ impl DbTrait for DbSqlite { self.insert_with_time(data, now()).await } async fn insert_with_time(&mut self, data: MimeDataMap, now: i64) -> Result<()> { - match alive_lock_file::try_lock(LOCK_FILE)? { - LockResult::Success => {} - LockResult::AlreadyLocked => { - info!("db already locked"); - return Ok(()); + match &self.lock { + Some(lock) => { + if !lock.has_lock() { + info!("db already locked"); + return Ok(()); + } } + None => match alive_lock_file::try_lock_until_dropped(LOCK_FILE)? { + LockResultWithDrop::Locked(lock) => { + self.lock = Some(LockResultWithDrop::Locked(lock)); + } + LockResultWithDrop::AlreadyLocked => { + info!("db already locked"); + self.lock = Some(LockResultWithDrop::AlreadyLocked); + return Ok(()); + } + }, } let hash = get_hash_entry_content(&data); diff --git a/src/db/test.rs b/src/db/test.rs index 0632c9c..768fd4d 100644 --- a/src/db/test.rs +++ b/src/db/test.rs @@ -62,29 +62,27 @@ async fn test_db(db: &mut DbSqlite) -> Result<()> { let data = build_content(&[("text/plain", "content")]); - db.insert(data).await.unwrap(); + db.insert_with_time(data.clone(), 10).await.unwrap(); assert!(db.len() == 1); sleep(Duration::from_millis(1000)); - let data = build_content(&[("text/plain", "content")]); - - db.insert(data).await.unwrap(); + db.insert_with_time(data.clone(), 20).await.unwrap(); assert!(db.len() == 1); sleep(Duration::from_millis(1000)); - let data = build_content(&[("text/plain", "content2")]); + let data2 = build_content(&[("text/plain", "content2")]); - db.insert(data.clone()).await.unwrap(); + db.insert_with_time(data2.clone(), 30).await.unwrap(); - assert!(db.len() == 2); + assert_eq!(db.len(), 2); let next = db.iter().next().unwrap(); - assert!(next.raw_content == data); + assert!(next.raw_content == data2); Ok(()) } @@ -108,13 +106,13 @@ async fn test_delete_old_one() { db.insert(data).await.unwrap(); - assert!(db.len() == 2); + assert_eq!(db.len(), 2); let db = DbSqlite::with_path(&Config::default(), &db_path) .await .unwrap(); - assert!(db.len() == 2); + assert_eq!(db.len(), 2); let config = Config { maximum_entries_lifetime: Some(0), @@ -122,7 +120,7 @@ async fn test_delete_old_one() { }; let db = DbSqlite::with_path(&config, &db_path).await.unwrap(); - assert!(db.len() == 0); + assert_eq!(db.len(), 0); } #[tokio::test] @@ -171,7 +169,7 @@ async fn favorites() { assert_eq!(db.favorites.len(), 0); - db.insert(data3).await.unwrap(); + db.insert_with_time(data3.clone(), now3).await.unwrap(); db.add_favorite(now1, None).await.unwrap(); @@ -185,6 +183,8 @@ async fn favorites() { db.remove_favorite(now2).await.unwrap(); + assert_eq!(db.len(), 3); + let db = DbSqlite::with_path( &Config { maximum_entries_lifetime: Some(0), @@ -195,7 +195,7 @@ async fn favorites() { .await .unwrap(); - assert_eq!(db.len(), 3); + assert_eq!(db.len(), 2); assert_eq!(db.favorites.len(), 2); assert_eq!(db.favorites.fav(), &vec![now1, now3]); From 668cce40ed6fe8f11bb8014b78c23a7ce962e5c3 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:18:09 +0100 Subject: [PATCH 08/13] fix --- Cargo.lock | 64 ++++++++++-------------------- Cargo.toml | 14 ++++--- migrations/20240723123147_init.sql | 3 +- src/clipboard.rs | 27 +++++++------ src/db/sqlite_db.rs | 31 ++++++++++----- 5 files changed, 66 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 931566e..6913ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "url", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.5", + "wayland-protocols", "zbus 4.4.0", ] @@ -1104,7 +1104,7 @@ dependencies = [ "libc", "smithay-client-toolkit 0.19.2 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-client", - "wayland-protocols 0.32.5", + "wayland-protocols", ] [[package]] @@ -1192,7 +1192,7 @@ dependencies = [ "serde", "smithay-client-toolkit 0.19.2 (git+https://github.com/Smithay/client-toolkit)", "tracing", - "wayland-protocols-wlr 0.3.5", + "wayland-protocols-wlr", "xdg-shell-wrapper-config", ] @@ -1204,8 +1204,8 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.5", - "wayland-protocols-wlr 0.3.5", + "wayland-protocols", + "wayland-protocols-wlr", "wayland-scanner", "wayland-server", ] @@ -2663,7 +2663,7 @@ dependencies = [ "tiny-xlib", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.5", + "wayland-protocols", "wayland-sys", "wgpu", "x11rb", @@ -2705,7 +2705,7 @@ dependencies = [ "tracing", "wasm-bindgen-futures", "wayland-backend", - "wayland-protocols 0.32.5", + "wayland-protocols", "web-sys", "winapi", "window_clipboard", @@ -5148,8 +5148,8 @@ dependencies = [ "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols 0.32.5", - "wayland-protocols-wlr 0.3.5", + "wayland-protocols", + "wayland-protocols-wlr", "wayland-scanner", "xkbcommon", "xkeysym", @@ -5175,8 +5175,8 @@ dependencies = [ "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols 0.32.5", - "wayland-protocols-wlr 0.3.5", + "wayland-protocols", + "wayland-protocols-wlr", "wayland-scanner", "xkbcommon", "xkeysym", @@ -6318,18 +6318,6 @@ dependencies = [ "xcursor", ] -[[package]] -name = "wayland-protocols" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" -dependencies = [ - "bitflags 2.6.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - [[package]] name = "wayland-protocols" version = "0.32.5" @@ -6352,20 +6340,7 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.5", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" -dependencies = [ - "bitflags 2.6.0", - "wayland-backend", - "wayland-client", - "wayland-protocols 0.31.2", + "wayland-protocols", "wayland-scanner", ] @@ -6378,7 +6353,7 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.5", + "wayland-protocols", "wayland-scanner", "wayland-server", ] @@ -6941,7 +6916,7 @@ dependencies = [ "wasm-bindgen-futures", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.5", + "wayland-protocols", "wayland-protocols-plasma", "web-sys", "web-time", @@ -6972,19 +6947,20 @@ dependencies = [ [[package]] name = "wl-clipboard-rs" version = "0.8.1" -source = "git+https://github.com/wiiznokes/wl-clipboard-rs.git?branch=watch#45ccfaf4469585c520a2df2c63b13a7800627d9f" +source = "git+https://github.com/wiiznokes/wl-clipboard-rs.git?branch=watch#10d95f5d5e22d062e51782d4270291295216c4e7" dependencies = [ "libc", "log", "os_pipe", "rustix 0.38.42", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.6", + "tokio", "tree_magic_mini", "wayland-backend", "wayland-client", - "wayland-protocols 0.31.2", - "wayland-protocols-wlr 0.2.0", + "wayland-protocols", + "wayland-protocols-wlr", ] [[package]] @@ -7059,7 +7035,7 @@ version = "0.1.0" source = "git+https://github.com/pop-os/cosmic-panel#1c9c4e2a2cf27efd0ca77b5ec21bc6f7fa92d9da" dependencies = [ "serde", - "wayland-protocols-wlr 0.3.5", + "wayland-protocols-wlr", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 93330c6..c078ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,12 +50,6 @@ features = ["applet", "tokio", "wayland", "qr_code"] [dependencies.wl-clipboard-rs] git = "https://github.com/wiiznokes/wl-clipboard-rs.git" branch = "watch" -# path = "../wl-clipboard-rs" - -# [patch."https://github.com/pop-os/libcosmic".libcosmic] -# git = "https://github.com/wiiznokes/libcosmic" -# branch = "fix_qr_code_theme" -# path = "../libcosmic" [dev-dependencies] @@ -66,3 +60,11 @@ configurator_schema = { git = "https://github.com/cosmic-utils/configurator.git" [profile.release-lto] inherits = "release" lto = "fat" + +# [patch."https://github.com/pop-os/libcosmic".libcosmic] +# git = "https://github.com/wiiznokes/libcosmic" +# branch = "fix_qr_code_theme" +# path = "../libcosmic" + +# [patch."https://github.com/wiiznokes/wl-clipboard-rs.git".wl-clipboard-rs] +# path = "../wl-clipboard-rs" diff --git a/migrations/20240723123147_init.sql b/migrations/20240723123147_init.sql index f4e92a6..e60c02b 100644 --- a/migrations/20240723123147_init.sql +++ b/migrations/20240723123147_init.sql @@ -6,9 +6,10 @@ CREATE TABLE IF NOT EXISTS ClipboardEntries ( CREATE INDEX IF NOT EXISTS index_creation ON ClipboardEntries (creation); CREATE TABLE IF NOT EXISTS ClipboardContents ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL, mime TEXT NOT NULL, content BLOB NOT NULL, + PRIMARY KEY (id, mime), FOREIGN KEY (id) REFERENCES ClipboardEntries(id) ON DELETE CASCADE ); diff --git a/src/clipboard.rs b/src/clipboard.rs index 30c01a6..83aa439 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,12 +1,11 @@ use std::{ collections::HashSet, - io::Read, sync::atomic::{self}, }; use cosmic::iced::{futures::SinkExt, stream::channel}; -use futures::Stream; -use tokio::sync::mpsc; +use futures::{future::join_all, Stream}; +use tokio::{io::AsyncReadExt, net::unix::pipe, sync::mpsc}; use wl_clipboard_rs::{ copy::{self, MimeSource}, paste_watch, @@ -16,7 +15,6 @@ use crate::{ config::PRIVATE_MODE, db::{EntryTrait, MimeDataMap}, }; -use os_pipe::PipeReader; #[derive(Debug, Clone)] pub enum ClipboardMessage { @@ -34,7 +32,7 @@ pub fn sub() -> impl Stream { match paste_watch::Watcher::init(paste_watch::ClipboardType::Regular) { Ok(mut clipboard_watcher) => { let (tx, mut rx) = - mpsc::channel::>>(5); + mpsc::channel::>>(5); tokio::task::spawn_blocking(move || loop { // return a vec of maximum 2 mimetypes @@ -70,13 +68,18 @@ pub fn sub() -> impl Stream { loop { match rx.recv().await { Some(Some(res)) => { - let data = res - .map(|(mut pipe, mime_type)| { - let mut contents = Vec::new(); - pipe.read_to_end(&mut contents).unwrap(); - (mime_type, contents) - }) - .collect(); + info!("start reading pipes"); + + let data = join_all(res.map(|(mut pipe, mime_type)| async move { + let mut contents = Vec::new(); + pipe.read_to_end(&mut contents).await.unwrap(); + (mime_type, contents) + })) + .await + .into_iter() + .collect(); + + info!("start sending pipes"); output.send(ClipboardMessage::Data(data)).await.unwrap(); } diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 0a34852..7ca4220 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -1,6 +1,7 @@ use alive_lock_file::LockResultWithDrop; use derivative::Derivative; use futures::StreamExt; +use itertools::Itertools; use sqlx::{migrate::MigrateDatabase, prelude::*, Sqlite, SqliteConnection}; use std::{ cell::RefCell, @@ -26,7 +27,7 @@ use super::{now, DbMessage, DbTrait, EntryId, EntryTrait, MimeDataMap}; type Time = i64; -const DB_VERSION: &str = "5"; +const DB_VERSION: &str = "7"; const DB_PATH: &str = constcat::concat!(APPID, "-db-", DB_VERSION, ".sqlite"); const LOCK_FILE: &str = constcat::concat!(APPID, "-db", ".lock"); @@ -106,9 +107,11 @@ impl Favorites { } fn hash_entry_content(data: &MimeDataMap, state: &mut H) { - for e in data.values() { - e.hash(state); - } + data.iter() + .sorted_by(|e1, e2| e1.0.cmp(e2.0)) + .for_each(|e| { + e.hash(state); + }); } fn get_hash_entry_content(data: &MimeDataMap) -> u64 { @@ -183,6 +186,8 @@ impl DbTrait for DbSqlite { let db_path = db_dir.join(DB_PATH); + info!("db_path: {}", db_path.display()); + let db_path = db_path .to_str() .ok_or(anyhow!("can't convert path to str"))?; @@ -283,6 +288,10 @@ impl DbTrait for DbSqlite { db.reload().await?; + dbg!(&db.hashs); + dbg!(&db.times); + dbg!(&db.entries); + Ok(db) } @@ -415,9 +424,11 @@ impl DbTrait for DbSqlite { if let Some(id) = self.hashs.get(&hash) { let entry = self.entries.get_mut(id).unwrap(); + let old_creation = entry.creation; entry.creation = now; - self.times.remove(&entry.creation); - self.times.insert(now, *id); + let res = self.times.remove(&old_creation); + assert!(res.is_some()); + self.times.insert(entry.creation, *id); let query_update_creation = r#" UPDATE ClipboardEntries @@ -465,7 +476,7 @@ impl DbTrait for DbSqlite { is_favorite: false, }; - self.times.insert(now, id); + self.times.insert(entry.creation, id); self.hashs.insert(hash, id); self.entries.insert(id, entry); } @@ -645,17 +656,17 @@ impl DbTrait for DbSqlite { fn iter(&self) -> Box + '_> { if self.is_search_active() { - Box::new(self.filtered.iter().filter_map(|id| self.entries.get(id))) + Box::new(self.filtered.iter().map(|id| &self.entries[id])) } else { Box::new( self.favorites .fav() .iter() - .filter_map(|id| self.entries.get(id)) + .map(|id| &self.entries[id]) .chain( self.times .values() - .filter_map(|id| self.entries.get(id)) + .map(|id| &self.entries[id]) .filter(|e| !e.is_favorite) .rev(), ), From 79ca7c446fe58a296c81e6422ca3a3d461d8e0ac Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:39:58 +0100 Subject: [PATCH 09/13] fix --- Cargo.lock | 2 +- src/db/mod.rs | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6913ba5..705ffa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6947,7 +6947,7 @@ dependencies = [ [[package]] name = "wl-clipboard-rs" version = "0.8.1" -source = "git+https://github.com/wiiznokes/wl-clipboard-rs.git?branch=watch#10d95f5d5e22d062e51782d4270291295216c4e7" +source = "git+https://github.com/wiiznokes/wl-clipboard-rs.git?branch=watch#2c10217b78dee4fe6435f29e263450da5398eb8e" dependencies = [ "libc", "log", diff --git a/src/db/mod.rs b/src/db/mod.rs index 0e5d726..02359f5 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -21,7 +21,7 @@ pub type MimeDataMap = HashMap>; pub enum Content<'a> { Text(&'a str), - Image(&'a Vec), + Image(&'a [u8]), UriList(Vec<&'a str>), } @@ -35,6 +35,8 @@ impl Debug for Content<'_> { } } +const PREFERRED_MIME_TYPES: &[&str] = &["text/plain"]; + pub trait EntryTrait: Debug + Clone + Send { fn is_favorite(&self) -> bool; @@ -49,9 +51,8 @@ pub trait EntryTrait: Debug + Clone + Send { self.raw_content().iter().next().unwrap().1 } - // todo: prioritize certain mime types fn viewable_content(&self) -> Result> { - for (mime, content) in self.raw_content() { + fn try_get_content<'a>(mime: &str, content: &'a [u8]) -> Result>> { if mime == "text/uri-list" { let text = core::str::from_utf8(content)?; @@ -60,15 +61,39 @@ pub trait EntryTrait: Debug + Clone + Send { .filter(|l| !l.is_empty() && !l.starts_with('#')) .collect(); - return Ok(Content::UriList(uris)); + return Ok(Some(Content::UriList(uris))); } if mime.starts_with("text/") { - return Ok(Content::Text(core::str::from_utf8(content)?)); + return Ok(Some(Content::Text(core::str::from_utf8(content)?))); } if mime.starts_with("image/") { - return Ok(Content::Image(content)); + return Ok(Some(Content::Image(content))); + } + + Ok(None) + } + + for pref_mime in PREFERRED_MIME_TYPES { + if let Some(content) = self.raw_content().get(*pref_mime) { + match try_get_content(pref_mime, content) { + Ok(Some(content)) => return Ok(content), + Ok(None) => error!("unsupported mime type {}", pref_mime), + Err(e) => { + error!("{e}"); + } + } + } + } + + for (mime, content) in self.raw_content() { + match try_get_content(mime, content) { + Ok(Some(content)) => return Ok(content), + Ok(None) => {} + Err(e) => { + error!("{e}"); + } } } From a746ad6750006afdf56da79486ca63855f3ac928 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:52:56 +0100 Subject: [PATCH 10/13] update prefered content function --- Cargo.lock | 1 + Cargo.toml | 1 + res/config_schema.json | 7 +++ src/app.rs | 62 +++++++++++++------ src/config.rs | 6 +- src/db/mod.rs | 133 +++++++++++++++++++++++++++-------------- src/db/sqlite_db.rs | 2 +- src/view.rs | 24 ++++---- 8 files changed, 160 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 705ffa3..6493d91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,6 +1156,7 @@ dependencies = [ "nucleo", "os_pipe", "paste", + "regex", "rust-embed", "serde", "serial_test", diff --git a/Cargo.toml b/Cargo.toml index c078ad0..2be9028 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ futures = "0.3" include_dir = "0.7" itertools = "0.13.0" alive_lock_file = "0.2" +regex = "1" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" diff --git a/res/config_schema.json b/res/config_schema.json index 13878db..e5d4847 100644 --- a/res/config_schema.json +++ b/res/config_schema.json @@ -42,6 +42,13 @@ "type": "integer", "format": "uint32", "minimum": 1.0 + }, + "preferred_mime_types": { + "default": [], + "type": "array", + "items": { + "type": "string" + } } }, "X_CONFIGURATOR_SOURCE_HOME_PATH": ".config/cosmic/io.github.wiiznokes.cosmic-ext-applet-clipboard-manager/v3", diff --git a/src/app.rs b/src/app.rs index 92286d7..45a7c5b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,6 +18,7 @@ use cosmic::widget::{MouseArea, Space}; use cosmic::{app::Task, Element}; use futures::executor::block_on; use futures::StreamExt; +use regex::Regex; use crate::config::{Config, PRIVATE_MODE}; use crate::db::{DbMessage, DbTrait, EntryTrait}; @@ -46,6 +47,7 @@ pub struct AppState { pub page: usize, pub qr_code: Option>, last_quit: Option<(i64, PopupKind)>, + pub preferred_mime_types_regex: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -222,9 +224,20 @@ impl cosmic::Application for AppState { clipboard_state: ClipboardState::Init, focused: 0, qr_code: None, - config, last_quit: None, page: 0, + preferred_mime_types_regex: config + .preferred_mime_types + .iter() + .filter_map(|r| match Regex::new(r) { + Ok(r) => Some(r), + Err(e) => { + error!("regex {e}"); + None + } + }) + .collect(), + config, }; #[cfg(debug_assertions)] @@ -261,10 +274,23 @@ impl cosmic::Application for AppState { match message { AppMsg::ChangeConfig(config) => { - if config != self.config { + if config.private_mode != self.config.private_mode { PRIVATE_MODE.store(config.private_mode, atomic::Ordering::Relaxed); - self.config = config; } + if config.preferred_mime_types != self.config.preferred_mime_types { + self.preferred_mime_types_regex = config + .preferred_mime_types + .iter() + .filter_map(|r| match Regex::new(r) { + Ok(r) => Some(r), + Err(e) => { + error!("regex {e}"); + None + } + }) + .collect(); + } + self.config = config; } AppMsg::ToggleQuickSettings => { return self.toggle_popup(PopupKind::QuickSettings); @@ -364,22 +390,24 @@ impl cosmic::Application for AppState { AppMsg::ShowQrCode(id) => { match self.db.get_from_id(id) { Some(entry) => { - let content = entry.qr_code_content(); - - // todo: handle better this error - if content.len() < 700 { - match qr_code::Data::new(content) { - Ok(s) => { - self.qr_code.replace(Ok(s)); - } - Err(e) => { - error!("{e}"); - self.qr_code.replace(Err(())); + if let Some(((_, content), _)) = + entry.preferred_content(&self.preferred_mime_types_regex) + { + // todo: handle better this error + if content.len() < 700 { + match qr_code::Data::new(content) { + Ok(s) => { + self.qr_code.replace(Ok(s)); + } + Err(e) => { + error!("{e}"); + self.qr_code.replace(Err(())); + } } + } else { + error!("qr code to long: {}", content.len()); + self.qr_code.replace(Err(())); } - } else { - error!("qr code to long: {}", content.len()); - self.qr_code.replace(Err(())); } } None => error!("id not found"), diff --git a/src/config.rs b/src/config.rs index 6d82fb6..f9075fc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,8 +34,11 @@ pub struct Config { /// Reset the database at each login pub unique_session: bool, pub maximum_entries_by_page: NonZeroU32, + pub preferred_mime_types: Vec, } +pub static PRIVATE_MODE: AtomicBool = AtomicBool::new(false); + impl Config { pub fn maximum_entries_lifetime(&self) -> Option { self.maximum_entries_lifetime @@ -52,12 +55,11 @@ impl Default for Config { horizontal: false, unique_session: false, maximum_entries_by_page: NonZero::new(50).unwrap(), + preferred_mime_types: Vec::new(), } } } -pub static PRIVATE_MODE: AtomicBool = AtomicBool::new(false); - pub fn sub() -> Subscription { struct ConfigSubscription; diff --git a/src/db/mod.rs b/src/db/mod.rs index 02359f5..5358f12 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,8 +1,9 @@ -use std::{collections::HashMap, fmt::Debug, path::Path}; +use std::{collections::HashMap, fmt::Debug, path::Path, sync::LazyLock}; -use anyhow::{bail, Result}; +use anyhow::Result; use chrono::Utc; +use regex::Regex; use crate::config::Config; @@ -17,7 +18,9 @@ fn now() -> i64 { } pub type EntryId = i64; -pub type MimeDataMap = HashMap>; +pub type Mime = String; +pub type RawContent = Vec; +pub type MimeDataMap = HashMap; pub enum Content<'a> { Text(&'a str), @@ -25,6 +28,31 @@ pub enum Content<'a> { UriList(Vec<&'a str>), } +impl<'a> Content<'a> { + fn try_new(mime: &str, content: &'a [u8]) -> Result> { + if mime == "text/uri-list" { + let text = core::str::from_utf8(content)?; + + let uris = text + .lines() + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .collect(); + + return Ok(Some(Content::UriList(uris))); + } + + if mime.starts_with("text/") { + return Ok(Some(Content::Text(core::str::from_utf8(content)?))); + } + + if mime.starts_with("image/") { + return Ok(Some(Content::Image(content))); + } + + Ok(None) + } +} + impl Debug for Content<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -35,7 +63,25 @@ impl Debug for Content<'_> { } } -const PREFERRED_MIME_TYPES: &[&str] = &["text/plain"]; +/// More we have mime types here, Less we spend time in the [`EntryTrait::preferred_content`] function. +const PRIV_MIME_TYPES_SIMPLE: &[&str] = &[ + "text/plain;charset=utf-8", + "text/plain", + "STRING", + "UTF8_STRING", + "TEXT", + "image/png", + "image/jpg", + "image/jpeg", +]; +const PRIV_MIME_TYPES_REGEX_STR: &[&str] = &["text/plain*", "text/*", "image/*"]; + +static PRIV_MIME_TYPES_REGEX: LazyLock> = LazyLock::new(|| { + PRIV_MIME_TYPES_REGEX_STR + .iter() + .map(|r| Regex::new(r).unwrap()) + .collect() +}); pub trait EntryTrait: Debug + Clone + Send { fn is_favorite(&self) -> bool; @@ -46,61 +92,58 @@ pub trait EntryTrait: Debug + Clone + Send { fn id(&self) -> EntryId; - // todo: prioritize certain mime types - fn qr_code_content(&self) -> &[u8] { - self.raw_content().iter().next().unwrap().1 - } - - fn viewable_content(&self) -> Result> { - fn try_get_content<'a>(mime: &str, content: &'a [u8]) -> Result>> { - if mime == "text/uri-list" { - let text = core::str::from_utf8(content)?; - - let uris = text - .lines() - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .collect(); - - return Ok(Some(Content::UriList(uris))); - } - - if mime.starts_with("text/") { - return Ok(Some(Content::Text(core::str::from_utf8(content)?))); - } - - if mime.starts_with("image/") { - return Ok(Some(Content::Image(content))); + fn preferred_content( + &self, + preferred_mime_types: &[Regex], + ) -> Option<((&str, &RawContent), Content<'_>)> { + for pref_mime_regex in preferred_mime_types { + for (mime, raw_content) in self.raw_content() { + if !raw_content.is_empty() && pref_mime_regex.is_match(mime) { + match Content::try_new(mime, raw_content) { + Ok(Some(content)) => return Some(((mime, raw_content), content)), + Ok(None) => error!("unsupported mime type {}", pref_mime_regex), + Err(e) => { + error!("{e}"); + } + } + } } - - Ok(None) } - for pref_mime in PREFERRED_MIME_TYPES { - if let Some(content) = self.raw_content().get(*pref_mime) { - match try_get_content(pref_mime, content) { - Ok(Some(content)) => return Ok(content), - Ok(None) => error!("unsupported mime type {}", pref_mime), - Err(e) => { - error!("{e}"); + for pref_mime in PRIV_MIME_TYPES_SIMPLE { + if let Some(raw_content) = self.raw_content().get(*pref_mime) { + if !raw_content.is_empty() { + match Content::try_new(pref_mime, raw_content) { + Ok(Some(content)) => return Some(((pref_mime, raw_content), content)), + Ok(None) => {} + Err(e) => { + error!("{e}"); + } } } } } - for (mime, content) in self.raw_content() { - match try_get_content(mime, content) { - Ok(Some(content)) => return Ok(content), - Ok(None) => {} - Err(e) => { - error!("{e}"); + for pref_mime_regex in PRIV_MIME_TYPES_REGEX.iter() { + for (mime, raw_content) in self.raw_content() { + if !raw_content.is_empty() && pref_mime_regex.is_match(mime) { + match Content::try_new(mime, raw_content) { + Ok(Some(content)) => return Some(((mime, raw_content), content)), + Ok(None) => {} + Err(e) => { + error!("{e}"); + } + } } } } - bail!( + warn!( "unsupported mime types {:#?}", self.raw_content().keys().collect::>() - ) + ); + + None } fn searchable_content(&self) -> impl Iterator { diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 7ca4220..075c246 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -163,7 +163,7 @@ impl Debug for Entry { f.debug_struct("Data") .field("id", &self.id) .field("creation", &self.creation) - .field("content", &self.viewable_content()) + .field("content", &self.preferred_content(&[])) .finish() } } diff --git a/src/view.rs b/src/view.rs index 55a3d7b..fc32e56 100644 --- a/src/view.rs +++ b/src/view.rs @@ -149,17 +149,19 @@ impl AppState { .iter() .enumerate() .get(range) - .filter_map(|(pos, data)| match data.viewable_content() { - Ok(c) => match c { - Content::Text(text) => self.text_entry(data, pos == self.focused, text), - Content::Image(image) => { - self.image_entry(data, pos == self.focused, image) - } - Content::UriList(uris) => { - self.uris_entry(data, pos == self.focused, &uris) - } - }, - Err(_) => None, + .filter_map(|(pos, data)| { + data.preferred_content(&self.preferred_mime_types_regex) + .and_then(|(_, content)| match content { + Content::Text(text) => { + self.text_entry(data, pos == self.focused, text) + } + Content::Image(image) => { + self.image_entry(data, pos == self.focused, image) + } + Content::UriList(uris) => { + self.uris_entry(data, pos == self.focused, &uris) + } + }) }) .collect(); From a8c8341fe3a94b7faa73ae7cf7e747cea896a407 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:24:36 +0100 Subject: [PATCH 11/13] fix search --- src/db/mod.rs | 11 ++++++++++- src/db/sqlite_db.rs | 42 +++++++++++++++++++++++++++--------------- src/view.rs | 2 +- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 5358f12..d9e2438 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -196,7 +196,16 @@ pub trait DbTrait: Sized { fn get_from_id(&self, id: EntryId) -> Option<&Self::Entry>; - fn iter(&self) -> Box + '_>; + fn iter(&self) -> impl Iterator; + + fn search_iter(&self) -> impl Iterator; + + fn either_iter( + &self, + ) -> itertools::Either< + impl Iterator, + impl Iterator, + >; fn len(&self) -> usize; diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 075c246..e9c02ae 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -623,6 +623,7 @@ impl DbTrait for DbSqlite { } }) .collect::>(); + dbg!(&self.filtered); } } @@ -654,23 +655,34 @@ impl DbTrait for DbSqlite { self.iter().nth(index) } - fn iter(&self) -> Box + '_> { - if self.is_search_active() { - Box::new(self.filtered.iter().map(|id| &self.entries[id])) - } else { - Box::new( - self.favorites - .fav() - .iter() + fn iter(&self) -> impl Iterator { + self.favorites + .fav() + .iter() + .map(|id| &self.entries[id]) + .chain( + self.times + .values() .map(|id| &self.entries[id]) - .chain( - self.times - .values() - .map(|id| &self.entries[id]) - .filter(|e| !e.is_favorite) - .rev(), - ), + .filter(|e| !e.is_favorite) + .rev(), ) + } + + fn search_iter(&self) -> impl Iterator { + self.filtered.iter().map(|id| &self.entries[id]) + } + + fn either_iter( + &self, + ) -> itertools::Either< + impl Iterator, + impl Iterator, + > { + if self.is_search_active() { + itertools::Either::Left(self.search_iter()) + } else { + itertools::Either::Right(self.iter()) } } diff --git a/src/view.rs b/src/view.rs index fc32e56..071d1da 100644 --- a/src/view.rs +++ b/src/view.rs @@ -146,7 +146,7 @@ impl AppState { let entries_view: Vec<_> = self .db - .iter() + .either_iter() .enumerate() .get(range) .filter_map(|(pos, data)| { From c98ba8f29c9b4ac1c8589c72d8e2218fc69c0e01 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:45:19 +0100 Subject: [PATCH 12/13] rework hash function --- src/db/sqlite_db.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index e9c02ae..31aef7e 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -1,7 +1,6 @@ use alive_lock_file::LockResultWithDrop; use derivative::Derivative; use futures::StreamExt; -use itertools::Itertools; use sqlx::{migrate::MigrateDatabase, prelude::*, Sqlite, SqliteConnection}; use std::{ cell::RefCell, @@ -23,7 +22,7 @@ use crate::{ utils::{self}, }; -use super::{now, DbMessage, DbTrait, EntryId, EntryTrait, MimeDataMap}; +use super::{now, DbMessage, DbTrait, EntryId, EntryTrait, MimeDataMap, PRIV_MIME_TYPES_SIMPLE}; type Time = i64; @@ -49,11 +48,11 @@ pub struct DbSqlite { lock: Option, } -#[derive(Clone, Eq, Derivative)] +#[derive(Clone, Derivative)] pub struct Entry { pub id: EntryId, pub creation: Time, - // todo: lazelly load image in memory, since we can't search them anyways + // todo: lazelly load image in memory, since we can't search them anyways? /// (Mime, Content) pub raw_content: MimeDataMap, pub is_favorite: bool, @@ -107,11 +106,21 @@ impl Favorites { } fn hash_entry_content(data: &MimeDataMap, state: &mut H) { - data.iter() - .sorted_by(|e1, e2| e1.0.cmp(e2.0)) - .for_each(|e| { - e.hash(state); - }); + for m in PRIV_MIME_TYPES_SIMPLE { + if let Some(content) = data.get(*m) { + if !content.is_empty() { + content.hash(state); + return; + } + } + } + + let mut sorted = data.iter().collect::>(); + sorted.sort_by(|(mime1, _), (mime2, _)| mime1.cmp(mime2)); + + for (_, content) in sorted { + content.hash(state); + } } fn get_hash_entry_content(data: &MimeDataMap) -> u64 { @@ -126,12 +135,6 @@ impl Hash for Entry { } } -impl PartialEq for Entry { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - impl EntryTrait for Entry { fn is_favorite(&self) -> bool { self.is_favorite @@ -424,6 +427,7 @@ impl DbTrait for DbSqlite { if let Some(id) = self.hashs.get(&hash) { let entry = self.entries.get_mut(id).unwrap(); + let old_creation = entry.creation; entry.creation = now; let res = self.times.remove(&old_creation); From fe818d9798845b766dfa6c3b04ecaf0790fbdfea Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:22:21 +0100 Subject: [PATCH 13/13] some fixes --- Cargo.lock | 80 ++++++++++++++++++++++----------------------- src/clipboard.rs | 44 +++++++++++++++---------- src/db/mod.rs | 7 ++-- src/db/sqlite_db.rs | 5 --- 4 files changed, 70 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6493d91..7ef6557 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,9 +823,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "jobserver", "libc", @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "quote", "syn 1.0.109", @@ -1161,7 +1161,7 @@ dependencies = [ "serde", "serial_test", "sqlx", - "thiserror 2.0.6", + "thiserror 2.0.7", "tokio", "tracing", "tracing-journald", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "almost", "cosmic-config", @@ -1286,18 +1286,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1314,18 +1314,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -2397,11 +2397,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2500,7 +2500,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "dnd", "iced_accessibility", @@ -2518,7 +2518,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "accesskit", "accesskit_winit", @@ -2527,7 +2527,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "bitflags 2.6.0", "bytes", @@ -2551,7 +2551,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "futures", "iced_core", @@ -2577,7 +2577,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -2599,7 +2599,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2611,7 +2611,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "bytemuck", "cosmic-text", @@ -2642,7 +2642,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "as-raw-xcb-connection", "bitflags 2.6.0", @@ -2673,7 +2673,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -2692,7 +2692,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3172,7 +3172,7 @@ checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#2c29a7b158d07f4479e20268ddae4e4860903a2d" +source = "git+https://github.com/pop-os/libcosmic#aeb87f88865a92fee6b96e061f618d20abe59aaa" dependencies = [ "apply", "ashpd 0.9.2", @@ -4838,9 +4838,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b202022bb57c049555430e11fc22fea12909276a80a4c3d368da36ac1d88ed" +checksum = "94b13f8ea6177672c49d12ed964cca44836f59621981b04a3e26b87e675181de" dependencies = [ "sdd", ] @@ -4897,9 +4897,9 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" +checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" [[package]] name = "self_cell" @@ -5642,11 +5642,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.7", ] [[package]] @@ -5662,9 +5662,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", @@ -6948,14 +6948,14 @@ dependencies = [ [[package]] name = "wl-clipboard-rs" version = "0.8.1" -source = "git+https://github.com/wiiznokes/wl-clipboard-rs.git?branch=watch#2c10217b78dee4fe6435f29e263450da5398eb8e" +source = "git+https://github.com/wiiznokes/wl-clipboard-rs.git?branch=watch#fa84b41ab555189bad1b25fa084b0ffb6a33cd97" dependencies = [ "libc", "log", "os_pipe", "rustix 0.38.42", "tempfile", - "thiserror 2.0.6", + "thiserror 2.0.7", "tokio", "tree_magic_mini", "wayland-backend", diff --git a/src/clipboard.rs b/src/clipboard.rs index 83aa439..77180f5 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,6 +1,6 @@ use std::{ - collections::HashSet, sync::atomic::{self}, + time::Duration, }; use cosmic::iced::{futures::SinkExt, stream::channel}; @@ -35,17 +35,7 @@ pub fn sub() -> impl Stream { mpsc::channel::>>(5); tokio::task::spawn_blocking(move || loop { - // return a vec of maximum 2 mimetypes - // 1.the main one - // optional 2. metadata - let mime_type_filter = |mime_types: HashSet| { - info!("mime type {:#?}", mime_types); - mime_types.into_iter().collect() - }; - - match clipboard_watcher - .start_watching(paste_watch::Seat::Unspecified, mime_type_filter) - { + match clipboard_watcher.start_watching(paste_watch::Seat::Unspecified) { Ok(res) => { if !PRIVATE_MODE.load(atomic::Ordering::Relaxed) { tx.blocking_send(Some(res)).expect("can't send"); @@ -68,19 +58,37 @@ pub fn sub() -> impl Stream { loop { match rx.recv().await { Some(Some(res)) => { - info!("start reading pipes"); - let data = join_all(res.map(|(mut pipe, mime_type)| async move { let mut contents = Vec::new(); - pipe.read_to_end(&mut contents).await.unwrap(); - (mime_type, contents) + + match tokio::time::timeout( + Duration::from_millis(100), + pipe.read_to_end(&mut contents), + ) + .await + { + Ok(Ok(_)) => Some((mime_type, contents)), + Ok(Err(e)) => { + warn!( + "read timeout on external pipe clipboard: {} {e}", + mime_type + ); + None + } + Err(e) => { + warn!( + "read timeout on external pipe clipboard: {} {e}", + mime_type + ); + None + } + } })) .await .into_iter() + .flatten() .collect(); - info!("start sending pipes"); - output.send(ClipboardMessage::Data(data)).await.unwrap(); } diff --git a/src/db/mod.rs b/src/db/mod.rs index d9e2438..4312b76 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -65,14 +65,15 @@ impl Debug for Content<'_> { /// More we have mime types here, Less we spend time in the [`EntryTrait::preferred_content`] function. const PRIV_MIME_TYPES_SIMPLE: &[&str] = &[ + "image/png", + "image/jpg", + "image/jpeg", + "image/bmp", "text/plain;charset=utf-8", "text/plain", "STRING", "UTF8_STRING", "TEXT", - "image/png", - "image/jpg", - "image/jpeg", ]; const PRIV_MIME_TYPES_REGEX_STR: &[&str] = &["text/plain*", "text/*", "image/*"]; diff --git a/src/db/sqlite_db.rs b/src/db/sqlite_db.rs index 31aef7e..51f5863 100644 --- a/src/db/sqlite_db.rs +++ b/src/db/sqlite_db.rs @@ -291,10 +291,6 @@ impl DbTrait for DbSqlite { db.reload().await?; - dbg!(&db.hashs); - dbg!(&db.times); - dbg!(&db.entries); - Ok(db) } @@ -627,7 +623,6 @@ impl DbTrait for DbSqlite { } }) .collect::>(); - dbg!(&self.filtered); } }