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();