From bd24bec35ae16c25cd9cb6dc92f6a839285d979e Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 19:09:22 +0000 Subject: [PATCH 01/12] doc: fix id stuff --- src/app/apps.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/apps.rs b/src/app/apps.rs index 5b7f40c..53dce9a 100644 --- a/src/app/apps.rs +++ b/src/app/apps.rs @@ -86,9 +86,6 @@ pub struct App { pub app_data: AppData, /// A unique ID generated for each instance of an App. - /// - /// This is made by atomically incrementing a counter every time a new instance of the struct - /// is made. The implementation of [`PartialEq`] uses this. id: usize, } @@ -99,7 +96,7 @@ impl PartialEq for App { } impl App { - /// Get the internal id + /// Get the numeric id of an app pub fn id(&self) -> usize { self.id } From cc429258123fbb523cb85918784f2b4ecef58535 Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 19:21:23 +0000 Subject: [PATCH 02/12] lint: use pedantic linting by default + warn on missing docs If you don't want the noise, you can alwats use cargo clippy -- "-Aclippy::pedantic" --- Cargo.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index db9ff09..cd3a9e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,17 @@ name = "rustcast" version = "0.5.2" edition = "2024" +[lints.clippy] +# NOTE: set "annoying" lints that aren't of note to allowed as they appear +pedantic = { level = "warn", priority = -1 } +cast_precision_loss = "allow" +cast_sign_loss = "allow" + + +[lints.rustdoc] +missing_docs = "warn" + + [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.52" windows = { version = "0.58", features = [ From 4e06a9a64d68eba9b3fe22f1e546c2535ab3fe5e Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 19:24:36 +0000 Subject: [PATCH 03/12] clean: clippy --fix --- src/app/apps.rs | 2 +- src/app/menubar.rs | 6 +- src/app/pages/clipboard.rs | 2 +- src/app/pages/emoji.rs | 4 +- src/app/tile/elm.rs | 10 ++-- src/app/tile/mod.rs | 12 ++-- src/app/tile/search_query.rs | 67 +++++++++++------------ src/app/tile/update.rs | 25 ++++----- src/calculator.rs | 7 +-- src/clipboard.rs | 2 +- src/commands.rs | 8 +-- src/config/mod.rs | 8 +-- src/config/patterns.rs | 2 +- src/cross_platform/windows/app_finding.rs | 37 ++++++------- src/cross_platform/windows/mod.rs | 6 +- src/main.rs | 10 ++-- src/unit_conversion/mod.rs | 2 +- src/utils.rs | 4 +- 18 files changed, 103 insertions(+), 111 deletions(-) diff --git a/src/app/apps.rs b/src/app/apps.rs index 53dce9a..5b2599a 100644 --- a/src/app/apps.rs +++ b/src/app/apps.rs @@ -306,7 +306,7 @@ impl App { .height(50); container(content) - .id(format!("result-{}", id_num)) + .id(format!("result-{id_num}")) .style(move |_| result_row_container_style(&theme, focused)) .padding(8) .width(Fill) diff --git a/src/app/menubar.rs b/src/app/menubar.rs index 6910982..97636a2 100644 --- a/src/app/menubar.rs +++ b/src/app/menubar.rs @@ -97,7 +97,7 @@ fn init_event_handler(sender: ExtSender) { } "open_issue_page" => { if let Err(e) = open::that("https://github.com/unsecretised/rustcast/issues/new") { - tracing::error!("Error opening url: {}", e) + tracing::error!("Error opening url: {}", e); } } "show_rustcast" => { @@ -112,7 +112,7 @@ fn init_event_handler(sender: ExtSender) { if let Err(e) = open::that( "https://github.com/unsecretised/rustcast/discussions/new?category=q-a", ) { - tracing::error!("Error opening url: {}", e) + tracing::error!("Error opening url: {}", e); } } "open_preferences" => { @@ -120,7 +120,7 @@ fn init_event_handler(sender: ExtSender) { } "open_github_page" => { if let Err(e) = open::that("https://github.com/unsecretised/rustcast") { - tracing::error!("Error opening url: {}", e) + tracing::error!("Error opening url: {}", e); } } _ => {} diff --git a/src/app/pages/clipboard.rs b/src/app/pages/clipboard.rs index 106137b..fe85fd4 100644 --- a/src/app/pages/clipboard.rs +++ b/src/app/pages/clipboard.rs @@ -31,7 +31,7 @@ pub fn clipboard_view( clipboard_content .get(focussed_id as usize) .map(|x| x.to_app().alias) - .unwrap_or("".to_string()), + .unwrap_or_default(), ) .height(385) .width(Length::Fill) diff --git a/src/app/pages/emoji.rs b/src/app/pages/emoji.rs index 978167f..45681ad 100644 --- a/src/app/pages/emoji.rs +++ b/src/app/pages/emoji.rs @@ -9,7 +9,7 @@ pub fn emoji_page( ) -> Element<'static, Message> { let emoji_vec = emojis .chunks(6) - .map(|x| x.to_vec()) + .map(<[crate::app::apps::App]>::to_vec) .collect::>>(); let mut column = Vec::new(); @@ -43,7 +43,7 @@ pub fn emoji_page( ) .width(70) .height(70) - .id(format!("result-{}", id_num)) + .id(format!("result-{id_num}")) .style(move |_| emoji_button_container_style(&theme_clone, focussed_id == id_num)), container( Text::new(emoji.desc) diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index be7a462..176d502 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -111,13 +111,13 @@ pub fn new( let options = index_installed_apps(config); if let Err(ref e) = options { - tracing::error!("Error indexing apps: {e}") + tracing::error!("Error indexing apps: {e}"); } // Still try to load the rest let mut options = options.unwrap_or_default(); - options.extend(config.shells.iter().map(|x| x.to_app())); + options.extend(config.shells.iter().map(crate::config::Shelly::to_app)); options.extend(App::basic_apps()); options.par_sort_by_key(|x| x.name.len()); let options = AppIndex::from_apps(options); @@ -133,7 +133,7 @@ pub fn new( visible: true, focused: false, config: config.clone(), - theme: config.theme.to_owned().into(), + theme: config.theme.clone().into(), clipboard_content: vec![], tray_icon: None, sender: None, @@ -204,7 +204,7 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { tile.config.theme.clone(), tile.emoji_apps .search_prefix(&tile.query_lc) - .map(|x| x.to_owned()) + .map(std::borrow::ToOwned::to_owned) .collect(), tile.focus_id, ) @@ -274,7 +274,7 @@ fn footer(theme: Theme, results_count: usize) -> Element<'static, Message> { } else if results_count == 1 { "1 result found" } else { - &format!("{} results found", results_count) + &format!("{results_count} results found") }; container( diff --git a/src/app/tile/mod.rs b/src/app/tile/mod.rs index 3105bc0..0d8110c 100644 --- a/src/app/tile/mod.rs +++ b/src/app/tile/mod.rs @@ -232,7 +232,7 @@ impl Tile { }; let results: Vec = options .search_prefix(&query) - .map(|x| x.to_owned()) + .map(std::borrow::ToOwned::to_owned) .collect(); self.results = results; @@ -292,9 +292,9 @@ impl Tile { fn handle_hot_reloading() -> impl futures::Stream { stream::channel(100, async |mut output| { let mut content = fs::read_to_string( - std::env::var("HOME").unwrap_or("".to_owned()) + "/.config/rustcast/config.toml", + std::env::var("HOME").unwrap_or_default() + "/.config/rustcast/config.toml", ) - .unwrap_or("".to_string()); + .unwrap_or_default(); let paths = default_app_paths(); let mut total_files: usize = paths @@ -304,9 +304,9 @@ fn handle_hot_reloading() -> impl futures::Stream { loop { let current_content = fs::read_to_string( - std::env::var("HOME").unwrap_or("".to_owned()) + "/.config/rustcast/config.toml", + std::env::var("HOME").unwrap_or_default() + "/.config/rustcast/config.toml", ) - .unwrap_or("".to_string()); + .unwrap_or_default(); let current_total_files: usize = paths .par_iter() @@ -334,7 +334,7 @@ fn count_dirs_in_dir(dir: &PathBuf) -> usize { }; entries - .filter_map(|entry| entry.ok()) + .filter_map(std::result::Result::ok) .filter(|entry| entry.file_type().map(|t| t.is_dir()).unwrap_or(false)) .count() } diff --git a/src/app/tile/search_query.rs b/src/app/tile/search_query.rs index 64c710b..af6f0b6 100644 --- a/src/app/tile/search_query.rs +++ b/src/app/tile/search_query.rs @@ -53,45 +53,44 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< height: 55. + DEFAULT_WINDOW_HEIGHT, }, ); - } else { - if tile.query_lc == "67" { - tile.results = vec![App::new_builtin( - "67", - "", - "Easter egg", - AppCommand::Function(Function::RandomVar(67)), - )]; - return window::resize( - id, - iced::Size { - width: WINDOW_WIDTH, - height: 55. + DEFAULT_WINDOW_HEIGHT, - }, - ); - } - if tile.query_lc.ends_with("?") { - tile.results = vec![App::new_builtin( - &format!("Search for: {}", tile.query), - "", - "Web Search", - AppCommand::Function(Function::GoogleSearch(tile.query.clone())), - )]; - return window::resize( - id, - iced::Size::new(WINDOW_WIDTH, 55. + DEFAULT_WINDOW_HEIGHT), - ); - } else if tile.query_lc == "cbhist" { - tile.page = Page::ClipboardHistory - } else if tile.query_lc == "main" { - tile.page = Page::Main - } + } + if tile.query_lc == "67" { + tile.results = vec![App::new_builtin( + "67", + "", + "Easter egg", + AppCommand::Function(Function::RandomVar(67)), + )]; + return window::resize( + id, + iced::Size { + width: WINDOW_WIDTH, + height: 55. + DEFAULT_WINDOW_HEIGHT, + }, + ); + } + if tile.query_lc.ends_with('?') { + tile.results = vec![App::new_builtin( + &format!("Search for: {}", tile.query), + "", + "Web Search", + AppCommand::Function(Function::GoogleSearch(tile.query.clone())), + )]; + return window::resize( + id, + iced::Size::new(WINDOW_WIDTH, 55. + DEFAULT_WINDOW_HEIGHT), + ); + } else if tile.query_lc == "cbhist" { + tile.page = Page::ClipboardHistory; + } else if tile.query_lc == "main" { + tile.page = Page::Main; } tile.handle_search_query_changed(); if tile.results.is_empty() && let Some(res) = Expr::from_str(&tile.query).ok() { - let res_string = res.eval().map(|x| x.to_string()).unwrap_or(String::new()); + let res_string = res.eval().map_or(String::new(), |x| x.to_string()); tile.results.push(App::new_builtin( RUSTCAST_DESC_NAME, &res_string, @@ -155,7 +154,7 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< tile.results = tile .emoji_apps .search_prefix("") - .map(|x| x.to_owned()) + .map(std::borrow::ToOwned::to_owned) .collect(); } diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 9575274..3f6e5a1 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -174,10 +174,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::ReloadConfig => { let new_config: Config = match toml::from_str( &fs::read_to_string( - std::env::var("HOME").unwrap_or("".to_owned()) + std::env::var("HOME").unwrap_or_default() + "/.config/rustcast/config.toml", ) - .unwrap_or("".to_owned()), + .unwrap_or_default(), ) { Ok(a) => a, Err(_) => return Task::none(), @@ -189,11 +189,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Err(e) => tracing::error!("Error indexing apps: {e}"), } - options.extend(new_config.shells.iter().map(|x| x.to_app())); + options.extend(new_config.shells.iter().map(crate::config::Shelly::to_app)); options.extend(App::basic_apps()); options.par_sort_by_key(|x| x.name.len()); - tile.theme = new_config.theme.to_owned().into(); + tile.theme = new_config.theme.clone().into(); tile.config = new_config; tile.options = AppIndex::from_apps(options); Task::none() @@ -227,8 +227,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { // Linux Clipboard and Open Hotkey are gonna be handled via a socket let is_clipboard_hotkey = tile .clipboard_hotkey - .map(|hotkey| hotkey.id == hk_id) - .unwrap_or(false); + .is_some_and(|hotkey| hotkey.id == hk_id); let is_open_hotkey = hk_id == tile.hotkey.id; @@ -309,15 +308,13 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::WindowFocusChanged(wid, focused) => { tile.focused = focused; - if !focused { - if cfg!(target_os = "macos") { - Task::done(Message::HideWindow(wid)) - .chain(Task::done(Message::ClearSearchQuery)) - } else { - // linux seems to not wanna unfocus it on start making it not show - Task::none() - } + if focused { + Task::none() + } else if cfg!(target_os = "macos") { + Task::done(Message::HideWindow(wid)) + .chain(Task::done(Message::ClearSearchQuery)) } else { + // linux seems to not wanna unfocus it on start making it not show Task::none() } } diff --git a/src/calculator.rs b/src/calculator.rs index 6012028..5bfe713 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -49,8 +49,8 @@ pub enum BinOp { impl Expr { pub fn eval(&self) -> Option { - use BinOp::*; - use UnaryOp::*; + use BinOp::{Add, Sub, Mul, Div, Pow}; + use UnaryOp::{Plus, Minus}; match self { Expr::Number(x) => Some(*x), @@ -196,9 +196,8 @@ impl<'a> Lexer<'a> { return self.lex_number(); } else if c.is_ascii_alphabetic() || c == '_' { return self.lex_ident(); - } else { - return Err(format!("Unexpected character: {c}")); } + return Err(format!("Unexpected character: {c}")); } }; Ok(tok) diff --git a/src/clipboard.rs b/src/clipboard.rs index 27d9c15..8c27d4a 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -32,7 +32,7 @@ impl ClipBoardContentType { &name, &name_lc, "Clipboard Item", - AppCommand::Function(Function::CopyToClipboard(self_clone.to_owned())), + AppCommand::Function(Function::CopyToClipboard(self_clone.clone())), ) } } diff --git a/src/commands.rs b/src/commands.rs index a43c8f0..b9ff96e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -37,7 +37,7 @@ impl Function { Function::RunShellCommand(command, alias) => { let query = query.to_string(); let final_command = - format!(r#"{} {}"#, command, query.strip_prefix(alias).unwrap_or("")); + format!(r"{} {}", command, query.strip_prefix(alias).unwrap_or("")); Command::new("sh") .arg("-c") .arg(final_command.trim()) @@ -52,7 +52,7 @@ impl Function { } Function::GoogleSearch(query_string) => { - let query_args = query_string.replace(" ", "+"); + let query_args = query_string.replace(' ', "+"); let query = config.search_url.replace("%s", &query_args); let query = query.strip_suffix("?").unwrap_or(&query).to_string(); @@ -63,7 +63,7 @@ impl Function { let open_url = if url.starts_with("http") { url.to_owned() } else { - format!("https://{}", url) + format!("https://{url}") }; // Should never get here without it being validated first @@ -73,7 +73,7 @@ impl Function { Function::Calculate(expr) => { Clipboard::new() .unwrap() - .set_text(expr.eval().map(|x| x.to_string()).unwrap_or("".to_string())) + .set_text(expr.eval().map_or(String::new(), |x| x.to_string())) .unwrap_or(()); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 706abe3..61fc52f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -167,8 +167,8 @@ impl Theme { /// The rules for the buffer AKA search results /// -/// - clear_on_hide is whether the buffer should be cleared when the window is hidden -/// - clear_on_enter is whether the buffer should be cleared when the user presses enter after +/// - `clear_on_hide` is whether the buffer should be cleared when the window is hidden +/// - `clear_on_enter` is whether the buffer should be cleared when the user presses enter after /// searching #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] @@ -187,7 +187,7 @@ impl Default for Buffer { } /// Command is the command it will run when the button is clicked -/// Icon_path is the path to an icon, but this is optional +/// `Icon_path` is the path to an icon, but this is optional /// Alias is the text that is used to call this command / search for it #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Shelly { @@ -202,7 +202,7 @@ impl Shelly { pub fn to_app(&self) -> App { let self_clone = self.clone(); let icon = self_clone.icon_path.map(|x| { - let x = x.replace("~", &std::env::var("HOME").unwrap()); + let x = x.replace('~', &std::env::var("HOME").unwrap()); get_img_handle(&PathBuf::from(x)) }); App::new( diff --git a/src/config/patterns.rs b/src/config/patterns.rs index 514a860..7442057 100644 --- a/src/config/patterns.rs +++ b/src/config/patterns.rs @@ -21,7 +21,7 @@ where { patterns .iter() - .map(|x| x.as_str()) + .map(glob::Pattern::as_str) .collect::>() .serialize(serializer) } diff --git a/src/cross_platform/windows/app_finding.rs b/src/cross_platform/windows/app_finding.rs index 66917c2..a55085e 100644 --- a/src/cross_platform/windows/app_finding.rs +++ b/src/cross_platform/windows/app_finding.rs @@ -18,7 +18,7 @@ use { /// `SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`. `apps` has the relvant items /// appended to it. /// -/// Based on https://stackoverflow.com/questions/2864984 +/// Based on pub fn get_apps_from_registry(apps: &mut Vec) { use std::ffi::OsString; let hkey = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); @@ -52,7 +52,7 @@ pub fn get_apps_from_registry(apps: &mut Vec) { // if there is something, it will be in the form of // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 let exe_string = exe_path.to_string_lossy(); - let exe_string = exe_string.split(",").next().unwrap(); + let exe_string = exe_string.split(',').next().unwrap(); // make sure it ends with .exe if !exe_string.ends_with(".exe") { @@ -66,7 +66,7 @@ pub fn get_apps_from_registry(apps: &mut Vec) { "Application", exe_path, None, - )) + )); } }); }); @@ -90,7 +90,7 @@ fn get_windows_path(folder_id: &GUID) -> Option { let folder = SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None); if let Ok(folder) = folder { let path = folder.to_string().ok()?; - CoTaskMemFree(Some(folder.0 as *mut _)); + CoTaskMemFree(Some(folder.0.cast())); Some(path.into()) } else { None @@ -101,7 +101,7 @@ fn get_windows_path(folder_id: &GUID) -> Option { pub fn index_start_menu() -> Vec { WalkDir::new(r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs") .into_iter() - .filter_map(|x| x.ok()) + .filter_map(std::result::Result::ok) .filter_map(|path| { let lnk = lnk::ShellLink::open(path.path(), get_acp()); @@ -110,21 +110,18 @@ pub fn index_start_menu() -> Vec { let target = x.link_target(); let file_name = path.file_name().to_string_lossy().to_string(); - match target { - Some(target) => Some(App::new_executable( - &file_name, - &file_name, - "", - PathBuf::from(target.clone()), - None, - )), - None => { - tracing::debug!( - "Link at {} has no target, skipped", - path.path().display() - ); - None - } + if let Some(target) = target { Some(App::new_executable( + &file_name, + &file_name, + "", + PathBuf::from(target.clone()), + None, + )) } else { + tracing::debug!( + "Link at {} has no target, skipped", + path.path().display() + ); + None } } Err(e) => { diff --git a/src/cross_platform/windows/mod.rs b/src/cross_platform/windows/mod.rs index 53794cc..970d626 100644 --- a/src/cross_platform/windows/mod.rs +++ b/src/cross_platform/windows/mod.rs @@ -16,9 +16,9 @@ pub fn open_on_focused_monitor() -> iced::Point { ..Default::default() }; - let _cursor = unsafe { GetCursorPos(&mut point) }; + let _cursor = unsafe { GetCursorPos(&raw mut point) }; let monitor = unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }; - let _monitor_infos = unsafe { GetMonitorInfoW(monitor, &mut monitor_info) }; + let _monitor_infos = unsafe { GetMonitorInfoW(monitor, &raw mut monitor_info) }; let monitor_width = monitor_info.rcMonitor.right - monitor_info.rcMonitor.left; let monitor_height = monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top; @@ -31,7 +31,7 @@ pub fn open_on_focused_monitor() -> iced::Point { iced::Point { x, y } } -/// Wrapper over GetACP that defaults to WINDOWS_1252 if the ACP isn't found +/// Wrapper over `GetACP` that defaults to `WINDOWS_1252` if the ACP isn't found pub fn get_acp() -> Encoding { unsafe { codepage::to_encoding(GetACP() as u16) }.unwrap_or_else(|| { tracing::warn!( diff --git a/src/main.rs b/src/main.rs index 4f0d951..54c893b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,11 +37,11 @@ fn main() -> iced::Result { let result = create_dir_all(config_dir.join("rustcast/")); if let Err(e) = result { - eprintln!("{}", e); + eprintln!("{e}"); std::process::exit(1); } } else { - eprintln!("{}", e); + eprintln!("{e}"); std::process::exit(1); } } @@ -50,7 +50,7 @@ fn main() -> iced::Result { let config = read_config_file(&file_path); if let Err(e) = config { // Tracing isn't inited yet - eprintln!("Error parsing config: {}", e); + eprintln!("Error parsing config: {e}"); std::process::exit(1); } @@ -135,9 +135,9 @@ fn main() -> iced::Result { if let Err(global_hotkey::Error::AlreadyRegistered(key)) = result { if key == show_hide { // It probably should give up here. - panic!("Couldn't register the key to open ({})", key) + panic!("Couldn't register the key to open ({key})") } else { - tracing::warn!("Couldn't register hotkey {}", key) + tracing::warn!("Couldn't register hotkey {}", key); } } else if let Err(e) = result { tracing::error!("{}", e.to_string()); diff --git a/src/unit_conversion/mod.rs b/src/unit_conversion/mod.rs index 0d6da02..9412ebc 100644 --- a/src/unit_conversion/mod.rs +++ b/src/unit_conversion/mod.rs @@ -136,7 +136,7 @@ fn parse_number_prefix(s: &str) -> Option<(&str, &str)> { let mut end = 0; let mut has_digit = false; - while let Some((idx, c)) = chars.peek().cloned() { + while let Some((idx, c)) = chars.peek().copied() { if c.is_ascii_digit() { has_digit = true; end = idx + c.len_utf8(); diff --git a/src/utils.rs b/src/utils.rs index 778a624..55510a5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -53,7 +53,7 @@ fn search_dir( .max_depth(max_depth) .into_iter() .par_bridge() - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .filter(|e| e.path().extension().is_some_and(|ext| ext == "exe")) .filter_map(|entry| { let path = entry.path(); @@ -136,7 +136,7 @@ pub fn index_installed_apps(config: &Config) -> anyhow::Result> { let config = read_config_file(path.as_path())?; if config.index_dirs.is_empty() { - tracing::debug!("No extra index dirs provided") + tracing::debug!("No extra index dirs provided"); } #[cfg(target_os = "windows")] From 99f1b56113a356b91769e531fbbb4cbd7bcdc37c Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 19:32:55 +0000 Subject: [PATCH 04/12] lint: i changed my mind --- Cargo.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd3a9e2..8effa82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,6 @@ name = "rustcast" version = "0.5.2" edition = "2024" -[lints.clippy] -# NOTE: set "annoying" lints that aren't of note to allowed as they appear -pedantic = { level = "warn", priority = -1 } -cast_precision_loss = "allow" -cast_sign_loss = "allow" - - [lints.rustdoc] missing_docs = "warn" From 43e73f2fc6a7352e4efc2613026dc10a1e3dd1d3 Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 21:01:59 +0000 Subject: [PATCH 05/12] clean: general pedantic fixes --- src/app/apps.rs | 12 ++-- src/app/menubar.rs | 4 +- src/app/pages/clipboard.rs | 10 ++-- src/app/pages/emoji.rs | 4 +- src/app/tile/elm.rs | 25 ++++---- src/app/tile/mod.rs | 9 +-- src/app/tile/search_query.rs | 7 ++- src/app/tile/update.rs | 7 ++- src/calculator.rs | 23 ++++---- src/cross_platform/windows/app_finding.rs | 71 ++++++++++++----------- src/cross_platform/windows/mod.rs | 4 ++ src/main.rs | 1 + src/utils.rs | 5 +- 13 files changed, 96 insertions(+), 86 deletions(-) diff --git a/src/app/apps.rs b/src/app/apps.rs index 5b2599a..52f7fb3 100644 --- a/src/app/apps.rs +++ b/src/app/apps.rs @@ -83,20 +83,22 @@ pub struct App { pub desc: String, /// The information specific to a certain type of app - pub app_data: AppData, + pub data: AppData, /// A unique ID generated for each instance of an App. + #[allow(unused)] id: usize, } impl PartialEq for App { fn eq(&self, other: &Self) -> bool { - self.app_data == other.app_data && self.name == other.name + self.data == other.data && self.name == other.name } } impl App { /// Get the numeric id of an app + #[allow(unused)] pub fn id(&self) -> usize { self.id } @@ -110,7 +112,7 @@ impl App { name: name.to_string(), desc: desc.to_string(), id: ID.fetch_add(1, Ordering::Relaxed), - app_data: data, + data, } } @@ -245,7 +247,7 @@ impl App { .height(50); if theme.show_icons { - match self.app_data { + match self.data { AppData::Command { icon: Some(ref icon), .. @@ -277,7 +279,7 @@ impl App { } row = row.push(container(text_block).width(Fill)); - let msg = match self.app_data { + let msg = match self.data { AppData::Builtin { command: AppCommand::Function(func), .. diff --git a/src/app/menubar.rs b/src/app/menubar.rs index 97636a2..8c9b7dd 100644 --- a/src/app/menubar.rs +++ b/src/app/menubar.rs @@ -27,7 +27,7 @@ pub fn menu_icon(#[cfg(not(target_os = "linux"))] hotkey: HotKey, sender: ExtSen let menu = Menu::with_items(&[ &version_item(), - &about_item(image), + &about_item(&image), &open_github_item(), &PredefinedMenuItem::separator(), &refresh_item(), @@ -196,7 +196,7 @@ fn quit_item() -> PredefinedMenuItem { PredefinedMenuItem::quit(Some("Quit")) } -fn about_item(image: DynamicImage) -> PredefinedMenuItem { +fn about_item(image: &DynamicImage) -> PredefinedMenuItem { let about_metadata_builder = AboutMetadataBuilder::new() .name(Some("RustCast")) .version(Some( diff --git a/src/app/pages/clipboard.rs b/src/app/pages/clipboard.rs index fe85fd4..a2b24cf 100644 --- a/src/app/pages/clipboard.rs +++ b/src/app/pages/clipboard.rs @@ -6,9 +6,9 @@ use iced::widget::{ use crate::{app::pages::prelude::*, clipboard::ClipBoardContentType}; pub fn clipboard_view( - clipboard_content: Vec, + clipboard_content: &[ClipBoardContentType], focussed_id: u32, - theme: Theme, + theme: &Theme, focus_id: u32, ) -> Element<'static, Message> { let theme_clone = theme.clone(); @@ -16,9 +16,11 @@ pub fn clipboard_view( container(Row::from_vec(vec![ container( scrollable( - Column::from_iter(clipboard_content.iter().enumerate().map(|(i, content)| { + clipboard_content.iter().enumerate().map(|(i, content)| { + // I'd be surprised if you get 4 billion entries + #[allow(clippy::cast_possible_truncation)] content.to_app().render(theme.clone(), i as u32, focus_id) - })) + }).collect::>() .width(WINDOW_WIDTH / 3.), ) .id("results"), diff --git a/src/app/pages/emoji.rs b/src/app/pages/emoji.rs index 45681ad..ae85c48 100644 --- a/src/app/pages/emoji.rs +++ b/src/app/pages/emoji.rs @@ -4,12 +4,12 @@ use crate::{app::pages::prelude::*, clipboard::ClipBoardContentType, commands::F pub fn emoji_page( tile_theme: Theme, - emojis: Vec, + emojis: &[App], focussed_id: u32, ) -> Element<'static, Message> { let emoji_vec = emojis .chunks(6) - .map(<[crate::app::apps::App]>::to_vec) + .map(<[App]>::to_vec) .collect::>>(); let mut column = Vec::new(); diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 176d502..9a531f0 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -192,36 +192,38 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { let results = if tile.page == Page::ClipboardHistory { clipboard_view( - tile.clipboard_content.clone(), + &tile.clipboard_content, tile.focus_id, - tile.config.theme.clone(), + &tile.config.theme, tile.focus_id, ) } else if tile.results.is_empty() { space().into() } else if tile.page == Page::EmojiSearch { - emoji_page( - tile.config.theme.clone(), - tile.emoji_apps + let results: Vec<_> = tile.emoji_apps .search_prefix(&tile.query_lc) .map(std::borrow::ToOwned::to_owned) - .collect(), + .collect(); + + emoji_page( + tile.config.theme.clone(), + &results, tile.focus_id, ) } else { - container(Column::from_iter(tile.results.iter().enumerate().map( + container(tile.results.iter().enumerate().map( |(i, app)| { + #[allow(clippy::cast_possible_truncation)] app.clone() .render(tile.config.theme.clone(), i as u32, tile.focus_id) - }, - ))) + } + ).collect::>()) .into() }; let results_count = match &tile.page { - Page::Main => tile.results.len(), Page::ClipboardHistory => tile.clipboard_content.len(), - Page::EmojiSearch => tile.results.len(), + Page::Main | Page::EmojiSearch => tile.results.len(), }; let height = if tile.page == Page::ClipboardHistory { @@ -230,6 +232,7 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { std::cmp::min(tile.results.len() * 60, 290) }; + #[allow(clippy::cast_possible_truncation)] let scrollable = Scrollable::with_direction(results, scrollbar_direction) .id("results") .height(height as u32); diff --git a/src/app/tile/mod.rs b/src/app/tile/mod.rs index 0d8110c..ac344b7 100644 --- a/src/app/tile/mod.rs +++ b/src/app/tile/mod.rs @@ -119,8 +119,8 @@ pub struct Tile { impl Tile { /// This returns the theme of the window - pub fn theme(&self, _: window::Id) -> Option { - Some(self.theme.clone()) + pub fn theme(&self, _: window::Id) -> Theme { + self.theme.clone() } /// This handles the subscriptions of the window @@ -328,10 +328,7 @@ fn handle_hot_reloading() -> impl futures::Stream { fn count_dirs_in_dir(dir: &PathBuf) -> usize { // Read the directory; if it fails, treat as empty - let entries = match fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return 0, - }; + let Ok(entries) = fs::read_dir(dir) else { return 0 }; entries .filter_map(std::result::Result::ok) diff --git a/src/app/tile/search_query.rs b/src/app/tile/search_query.rs index af6f0b6..34ee756 100644 --- a/src/app/tile/search_query.rs +++ b/src/app/tile/search_query.rs @@ -7,7 +7,7 @@ use std::cmp; use super::Tile; use crate::{ app::{ - ArrowKey, DEFAULT_WINDOW_HEIGHT, Message, Page, RUSTCAST_DESC_NAME, WINDOW_WIDTH, + ArrowKey, DEFAULT_WINDOW_HEIGHT, Message, Page, WINDOW_WIDTH, apps::{App, AppCommand}, }, calculator::Expr, @@ -19,6 +19,7 @@ use crate::{ #[cfg(target_os = "macos")] use crate::cross_platform::macos::haptics::{HapticPattern, perform_haptic}; +#[allow(clippy::too_many_lines)] pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task { tile.focus_id = 0; #[cfg(target_os = "macos")] @@ -92,9 +93,9 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< { let res_string = res.eval().map_or(String::new(), |x| x.to_string()); tile.results.push(App::new_builtin( - RUSTCAST_DESC_NAME, &res_string, "", + "Calculation result", AppCommand::Function(Function::Calculate(res.clone())), )); } else if tile.results.is_empty() @@ -162,6 +163,7 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< let max_elem = cmp::min(5, new_length); if prev_size != new_length && tile.page != Page::ClipboardHistory { + #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)] Task::batch([ window::resize( id, @@ -173,6 +175,7 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< Task::done(Message::ChangeFocus(ArrowKey::Left)), ]) } else if tile.page == Page::ClipboardHistory { + #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)] Task::batch([ window::resize( id, diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 3f6e5a1..742ae2f 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -21,6 +21,7 @@ use crate::commands::Function; use crate::config::Config; use crate::utils::index_installed_apps; +#[allow(clippy::too_many_lines)] pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tracing::trace!("Handling update (message: {:?})", message); @@ -95,10 +96,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Message::ChangeFocus(key) => { + #[allow(clippy::cast_possible_truncation)] // No, there won't be more than 2^32-1 items in a list let len = match tile.page { Page::ClipboardHistory => tile.clipboard_content.len() as u32, Page::EmojiSearch => tile.emoji_apps.search_prefix(&tile.query_lc).count() as u32, // or tile.results.len() - _ => tile.results.len() as u32, + Page::Main => tile.results.len() as u32, }; let old_focus_id = tile.focus_id; @@ -139,6 +141,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Page::EmojiSearch => 5., }; + #[allow(clippy::cast_precision_loss)] Task::batch([ task, operation::scroll_to( @@ -154,7 +157,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::OpenFocused => match tile .results .get(tile.focus_id as usize) - .map(|x| &x.app_data) + .map(|x| &x.data) { Some(AppData::Builtin { command: AppCommand::Function(func), diff --git a/src/calculator.rs b/src/calculator.rs index 5bfe713..9258de8 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -101,7 +101,7 @@ impl Expr { pub fn from_str(s: &str) -> Result { let mut p = Parser::new(s); let expr = p.parse_expr()?; - p.expect(Token::End)?; + p.expect(&Token::End)?; Ok(expr) } } @@ -151,10 +151,7 @@ impl<'a> Lexer<'a> { fn next_token(&mut self) -> Result { self.skip_ws(); - let c = match self.peek_char() { - Some(c) => c, - None => return Ok(Token::End), - }; + let Some(c) = self.peek_char() else { return Ok(Token::End) }; // single-char tokens let tok = match c { @@ -195,7 +192,7 @@ impl<'a> Lexer<'a> { if c.is_ascii_digit() || c == '.' { return self.lex_number(); } else if c.is_ascii_alphabetic() || c == '_' { - return self.lex_ident(); + return Ok(self.lex_ident()); } return Err(format!("Unexpected character: {c}")); } @@ -232,7 +229,7 @@ impl<'a> Lexer<'a> { Ok(Token::Number(n)) } - fn lex_ident(&mut self) -> Result { + fn lex_ident(&mut self) -> Token { let start = self.i; while let Some(c) = self.peek_char() { if c.is_ascii_alphanumeric() || c == '_' { @@ -241,7 +238,7 @@ impl<'a> Lexer<'a> { break; } } - Ok(Token::Ident(self.input[start..self.i].to_string())) + Token::Ident(self.input[start..self.i].to_string()) } } @@ -264,8 +261,8 @@ impl<'a> Parser<'a> { Ok(()) } - fn expect(&mut self, t: Token) -> Result<(), String> { - if self.cur == t { + fn expect(&mut self, t: &Token) -> Result<(), String> { + if self.cur == *t { self.bump() } else { Err(format!("Expected {:?}, found {:?}", t, self.cur)) @@ -359,14 +356,14 @@ impl<'a> Parser<'a> { Token::LParen => { self.bump()?; let e = self.parse_expr()?; - self.expect(Token::RParen)?; + self.expect(&Token::RParen)?; Ok(e) } Token::Ident(name) => { let name = name.clone(); self.bump()?; // function call must be ident '(' ... - self.expect(Token::LParen)?; + self.expect(&Token::LParen)?; let mut args = Vec::new(); if self.cur != Token::RParen { loop { @@ -378,7 +375,7 @@ impl<'a> Parser<'a> { break; } } - self.expect(Token::RParen)?; + self.expect(&Token::RParen)?; Ok(Expr::Func { name, args }) } _ => Err(format!("Unexpected token: {:?}", self.cur)), diff --git a/src/cross_platform/windows/app_finding.rs b/src/cross_platform/windows/app_finding.rs index a55085e..14a26e7 100644 --- a/src/cross_platform/windows/app_finding.rs +++ b/src/cross_platform/windows/app_finding.rs @@ -30,46 +30,47 @@ pub fn get_apps_from_registry(apps: &mut Vec) { .unwrap(), ]; - registers.iter().for_each(|reg| { - reg.enum_keys().for_each(|key| { - // Not debug only just because it doesn't run too often - tracing::trace!("App added [reg]: {:?}", key); + for (key, reg) in registers + .iter() + .flat_map(|reg| reg.enum_keys().zip(std::iter::repeat(reg))) + { + // Not debug only just because it doesn't run too often + tracing::trace!("App added [reg]: {:?}", key); - // https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key - let name = key.unwrap(); - let key = reg.open_subkey(&name).unwrap(); - let display_name: OsString = key.get_value("DisplayName").unwrap_or_default(); + // https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key + let name = key.unwrap(); + let key = reg.open_subkey(&name).unwrap(); + let display_name: OsString = key.get_value("DisplayName").unwrap_or_default(); - // they might be useful one day ? - // let publisher = key.get_value("Publisher").unwrap_or(OsString::new()); - // let version = key.get_value("DisplayVersion").unwrap_or(OsString::new()); + // they might be useful one day ? + // let publisher = key.get_value("Publisher").unwrap_or(OsString::new()); + // let version = key.get_value("DisplayVersion").unwrap_or(OsString::new()); - // Trick, I saw on internet to point to the exe location.. - let exe_path: OsString = key.get_value("DisplayIcon").unwrap_or_default(); - if exe_path.is_empty() { - return; - } - // if there is something, it will be in the form of - // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 - let exe_string = exe_path.to_string_lossy(); - let exe_string = exe_string.split(',').next().unwrap(); + // Trick, I saw on internet to point to the exe location.. + let exe_path: OsString = key.get_value("DisplayIcon").unwrap_or_default(); + if exe_path.is_empty() { + return; + } + // if there is something, it will be in the form of + // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 + let exe_string = exe_path.to_string_lossy(); + let exe_string = exe_string.split(',').next().unwrap(); - // make sure it ends with .exe - if !exe_string.ends_with(".exe") { - return; - } + // make sure it ends with .exe + if !exe_string.ends_with(".exe") { + return; + } - if !display_name.is_empty() { - apps.push(App::new_executable( - &display_name.clone().to_string_lossy(), - &display_name.clone().to_string_lossy().to_lowercase(), - "Application", - exe_path, - None, - )); - } - }); - }); + if !display_name.is_empty() { + apps.push(App::new_executable( + &display_name.clone().to_string_lossy(), + &display_name.clone().to_string_lossy().to_lowercase(), + "Application", + exe_path, + None, + )); + } + } } /// Returns the set of known paths diff --git a/src/cross_platform/windows/mod.rs b/src/cross_platform/windows/mod.rs index 970d626..2dd575b 100644 --- a/src/cross_platform/windows/mod.rs +++ b/src/cross_platform/windows/mod.rs @@ -3,6 +3,7 @@ use windows::Win32::{Globalization::GetACP, UI::WindowsAndMessaging::GetCursorPo pub mod app_finding; +#[allow(clippy::cast_precision_loss)] pub fn open_on_focused_monitor() -> iced::Point { use windows::Win32::Foundation::POINT; use windows::Win32::Graphics::Gdi::{ @@ -11,6 +12,8 @@ pub fn open_on_focused_monitor() -> iced::Point { use crate::app::{DEFAULT_WINDOW_HEIGHT, WINDOW_WIDTH}; let mut point = POINT { x: 0, y: 0 }; + + #[allow(clippy::cast_possible_truncation)] let mut monitor_info = MONITORINFO { cbSize: std::mem::size_of::() as u32, ..Default::default() @@ -33,6 +36,7 @@ pub fn open_on_focused_monitor() -> iced::Point { /// Wrapper over `GetACP` that defaults to `WINDOWS_1252` if the ACP isn't found pub fn get_acp() -> Encoding { + #[allow(clippy::cast_possible_truncation)] unsafe { codepage::to_encoding(GetACP() as u16) }.unwrap_or_else(|| { tracing::warn!( "ACP not found, falling back to WINDOWS_1252 as the default shortcut encoding" diff --git a/src/main.rs b/src/main.rs index 54c893b..47bd035 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ use tracing_subscriber::layer::SubscriberExt; #[cfg(target_os = "linux")] const SOCKET_PATH: &str = "/tmp/rustcast.sock"; +#[allow(clippy::too_many_lines)] // Not reasonably splittable without being less readable fn main() -> iced::Result { #[cfg(target_os = "macos")] cross_platform::macos::set_activation_policy_accessory(); diff --git a/src/utils.rs b/src/utils.rs index 55510a5..503dcd2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -241,10 +241,7 @@ pub fn is_url_like(s: &str) -> bool { } let mut parts = s.split('.'); - let tld = match parts.next_back() { - Some(p) => p, - None => return false, - }; + let Some(tld) = parts.next_back() else { return false }; if tld.is_empty() || tld.len() > 63 || !tld.chars().all(|c| c.is_ascii_alphabetic()) { return false; From 4a42fdab726e6936fc0ae2db6a85a59fac8ad3f5 Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 21:07:31 +0000 Subject: [PATCH 06/12] fix: stuff --- src/cross_platform/windows/app_finding.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cross_platform/windows/app_finding.rs b/src/cross_platform/windows/app_finding.rs index 14a26e7..2898fab 100644 --- a/src/cross_platform/windows/app_finding.rs +++ b/src/cross_platform/windows/app_finding.rs @@ -57,14 +57,15 @@ pub fn get_apps_from_registry(apps: &mut Vec) { let exe_string = exe_string.split(',').next().unwrap(); // make sure it ends with .exe - if !exe_string.ends_with(".exe") { + if !exe_string.to_lowercase().ends_with(".exe"){ return; } + let display_name = display_name.to_string_lossy(); if !display_name.is_empty() { apps.push(App::new_executable( - &display_name.clone().to_string_lossy(), - &display_name.clone().to_string_lossy().to_lowercase(), + &display_name, + &display_name, "Application", exe_path, None, From 404b352e40f3759ecdf350d3f00ed1e4e84abc3c Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 21:14:04 +0000 Subject: [PATCH 07/12] clean: cargo fmt --- src/app/pages/clipboard.rs | 16 +++++++---- src/app/tile/elm.rs | 35 ++++++++++++----------- src/app/tile/mod.rs | 4 ++- src/app/tile/search_query.rs | 12 ++++++-- src/app/tile/update.rs | 15 ++++------ src/calculator.rs | 8 ++++-- src/cross_platform/windows/app_finding.rs | 23 +++++++-------- src/utils.rs | 4 ++- 8 files changed, 65 insertions(+), 52 deletions(-) diff --git a/src/app/pages/clipboard.rs b/src/app/pages/clipboard.rs index a2b24cf..f255e5f 100644 --- a/src/app/pages/clipboard.rs +++ b/src/app/pages/clipboard.rs @@ -16,12 +16,16 @@ pub fn clipboard_view( container(Row::from_vec(vec![ container( scrollable( - clipboard_content.iter().enumerate().map(|(i, content)| { - // I'd be surprised if you get 4 billion entries - #[allow(clippy::cast_possible_truncation)] - content.to_app().render(theme.clone(), i as u32, focus_id) - }).collect::>() - .width(WINDOW_WIDTH / 3.), + clipboard_content + .iter() + .enumerate() + .map(|(i, content)| { + // I'd be surprised if you get 4 billion entries + #[allow(clippy::cast_possible_truncation)] + content.to_app().render(theme.clone(), i as u32, focus_id) + }) + .collect::>() + .width(WINDOW_WIDTH / 3.), ) .id("results"), ) diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 9a531f0..28a8f1f 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -200,24 +200,25 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { } else if tile.results.is_empty() { space().into() } else if tile.page == Page::EmojiSearch { - let results: Vec<_> = tile.emoji_apps - .search_prefix(&tile.query_lc) - .map(std::borrow::ToOwned::to_owned) - .collect(); - - emoji_page( - tile.config.theme.clone(), - &results, - tile.focus_id, - ) + let results: Vec<_> = tile + .emoji_apps + .search_prefix(&tile.query_lc) + .map(std::borrow::ToOwned::to_owned) + .collect(); + + emoji_page(tile.config.theme.clone(), &results, tile.focus_id) } else { - container(tile.results.iter().enumerate().map( - |(i, app)| { - #[allow(clippy::cast_possible_truncation)] - app.clone() - .render(tile.config.theme.clone(), i as u32, tile.focus_id) - } - ).collect::>()) + container( + tile.results + .iter() + .enumerate() + .map(|(i, app)| { + #[allow(clippy::cast_possible_truncation)] + app.clone() + .render(tile.config.theme.clone(), i as u32, tile.focus_id) + }) + .collect::>(), + ) .into() }; diff --git a/src/app/tile/mod.rs b/src/app/tile/mod.rs index ac344b7..df038b3 100644 --- a/src/app/tile/mod.rs +++ b/src/app/tile/mod.rs @@ -328,7 +328,9 @@ fn handle_hot_reloading() -> impl futures::Stream { fn count_dirs_in_dir(dir: &PathBuf) -> usize { // Read the directory; if it fails, treat as empty - let Ok(entries) = fs::read_dir(dir) else { return 0 }; + let Ok(entries) = fs::read_dir(dir) else { + return 0; + }; entries .filter_map(std::result::Result::ok) diff --git a/src/app/tile/search_query.rs b/src/app/tile/search_query.rs index 34ee756..513a9bb 100644 --- a/src/app/tile/search_query.rs +++ b/src/app/tile/search_query.rs @@ -163,7 +163,11 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< let max_elem = cmp::min(5, new_length); if prev_size != new_length && tile.page != Page::ClipboardHistory { - #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] Task::batch([ window::resize( id, @@ -175,7 +179,11 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< Task::done(Message::ChangeFocus(ArrowKey::Left)), ]) } else if tile.page == Page::ClipboardHistory { - #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] Task::batch([ window::resize( id, diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 742ae2f..8910577 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -96,7 +96,8 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Message::ChangeFocus(key) => { - #[allow(clippy::cast_possible_truncation)] // No, there won't be more than 2^32-1 items in a list + #[allow(clippy::cast_possible_truncation)] + // No, there won't be more than 2^32-1 items in a list let len = match tile.page { Page::ClipboardHistory => tile.clipboard_content.len() as u32, Page::EmojiSearch => tile.emoji_apps.search_prefix(&tile.query_lc).count() as u32, // or tile.results.len() @@ -154,11 +155,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ]) } - Message::OpenFocused => match tile - .results - .get(tile.focus_id as usize) - .map(|x| &x.data) - { + Message::OpenFocused => match tile.results.get(tile.focus_id as usize).map(|x| &x.data) { Some(AppData::Builtin { command: AppCommand::Function(func), .. @@ -177,8 +174,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::ReloadConfig => { let new_config: Config = match toml::from_str( &fs::read_to_string( - std::env::var("HOME").unwrap_or_default() - + "/.config/rustcast/config.toml", + std::env::var("HOME").unwrap_or_default() + "/.config/rustcast/config.toml", ) .unwrap_or_default(), ) { @@ -314,8 +310,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { if focused { Task::none() } else if cfg!(target_os = "macos") { - Task::done(Message::HideWindow(wid)) - .chain(Task::done(Message::ClearSearchQuery)) + Task::done(Message::HideWindow(wid)).chain(Task::done(Message::ClearSearchQuery)) } else { // linux seems to not wanna unfocus it on start making it not show Task::none() diff --git a/src/calculator.rs b/src/calculator.rs index 9258de8..25b6da1 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -49,8 +49,8 @@ pub enum BinOp { impl Expr { pub fn eval(&self) -> Option { - use BinOp::{Add, Sub, Mul, Div, Pow}; - use UnaryOp::{Plus, Minus}; + use BinOp::{Add, Div, Mul, Pow, Sub}; + use UnaryOp::{Minus, Plus}; match self { Expr::Number(x) => Some(*x), @@ -151,7 +151,9 @@ impl<'a> Lexer<'a> { fn next_token(&mut self) -> Result { self.skip_ws(); - let Some(c) = self.peek_char() else { return Ok(Token::End) }; + let Some(c) = self.peek_char() else { + return Ok(Token::End); + }; // single-char tokens let tok = match c { diff --git a/src/cross_platform/windows/app_finding.rs b/src/cross_platform/windows/app_finding.rs index 2898fab..2ac7580 100644 --- a/src/cross_platform/windows/app_finding.rs +++ b/src/cross_platform/windows/app_finding.rs @@ -57,7 +57,7 @@ pub fn get_apps_from_registry(apps: &mut Vec) { let exe_string = exe_string.split(',').next().unwrap(); // make sure it ends with .exe - if !exe_string.to_lowercase().ends_with(".exe"){ + if !exe_string.to_lowercase().ends_with(".exe") { return; } @@ -112,17 +112,16 @@ pub fn index_start_menu() -> Vec { let target = x.link_target(); let file_name = path.file_name().to_string_lossy().to_string(); - if let Some(target) = target { Some(App::new_executable( - &file_name, - &file_name, - "", - PathBuf::from(target.clone()), - None, - )) } else { - tracing::debug!( - "Link at {} has no target, skipped", - path.path().display() - ); + if let Some(target) = target { + Some(App::new_executable( + &file_name, + &file_name, + "", + PathBuf::from(target.clone()), + None, + )) + } else { + tracing::debug!("Link at {} has no target, skipped", path.path().display()); None } } diff --git a/src/utils.rs b/src/utils.rs index 503dcd2..afdfe57 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -241,7 +241,9 @@ pub fn is_url_like(s: &str) -> bool { } let mut parts = s.split('.'); - let Some(tld) = parts.next_back() else { return false }; + let Some(tld) = parts.next_back() else { + return false; + }; if tld.is_empty() || tld.len() > 63 || !tld.chars().all(|c| c.is_ascii_alphabetic()) { return false; From 27dece10f728b94c269d76fb476dad3b13407a19 Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Thu, 12 Feb 2026 22:48:18 +0000 Subject: [PATCH 08/12] refactor!: extract application discovery to its own module --- src/{cross_platform => app_finding}/linux.rs | 0 src/app_finding/macos.rs | 147 +++++++++++++++++++ src/app_finding/mod.rs | 6 + src/app_finding/windows.rs | 138 +++++++++++++++++ src/cross_platform/macos/mod.rs | 143 ------------------ src/main.rs | 1 + 6 files changed, 292 insertions(+), 143 deletions(-) rename src/{cross_platform => app_finding}/linux.rs (100%) create mode 100644 src/app_finding/macos.rs create mode 100644 src/app_finding/mod.rs create mode 100644 src/app_finding/windows.rs diff --git a/src/cross_platform/linux.rs b/src/app_finding/linux.rs similarity index 100% rename from src/cross_platform/linux.rs rename to src/app_finding/linux.rs diff --git a/src/app_finding/macos.rs b/src/app_finding/macos.rs new file mode 100644 index 0000000..b823743 --- /dev/null +++ b/src/app_finding/macos.rs @@ -0,0 +1,147 @@ +use std::path::PathBuf; +use std::process::exit; +use crate::{app::apps::App, config::Config, utils::index_installed_apps}; + + +fn get_installed_apps(dir: impl AsRef, store_icons: bool) -> Vec { + let entries: Vec<_> = fs::read_dir(dir.as_ref()) + .unwrap_or_else(|x| { + tracing::error!( + "An error occurred while reading dir ({}) {}", + dir.as_ref().to_str().unwrap_or(""), + x + ); + exit(-1) + }) + .filter_map(|x| x.ok()) + .collect(); + + entries + .into_par_iter() + .filter_map(|x| { + let file_type = x.file_type().unwrap_or_else(|e| { + tracing::error!("Failed to get file type: {}", e.to_string()); + exit(-1) + }); + if !file_type.is_dir() { + return None; + } + + let file_name_os = x.file_name(); + let file_name = file_name_os.into_string().unwrap_or_else(|e| { + tracing::error!("Failed to to get file_name_os: {}", e.to_string_lossy()); + exit(-1) + }); + if !file_name.ends_with(".app") { + return None; + } + + let path = x.path(); + let path_str = path.to_str().map(|x| x.to_string()).unwrap_or_else(|| { + tracing::error!("Unable to get file_name"); + exit(-1) + }); + + let icons = if store_icons { + match fs::read_to_string(format!("{}/Contents/Info.plist", path_str)).map( + |content| { + let icon_line = content + .lines() + .scan(false, |expect_next, line| { + if *expect_next { + *expect_next = false; + // Return this line to the iterator + return Some(Some(line)); + } + + if line.trim() == "CFBundleIconFile" { + *expect_next = true; + } + + // For lines that are not the one after the key, return None to skip + Some(None) + }) + .flatten() // remove the Nones + .next() + .map(|x| { + x.trim() + .strip_prefix("") + .unwrap_or("") + .strip_suffix("") + .unwrap_or("") + }); + + handle_from_icns(Path::new(&format!( + "{}/Contents/Resources/{}", + path_str, + icon_line.unwrap_or("AppIcon.icns") + ))) + }, + ) { + Ok(Some(a)) => Some(a), + _ => { + // Fallback method + let direntry = fs::read_dir(format!("{}/Contents/Resources", path_str)) + .into_iter() + .flatten() + .filter_map(|x| { + let file = x.ok()?; + let name = file.file_name(); + let file_name = name.to_str()?; + if file_name.ends_with(".icns") { + Some(file.path()) + } else { + None + } + }) + .collect::>(); + + if direntry.len() > 1 { + let icns_vec = direntry + .iter() + .filter(|x| x.ends_with("AppIcon.icns")) + .collect::>(); + handle_from_icns(icns_vec.first().unwrap_or(&&PathBuf::new())) + } else if !direntry.is_empty() { + handle_from_icns(direntry.first().unwrap_or(&PathBuf::new())) + } else { + None + } + } + } + } else { + None + }; + + let name = file_name.strip_suffix(".app").unwrap().to_string(); + Some(App::new_executable( + &name, + &name.to_lowercase(), + "Application", + path, + icons, + )) + }) + .collect() +} + +pub fn get_installed_macos_apps(config: &Config) -> anyhow::Result> { + let store_icons = config.theme.show_icons; + let user_local_path = std::env::var("HOME").unwrap() + "/Applications/"; + let paths: Vec = vec![ + "/Applications/".to_string(), + user_local_path.to_string(), + "/System/Applications/".to_string(), + "/System/Applications/Utilities/".to_string(), + ]; + + let mut apps = index_installed_apps(config)?; + apps.par_extend( + paths + .par_iter() + .map(|path| get_installed_apps(path, store_icons)) + .flatten(), + ); + + Ok(apps) +} \ No newline at end of file diff --git a/src/app_finding/mod.rs b/src/app_finding/mod.rs new file mode 100644 index 0000000..fb35e85 --- /dev/null +++ b/src/app_finding/mod.rs @@ -0,0 +1,6 @@ +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "windows")] +mod windows; \ No newline at end of file diff --git a/src/app_finding/windows.rs b/src/app_finding/windows.rs new file mode 100644 index 0000000..cb4ee4d --- /dev/null +++ b/src/app_finding/windows.rs @@ -0,0 +1,138 @@ +use { + crate::{app::apps::App, cross_platform::windows::get_acp}, + std::{iter::repeat, path::PathBuf}, + walkdir::WalkDir, + windows::{ + Win32::{ + System::Com::CoTaskMemFree, + UI::Shell::{ + FOLDERID_LocalAppData, FOLDERID_ProgramFiles, FOLDERID_ProgramFilesX86, + KF_FLAG_DEFAULT, SHGetKnownFolderPath, + }, + }, + core::GUID, + }, +}; + +/// Loads apps from the registry keys `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` and +/// `SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`. `apps` has the relvant items +/// appended to it. +/// +/// Based on +pub fn get_apps_from_registry(apps: &mut Vec) { + use std::ffi::OsString; + let hkey = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); + + let registers = [ + hkey.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall") + .unwrap(), + hkey.open_subkey("SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall") + .unwrap(), + ]; + + for (key, reg) in registers.iter().flat_map(|reg| reg.enum_keys().zip(repeat(reg))) { + // Not debug only just because it doesn't run too often + tracing::trace!("App added [reg]: {:?}", key); + + // https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key + let name = key.unwrap(); + let key = reg.open_subkey(&name).unwrap(); + let display_name: OsString = key.get_value("DisplayName").unwrap_or_default(); + + // they might be useful one day ? + // let publisher = key.get_value("Publisher").unwrap_or(OsString::new()); + // let version = key.get_value("DisplayVersion").unwrap_or(OsString::new()); + + // Trick, I saw on internet to point to the exe location.. + let exe_path: OsString = key.get_value("DisplayIcon").unwrap_or_default(); + if exe_path.is_empty() { + return; + } + // if there is something, it will be in the form of + // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 + let exe_string = exe_path.to_string_lossy(); + let exe_string = exe_string.split(',').next().unwrap(); + + // make sure it ends with .exe + if !exe_string.to_lowercase().ends_with(".exe") { + return; + } + + if !display_name.is_empty() { + apps.push(App::new_executable( + &display_name.clone().to_string_lossy(), + &display_name.clone().to_string_lossy().to_lowercase(), + "Application", + exe_path, + None, + )); + } + } +} + +/// Returns the set of known paths +pub fn get_known_paths() -> Vec { + let paths = vec![ + get_windows_path(&FOLDERID_ProgramFiles).unwrap_or_default(), + get_windows_path(&FOLDERID_ProgramFilesX86).unwrap_or_default(), + (get_windows_path(&FOLDERID_LocalAppData) + .unwrap_or_default() + .join("Programs")), + ]; + paths +} + +/// Wrapper around `SHGetKnownFolderPath` to get paths to known folders +fn get_windows_path(folder_id: &GUID) -> Option { + unsafe { + let folder = SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None); + if let Ok(folder) = folder { + let path = folder.to_string().ok()?; + + #[allow(clippy::ptr_as_ptr)] + CoTaskMemFree(Some(folder.0 as *mut _)); + + Some(path.into()) + } else { + None + } + } +} + +pub fn index_start_menu() -> Vec { + WalkDir::new(r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs") + .into_iter() + .filter_map(std::result::Result::ok) + .filter_map(|path| { + let lnk = lnk::ShellLink::open(path.path(), get_acp()); + + match lnk { + Ok(x) => { + let target = x.link_target(); + let file_name = path.file_name().to_string_lossy().to_string(); + + if let Some(target) = target { Some(App::new_executable( + &file_name, + &file_name, + "", + PathBuf::from(target.clone()), + None, + )) } else { + tracing::debug!( + "Link at {} has no target, skipped", + path.path().display() + ); + None + } + } + Err(e) => { + tracing::debug!( + "Error opening link {} ({e}), skipped", + path.path().to_string_lossy() + ); + None + } + } + }) + .collect() +} diff --git a/src/cross_platform/macos/mod.rs b/src/cross_platform/macos/mod.rs index 48af4d8..f9a34cb 100644 --- a/src/cross_platform/macos/mod.rs +++ b/src/cross_platform/macos/mod.rs @@ -102,149 +102,6 @@ pub fn transform_process_to_ui_element() -> u32 { } } -fn get_installed_apps(dir: impl AsRef, store_icons: bool) -> Vec { - let entries: Vec<_> = fs::read_dir(dir.as_ref()) - .unwrap_or_else(|x| { - tracing::error!( - "An error occurred while reading dir ({}) {}", - dir.as_ref().to_str().unwrap_or(""), - x - ); - exit(-1) - }) - .filter_map(|x| x.ok()) - .collect(); - - entries - .into_par_iter() - .filter_map(|x| { - let file_type = x.file_type().unwrap_or_else(|e| { - tracing::error!("Failed to get file type: {}", e.to_string()); - exit(-1) - }); - if !file_type.is_dir() { - return None; - } - - let file_name_os = x.file_name(); - let file_name = file_name_os.into_string().unwrap_or_else(|e| { - tracing::error!("Failed to to get file_name_os: {}", e.to_string_lossy()); - exit(-1) - }); - if !file_name.ends_with(".app") { - return None; - } - - let path = x.path(); - let path_str = path.to_str().map(|x| x.to_string()).unwrap_or_else(|| { - tracing::error!("Unable to get file_name"); - exit(-1) - }); - - let icons = if store_icons { - match fs::read_to_string(format!("{}/Contents/Info.plist", path_str)).map( - |content| { - let icon_line = content - .lines() - .scan(false, |expect_next, line| { - if *expect_next { - *expect_next = false; - // Return this line to the iterator - return Some(Some(line)); - } - - if line.trim() == "CFBundleIconFile" { - *expect_next = true; - } - - // For lines that are not the one after the key, return None to skip - Some(None) - }) - .flatten() // remove the Nones - .next() - .map(|x| { - x.trim() - .strip_prefix("") - .unwrap_or("") - .strip_suffix("") - .unwrap_or("") - }); - - handle_from_icns(Path::new(&format!( - "{}/Contents/Resources/{}", - path_str, - icon_line.unwrap_or("AppIcon.icns") - ))) - }, - ) { - Ok(Some(a)) => Some(a), - _ => { - // Fallback method - let direntry = fs::read_dir(format!("{}/Contents/Resources", path_str)) - .into_iter() - .flatten() - .filter_map(|x| { - let file = x.ok()?; - let name = file.file_name(); - let file_name = name.to_str()?; - if file_name.ends_with(".icns") { - Some(file.path()) - } else { - None - } - }) - .collect::>(); - - if direntry.len() > 1 { - let icns_vec = direntry - .iter() - .filter(|x| x.ends_with("AppIcon.icns")) - .collect::>(); - handle_from_icns(icns_vec.first().unwrap_or(&&PathBuf::new())) - } else if !direntry.is_empty() { - handle_from_icns(direntry.first().unwrap_or(&PathBuf::new())) - } else { - None - } - } - } - } else { - None - }; - - let name = file_name.strip_suffix(".app").unwrap().to_string(); - Some(App::new_executable( - &name, - &name.to_lowercase(), - "Application", - path, - icons, - )) - }) - .collect() -} - -pub fn get_installed_macos_apps(config: &Config) -> anyhow::Result> { - let store_icons = config.theme.show_icons; - let user_local_path = std::env::var("HOME").unwrap() + "/Applications/"; - let paths: Vec = vec![ - "/Applications/".to_string(), - user_local_path.to_string(), - "/System/Applications/".to_string(), - "/System/Applications/Utilities/".to_string(), - ]; - - let mut apps = index_installed_apps(config)?; - apps.par_extend( - paths - .par_iter() - .map(|path| get_installed_apps(path, store_icons)) - .flatten(), - ); - - Ok(apps) -} - /// Open the settings file with the system default editor pub fn open_settings() { thread::spawn(move || { diff --git a/src/main.rs b/src/main.rs index 47bd035..3c97454 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod config; mod styles; mod unit_conversion; mod utils; +mod app_finding; mod cross_platform; From 9ce7121e8b2c40e5a1acfdb5b0b7fbcdccf383fe Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Fri, 13 Feb 2026 19:19:44 +0000 Subject: [PATCH 09/12] clean: general minor fixes --- src/app_finding/windows.rs | 103 ++++++++++------------ src/cross_platform/windows/app_finding.rs | 38 +++----- 2 files changed, 62 insertions(+), 79 deletions(-) diff --git a/src/app_finding/windows.rs b/src/app_finding/windows.rs index 66917c2..714a19d 100644 --- a/src/app_finding/windows.rs +++ b/src/app_finding/windows.rs @@ -1,6 +1,6 @@ use { crate::{app::apps::App, cross_platform::windows::get_acp}, - std::path::PathBuf, + std::{iter::repeat, path::PathBuf}, walkdir::WalkDir, windows::{ Win32::{ @@ -18,7 +18,7 @@ use { /// `SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`. `apps` has the relvant items /// appended to it. /// -/// Based on https://stackoverflow.com/questions/2864984 +/// Based on pub fn get_apps_from_registry(apps: &mut Vec) { use std::ffi::OsString; let hkey = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); @@ -30,46 +30,44 @@ pub fn get_apps_from_registry(apps: &mut Vec) { .unwrap(), ]; - registers.iter().for_each(|reg| { - reg.enum_keys().for_each(|key| { - // Not debug only just because it doesn't run too often - tracing::trace!("App added [reg]: {:?}", key); + for (key, reg) in registers.iter().flat_map(|reg| reg.enum_keys().zip(repeat(reg))) { + // Not debug only just because it doesn't run too often + tracing::trace!("App added [reg]: {:?}", key); - // https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key - let name = key.unwrap(); - let key = reg.open_subkey(&name).unwrap(); - let display_name: OsString = key.get_value("DisplayName").unwrap_or_default(); + // https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key + let name = key.unwrap(); + let key = reg.open_subkey(&name).unwrap(); + let display_name: OsString = key.get_value("DisplayName").unwrap_or_default(); - // they might be useful one day ? - // let publisher = key.get_value("Publisher").unwrap_or(OsString::new()); - // let version = key.get_value("DisplayVersion").unwrap_or(OsString::new()); + // they might be useful one day ? + // let publisher = key.get_value("Publisher").unwrap_or(OsString::new()); + // let version = key.get_value("DisplayVersion").unwrap_or(OsString::new()); - // Trick, I saw on internet to point to the exe location.. - let exe_path: OsString = key.get_value("DisplayIcon").unwrap_or_default(); - if exe_path.is_empty() { - return; - } - // if there is something, it will be in the form of - // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 - let exe_string = exe_path.to_string_lossy(); - let exe_string = exe_string.split(",").next().unwrap(); + // Trick, I saw on internet to point to the exe location.. + let exe_path: OsString = key.get_value("DisplayIcon").unwrap_or_default(); + if exe_path.is_empty() { + return; + } + // if there is something, it will be in the form of + // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 + let exe_string = exe_path.to_string_lossy(); + let exe_string = exe_string.split(',').next().unwrap(); - // make sure it ends with .exe - if !exe_string.ends_with(".exe") { - return; - } + // make sure it ends with .exe + if !exe_string.to_lowercase().ends_with(".exe") { + return; + } - if !display_name.is_empty() { - apps.push(App::new_executable( - &display_name.clone().to_string_lossy(), - &display_name.clone().to_string_lossy().to_lowercase(), - "Application", - exe_path, - None, - )) - } - }); - }); + if !display_name.is_empty() { + apps.push(App::new_executable( + &display_name.clone().to_string_lossy(), + &display_name.clone().to_string_lossy().to_lowercase(), + "Application", + exe_path, + None, + )); + } + } } /// Returns the set of known paths @@ -90,7 +88,7 @@ fn get_windows_path(folder_id: &GUID) -> Option { let folder = SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None); if let Ok(folder) = folder { let path = folder.to_string().ok()?; - CoTaskMemFree(Some(folder.0 as *mut _)); + CoTaskMemFree(Some(folder.0.cast())); Some(path.into()) } else { None @@ -101,7 +99,7 @@ fn get_windows_path(folder_id: &GUID) -> Option { pub fn index_start_menu() -> Vec { WalkDir::new(r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs") .into_iter() - .filter_map(|x| x.ok()) + .filter_map(std::result::Result::ok) .filter_map(|path| { let lnk = lnk::ShellLink::open(path.path(), get_acp()); @@ -110,21 +108,18 @@ pub fn index_start_menu() -> Vec { let target = x.link_target(); let file_name = path.file_name().to_string_lossy().to_string(); - match target { - Some(target) => Some(App::new_executable( - &file_name, - &file_name, - "", - PathBuf::from(target.clone()), - None, - )), - None => { - tracing::debug!( - "Link at {} has no target, skipped", - path.path().display() - ); - None - } + if let Some(target) = target { Some(App::new_executable( + &file_name, + &file_name, + "", + PathBuf::from(target.clone()), + None, + )) } else { + tracing::debug!( + "Link at {} has no target, skipped", + path.path().display() + ); + None } } Err(e) => { diff --git a/src/cross_platform/windows/app_finding.rs b/src/cross_platform/windows/app_finding.rs index fd5decf..fafc281 100644 --- a/src/cross_platform/windows/app_finding.rs +++ b/src/cross_platform/windows/app_finding.rs @@ -35,7 +35,9 @@ pub fn get_apps_from_registry(apps: &mut Vec) { .flat_map(|reg| reg.enum_keys().zip(std::iter::repeat(reg))) { // Not debug only just because it doesn't run too often - tracing::trace!( + tracing::trace!("App added [reg]: {:?}", key); + // Not debug only just because it doesn't run too often + tracing::trace!( target: "reg_app_search", "App added: {:?}", key @@ -117,6 +119,11 @@ pub fn index_start_menu() -> Vec { let file_name = path.file_name().to_string_lossy().to_string(); if let Some(target) = target { + tracing::trace!( + target: "smenu_app_search", + "Link at {} added", + path.path().display() + ); Some(App::new_executable( &file_name, &file_name, @@ -125,31 +132,12 @@ pub fn index_start_menu() -> Vec { None, )) } else { - tracing::debug!("Link at {} has no target, skipped", path.path().display()); + tracing::trace!( + target: "smenu_app_search", + "Link at {} has no target, skipped", + path.path().display() + ); None - match target { - Some(target) => { - tracing::trace!( - target: "smenu_app_search", - "Link at {} added", - path.path().display() - ); - Some(App::new_executable( - &file_name, - &file_name, - "", - PathBuf::from(target.clone()), - None, - )) - }, - None => { - tracing::trace!( - target: "smenu_app_search", - "Link at {} has no target, skipped", - path.path().display() - ); - None - } } } Err(e) => { From b67752651f50973318569e1974047ec4b44830e5 Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Sat, 14 Feb 2026 15:55:48 +0000 Subject: [PATCH 10/12] fix: unbreaking --- src/app/tile/elm.rs | 6 +- src/app/tile/search_query.rs | 2 +- src/app/tile/update.rs | 2 +- src/app_finding/macos.rs | 147 ------------------ src/app_finding/mod.rs | 175 +++++++++++++++++++++- src/app_finding/windows.rs | 24 +-- src/config/mod.rs | 5 +- src/cross_platform/macos/mod.rs | 11 +- src/cross_platform/windows/app_finding.rs | 154 ------------------- src/cross_platform/windows/mod.rs | 2 - src/main.rs | 6 +- src/utils.rs | 164 +------------------- 12 files changed, 200 insertions(+), 498 deletions(-) delete mode 100644 src/cross_platform/windows/app_finding.rs diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 79c15cd..0db89dd 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -13,15 +13,13 @@ use iced::{Length::Fill, widget::text_input}; use rayon::slice::ParallelSliceMut; -#[cfg(target_os = "windows")] -use crate::app; use crate::app::WINDOW_WIDTH; use crate::app::pages::clipboard::clipboard_view; use crate::app::pages::emoji::emoji_page; use crate::app::tile::AppIndex; use crate::config::Theme; use crate::styles::{contents_style, rustcast_text_input_style, tint, with_alpha}; -use crate::utils::index_installed_apps; +use crate::app_finding::index_installed_apps; use crate::{ app::{Message, Page, apps::App, default_settings, tile::Tile}, config::Config, @@ -160,7 +158,7 @@ pub fn new( clipboard_hotkey: config .clipboard_hotkey .clone() - .and_then(|x| x.parse::().ok()), + .and_then(|x: String| x.parse().ok()), }, open, ) diff --git a/src/app/tile/search_query.rs b/src/app/tile/search_query.rs index 513a9bb..567a43c 100644 --- a/src/app/tile/search_query.rs +++ b/src/app/tile/search_query.rs @@ -141,7 +141,7 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< } else if tile.results.is_empty() && tile.query_lc == "lemon" { #[cfg(target_os = "macos")] { - use std::path::Path; + tile.results.push(App::new_builtin( "Easter Egg", diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 87d5738..9c36808 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -19,7 +19,7 @@ use crate::cross_platform::macos; use crate::commands::Function; use crate::config::Config; -use crate::utils::index_installed_apps; +use crate::app_finding::index_installed_apps; #[allow(clippy::too_many_lines)] pub fn handle_update(tile: &mut Tile, message: Message) -> Task { diff --git a/src/app_finding/macos.rs b/src/app_finding/macos.rs index b823743..e69de29 100644 --- a/src/app_finding/macos.rs +++ b/src/app_finding/macos.rs @@ -1,147 +0,0 @@ -use std::path::PathBuf; -use std::process::exit; -use crate::{app::apps::App, config::Config, utils::index_installed_apps}; - - -fn get_installed_apps(dir: impl AsRef, store_icons: bool) -> Vec { - let entries: Vec<_> = fs::read_dir(dir.as_ref()) - .unwrap_or_else(|x| { - tracing::error!( - "An error occurred while reading dir ({}) {}", - dir.as_ref().to_str().unwrap_or(""), - x - ); - exit(-1) - }) - .filter_map(|x| x.ok()) - .collect(); - - entries - .into_par_iter() - .filter_map(|x| { - let file_type = x.file_type().unwrap_or_else(|e| { - tracing::error!("Failed to get file type: {}", e.to_string()); - exit(-1) - }); - if !file_type.is_dir() { - return None; - } - - let file_name_os = x.file_name(); - let file_name = file_name_os.into_string().unwrap_or_else(|e| { - tracing::error!("Failed to to get file_name_os: {}", e.to_string_lossy()); - exit(-1) - }); - if !file_name.ends_with(".app") { - return None; - } - - let path = x.path(); - let path_str = path.to_str().map(|x| x.to_string()).unwrap_or_else(|| { - tracing::error!("Unable to get file_name"); - exit(-1) - }); - - let icons = if store_icons { - match fs::read_to_string(format!("{}/Contents/Info.plist", path_str)).map( - |content| { - let icon_line = content - .lines() - .scan(false, |expect_next, line| { - if *expect_next { - *expect_next = false; - // Return this line to the iterator - return Some(Some(line)); - } - - if line.trim() == "CFBundleIconFile" { - *expect_next = true; - } - - // For lines that are not the one after the key, return None to skip - Some(None) - }) - .flatten() // remove the Nones - .next() - .map(|x| { - x.trim() - .strip_prefix("") - .unwrap_or("") - .strip_suffix("") - .unwrap_or("") - }); - - handle_from_icns(Path::new(&format!( - "{}/Contents/Resources/{}", - path_str, - icon_line.unwrap_or("AppIcon.icns") - ))) - }, - ) { - Ok(Some(a)) => Some(a), - _ => { - // Fallback method - let direntry = fs::read_dir(format!("{}/Contents/Resources", path_str)) - .into_iter() - .flatten() - .filter_map(|x| { - let file = x.ok()?; - let name = file.file_name(); - let file_name = name.to_str()?; - if file_name.ends_with(".icns") { - Some(file.path()) - } else { - None - } - }) - .collect::>(); - - if direntry.len() > 1 { - let icns_vec = direntry - .iter() - .filter(|x| x.ends_with("AppIcon.icns")) - .collect::>(); - handle_from_icns(icns_vec.first().unwrap_or(&&PathBuf::new())) - } else if !direntry.is_empty() { - handle_from_icns(direntry.first().unwrap_or(&PathBuf::new())) - } else { - None - } - } - } - } else { - None - }; - - let name = file_name.strip_suffix(".app").unwrap().to_string(); - Some(App::new_executable( - &name, - &name.to_lowercase(), - "Application", - path, - icons, - )) - }) - .collect() -} - -pub fn get_installed_macos_apps(config: &Config) -> anyhow::Result> { - let store_icons = config.theme.show_icons; - let user_local_path = std::env::var("HOME").unwrap() + "/Applications/"; - let paths: Vec = vec![ - "/Applications/".to_string(), - user_local_path.to_string(), - "/System/Applications/".to_string(), - "/System/Applications/Utilities/".to_string(), - ]; - - let mut apps = index_installed_apps(config)?; - apps.par_extend( - paths - .par_iter() - .map(|path| get_installed_apps(path, store_icons)) - .flatten(), - ); - - Ok(apps) -} \ No newline at end of file diff --git a/src/app_finding/mod.rs b/src/app_finding/mod.rs index fb35e85..ad81f65 100644 --- a/src/app_finding/mod.rs +++ b/src/app_finding/mod.rs @@ -1,6 +1,179 @@ +use std::path::Path; + +use crate::{app::apps::App, config::Config, utils::{get_config_file_path, read_config_file}}; +use rayon::prelude::*; + +#[cfg(target_os = "macos")] +use std::time::Instant; + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "windows")] -mod windows; \ No newline at end of file +mod windows; + +// Since this is useful externally +#[cfg(target_os = "windows")] +pub use self::windows::get_known_paths; + + +/// Recursively loads apps from a set of folders. +/// +/// [`exclude_patterns`] is a set of glob patterns to include, while [`include_patterns`] is a set of +/// patterns to include ignoring [`exclude_patterns`]. +fn search_dir( + path: impl AsRef, + exclude_patterns: &[glob::Pattern], + include_patterns: &[glob::Pattern], + max_depth: usize, +) -> impl ParallelIterator { + use walkdir::WalkDir; + + WalkDir::new(path.as_ref()) + .follow_links(false) + .max_depth(max_depth) + .into_iter() + .par_bridge() + .filter_map(std::result::Result::ok) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "exe")) + .filter_map(|entry| { + let path = entry.path(); + + if exclude_patterns.iter().any(|x| x.matches_path(path)) + && !include_patterns.iter().any(|x| x.matches_path(path)) + { + #[cfg(debug_assertions)] + tracing::trace!( + target: "dir_app_search", + "App excluded: {:?}", path.to_str() + ); + + return None; + } + + let file_name = path.file_name().unwrap().to_string_lossy(); + let name = file_name.replace(".exe", ""); + + #[cfg(debug_assertions)] + tracing::trace!( + target: "dir_app_search", + "App added: {:?}", path.to_str() + ); + + Some(App::new_executable( + &name, + &name.to_lowercase(), + "Application", + path, + None, + )) + }) +} + +/// The "main" function. Indexes all apps, given a config. +pub fn index_installed_apps(config: &Config) -> anyhow::Result> { + tracing::debug!("Indexing installed apps"); + tracing::debug!("Exclude patterns: {:?}", &config.index_exclude_patterns); + tracing::debug!("Include patterns: {:?}", &config.index_include_patterns); + + let path = get_config_file_path(); + let config = read_config_file(path.as_path())?; + + if config.index_dirs.is_empty() { + tracing::debug!("No extra index dirs provided"); + } + + #[cfg(target_os = "windows")] + { + use std::time::Instant; + + use self::windows::get_apps_from_registry; + use self::windows::index_start_menu; + + let start = Instant::now(); + + let mut other_apps = index_start_menu(); + get_apps_from_registry(&mut other_apps); + + let res = config + .index_dirs + .par_iter() + .flat_map(|x| { + search_dir( + &x.path, + &config.index_exclude_patterns, + &config.index_include_patterns, + x.max_depth, + ) + }) + .chain(other_apps.into_par_iter()) + .collect(); + + let end = Instant::now(); + tracing::info!( + "Finished indexing apps (t = {}s)", + (end - start).as_secs_f32() + ); + + Ok(res) + } + + #[cfg(target_os = "macos")] + { + let start = Instant::now(); + + let res = config + .index_dirs + .par_iter() + .flat_map(|x| { + search_dir( + &x.path, + &config.index_exclude_patterns, + &config.index_include_patterns, + x.max_depth, + ) + }) + .collect(); + + let end = Instant::now(); + tracing::info!( + "Finished indexing apps (t = {}s)", + (end - start).as_secs_f32() + ); + + Ok(res) + } + + #[cfg(target_os = "linux")] + { + let start = Instant::now(); + + let other_apps = get_installed_linux_apps(&config); + + let start2 = Instant::now(); + + let res = config + .index_dirs + .par_iter() + .flat_map(|x| { + search_dir( + &x.path, + &config.index_exclude_patterns, + &config.index_include_patterns, + x.max_depth, + ) + }) + .chain(other_apps.into_par_iter()) + .collect(); + + let end = Instant::now(); + tracing::info!( + "Finished indexing apps (t = {}s) (t2 = {}s)", + (end - start).as_secs_f32(), + (end - start2).as_secs_f32(), + ); + + Ok(res) + } +} \ No newline at end of file diff --git a/src/app_finding/windows.rs b/src/app_finding/windows.rs index 714a19d..356f981 100644 --- a/src/app_finding/windows.rs +++ b/src/app_finding/windows.rs @@ -70,18 +70,6 @@ pub fn get_apps_from_registry(apps: &mut Vec) { } } -/// Returns the set of known paths -pub fn get_known_paths() -> Vec { - let paths = vec![ - get_windows_path(&FOLDERID_ProgramFiles).unwrap_or_default(), - get_windows_path(&FOLDERID_ProgramFilesX86).unwrap_or_default(), - (get_windows_path(&FOLDERID_LocalAppData) - .unwrap_or_default() - .join("Programs")), - ]; - paths -} - /// Wrapper around `SHGetKnownFolderPath` to get paths to known folders fn get_windows_path(folder_id: &GUID) -> Option { unsafe { @@ -133,3 +121,15 @@ pub fn index_start_menu() -> Vec { }) .collect() } + +/// Returns the set of known paths +pub fn get_known_paths() -> Vec { + let paths = vec![ + get_windows_path(&FOLDERID_ProgramFiles).unwrap_or_default(), + get_windows_path(&FOLDERID_ProgramFilesX86).unwrap_or_default(), + (get_windows_path(&FOLDERID_LocalAppData) + .unwrap_or_default() + .join("Programs")), + ]; + paths +} \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 61fc52f..0eff0e7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,8 +4,9 @@ use std::{path::PathBuf, sync::Arc}; use iced::{Font, font::Family, theme::Custom}; use serde::{Deserialize, Serialize}; -#[cfg(target_os = "windows")] -use crate::cross_platform::windows::app_finding::get_known_paths; +#[cfg(target_os="windows")] +use crate::app_finding::get_known_paths; + use crate::{ app::apps::{App, AppData}, cross_platform::get_img_handle, diff --git a/src/cross_platform/macos/mod.rs b/src/cross_platform/macos/mod.rs index f9a34cb..c214b5b 100644 --- a/src/cross_platform/macos/mod.rs +++ b/src/cross_platform/macos/mod.rs @@ -3,12 +3,7 @@ pub mod haptics; -use crate::app::apps::{App, AppCommand}; -use crate::commands::Function; -use crate::config::Config; -use crate::utils::index_installed_apps; use icns::IconFamily; -use rayon::iter::ParallelExtend; use { iced::wgpu::rwh::RawWindowHandle, iced::wgpu::rwh::WindowHandle, @@ -22,10 +17,8 @@ use { }; use objc2_app_kit::NSWorkspace; -use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; -use std::path::{Path, PathBuf}; -use std::process::exit; -use std::{fs, thread}; +use std::path::Path; +use std::thread; /// This sets the activation policy of the app to Accessory, allowing rustcast to be visible ontop /// of fullscreen apps diff --git a/src/cross_platform/windows/app_finding.rs b/src/cross_platform/windows/app_finding.rs deleted file mode 100644 index fafc281..0000000 --- a/src/cross_platform/windows/app_finding.rs +++ /dev/null @@ -1,154 +0,0 @@ -use { - crate::{app::apps::App, cross_platform::windows::get_acp}, - std::path::PathBuf, - walkdir::WalkDir, - windows::{ - Win32::{ - System::Com::CoTaskMemFree, - UI::Shell::{ - FOLDERID_LocalAppData, FOLDERID_ProgramFiles, FOLDERID_ProgramFilesX86, - KF_FLAG_DEFAULT, SHGetKnownFolderPath, - }, - }, - core::GUID, - }, -}; - -/// Loads apps from the registry keys `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` and -/// `SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`. `apps` has the relvant items -/// appended to it. -/// -/// Based on -pub fn get_apps_from_registry(apps: &mut Vec) { - use std::ffi::OsString; - let hkey = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); - - let registers = [ - hkey.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall") - .unwrap(), - hkey.open_subkey("SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall") - .unwrap(), - ]; - - for (key, reg) in registers - .iter() - .flat_map(|reg| reg.enum_keys().zip(std::iter::repeat(reg))) - { - // Not debug only just because it doesn't run too often - tracing::trace!("App added [reg]: {:?}", key); - // Not debug only just because it doesn't run too often - tracing::trace!( - target: "reg_app_search", - "App added: {:?}", - key - ); - - // https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key - let name = key.unwrap(); - let key = reg.open_subkey(&name).unwrap(); - let display_name: OsString = key.get_value("DisplayName").unwrap_or_default(); - - // they might be useful one day ? - // let publisher = key.get_value("Publisher").unwrap_or(OsString::new()); - // let version = key.get_value("DisplayVersion").unwrap_or(OsString::new()); - - // Trick, I saw on internet to point to the exe location.. - let exe_path: OsString = key.get_value("DisplayIcon").unwrap_or_default(); - if exe_path.is_empty() { - return; - } - // if there is something, it will be in the form of - // "C:\Program Files\Microsoft Office\Office16\WINWORD.EXE",0 - let exe_string = exe_path.to_string_lossy(); - let exe_string = exe_string.split(',').next().unwrap(); - - // make sure it ends with .exe - if !exe_string.to_lowercase().ends_with(".exe") { - return; - } - - let display_name = display_name.to_string_lossy(); - if !display_name.is_empty() { - apps.push(App::new_executable( - &display_name, - &display_name, - "Application", - exe_path, - None, - )); - } - } -} - -/// Returns the set of known paths -pub fn get_known_paths() -> Vec { - let paths = vec![ - get_windows_path(&FOLDERID_ProgramFiles).unwrap_or_default(), - get_windows_path(&FOLDERID_ProgramFilesX86).unwrap_or_default(), - (get_windows_path(&FOLDERID_LocalAppData) - .unwrap_or_default() - .join("Programs")), - ]; - paths -} - -/// Wrapper around `SHGetKnownFolderPath` to get paths to known folders -fn get_windows_path(folder_id: &GUID) -> Option { - unsafe { - let folder = SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None); - if let Ok(folder) = folder { - let path = folder.to_string().ok()?; - CoTaskMemFree(Some(folder.0.cast())); - Some(path.into()) - } else { - None - } - } -} - -pub fn index_start_menu() -> Vec { - WalkDir::new(r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs") - .into_iter() - .filter_map(std::result::Result::ok) - .filter_map(|path| { - let lnk = lnk::ShellLink::open(path.path(), get_acp()); - - match lnk { - Ok(x) => { - let target = x.link_target(); - let file_name = path.file_name().to_string_lossy().to_string(); - - if let Some(target) = target { - tracing::trace!( - target: "smenu_app_search", - "Link at {} added", - path.path().display() - ); - Some(App::new_executable( - &file_name, - &file_name, - "", - PathBuf::from(target.clone()), - None, - )) - } else { - tracing::trace!( - target: "smenu_app_search", - "Link at {} has no target, skipped", - path.path().display() - ); - None - } - } - Err(e) => { - tracing::trace!( - target: "smenu_app_search", - "Error opening link {} ({e}), skipped", - path.path().to_string_lossy() - ); - None - } - } - }) - .collect() -} diff --git a/src/cross_platform/windows/mod.rs b/src/cross_platform/windows/mod.rs index 2dd575b..3706b2a 100644 --- a/src/cross_platform/windows/mod.rs +++ b/src/cross_platform/windows/mod.rs @@ -1,8 +1,6 @@ use lnk::{Encoding, encoding::WINDOWS_1252}; use windows::Win32::{Globalization::GetACP, UI::WindowsAndMessaging::GetCursorPos}; -pub mod app_finding; - #[allow(clippy::cast_precision_loss)] pub fn open_on_focused_monitor() -> iced::Point { use windows::Win32::Foundation::POINT; diff --git a/src/main.rs b/src/main.rs index 3c97454..1e530ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,14 +120,16 @@ fn main() -> iced::Result { #[cfg(not(target_os = "linux"))] let show_hide_bind = { + use global_hotkey::hotkey::HotKey; + let manager = GlobalHotKeyManager::new().unwrap(); let show_hide = config.toggle_hotkey.parse().unwrap(); - let mut hotkeys = vec![show_hide]; + let mut hotkeys: Vec = vec![show_hide]; if let Some(show_clipboard) = &config.clipboard_hotkey - && let Some(cb_page_hk) = show_clipboard.parse().ok() + && let Ok(cb_page_hk) = show_clipboard.parse() { hotkeys.push(cb_page_hk); } diff --git a/src/utils.rs b/src/utils.rs index cb22e8f..c82efa3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,9 @@ //! This has all the utility functions that rustcast uses use std::{ io, - path::{Path, PathBuf}, - time::Instant, + path::{Path, PathBuf} }; -use rayon::prelude::*; - #[cfg(target_os = "macos")] use {objc2_app_kit::NSWorkspace, objc2_foundation::NSURL}; @@ -16,8 +13,6 @@ use crate::cross_platform::linux::get_installed_linux_apps; #[cfg(any(target_os = "windows", target_os = "linux"))] use std::process::Command; -use crate::app::apps::App; - pub fn get_config_installation_dir() -> PathBuf { if cfg!(target_os = "windows") { std::env::var("LOCALAPPDATA").unwrap().into() @@ -36,59 +31,6 @@ pub fn get_config_file_path() -> PathBuf { } } -/// Recursively loads apps from a set of folders. -/// -/// [`exclude_patterns`] is a set of glob patterns to include, while [`include_patterns`] is a set of -/// patterns to include ignoring [`exclude_patterns`]. -fn search_dir( - path: impl AsRef, - exclude_patterns: &[glob::Pattern], - include_patterns: &[glob::Pattern], - max_depth: usize, -) -> impl ParallelIterator { - use walkdir::WalkDir; - - WalkDir::new(path.as_ref()) - .follow_links(false) - .max_depth(max_depth) - .into_iter() - .par_bridge() - .filter_map(std::result::Result::ok) - .filter(|e| e.path().extension().is_some_and(|ext| ext == "exe")) - .filter_map(|entry| { - let path = entry.path(); - - if exclude_patterns.iter().any(|x| x.matches_path(path)) - && !include_patterns.iter().any(|x| x.matches_path(path)) - { - #[cfg(debug_assertions)] - tracing::trace!( - target: "dir_app_search", - "App excluded: {:?}", path.to_str() - ); - - return None; - } - - let file_name = path.file_name().unwrap().to_string_lossy(); - let name = file_name.replace(".exe", ""); - - #[cfg(debug_assertions)] - tracing::trace!( - target: "dir_app_search", - "App added: {:?}", path.to_str() - ); - - Some(App::new_executable( - &name, - &name.to_lowercase(), - "Application", - path, - None, - )) - }) -} - use crate::config::Config; pub fn read_config_file(file_path: &Path) -> anyhow::Result { @@ -133,110 +75,6 @@ pub fn open_application(path: impl AsRef) { } } -pub fn index_installed_apps(config: &Config) -> anyhow::Result> { - tracing::debug!("Indexing installed apps"); - tracing::debug!("Exclude patterns: {:?}", &config.index_exclude_patterns); - tracing::debug!("Include patterns: {:?}", &config.index_include_patterns); - - let path = get_config_file_path(); - let config = read_config_file(path.as_path())?; - - if config.index_dirs.is_empty() { - tracing::debug!("No extra index dirs provided"); - } - - #[cfg(target_os = "windows")] - { - use crate::cross_platform::windows::app_finding::get_apps_from_registry; - use crate::cross_platform::windows::app_finding::index_start_menu; - - let start = Instant::now(); - - let mut other_apps = index_start_menu(); - get_apps_from_registry(&mut other_apps); - - let res = config - .index_dirs - .par_iter() - .flat_map(|x| { - search_dir( - &x.path, - &config.index_exclude_patterns, - &config.index_include_patterns, - x.max_depth, - ) - }) - .chain(other_apps.into_par_iter()) - .collect(); - - let end = Instant::now(); - tracing::info!( - "Finished indexing apps (t = {}s)", - (end - start).as_secs_f32() - ); - - Ok(res) - } - - #[cfg(target_os = "macos")] - { - let start = Instant::now(); - - let res = config - .index_dirs - .par_iter() - .flat_map(|x| { - search_dir( - &x.path, - &config.index_exclude_patterns, - &config.index_include_patterns, - x.max_depth, - ) - }) - .collect(); - - let end = Instant::now(); - tracing::info!( - "Finished indexing apps (t = {}s)", - (end - start).as_secs_f32() - ); - - Ok(res) - } - - #[cfg(target_os = "linux")] - { - let start = Instant::now(); - - let other_apps = get_installed_linux_apps(&config); - - let start2 = Instant::now(); - - let res = config - .index_dirs - .par_iter() - .flat_map(|x| { - search_dir( - &x.path, - &config.index_exclude_patterns, - &config.index_include_patterns, - x.max_depth, - ) - }) - .chain(other_apps.into_par_iter()) - .collect(); - - let end = Instant::now(); - tracing::info!( - "Finished indexing apps (t = {}s) (t2 = {}s)", - (end - start).as_secs_f32(), - (end - start2).as_secs_f32(), - ); - - Ok(res) - } -} - /// Check if the provided string looks like a valid url pub fn is_url_like(s: &str) -> bool { if s.starts_with("http://") || s.starts_with("https://") { From 2aaa1ade8e5f889a66366b26eb1c8b160ec6ffbb Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Sat, 14 Feb 2026 16:11:57 +0000 Subject: [PATCH 11/12] fix: remove usage of nonexistent module --- src/cross_platform/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cross_platform/mod.rs b/src/cross_platform/mod.rs index eb304b6..880846b 100644 --- a/src/cross_platform/mod.rs +++ b/src/cross_platform/mod.rs @@ -8,9 +8,6 @@ pub mod macos; #[cfg(target_os = "windows")] pub mod windows; -#[cfg(target_os = "linux")] -pub mod linux; - /// Opens the settings file pub fn open_settings() { #[cfg(target_os = "macos")] From 62c39019626d4c1310eecdfe2a9cb4846b98027d Mon Sep 17 00:00:00 2001 From: Mnem42 Date: Sat, 14 Feb 2026 16:12:09 +0000 Subject: [PATCH 12/12] clean: run cargo fmt --- src/app/tile/elm.rs | 6 +++--- src/app/tile/search_query.rs | 2 -- src/app/tile/update.rs | 2 +- src/app_finding/macos.rs | 1 + src/app_finding/mod.rs | 9 ++++++--- src/app_finding/windows.rs | 28 +++++++++++++++------------- src/config/mod.rs | 2 +- src/main.rs | 2 +- src/utils.rs | 2 +- 9 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 0db89dd..221552b 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -17,9 +17,9 @@ use crate::app::WINDOW_WIDTH; use crate::app::pages::clipboard::clipboard_view; use crate::app::pages::emoji::emoji_page; use crate::app::tile::AppIndex; +use crate::app_finding::index_installed_apps; use crate::config::Theme; use crate::styles::{contents_style, rustcast_text_input_style, tint, with_alpha}; -use crate::app_finding::index_installed_apps; use crate::{ app::{Message, Page, apps::App, default_settings, tile::Tile}, config::Config, @@ -75,7 +75,7 @@ pub fn new( config: &Config, ) -> (Tile, Task) { tracing::trace!(target: "elm_init", "Initing ELM"); - + #[allow(unused_mut)] let mut settings = default_settings(); @@ -90,7 +90,7 @@ pub fn new( } tracing::trace!(target: "elm_init", "Opening window"); - + // id unused on windows, but not macos #[allow(unused)] let (id, open) = window::open(settings); diff --git a/src/app/tile/search_query.rs b/src/app/tile/search_query.rs index 567a43c..89c20c6 100644 --- a/src/app/tile/search_query.rs +++ b/src/app/tile/search_query.rs @@ -141,8 +141,6 @@ pub(super) fn handle_change(tile: &mut Tile, input: &str, id: Id) -> iced::Task< } else if tile.results.is_empty() && tile.query_lc == "lemon" { #[cfg(target_os = "macos")] { - - tile.results.push(App::new_builtin( "Easter Egg", "Lemon", diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 9c36808..1b39e55 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -17,9 +17,9 @@ use crate::app::{ #[cfg(target_os = "macos")] use crate::cross_platform::macos; +use crate::app_finding::index_installed_apps; use crate::commands::Function; use crate::config::Config; -use crate::app_finding::index_installed_apps; #[allow(clippy::too_many_lines)] pub fn handle_update(tile: &mut Tile, message: Message) -> Task { diff --git a/src/app_finding/macos.rs b/src/app_finding/macos.rs index e69de29..8b13789 100644 --- a/src/app_finding/macos.rs +++ b/src/app_finding/macos.rs @@ -0,0 +1 @@ + diff --git a/src/app_finding/mod.rs b/src/app_finding/mod.rs index ad81f65..2545c52 100644 --- a/src/app_finding/mod.rs +++ b/src/app_finding/mod.rs @@ -1,6 +1,10 @@ use std::path::Path; -use crate::{app::apps::App, config::Config, utils::{get_config_file_path, read_config_file}}; +use crate::{ + app::apps::App, + config::Config, + utils::{get_config_file_path, read_config_file}, +}; use rayon::prelude::*; #[cfg(target_os = "macos")] @@ -17,7 +21,6 @@ mod windows; #[cfg(target_os = "windows")] pub use self::windows::get_known_paths; - /// Recursively loads apps from a set of folders. /// /// [`exclude_patterns`] is a set of glob patterns to include, while [`include_patterns`] is a set of @@ -176,4 +179,4 @@ pub fn index_installed_apps(config: &Config) -> anyhow::Result> { Ok(res) } -} \ No newline at end of file +} diff --git a/src/app_finding/windows.rs b/src/app_finding/windows.rs index 356f981..ee8201b 100644 --- a/src/app_finding/windows.rs +++ b/src/app_finding/windows.rs @@ -30,7 +30,10 @@ pub fn get_apps_from_registry(apps: &mut Vec) { .unwrap(), ]; - for (key, reg) in registers.iter().flat_map(|reg| reg.enum_keys().zip(repeat(reg))) { + for (key, reg) in registers + .iter() + .flat_map(|reg| reg.enum_keys().zip(repeat(reg))) + { // Not debug only just because it doesn't run too often tracing::trace!("App added [reg]: {:?}", key); @@ -96,17 +99,16 @@ pub fn index_start_menu() -> Vec { let target = x.link_target(); let file_name = path.file_name().to_string_lossy().to_string(); - if let Some(target) = target { Some(App::new_executable( - &file_name, - &file_name, - "", - PathBuf::from(target.clone()), - None, - )) } else { - tracing::debug!( - "Link at {} has no target, skipped", - path.path().display() - ); + if let Some(target) = target { + Some(App::new_executable( + &file_name, + &file_name, + "", + PathBuf::from(target.clone()), + None, + )) + } else { + tracing::debug!("Link at {} has no target, skipped", path.path().display()); None } } @@ -132,4 +134,4 @@ pub fn get_known_paths() -> Vec { .join("Programs")), ]; paths -} \ No newline at end of file +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 0eff0e7..299faf1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,7 +4,7 @@ use std::{path::PathBuf, sync::Arc}; use iced::{Font, font::Family, theme::Custom}; use serde::{Deserialize, Serialize}; -#[cfg(target_os="windows")] +#[cfg(target_os = "windows")] use crate::app_finding::get_known_paths; use crate::{ diff --git a/src/main.rs b/src/main.rs index 1e530ac..87d6992 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod app_finding; mod calculator; mod clipboard; mod commands; @@ -6,7 +7,6 @@ mod config; mod styles; mod unit_conversion; mod utils; -mod app_finding; mod cross_platform; diff --git a/src/utils.rs b/src/utils.rs index c82efa3..4771248 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,7 @@ //! This has all the utility functions that rustcast uses use std::{ io, - path::{Path, PathBuf} + path::{Path, PathBuf}, }; #[cfg(target_os = "macos")]