From d8a171da2fa472e6bb39a97412e23ca0c2a303b2 Mon Sep 17 00:00:00 2001 From: StudentWeis Date: Sat, 31 Jan 2026 20:51:47 +0800 Subject: [PATCH] refactor: use strict clippy lints to improve the quality of code --- .github/instructions/Base.instructions.md | 2 +- Cargo.toml | 16 ++ cliff.toml | 2 - rust-toolchain.toml | 2 +- src/clipboard/listener.rs | 37 ++- src/clipboard/utils.rs | 2 +- src/clipboard/writer.rs | 22 +- src/config/autostart.rs | 29 +- src/config/settings.rs | 24 +- src/gui/app.rs | 104 +++++--- src/gui/board/about.rs | 5 +- src/gui/board/actions.rs | 1 + src/gui/board/mod.rs | 70 +++-- src/gui/board/preview.rs | 8 +- src/gui/board/render.rs | 307 ++++++++++++---------- src/gui/board/settings.rs | 113 ++++---- src/gui/hotkey.rs | 14 +- src/gui/tray.rs | 18 +- src/gui/utils.rs | 10 +- src/i18n/mod.rs | 20 +- src/repository/errors.rs | 18 +- src/repository/models.rs | 6 +- src/repository/repo.rs | 10 +- 23 files changed, 479 insertions(+), 361 deletions(-) diff --git a/.github/instructions/Base.instructions.md b/.github/instructions/Base.instructions.md index e3eaea7..5dc09c5 100644 --- a/.github/instructions/Base.instructions.md +++ b/.github/instructions/Base.instructions.md @@ -3,6 +3,6 @@ applyTo: "**" --- - 仔细阅读整个项目以及 doc 文件夹中的文档。 -- 遵循良好的 Rust 开发规范。 +- 遵循良好的 Rust 开发规范及最佳时间,满足 clippy 严格要求。 - 编写可以运行的单元测试。 - 使用 MCP 工具 context7 查询库的文档。 diff --git a/Cargo.toml b/Cargo.toml index 83bb190..3bf48b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,22 @@ tempfile = "3.24.0" winres = "0.1" image = "0.25" +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +unwrap_used = "warn" +expect_used = "warn" +print_stdout = "warn" +print_stderr = "warn" +# Common allows for pedantic +module_name_repetitions = "allow" +cast_possible_truncation = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" + # The profile that 'dist' will build with [profile.dist] inherits = "release" diff --git a/cliff.toml b/cliff.toml index e4cdaaf..a8d6f0b 100644 --- a/cliff.toml +++ b/cliff.toml @@ -1,8 +1,6 @@ # git-cliff configuration for generating CHANGELOG.md with GitHub usernames and contributors # see: https://git-cliff.org/docs/integration/github - [remote.github] -# Project GitHub repo used for templating contributors/PR links owner = "StudentWeis" repo = "ropy" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 292fe49..79d2099 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "stable" +channel = "1.93" diff --git a/src/clipboard/listener.rs b/src/clipboard/listener.rs index f5f9938..08d6c16 100644 --- a/src/clipboard/listener.rs +++ b/src/clipboard/listener.rs @@ -34,14 +34,20 @@ impl ClipboardMonitor { tx: Sender, image_tx: Sender, last_copy: Arc>, - ) -> Self { - let ctx = ClipboardContext::new().unwrap(); - Self { + ) -> Option { + let ctx = match ClipboardContext::new() { + Ok(ctx) => ctx, + Err(e) => { + eprintln!("[ropy] Failed to initialize clipboard context: {e}"); + return None; + } + }; + Some(Self { tx, image_tx, - last_copy, ctx, - } + last_copy, + }) } } @@ -76,17 +82,19 @@ impl ClipboardHandler for ClipboardMonitor { /// Spawn a clipboard listener thread that watches for clipboard changes. pub fn start_clipboard_monitor( tx: Sender, - async_app: AsyncApp, + async_app: &AsyncApp, last_copy: Arc>, ) { let (image_tx, image_rx) = async_channel::unbounded::(); - let monitor = ClipboardMonitor::new(tx.clone(), image_tx, last_copy); + let Some(monitor) = ClipboardMonitor::new(tx.clone(), image_tx, last_copy) else { + return; + }; let executor = async_app.background_executor(); executor .spawn(async move { while let Ok(image) = image_rx.recv().await { - if let Some(path) = super::save_image(image) { + if let Some(path) = super::save_image(&image) { let _ = tx.send_blocking(ClipboardEvent::Image(path)); } } @@ -95,7 +103,13 @@ pub fn start_clipboard_monitor( executor .spawn(async move { - let mut watcher = ClipboardWatcherContext::new().unwrap(); + let mut watcher = match ClipboardWatcherContext::new() { + Ok(w) => w, + Err(e) => { + eprintln!("[ropy] Failed to create clipboard watcher: {e}"); + return; + } + }; watcher.add_handler(monitor); watcher.start_watch(); }) @@ -132,7 +146,10 @@ pub fn start_clipboard_listener( }; guard.insert(0, record); let max_history_records = { - let settings_guard = settings.read().unwrap(); + let settings_guard = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + }; settings_guard.storage.max_history_records }; if guard.len() > max_history_records { diff --git a/src/clipboard/utils.rs b/src/clipboard/utils.rs index 28ad27e..7292e9e 100644 --- a/src/clipboard/utils.rs +++ b/src/clipboard/utils.rs @@ -1,7 +1,7 @@ use chrono::Local; use image::DynamicImage; -pub fn save_image(image: DynamicImage) -> Option { +pub fn save_image(image: &DynamicImage) -> Option { let data_dir = dirs::data_local_dir()?.join("ropy").join("images"); if !data_dir.exists() { std::fs::create_dir_all(&data_dir).ok()?; diff --git a/src/clipboard/writer.rs b/src/clipboard/writer.rs index 6240626..b726c7d 100644 --- a/src/clipboard/writer.rs +++ b/src/clipboard/writer.rs @@ -5,21 +5,27 @@ use image::ImageReader; use super::CopyRequest; /// Start a background task to handle clipboard write requests. -/// This avoids creating a new ClipboardContext and spawning a new task for each write. -pub fn start_clipboard_writer(async_app: AsyncApp) -> async_channel::Sender { +/// This avoids creating a new `ClipboardContext` and spawning a new task for each write. +pub fn start_clipboard_writer(async_app: &AsyncApp) -> async_channel::Sender { let (tx, rx) = async_channel::unbounded(); let executor = async_app.background_executor(); executor .spawn(async move { - let ctx = ClipboardContext::new().unwrap(); + let ctx = match ClipboardContext::new() { + Ok(ctx) => ctx, + Err(e) => { + eprintln!("[ropy] Failed to create clipboard output context: {e}"); + return; + } + }; while let Ok(req) = rx.recv().await { match req { CopyRequest::Text(text) => { set_text(&ctx, text); } CopyRequest::Image(path) => { - set_image(&ctx, path); + set_image(&ctx, &path); } } } @@ -35,10 +41,10 @@ fn set_text(ctx: &ClipboardContext, text: String) { /// Set image to clipboard. The image is read from the given file path. /// After setting the image, the original file and its thumbnail are deleted. -fn set_image(ctx: &ClipboardContext, path: String) { - let img_res = ImageReader::open(&path) +fn set_image(ctx: &ClipboardContext, path: &str) { + let img_res = ImageReader::open(path) .map_err(image::ImageError::from) - .and_then(|r| r.decode()); + .and_then(image::ImageReader::decode); if let Ok(img) = img_res { #[cfg(target_os = "macos")] { @@ -59,7 +65,7 @@ fn set_image(ctx: &ClipboardContext, path: String) { .is_ok() && let Err(e) = ctx.set_buffer("public.png", bytes) { - eprintln!("Failed to set image to clipboard: {}", e); + eprintln!("Failed to set image to clipboard: {e}"); } } diff --git a/src/config/autostart.rs b/src/config/autostart.rs index 7deaa1b..bdfb026 100644 --- a/src/config/autostart.rs +++ b/src/config/autostart.rs @@ -24,13 +24,13 @@ pub struct AutoStartManager { } impl AutoStartManager { - /// Create a new AutoStartManager instance + /// Create a new `AutoStartManager` instance /// /// # Arguments /// * `app_name` - The name of the application /// /// # Returns - /// Result containing AutoStartManager or AutoStartError + /// Result containing `AutoStartManager` or `AutoStartError` pub fn new(app_name: &str) -> Result { let app_path = Self::get_app_path()?; @@ -49,7 +49,7 @@ impl AutoStartManager { /// Get the application executable path /// /// For development builds, returns the debug executable path - /// For bundled macOS apps, returns the .app bundle path + /// For bundled `macOS` apps, returns the `.app` bundle path fn get_app_path() -> Result { // Get the current executable path let exe_path = @@ -67,9 +67,12 @@ impl AutoStartManager { } // For non-bundled or Windows builds, return the executable path - exe_path.to_str().map(|s| s.to_string()).ok_or_else(|| { - AutoStartError::ExecutablePath("Path contains invalid UTF-8".to_string()) - }) + exe_path + .to_str() + .map(std::string::ToString::to_string) + .ok_or_else(|| { + AutoStartError::ExecutablePath("Path contains invalid UTF-8".to_string()) + }) } /// Enable auto-start at system startup @@ -99,14 +102,12 @@ impl AutoStartManager { /// * `enabled` - Whether auto-start should be enabled pub fn sync_state(&self, enabled: bool) -> Result<(), AutoStartError> { let current_enabled = self.is_enabled().unwrap_or(false); - if current_enabled != enabled { - if enabled { - self.enable() - } else { - self.disable() - } - } else { + if current_enabled == enabled { Ok(()) + } else if enabled { + self.enable() + } else { + self.disable() } } } @@ -138,7 +139,7 @@ mod tests { // Verify state if possible if let Ok(enabled) = manager.is_enabled() { - assert!(!enabled) + assert!(!enabled); } } } diff --git a/src/config/settings.rs b/src/config/settings.rs index 0d176d4..b243cfb 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -30,10 +30,9 @@ pub enum AppTheme { impl AppTheme { pub fn get_theme(&self) -> Self { match self { - AppTheme::System => match dark_light::detect().unwrap_or(dark_light::Mode::Light) { - dark_light::Mode::Dark => AppTheme::Dark, - dark_light::Mode::Light => AppTheme::Light, - _ => AppTheme::Light, + Self::System => match dark_light::detect().unwrap_or(dark_light::Mode::Light) { + dark_light::Mode::Dark => Self::Dark, + _ => Self::Light, }, _ => self.clone(), } @@ -42,7 +41,7 @@ impl AppTheme { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HotkeySettings { - /// Global hotkey to activate clipboard manager (e.g., "cmd+shift+v") + /// Global hotkey to activate clipboard manager (e.g., "`cmd+shift+v`") pub activation_key: String, } @@ -102,11 +101,16 @@ impl Settings { std::fs::create_dir_all(&config_dir).map_err(|e| ConfigError::Foreign(Box::new(e)))?; } - let builder = Config::builder() + let mut builder = Config::builder() // Start with default values - .add_source(Config::try_from(&Settings::default())?) - // Add configuration from file (optional) - .add_source(File::with_name(config_file.to_str().unwrap()).required(false)); + .add_source(Config::try_from(&Self::default())?); + + // Add configuration from file (optional) + if let Some(path_str) = config_file.to_str() { + builder = builder.add_source(File::with_name(path_str).required(false)); + } else { + eprintln!("[ropy] Warning: Config file path contains invalid UTF-8 characters"); + } let config = builder.build()?; let mut settings: Self = config.try_deserialize()?; @@ -115,7 +119,7 @@ impl Settings { if settings.hotkey.activation_key.is_empty() || global_hotkey::hotkey::HotKey::from_str(&settings.hotkey.activation_key).is_err() { - settings.hotkey.activation_key = Settings::default().hotkey.activation_key; + settings.hotkey.activation_key = Self::default().hotkey.activation_key; } Ok(settings) diff --git a/src/gui/app.rs b/src/gui/app.rs index c84ba83..e877875 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -52,19 +52,29 @@ fn initialize_repository() -> Option> { } fn load_initial_records( - repository: &Option>, + repository: Option<&Arc>, settings: &Arc>, ) -> Vec { - let max_records = settings.read().unwrap().storage.max_history_records; + let max_records = { + let settings_guard = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + }; + settings_guard.storage.max_history_records + }; repository - .as_ref() .and_then(|repo| repo.get_recent(max_records).ok()) .unwrap_or_default() } /// Synchronize auto-start state with system on application launch fn sync_autostart_on_launch(settings: &Arc>) { - let autostart_enabled = settings.read().unwrap().autostart.enabled; + let autostart_enabled = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .autostart + .enabled; match AutoStartManager::new("Ropy") { Ok(manager) => { @@ -88,7 +98,7 @@ fn sync_autostart_on_launch(settings: &Arc>) { } fn start_clipboard_monitor( - async_app: AsyncApp, + async_app: &AsyncApp, last_copy: Arc>, ) -> async_channel::Receiver { let (clipboard_tx, clipboard_rx) = async_channel::unbounded::(); @@ -99,16 +109,22 @@ fn start_clipboard_monitor( fn setup_hotkey_listener( window_handle: WindowHandle, async_app: AsyncApp, - settings: Arc>, + settings: &Arc>, ) -> async_channel::Sender { let fg_executor = async_app.foreground_executor().clone(); let bg_executor = async_app.background_executor().clone(); - let hotkey_str = settings.read().unwrap().hotkey.activation_key.clone(); - crate::gui::hotkey::start_hotkey_listener(hotkey_str, fg_executor, bg_executor, move || { + let hotkey_str = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .hotkey + .activation_key + .clone(); + crate::gui::hotkey::start_hotkey_listener(hotkey_str, &fg_executor, bg_executor, move || { let _ = async_app.update(move |cx| { window_handle .update(cx, |_, window, cx| { - window.dispatch_action(Box::new(crate::gui::board::Active), cx) + window.dispatch_action(Box::new(crate::gui::board::Active), cx); }) .ok(); }); @@ -135,7 +151,12 @@ fn create_window( }, |window, cx| { // Apply the application theme based on settings - let app_theme = &settings.read().unwrap().theme.get_theme(); + let app_theme = &match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .theme + .get_theme(); set_app_theme(window, cx, app_theme); let view = cx.new(|cx| { @@ -152,7 +173,10 @@ fn create_window( cx.new(|cx| Root::new(view, window, cx)) }, ) - .unwrap() + .unwrap_or_else(|e| { + eprintln!("[ropy] Fatal: Failed to create window: {e}"); + std::process::exit(1); + }) } /// Set the application theme (light or dark) @@ -161,28 +185,28 @@ pub fn set_app_theme(window: &mut gpui::Window, cx: &mut App, app_theme: &AppThe AppTheme::Dark => { Theme::change(ThemeMode::Dark, Some(window), cx); let theme = Theme::global_mut(cx); - theme.background = rgb(0x2d2d2d).into(); - theme.foreground = rgb(0xffffff).into(); - theme.secondary = rgb(0x3d3d3d).into(); - theme.secondary_foreground = rgb(0xffffff).into(); - theme.border = rgb(0x4d4d4d).into(); - theme.accent = rgb(0x4d4d4d).into(); - theme.muted_foreground = rgb(0x888888).into(); - theme.input = rgb(0x555555).into(); + theme.background = rgb(0x002d_2d2d).into(); + theme.foreground = rgb(0x00ff_ffff).into(); + theme.secondary = rgb(0x003d_3d3d).into(); + theme.secondary_foreground = rgb(0x00ff_ffff).into(); + theme.border = rgb(0x004d_4d4d).into(); + theme.accent = rgb(0x004d_4d4d).into(); + theme.muted_foreground = rgb(0x0088_8888).into(); + theme.input = rgb(0x0055_5555).into(); } AppTheme::Light => { Theme::change(ThemeMode::Light, Some(window), cx); let theme = Theme::global_mut(cx); - theme.background = rgb(0xffffff).into(); - theme.foreground = rgb(0x1a1a1a).into(); - theme.secondary = rgb(0xf5f5f5).into(); - theme.secondary_foreground = rgb(0x1a1a1a).into(); - theme.border = rgb(0xe0e0e0).into(); - theme.accent = rgb(0xadd8e6).into(); - theme.muted_foreground = rgb(0x6b6b6b).into(); - theme.input = rgb(0xf0f0f0).into(); + theme.background = rgb(0x00ff_ffff).into(); + theme.foreground = rgb(0x001a_1a1a).into(); + theme.secondary = rgb(0x00f5_f5f5).into(); + theme.secondary_foreground = rgb(0x001a_1a1a).into(); + theme.border = rgb(0x00e0_e0e0).into(); + theme.accent = rgb(0x00ad_d8e6).into(); + theme.muted_foreground = rgb(0x006b_6b6b).into(); + theme.input = rgb(0x00f0_f0f0).into(); } - _ => {} + AppTheme::System => todo!(), } } @@ -207,38 +231,38 @@ pub fn launch_app() { sync_autostart_on_launch(&settings); let repository = initialize_repository(); - let initial_records = load_initial_records(&repository, &settings); + let initial_records = load_initial_records(repository.as_ref(), &settings); let shared_records = Arc::new(Mutex::new(initial_records)); - let last_copy = Arc::new(Mutex::new(LastCopyState::Text("".to_string()))); + let last_copy = Arc::new(Mutex::new(LastCopyState::Text(String::new()))); let async_app = cx.to_async(); - let clipboard_rx = start_clipboard_monitor(async_app.clone(), last_copy.clone()); - let copy_tx = clipboard::start_clipboard_writer(async_app.clone()); + let clipboard_rx = start_clipboard_monitor(&async_app, last_copy.clone()); + let copy_tx = clipboard::start_clipboard_writer(&async_app); let window_handle = create_window( cx, shared_records.clone(), repository.clone(), settings.clone(), - last_copy.clone(), + last_copy, copy_tx, is_silent, ); clipboard::start_clipboard_listener( clipboard_rx, shared_records, - repository.clone(), + repository, settings.clone(), async_app.clone(), window_handle, ); - let hotkey_tx = setup_hotkey_listener(window_handle, async_app.clone(), settings.clone()); + let hotkey_tx = setup_hotkey_listener(window_handle, async_app.clone(), &settings); let _ = window_handle.update(cx, |root, _, cx| { - root.view() - .clone() - .downcast::() - .unwrap() - .update(cx, |board, _| { + if let Ok(board) = root.view().clone().downcast::() { + board.update(cx, |board, _| { board.set_hotkey_tx(hotkey_tx); }); + } else { + eprintln!("[ropy] Failed to downcast root view to RopyBoard"); + } }); super::tray::start_tray_handler(settings, async_app, window_handle); diff --git a/src/gui/board/about.rs b/src/gui/board/about.rs index c8167eb..dd6b66b 100644 --- a/src/gui/board/about.rs +++ b/src/gui/board/about.rs @@ -12,10 +12,7 @@ use gpui_component::{ use super::RopyBoard; /// Render the about panel content -pub(super) fn render_about_content( - board: &mut RopyBoard, - cx: &mut Context, -) -> impl IntoElement { +pub(super) fn render_about_content(board: &RopyBoard, cx: &Context) -> impl IntoElement { let version = env!("CARGO_PKG_VERSION"); let header = h_flex() diff --git a/src/gui/board/actions.rs b/src/gui/board/actions.rs index 305f850..58f1862 100644 --- a/src/gui/board/actions.rs +++ b/src/gui/board/actions.rs @@ -61,6 +61,7 @@ impl RopyBoard { self.pinned = false; } + #[allow(clippy::unused_self)] pub fn on_quit_action(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context) { cx.quit(); } diff --git a/src/gui/board/mod.rs b/src/gui/board/mod.rs index b8ac9bc..655eb36 100644 --- a/src/gui/board/mod.rs +++ b/src/gui/board/mod.rs @@ -6,7 +6,7 @@ mod settings; use std::{ str::FromStr, - sync::{Arc, Mutex, RwLock}, + sync::{Arc, Mutex, PoisonError, RwLock}, }; // Re-export utilities for external use @@ -29,7 +29,8 @@ use crate::{ repository::{ClipboardRecord, ClipboardRepository, models::ContentType}, }; -/// RopyBoard Main Window Component +/// `RopyBoard` Main Window Component +#[allow(clippy::struct_excessive_bools)] pub struct RopyBoard { records: Arc>>, filtered_records: Vec, // The final shown records @@ -77,7 +78,7 @@ impl RopyBoard { window.focus(&focus_handle); // Subscribe to focus out events to hide the window - let _focus_out_subscription = + let focus_out_subscription = cx.on_focus_out(&focus_handle, window, move |this, _event, window, cx| { // When the window loses focus, hide the window if !this.pinned { @@ -90,7 +91,10 @@ impl RopyBoard { let list_state = ListState::new(0, ListAlignment::Top, gpui::px(100.)); let (max_history_records, activation_key, theme_index, language) = { - let settings_guard = settings.read().unwrap(); + let settings_guard = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + }; let theme_idx = match settings_guard.theme { crate::config::AppTheme::Light => 0, crate::config::AppTheme::Dark => 1, @@ -103,9 +107,14 @@ impl RopyBoard { settings_guard.language, ) }; - let autostart_enabled = settings.read().unwrap().autostart.enabled; + let autostart_enabled = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .autostart + .enabled; let settings_activation_key_input = - cx.new(|cx| InputState::new(window, cx).placeholder(activation_key.to_string())); + cx.new(|cx| InputState::new(window, cx).placeholder(activation_key.clone())); let settings_max_history_input = cx.new(|cx| InputState::new(window, cx).placeholder(max_history_records.to_string())); @@ -121,7 +130,7 @@ impl RopyBoard { repository, settings, focus_handle, - _focus_out_subscription, + _focus_out_subscription: focus_out_subscription, search_input, selected_index: 0, last_copy, @@ -144,11 +153,11 @@ impl RopyBoard { } /// Copy content to clipboard - fn copy_to_clipboard(&mut self, content: &str, content_type: &ContentType) { + fn copy_to_clipboard(&self, content: &str, content_type: &ContentType) { let request = match content_type { ContentType::Text => Some(crate::clipboard::CopyRequest::Text(content.to_string())), ContentType::Image => Some(crate::clipboard::CopyRequest::Image(content.to_string())), - _ => None, + ContentType::FilePath => todo!(), }; if let Some(req) = request { @@ -157,19 +166,19 @@ impl RopyBoard { } /// Clear clipboard history - fn clear_history(&mut self) { + fn clear_history(&self) { if let Some(ref repo) = self.repository { if let Err(e) = repo.clear() { eprintln!("[ropy] Failed to clear clipboard history: {e}"); } else { - let mut guard = self.records.lock().unwrap(); + let mut guard = self.records.lock().unwrap_or_else(PoisonError::into_inner); guard.clear(); } } } /// Clear last copy state - fn clear_last_copy_state(&mut self) { + fn clear_last_copy_state(&self) { match self.last_copy.lock() { Ok(mut guard) => { *guard = LastCopyState::Text(String::new()); @@ -186,8 +195,10 @@ impl RopyBoard { if let Err(e) = repo.delete(id) { eprintln!("[ropy] Failed to delete clipboard record: {e}"); } else { - let mut guard = self.records.lock().unwrap(); - guard.retain(|record| record.id != id); + self.records + .lock() + .unwrap_or_else(PoisonError::into_inner) + .retain(|record| record.id != id); // Mark that we're in a delete operation to preserve scroll position self.deleting_record = true; } @@ -197,8 +208,10 @@ impl RopyBoard { /// Get filtered records based on search query fn get_filtered_records(&self, query: &str) -> Vec { if query.is_empty() { - let guard = self.records.lock().unwrap(); - guard.clone() + self.records + .lock() + .unwrap_or_else(PoisonError::into_inner) + .clone() } else if let Some(ref repo) = self.repository { repo.search(query).unwrap_or_default() } else { @@ -207,7 +220,7 @@ impl RopyBoard { } /// Confirm, hide and delete. - fn confirm_record(&mut self, window: &mut Window, cx: &mut Context, index: usize) { + fn confirm_record(&mut self, window: &mut Window, cx: &Context, index: usize) { let (id, content, content_type) = { if let Some(record) = self.filtered_records.get(index) { ( @@ -237,7 +250,14 @@ impl RopyBoard { let mut is_hotkey_invalid = false; if activation_key.is_empty() { - activation_key = self.settings.read().unwrap().hotkey.activation_key.clone(); + activation_key.clone_from( + &match self.settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .hotkey + .activation_key, + ); // If current setting is also empty (should not happen with load fix), use default if activation_key.is_empty() { activation_key = Settings::default().hotkey.activation_key; @@ -248,7 +268,12 @@ impl RopyBoard { } // Get current max_history_records from settings as fallback - let current_max_history = self.settings.read().unwrap().storage.max_history_records; + let current_max_history = match self.settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .storage + .max_history_records; let max_history = self .settings_max_history_input @@ -270,8 +295,11 @@ impl RopyBoard { .unwrap_or_default(); { - let mut settings = self.settings.write().unwrap(); - settings.hotkey.activation_key = activation_key.clone(); + let mut settings = match self.settings.write() { + Ok(g) => g, + Err(e) => e.into_inner(), + }; + settings.hotkey.activation_key.clone_from(&activation_key); settings.storage.max_history_records = max_history; settings.theme = theme.clone(); settings.autostart.enabled = self.autostart_enabled; diff --git a/src/gui/board/preview.rs b/src/gui/board/preview.rs index f3027ea..16ef2cb 100644 --- a/src/gui/board/preview.rs +++ b/src/gui/board/preview.rs @@ -9,7 +9,7 @@ use image::ImageReader; /// Create a tooltip preview that supports automatic line wrapping /// -/// This implementation returns a View that will be correctly rendered by GPUI's tooltip system +/// This implementation returns a `View` that will be correctly rendered by `GPUI`'s tooltip system /// /// # Usage Example /// ```rust @@ -18,7 +18,7 @@ use image::ImageReader; /// simple_tooltip("This is tooltip content", window, cx) /// }) /// ``` -pub fn simple_tooltip(content: impl Into, window: &mut Window, cx: &mut App) -> AnyView { +pub fn simple_tooltip(content: impl Into, window: &Window, cx: &mut App) -> AnyView { let content = content.into(); let window_width = window.bounds().size.width; let max_width = (window_width - px(40.0)).into(); @@ -67,7 +67,7 @@ impl Render for TooltipView { /// image_tooltip("/path/to/image.png", window, cx) /// }) /// ``` -pub fn image_tooltip(image_path: impl Into, window: &mut Window, cx: &mut App) -> AnyView { +pub fn image_tooltip(image_path: impl Into, window: &Window, cx: &mut App) -> AnyView { let image_path = image_path.into(); let window_width = window.bounds().size.width; let window_height = window.bounds().size.height; @@ -89,7 +89,7 @@ fn calculate_image_size( max_w: gpui::Pixels, max_h: gpui::Pixels, ) -> (gpui::Pixels, gpui::Pixels) { - if let Ok(reader) = ImageReader::open(path).and_then(|r| r.with_guessed_format()) + if let Ok(reader) = ImageReader::open(path).and_then(image::ImageReader::with_guessed_format) && let Ok(dims) = reader.into_dimensions() { let w = dims.0 as f32; diff --git a/src/gui/board/render.rs b/src/gui/board/render.rs index 8a96a56..6aa5f94 100644 --- a/src/gui/board/render.rs +++ b/src/gui/board/render.rs @@ -19,8 +19,14 @@ use crate::repository::{ClipboardRecord, models::ContentType}; fn get_hex_color(content: &str) -> Option { static HEX_REGEX: OnceLock = OnceLock::new(); - let regex = - HEX_REGEX.get_or_init(|| Regex::new(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").unwrap()); + let regex = HEX_REGEX.get_or_init(|| { + Regex::new(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").unwrap_or_else(|e| { + eprintln!("[ropy] Fatel: Invalid hex color regex: {e}"); + // Fallback to a regex that matches nothing to avoid crash + #[allow(clippy::unwrap_used)] + Regex::new(r"a^").unwrap() + }) + }); if regex.is_match(content) { let hex = content.trim_start_matches('#'); @@ -28,7 +34,7 @@ fn get_hex_color(content: &str) -> Option { let r = u8::from_str_radix(&hex[0..1], 16).ok()?; let g = u8::from_str_radix(&hex[1..2], 16).ok()?; let b = u8::from_str_radix(&hex[2..3], 16).ok()?; - ((r as u32 * 17) << 16) | ((g as u32 * 17) << 8) | (b as u32 * 17) + ((u32::from(r) * 17) << 16) | ((u32::from(g) * 17) << 8) | (u32::from(b) * 17) } else { u32::from_str_radix(hex, 16).ok()? }; @@ -41,7 +47,7 @@ fn get_hex_color(content: &str) -> Option { /// Create the "Clear" button element pub(super) fn create_clear_button( board: &RopyBoard, - cx: &mut Context<'_, RopyBoard>, + cx: &Context<'_, RopyBoard>, ) -> impl IntoElement { Button::new("clear-button") .ghost() @@ -67,7 +73,7 @@ pub(super) fn format_clipboard_content(record: &ClipboardRecord) -> String { } /// Render the header section with title and settings/clear buttons -pub fn render_header(board: &RopyBoard, cx: &mut Context<'_, RopyBoard>) -> impl IntoElement { +pub fn render_header(board: &RopyBoard, cx: &Context<'_, RopyBoard>) -> impl IntoElement { let is_pinned = board.pinned; let pin_tooltip = if is_pinned { board.i18n.t("unpin") @@ -142,7 +148,7 @@ pub fn render_header(board: &RopyBoard, cx: &mut Context<'_, RopyBoard>) -> impl /// Render the search input section pub(super) fn render_search_input( search_input: &Entity, - cx: &mut Context<'_, RopyBoard>, + cx: &Context<'_, RopyBoard>, ) -> impl IntoElement { v_flex().w_full().mb_4().child( Input::new(search_input) @@ -170,7 +176,7 @@ fn render_image_record(record: &ClipboardRecord) -> gpui::AnyElement { img(display_path).max_h(px(100.0)).into_any_element() } -fn render_text_record(cx: &mut gpui::App, record: &ClipboardRecord) -> gpui::AnyElement { +fn render_text_record(cx: &gpui::App, record: &ClipboardRecord) -> gpui::AnyElement { let display_content = format_clipboard_content(record); let hex_color = get_hex_color(&record.content); @@ -203,160 +209,171 @@ fn render_text_record(cx: &mut gpui::App, record: &ClipboardRecord) -> gpui::Any fn create_preview( content_type: &ContentType, record_content: &str, - window: &mut gpui::Window, + window: &gpui::Window, cx: &mut gpui::App, ) -> gpui::AnyView { - match content_type { - ContentType::Image => preview::image_tooltip(record_content, window, cx), - _ => { - let content = if record_content.len() > 800 { - record_content.chars().take(800).collect::() - } else { - record_content.to_string() - }; - preview::simple_tooltip(content, window, cx) - } + if content_type == &ContentType::Image { + preview::image_tooltip(record_content, window, cx) + } else { + let content = if record_content.len() > 800 { + record_content.chars().take(800).collect::() + } else { + record_content.to_string() + }; + preview::simple_tooltip(content, window, cx) } } -impl RopyBoard { - /// Render the scrollable list of clipboard records - pub fn render_records_list(&self, context: &mut Context<'_, RopyBoard>) -> impl IntoElement { - let records = self.filtered_records.clone(); - let list_state = self.list_state.clone(); - let selected_index = self.selected_index; - let show_preview = self.show_preview; - let view = context.weak_entity(); - list(list_state, move |index, window, cx| { - let record = &records[index]; - let record_id = record.id; - let is_selected = index == selected_index; - let content_type = record.content_type.clone(); - let view_click = view.clone(); - let view_delete = view.clone(); - let record_content = record.content.clone(); +#[allow(clippy::too_many_lines)] +fn render_list_item( + index: usize, + record: &ClipboardRecord, + is_selected: bool, + show_preview: bool, + view: &gpui::WeakEntity, + window: &gpui::Window, + cx: &mut gpui::App, +) -> gpui::AnyElement { + let record_id = record.id; + let content_type = record.content_type.clone(); + let view_click = view.clone(); + let view_delete = view.clone(); + let record_content = record.content.clone(); - let preview_data = (content_type.clone(), record_content.clone()); + let preview_data = (content_type.clone(), record_content); - let mut item = div().pb_2().relative().child( - v_flex() - .w_full() - .p_3() - .bg(if is_selected { - cx.theme().accent - } else { - cx.theme().secondary - }) - .rounded_md() - .border_1() - .border_color(if is_selected { - cx.theme().accent - } else { - cx.theme().border - }) - .hover(|style| style.bg(cx.theme().accent).border_color(cx.theme().accent)) - .id(("record", index)) - .child( - h_flex() - .justify_between() - .items_start() - .gap_2() - .child({ - let mut content_div = div() - .flex_1() - .min_w_0() - .cursor_pointer() - .id(("record-content", index)) - .on_click(move |_event, window, cx| { - view_click - .update(cx, |this, cx| { - this.confirm_record(window, cx, index); - }) - .ok(); - }); + let mut item = div().pb_2().relative().child( + v_flex() + .w_full() + .p_3() + .bg(if is_selected { + cx.theme().accent + } else { + cx.theme().secondary + }) + .rounded_md() + .border_1() + .border_color(if is_selected { + cx.theme().accent + } else { + cx.theme().border + }) + .hover(|style| style.bg(cx.theme().accent).border_color(cx.theme().accent)) + .id(("record", index)) + .child( + h_flex() + .justify_between() + .items_start() + .gap_2() + .child({ + let mut content_div = div() + .flex_1() + .min_w_0() + .cursor_pointer() + .id(("record-content", index)) + .on_click(move |_event, window, cx| { + view_click + .update(cx, |this, cx| { + this.confirm_record(window, cx, index); + }) + .ok(); + }); - if !show_preview { - content_div = content_div.tooltip({ - let (content_type, record_content) = preview_data.clone(); - move |window, cx| { - create_preview( - &content_type, - &record_content, - window, - cx, - ) - } - }); + if !show_preview { + content_div = content_div.tooltip({ + let (content_type, record_content) = preview_data.clone(); + move |window, cx| { + create_preview(&content_type, &record_content, window, cx) } + }); + } - content_div - .child(match content_type { - ContentType::Text => render_text_record(cx, record), - ContentType::Image => render_image_record(record), - _ => div().child("Unknown content").into_any_element(), - }) + content_div + .child(match content_type { + ContentType::Text => render_text_record(cx, record), + ContentType::Image => render_image_record(record), + ContentType::FilePath => div().child("File").into_any_element(), + }) + .child( + h_flex() + .items_center() + .gap_1() + .mt_1() .child( - h_flex() - .items_center() - .gap_1() - .mt_1() - .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .bg(cx.theme().background) - .px_1() - .py_0() - .rounded_sm() - .child(format!("{}", index + 1)), - ) + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .bg(cx.theme().background) + .px_1() + .py_0() + .rounded_sm() + .child(format!("{}", index + 1)), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .child( - record - .created_at - .format("%Y-%m-%d %H:%M:%S") - .to_string(), - ), + record + .created_at + .format("%Y-%m-%d %H:%M:%S") + .to_string(), ), - ) - }) - .child( - Button::new(("delete-btn", index)) - .xsmall() - .ghost() - .label("×") - .on_click(move |_event, _window, cx| { - view_delete - .update(cx, |this, cx| { - this.delete_record(record_id); - // TODO Delete associated last copy state - cx.notify(); - }) - .ok(); - }), - ), + ), + ) + }) + .child( + Button::new(("delete-btn", index)) + .xsmall() + .ghost() + .label("×") + .on_click(move |_event, _window, cx| { + view_delete + .update(cx, |this, cx| { + this.delete_record(record_id); + // TODO Delete associated last copy state + cx.notify(); + }) + .ok(); + }), ), - ); + ), + ); - if is_selected && show_preview { - let (content_type, record_content) = preview_data; - item = - item.child( - deferred( - div().absolute().top_full().left_0().child( - anchored().snap_to_window().child(div().mt_1().child( - create_preview(&content_type, &record_content, window, cx), - )), - ), - ) - .with_priority(1), - ); - } + if is_selected && show_preview { + let (content_type, record_content) = preview_data; + item = item.child( + deferred( + div().absolute().top_full().left_0().child( + anchored() + .snap_to_window() + .child(div().mt_1().child(create_preview( + &content_type, + &record_content, + window, + cx, + ))), + ), + ) + .with_priority(1), + ); + } + + item.into_any_element() +} - item.into_any_element() +impl RopyBoard { + /// Render the scrollable list of clipboard records + pub fn render_records_list(&self, context: &Context<'_, Self>) -> impl IntoElement { + let records = self.filtered_records.clone(); + let list_state = self.list_state.clone(); + let selected_index = self.selected_index; + let show_preview = self.show_preview; + let view = context.weak_entity(); + list(list_state, move |index, window, cx| { + let record = &records[index]; + let is_selected = index == selected_index; + render_list_item(index, record, is_selected, show_preview, &view, window, cx) }) .w_full() .flex_1() diff --git a/src/gui/board/settings.rs b/src/gui/board/settings.rs index d9f048f..4a53c00 100644 --- a/src/gui/board/settings.rs +++ b/src/gui/board/settings.rs @@ -17,12 +17,9 @@ use super::RopyBoard; use crate::i18n::{I18n, Language}; /// Render language selection buttons -/// Note: Uses index-based selection from Language::all() which maintains a stable order. -/// The order is: English, ChineseSimplified -fn render_language_selector( - board: &mut RopyBoard, - cx: &mut Context, -) -> impl IntoElement { +/// Note: Uses index-based selection from `Language::all()` which maintains a stable order. +/// The order is: English, `ChineseSimplified` +fn render_language_selector(board: &RopyBoard, cx: &Context) -> impl IntoElement { let languages = Language::all(); h_flex() @@ -56,7 +53,7 @@ fn render_language_selector( } /// Render theme selection buttons -fn render_theme_selector(board: &mut RopyBoard, cx: &mut Context) -> impl IntoElement { +fn render_theme_selector(board: &RopyBoard, cx: &Context) -> impl IntoElement { let theme_names = vec![ board.i18n.t("settings_theme_light"), board.i18n.t("settings_theme_dark"), @@ -81,11 +78,7 @@ fn render_theme_selector(board: &mut RopyBoard, cx: &mut Context) -> })) } -/// Render the settings panel content -pub(super) fn render_settings_content( - board: &mut RopyBoard, - cx: &mut Context, -) -> impl IntoElement { +fn render_storage_section(board: &RopyBoard, cx: &Context) -> impl IntoElement { let max_history_input_field = h_flex() .gap_2() .items_center() @@ -105,6 +98,20 @@ pub(super) fn render_settings_content( .px_3() .py_2(), ); + + v_flex() + .gap_2() + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .font_weight(gpui::FontWeight::BOLD) + .child(board.i18n.t("settings_storage")), + ) + .child(max_history_input_field) +} + +fn render_hotkey_section(board: &RopyBoard, cx: &Context) -> impl IntoElement { let activation_key_label = v_flex() .gap_1() .child( @@ -128,49 +135,21 @@ pub(super) fn render_settings_content( .text_color(cx.theme().muted_foreground) .child(board.i18n.t("settings_hotkey_hint")), ); - let hotkey_section = v_flex() - .gap_2() - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .font_weight(gpui::FontWeight::BOLD) - .child(board.i18n.t("settings_hotkey")), - ) - .child(activation_key_label); - let language_section = v_flex() + v_flex() .gap_2() .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .font_weight(gpui::FontWeight::BOLD) - .child(board.i18n.t("settings_language")), + .child(board.i18n.t("settings_hotkey")), ) - .child(render_language_selector(board, cx)); + .child(activation_key_label) +} - let theme_section = v_flex() - .gap_2() - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .font_weight(gpui::FontWeight::BOLD) - .child(board.i18n.t("settings_theme")), - ) - .child(render_theme_selector(board, cx)); - let storage_section = v_flex() - .gap_2() - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .font_weight(gpui::FontWeight::BOLD) - .child(board.i18n.t("settings_storage")), - ) - .child(max_history_input_field); - let autostart_section = v_flex() +fn render_system_section(board: &RopyBoard, cx: &Context) -> impl IntoElement { + v_flex() .gap_2() .child( div() @@ -203,7 +182,36 @@ pub(super) fn render_settings_content( board.toggle_autostart(cx); })) }), - ); + ) +} + +/// Render the settings panel content +pub(super) fn render_settings_content( + board: &RopyBoard, + cx: &Context, +) -> impl IntoElement { + let language_section = v_flex() + .gap_2() + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .font_weight(gpui::FontWeight::BOLD) + .child(board.i18n.t("settings_language")), + ) + .child(render_language_selector(board, cx)); + + let theme_section = v_flex() + .gap_2() + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .font_weight(gpui::FontWeight::BOLD) + .child(board.i18n.t("settings_theme")), + ) + .child(render_theme_selector(board, cx)); + let header = h_flex() .justify_between() .items_center() @@ -245,9 +253,9 @@ pub(super) fn render_settings_content( .flex_1() .child(language_section) .child(theme_section) - .child(hotkey_section) - .child(storage_section) - .child(autostart_section), + .child(render_hotkey_section(board, cx)) + .child(render_storage_section(board, cx)) + .child(render_system_section(board, cx)), ) } @@ -257,7 +265,10 @@ pub fn reset_settings_dialog( cx: &mut Context<'_, RopyBoard>, ) { // Reset selections to persisted values - let settings_guard = board.settings.read().unwrap(); + let settings_guard = match board.settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + }; board.selected_language = Language::all() .iter() .position(|&lang| lang == settings_guard.language) diff --git a/src/gui/hotkey.rs b/src/gui/hotkey.rs index 424a35b..f3e389e 100644 --- a/src/gui/hotkey.rs +++ b/src/gui/hotkey.rs @@ -9,7 +9,7 @@ use gpui::{BackgroundExecutor, ForegroundExecutor}; /// Returns a sender to update the hotkey string dynamically. pub fn start_hotkey_listener( initial_hotkey: String, - fg_executor: ForegroundExecutor, + fg_executor: &ForegroundExecutor, bg_executor: BackgroundExecutor, on_hotkey: F, ) -> async_channel::Sender @@ -20,7 +20,7 @@ where fg_executor .spawn(async move { let mut current_hotkey = initial_hotkey; - let mut _manage_handle = register_hotkey(¤t_hotkey); + let mut manage_handle = register_hotkey(¤t_hotkey); let receiver = GlobalHotKeyEvent::receiver(); loop { // Check for hotkey updates @@ -31,8 +31,8 @@ where } if updated { - drop(_manage_handle); - _manage_handle = register_hotkey(¤t_hotkey); + drop(manage_handle); + manage_handle = register_hotkey(¤t_hotkey); } // Poll for hotkey events @@ -65,8 +65,7 @@ fn register_hotkey(hotkey_str: &str) -> Option { Ok(hotkey) => { if let Err(err) = manager.register(hotkey) { eprintln!( - "Failed to register hotkey {}: {}. The hotkey listener will not be available.", - hotkey_str, err + "Failed to register hotkey {hotkey_str}: {err}. The hotkey listener will not be available." ); None } else { @@ -75,8 +74,7 @@ fn register_hotkey(hotkey_str: &str) -> Option { } Err(err) => { eprintln!( - "Failed to parse hotkey {}: {}. The hotkey listener will not be available.", - hotkey_str, err + "Failed to parse hotkey {hotkey_str}: {err}. The hotkey listener will not be available." ); None } diff --git a/src/gui/tray.rs b/src/gui/tray.rs index 8466817..0d5dd6a 100644 --- a/src/gui/tray.rs +++ b/src/gui/tray.rs @@ -14,9 +14,13 @@ use crate::{config::Settings, i18n::I18n}; /// Initialize and return the tray icon pub fn init_tray( - settings: Arc>, + settings: &Arc>, ) -> Result<(TrayIcon, MenuId, MenuId), Box> { - let language = settings.read().unwrap().language; + let language = match settings.read() { + Ok(g) => g, + Err(e) => e.into_inner(), + } + .language; let i18n = I18n::new(language).unwrap_or_default(); // Create menu items @@ -69,13 +73,13 @@ pub fn start_tray_handler( let bg_executor_clone = bg_executor.clone(); #[cfg(not(target_os = "linux"))] - start_tray_handler_inner(settings, tx, bg_executor_clone); + start_tray_handler_inner(&settings, tx, &bg_executor_clone); #[cfg(target_os = "linux")] bg_executor .spawn(async move { gtk::init().expect("Failed to init gtk modules"); - start_tray_handler_inner(settings, tx, bg_executor_clone); + start_tray_handler_inner(&settings, tx, &bg_executor_clone); gtk::main(); }) .detach(); @@ -106,9 +110,9 @@ pub fn start_tray_handler( /// Start the system tray handler pub fn start_tray_handler_inner( - settings: Arc>, + settings: &Arc>, tx: Sender, - bg_executor: BackgroundExecutor, + bg_executor: &BackgroundExecutor, ) { match init_tray(settings) { Ok((tray, show_id, quit_id)) => { @@ -155,7 +159,7 @@ pub fn start_tray_handler_inner( pub fn send_active_action(window_handle: WindowHandle, cx: &mut gpui::App) { window_handle .update(cx, |_, window, cx| { - window.dispatch_action(Box::new(crate::gui::board::Active), cx) + window.dispatch_action(Box::new(crate::gui::board::Active), cx); }) .ok(); } diff --git a/src/gui/utils.rs b/src/gui/utils.rs index 141f1a5..6899567 100644 --- a/src/gui/utils.rs +++ b/src/gui/utils.rs @@ -8,7 +8,7 @@ use { }; /// Hide the window based on the platform -pub fn hide_window(_window: &mut Window, _cx: &mut Context, pinned: bool) { +pub fn hide_window(_window: &mut Window, cx: &Context, pinned: bool) { if pinned { return; } @@ -23,7 +23,7 @@ pub fn hide_window(_window: &mut Window, _cx: &mut Context, pinned: bool) } #[cfg(target_os = "macos")] - _cx.hide(); + cx.hide(); #[cfg(target_os = "linux")] if let Some(x11) = crate::gui::app::X11.get() { @@ -34,7 +34,7 @@ pub fn hide_window(_window: &mut Window, _cx: &mut Context, pinned: bool) } /// Activate the window based on the platform -pub fn active_window(_window: &mut Window, _cx: &mut Context) { +pub fn active_window(_window: &mut Window, cx: &Context) { #[cfg(target_os = "windows")] if let Ok(handle) = _window.window_handle() && let RawWindowHandle::Win32(handle) = handle.as_raw() @@ -47,7 +47,7 @@ pub fn active_window(_window: &mut Window, _cx: &mut Context) { } #[cfg(target_os = "macos")] - _cx.activate(true); + cx.activate(true); #[cfg(target_os = "linux")] if let Some(x11) = crate::gui::app::X11.get() { @@ -58,7 +58,7 @@ pub fn active_window(_window: &mut Window, _cx: &mut Context) { } /// Set the window to be always on top -pub fn set_always_on_top(_window: &mut Window, _cx: &mut Context, _pinned: bool) { +pub const fn set_always_on_top(_window: &mut Window, _cx: &mut Context, _pinned: bool) { #[cfg(target_os = "windows")] if let Ok(handle) = _window.window_handle() && let RawWindowHandle::Win32(handle) = handle.as_raw() diff --git a/src/i18n/mod.rs b/src/i18n/mod.rs index 2384499..e34e03c 100644 --- a/src/i18n/mod.rs +++ b/src/i18n/mod.rs @@ -16,20 +16,16 @@ pub enum Language { } impl Language { - pub fn display_name(&self) -> &'static str { + pub const fn display_name(self) -> &'static str { match self { - Language::English => "English", - Language::ChineseSimplified => "简体中文", - Language::Japanese => "日本語", + Self::English => "English", + Self::ChineseSimplified => "简体中文", + Self::Japanese => "日本語", } } - pub fn all() -> Vec { - vec![ - Language::English, - Language::ChineseSimplified, - Language::Japanese, - ] + pub fn all() -> Vec { + vec![Self::English, Self::ChineseSimplified, Self::Japanese] } } @@ -40,7 +36,7 @@ pub struct Translations { } impl Translations { - /// Load translations from a TOML string + /// Load translations from a `TOML` string pub fn from_toml(content: &str) -> Result { let strings: HashMap = toml::from_str(content).map_err(|e| I18nError::ParseError(e.to_string()))?; @@ -52,7 +48,7 @@ impl Translations { self.strings .get(key) .cloned() - .unwrap_or_else(|| format!("[Missing: {}]", key)) + .unwrap_or_else(|| format!("[Missing: {key}]")) } } diff --git a/src/repository/errors.rs b/src/repository/errors.rs index 7ad2ec1..e137d2c 100644 --- a/src/repository/errors.rs +++ b/src/repository/errors.rs @@ -25,15 +25,15 @@ pub enum RepositoryError { impl std::fmt::Display for RepositoryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - RepositoryError::DataDirNotFound => write!(f, "Data directory not found"), - RepositoryError::DatabaseOpen(e) => write!(f, "Database open failed: {e}"), - RepositoryError::TreeOpen(e) => write!(f, "Tree open failed: {e}"), - RepositoryError::Serialization(e) => write!(f, "Serialization error: {e}"), - RepositoryError::Deserialization(e) => write!(f, "Deserialization error: {e}"), - RepositoryError::Insert(e) => write!(f, "Insert error: {e}"), - RepositoryError::Query(e) => write!(f, "Query error: {e}"), - RepositoryError::Delete(e) => write!(f, "Delete error: {e}"), - RepositoryError::Flush(e) => write!(f, "Flush error: {e}"), + Self::DataDirNotFound => write!(f, "Data directory not found"), + Self::DatabaseOpen(e) => write!(f, "Database open failed: {e}"), + Self::TreeOpen(e) => write!(f, "Tree open failed: {e}"), + Self::Serialization(e) => write!(f, "Serialization error: {e}"), + Self::Deserialization(e) => write!(f, "Deserialization error: {e}"), + Self::Insert(e) => write!(f, "Insert error: {e}"), + Self::Query(e) => write!(f, "Query error: {e}"), + Self::Delete(e) => write!(f, "Delete error: {e}"), + Self::Flush(e) => write!(f, "Flush error: {e}"), } } } diff --git a/src/repository/models.rs b/src/repository/models.rs index 4811953..0be4d5f 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; /// Data model for clipboard records -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ClipboardRecord { /// Unique identifier (timestamp in nanoseconds) pub id: u64, @@ -17,11 +17,11 @@ pub struct ClipboardRecord { } /// Content type enumeration -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum ContentType { /// Plain text Text, - /// Image (stored as base64) + /// Image (stored as `base64`) Image, /// File path FilePath, diff --git a/src/repository/repo.rs b/src/repository/repo.rs index b0fb212..b34130f 100644 --- a/src/repository/repo.rs +++ b/src/repository/repo.rs @@ -25,12 +25,12 @@ impl ClipboardRepository { .ok_or(RepositoryError::DataDirNotFound)? .join("ropy") .join("images"); - Self::init(db_path, images_dir) + Self::init(&db_path, images_dir) } /// Initialize repository with specific paths - pub fn init(db_path: PathBuf, images_dir: PathBuf) -> Result { - let db = sled::open(&db_path).map_err(|e| RepositoryError::DatabaseOpen(e.to_string()))?; + pub fn init(db_path: &PathBuf, images_dir: PathBuf) -> Result { + let db = sled::open(db_path).map_err(|e| RepositoryError::DatabaseOpen(e.to_string()))?; let records_tree = db .open_tree("clipboard_records") .map_err(|e| RepositoryError::TreeOpen(e.to_string()))?; @@ -240,7 +240,7 @@ mod tests { fn create_test_repo() -> ClipboardRepository { let temp_dir = tempdir().expect("Failed to create temp dir"); let db_path = temp_dir.path().join("test.db"); - ClipboardRepository::init(db_path, temp_dir.path().join("images")) + ClipboardRepository::init(&db_path, temp_dir.path().join("images")) .expect("Failed to create test repository") } @@ -334,7 +334,7 @@ mod tests { let repo = create_test_repo(); for i in 1..=10 { - repo.save_text(format!("Record {}", i)) + repo.save_text(format!("Record {i}")) .expect("Failed to save"); thread::sleep(Duration::from_millis(10)); }