diff --git a/Cargo.lock b/Cargo.lock index 0216a74..387182d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2256,6 +2256,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lebe" version = "0.5.3" @@ -2654,6 +2660,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3745,6 +3760,9 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", "tray-icon", "url", + "walkdir", + "windows 0.58.0", + "winreg", ] [[package]] @@ -3884,6 +3902,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -4461,7 +4488,23 @@ version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -4550,6 +4593,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4580,6 +4624,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version-compare" version = "0.2.1" @@ -5221,6 +5271,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5272,6 +5331,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5320,6 +5394,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5338,6 +5418,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5356,6 +5442,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5386,6 +5478,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5404,6 +5502,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5422,6 +5526,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5440,6 +5550,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5522,6 +5638,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 386a1b5..ca9c4cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,14 +3,11 @@ name = "rustcast" version = "0.1.0" edition = "2024" -[dependencies] -anyhow = "1.0.100" -arboard = "3.6.1" -emojis = "0.8.0" -global-hotkey = "0.7.0" -iced = { version = "0.14.0", features = ["image", "tokio"] } -icns = "0.3.1" -image = "0.25.9" +[target.'cfg(target_os = "windows")'.dependencies] +winreg = "0.52" +windows = { version = "0.58", features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Com", "Win32_UI_Shell"] } + +[target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.3" objc2-app-kit = { version = "0.3.2", features = ["NSImage"] } objc2-application-services = { version = "0.3.2", default-features = false, features = [ @@ -19,11 +16,41 @@ objc2-application-services = { version = "0.3.2", default-features = false, feat ] } objc2-core-foundation = "0.3.2" objc2-foundation = { version = "0.3.2", features = ["NSString"] } +icns = "0.3.1" + +[dependencies] +anyhow = "1.0.100" +emojis = "0.8.0" +arboard = "3.6.1" +global-hotkey = "0.7.0" +iced = { version = "0.14.0", features = ["image", "tokio"] } +image = "0.25.9" once_cell = "1.21.3" rand = "0.9.2" rayon = "1.11.0" serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.48.0", features = ["full"] } toml = "0.9.8" +walkdir = "2" tray-icon = "0.21.3" -url = { version = "2.5.8", default-features = false } +url = "2.5.8" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" + +[package.metadata.bundle] +name = "RustCast" +identifier = "com.umangsurana.rustcast" +icon = ["bundling/icon.icns"] +version = "1.0.0" +resources = [] +copyright = "Copyright Umang Surana (c) 2025" +category = "Developer Tool" +short_description = "An open source alternative to Raycast, and in rust" +osx_minimum_system_version = "10.15" + +[package.metadata.bundle.osx] +info_plist_path = "bundling/Info.plist" + +[package.metadata.bundle.osx.info] +LSUIElement = true +NSHighResolutionCapable = true diff --git a/build.rs b/build.rs index a4b0b05..e2dfc45 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,8 @@ fn main() { - println!("cargo:rustc-link-search=framework=/System/Library/PrivateFrameworks"); - println!("cargo:rustc-link-lib=framework=IOKit"); - println!("cargo:rustc-link-lib=framework=MultitouchSupport"); + #[cfg(target_os = "macos")] + { + println!("cargo:rustc-link-search=framework=/System/Library/PrivateFrameworks"); + println!("cargo:rustc-link-lib=framework=IOKit"); + println!("cargo:rustc-link-lib=framework=MultitouchSupport"); + } } diff --git a/src/app.rs b/src/app.rs index 803342b..e33ef0b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,7 @@ //! Main logic for the app use crate::commands::Function; +use iced::window::{self, Id, Settings}; + use crate::{app::tile::ExtSender, clipboard::ClipBoardContentType}; pub mod apps; @@ -7,7 +9,6 @@ pub mod menubar; pub mod pages; pub mod tile; -use iced::window::{self, Id, Settings}; /// The default window width pub const WINDOW_WIDTH: f32 = 500.; @@ -81,3 +82,12 @@ pub fn default_settings() -> Settings { ..Default::default() } } + +// Message::ReloadConfig => { +// self.config = toml::from_str( +// &fs::read_to_string(get_config_file_path()).unwrap_or("".to_owned()), +// ) +// .unwrap(); +// +// Task::none() +// } diff --git a/src/app/menubar.rs b/src/app/menubar.rs index 2f87195..ba961fb 100644 --- a/src/app/menubar.rs +++ b/src/app/menubar.rs @@ -1,7 +1,8 @@ //! This has the menubar icon logic for the app use global_hotkey::hotkey::{Code, HotKey, Modifiers}; -use image::{DynamicImage, ImageReader}; +use image::DynamicImage; +use tokio::runtime::Runtime; use tray_icon::{ Icon, TrayIcon, TrayIconBuilder, menu::{ @@ -12,7 +13,7 @@ use tray_icon::{ use crate::{ app::{Message, tile::ExtSender}, - utils::{open_settings, open_url}, + cross_platform::{open_settings, open_url}, }; const DISCORD_LINK: &str = "https://discord.gg/bDfNYPbnC5"; @@ -54,16 +55,31 @@ pub fn menu_icon(hotkey: HotKey, sender: ExtSender) -> TrayIcon { } fn get_image() -> DynamicImage { - let image_path = if cfg!(debug_assertions) { - "docs/icon.png" - } else { - "/Applications/Rustcast.app/Contents/Resources/icon.png" - }; - - ImageReader::open(image_path).unwrap().decode().unwrap() + #[cfg(target_os = "macos")] + { + use image::ImageReader; + + let image_path = if cfg!(debug_assertions) && !cfg!(target_os = "macos") { + "docs/icon.png" + } else { + "/Applications/Rustcast.app/Contents/Resources/icon.png" + }; + + ImageReader::open(image_path).unwrap().decode().unwrap() + } + + #[cfg(target_os = "windows")] + { + DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( + 64, + 64, + image::Rgba([0, 0, 0, 255]), + )) + } } fn init_event_handler(sender: ExtSender, hotkey_id: u32) { + tracing::debug!("Initing event handler"); let runtime = Runtime::new().unwrap(); MenuEvent::set_event_handler(Some(move |x: MenuEvent| { diff --git a/src/app/tile.rs b/src/app/tile.rs index 830abdb..ac58e42 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -2,12 +2,17 @@ pub mod elm; pub mod update; +#[cfg(target_os = "windows")] +use { + windows::Win32::Foundation::HWND, windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow, +}; + use crate::app::apps::App; use crate::app::tile::elm::default_app_paths; use crate::app::{ArrowKey, Message, Move, Page}; use crate::clipboard::ClipBoardContentType; use crate::config::Config; -use crate::utils::open_settings; +use crate::cross_platform::open_settings; use arboard::Clipboard; use global_hotkey::hotkey::HotKey; @@ -23,7 +28,9 @@ use iced::{ }; use iced::{event, window}; +#[cfg(target_os = "macos")] use objc2::rc::Retained; +#[cfg(target_os = "macos")] use objc2_app_kit::NSRunningApplication; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use tray_icon::TrayIcon; @@ -95,7 +102,10 @@ pub struct Tile { emoji_apps: AppIndex, visible: bool, focused: bool, + #[cfg(target_os = "macos")] frontmost: Option>, + #[cfg(target_os = "windows")] + frontmost: Option, pub config: Config, /// The opening hotkey hotkey: HotKey, @@ -224,21 +234,41 @@ impl Tile { self.results = results; } - /// Gets the frontmost application to focus later. + // Unused, keeping it for now pub fn capture_frontmost(&mut self) { - use objc2_app_kit::NSWorkspace; + #[cfg(target_os = "macos")] + { + use objc2_app_kit::NSWorkspace; - let ws = NSWorkspace::sharedWorkspace(); - self.frontmost = ws.frontmostApplication(); + let ws = NSWorkspace::sharedWorkspace(); + self.frontmost = ws.frontmostApplication(); + }; + + #[cfg(target_os = "windows")] + { + self.frontmost = Some(unsafe { GetForegroundWindow() }); + } } /// Restores the frontmost application. #[allow(deprecated)] pub fn restore_frontmost(&mut self) { - use objc2_app_kit::NSApplicationActivationOptions; + #[cfg(target_os = "macos")] + { + if let Some(app) = self.frontmost.take() { + use objc2_app_kit::NSApplicationActivationOptions; - if let Some(app) = self.frontmost.take() { - app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); + app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); + } + } + + #[cfg(target_os = "windows")] + { + if let Some(handle) = self.frontmost { + unsafe { + let _ = SetForegroundWindow(handle); + } + } } } } @@ -341,10 +371,9 @@ fn handle_clipboard_history() -> impl futures::Stream { fn handle_recipient() -> impl futures::Stream { stream::channel(100, async |mut output| { let (sender, mut recipient) = channel(100); - output - .send(Message::SetSender(ExtSender(sender))) - .await - .expect("Sender not sent"); + let msg = Message::SetSender(ExtSender(sender)); + tracing::debug!("Sending ExtSender"); + output.send(msg).await.expect("Sender not sent"); loop { let abcd = recipient .try_next() diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 42a9a23..a010cf2 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -10,55 +10,73 @@ use iced::{Alignment, Color, Length, Vector, window}; use iced::{Element, Task}; use iced::{Length::Fill, widget::text_input}; -use rayon::{ - iter::{IntoParallelRefIterator, ParallelIterator}, - slice::ParallelSliceMut, -}; +use rayon::slice::ParallelSliceMut; 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::utils::get_installed_apps; use crate::config::Theme; use crate::styles::{contents_style, rustcast_text_input_style, tint, with_alpha}; use crate::{ app::{Message, Page, apps::App, default_settings, tile::Tile}, config::Config, - macos::{self, transform_process_to_ui_element}, - utils::get_installed_apps, }; +#[cfg(target_os = "macos")] +use crate::cross_platform::macos::{self, transform_process_to_ui_element}; + pub fn default_app_paths() -> Vec { - let user_local_path = std::env::var("HOME").unwrap() + "/Applications/"; + #[cfg(target_os = "macos")] + { + let user_local_path = std::env::var("HOME").unwrap() + "/Applications/"; - let paths = vec![ - "/Applications/".to_string(), - user_local_path, - "/System/Applications/".to_string(), - "/System/Applications/Utilities/".to_string(), - ]; + let paths = vec![ + "/Applications/".to_string(), + user_local_path, + "/System/Applications/".to_string(), + "/System/Applications/Utilities/".to_string(), + ]; + paths + } - paths + #[cfg(target_os = "windows")] + { + Vec::new() + } } /// Initialise the base window pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) { - let (id, open) = window::open(default_settings()); + #[allow(unused_mut)] + let mut settings = default_settings(); + + // get normal settings and modify position + #[cfg(target_os = "windows")] + { + use iced::window::Position; + use crate::cross_platform::windows::open_on_focused_monitor; + let pos = open_on_focused_monitor(); + settings.position = Position::Specific(pos); + } + + // id unused on windows, but not macos + #[cfg_attr(target_os = "windows", allow(unused))] + let (id, open) = window::open(settings); + + #[cfg(target_os = "windows")] + let open: Task = open.discard(); + + #[cfg(target_os = "macos")] let open = open.discard().chain(window::run(id, |handle| { macos::macos_window_config(&handle.window_handle().expect("Unable to get window handle")); transform_process_to_ui_element(); + Message::OpenWindow })); - let store_icons = config.theme.show_icons; - - let paths = default_app_paths(); - - let mut options: Vec = paths - .par_iter() - .map(|path| get_installed_apps(path, store_icons)) - .flatten() - .collect(); + let mut options: Vec = get_installed_apps(config); options.extend(config.shells.iter().map(|x| x.to_app())); options.extend(App::basic_apps()); @@ -88,7 +106,7 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) { sender: None, page: Page::Main, }, - Task::batch([open.map(|_| Message::OpenWindow)]), + open, ) } diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 67eaf5d..507b00b 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -1,16 +1,16 @@ //! This handles the update logic for the tile (AKA rustcast's main window) use std::cmp::min; use std::fs; +#[cfg(target_os = "macos")] use std::path::Path; use std::thread; use iced::Task; +#[cfg(target_os = "macos")] use iced::widget::image::Handle; use iced::widget::operation; use iced::widget::operation::AbsoluteOffset; use iced::window; -use rayon::iter::IntoParallelRefIterator; -use rayon::iter::ParallelIterator; use rayon::slice::ParallelSliceMut; use crate::app::ArrowKey; @@ -23,26 +23,34 @@ use crate::app::apps::AppCommand; use crate::app::default_settings; use crate::app::menubar::menu_icon; use crate::app::tile::AppIndex; -use crate::app::tile::elm::default_app_paths; +use crate::app::{Message, Page, tile::Tile}; + +#[cfg(target_os = "windows")] +use crate::utils::get_config_installation_dir; + use crate::calculator::Expr; use crate::clipboard::ClipBoardContentType; use crate::commands::Function; use crate::config::Config; -use crate::haptics::HapticPattern; -use crate::haptics::perform_haptic; use crate::unit_conversion; use crate::utils::get_installed_apps; + use crate::utils::is_valid_url; +#[cfg(target_os = "macos")] use crate::{ - app::{Message, Page, tile::Tile}, - macos::focus_this_app, + cross_platform::macos::focus_this_app, + cross_platform::macos::haptics::{HapticPattern, perform_haptic}, }; pub fn handle_update(tile: &mut Tile, message: Message) -> Task { + tracing::debug!("Handling update (message: {:?})", message); match message { Message::OpenWindow => { - tile.capture_frontmost(); - focus_this_app(); + #[cfg(target_os = "macos")] + { + tile.capture_frontmost(); + focus_this_app(); + } tile.focused = true; tile.visible = true; Task::none() @@ -439,6 +447,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { name_lc: String::new(), }); } else if tile.results.is_empty() && tile.query_lc == "lemon" { + #[cfg(target_os = "macos")] tile.results.push(App { open_command: AppCommand::Display, desc: "Easter Egg".to_string(), diff --git a/src/commands.rs b/src/commands.rs index bfffee1..3b5fc7b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,11 +1,16 @@ //! This handles all the different commands that rustcast can perform, such as opening apps, //! copying to clipboard, etc. -use std::{process::Command, thread}; +use std::process::Command; +#[cfg(target_os = "macos")] +use std::thread; use arboard::Clipboard; +#[cfg(target_os = "macos")] use objc2_app_kit::NSWorkspace; +#[cfg(target_os = "macos")] use objc2_foundation::NSURL; +use crate::utils::open_application; use crate::{calculator::Expr, clipboard::ClipBoardContentType, config::Config}; /// The different functions that rustcast can perform @@ -25,14 +30,10 @@ pub enum Function { impl Function { /// Run the command pub fn execute(&self, config: &Config, query: &str) { + tracing::debug!("Executing command: {:?}", self); match self { Function::OpenApp(path) => { - let path = path.to_owned(); - thread::spawn(move || { - NSWorkspace::new().openURL(&NSURL::fileURLWithPath( - &objc2_foundation::NSString::from_str(&path), - )); - }); + open_application(path); } Function::RunShellCommand(command, alias) => { let query = query.to_string(); @@ -54,28 +55,38 @@ impl Function { Function::GoogleSearch(query_string) => { 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(); - thread::spawn(move || { - NSWorkspace::new().openURL( - &NSURL::URLWithString_relativeToURL( - &objc2_foundation::NSString::from_str(&query), - None, - ) - .unwrap(), - ); - }); + let query = query.strip_suffix("?").unwrap_or(&query); + + #[cfg(target_os = "windows")] + { + Command::new("powershell") + .args(["-Command", &format!("Start-Process {}", query)]) + .status() + .ok(); + } + + #[cfg(target_os = "macos")] + NSWorkspace::new().openURL( + &NSURL::URLWithString_relativeToURL( + &objc2_foundation::NSString::from_str(query), + None, + ) + .unwrap(), + ); } + #[cfg(target_os = "macos")] Function::OpenWebsite(url) => { - let open = if url.starts_with("http") { + let open_url = if url.starts_with("http") { url.to_owned() } else { format!("https://{}", url) }; + thread::spawn(move || { NSWorkspace::new().openURL( &NSURL::URLWithString_relativeToURL( - &objc2_foundation::NSString::from_str(&open), + &objc2_foundation::NSString::from_str(&open_url), None, ) .unwrap(), @@ -100,6 +111,7 @@ impl Function { }, Function::OpenPrefPane => { + #[cfg(target_os = "macos")] thread::spawn(move || { NSWorkspace::new().openURL(&NSURL::fileURLWithPath( &objc2_foundation::NSString::from_str( @@ -109,6 +121,7 @@ impl Function { )); }); } + Function::Quit => std::process::exit(0), } } diff --git a/src/config.rs b/src/config.rs index c25f5b6..8dcedfd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,6 +23,7 @@ pub struct Config { pub haptic_feedback: bool, pub show_trayicon: bool, pub shells: Vec, + pub index_dirs: Vec, } impl Default for Config { @@ -38,6 +39,7 @@ impl Default for Config { haptic_feedback: false, show_trayicon: true, shells: vec![], + index_dirs: vec![], } } } diff --git a/src/haptics.rs b/src/cross_platform/macos/haptics.rs similarity index 100% rename from src/haptics.rs rename to src/cross_platform/macos/haptics.rs diff --git a/src/cross_platform/macos/mod.rs b/src/cross_platform/macos/mod.rs new file mode 100644 index 0000000..7b29f44 --- /dev/null +++ b/src/cross_platform/macos/mod.rs @@ -0,0 +1,266 @@ +//! Macos specific logic, such as window settings, etc. +#![allow(deprecated)] + +pub mod haptics; + +use crate::app::apps::{App, AppCommand}; +use crate::commands::Function; +use crate::config::Config; +use crate::utils::handle_from_icns; +use crate::utils::index_dirs_from_config; +use { + iced::wgpu::rwh::RawWindowHandle, + iced::wgpu::rwh::WindowHandle, + objc2::MainThreadMarker, + objc2::rc::Retained, + objc2_app_kit::NSView, + objc2_app_kit::{NSApp, NSApplicationActivationPolicy}, + objc2_app_kit::{NSFloatingWindowLevel, NSWindowCollectionBehavior}, + objc2_foundation::NSURL, +}; + +use objc2_app_kit::NSWorkspace; +use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use std::path::{Path, PathBuf}; +use std::process::exit; +use std::{fs, thread}; + +/// This sets the activation policy of the app to Accessory, allowing rustcast to be visible ontop +/// of fullscreen apps +pub fn set_activation_policy_accessory() { + let mtm = MainThreadMarker::new().expect("must be on main thread"); + let app = NSApp(mtm); + app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); +} + +/// This carries out the window configuration for the macos window (only things that are macos specific) +pub fn macos_window_config(handle: &WindowHandle) { + match handle.as_raw() { + RawWindowHandle::AppKit(handle) => { + let ns_view = handle.ns_view.as_ptr(); + let ns_view: Retained = unsafe { Retained::retain(ns_view.cast()) }.unwrap(); + let ns_window = ns_view + .window() + .expect("view was not installed in a window"); + + ns_window.setLevel(NSFloatingWindowLevel); + + ns_window.setCollectionBehavior(NSWindowCollectionBehavior::CanJoinAllSpaces); + } + _ => { + panic!( + "Why are you running this as a non-appkit window? this is a macos only app as of now" + ); + } + } +} + +/// This is the function that forces focus onto rustcast +#[allow(deprecated)] +pub fn focus_this_app() { + use objc2::MainThreadMarker; + use objc2_app_kit::NSApp; + + let mtm = MainThreadMarker::new().expect("must be on main thread"); + let app = NSApp(mtm); + + app.activateIgnoringOtherApps(true); +} + +/// This is the struct that represents the process serial number, allowing us to transform the process to a UI element +#[repr(C)] +struct ProcessSerialNumber { + low: u32, + hi: u32, +} + +/// This is the function that transforms the process to a UI element, and hides the dock icon +/// +/// see mostly +/// +/// returns ApplicationServices OSStatus (u32) +/// +/// doesn't seem to do anything if you haven't opened a window yet, so wait to call it until after that. +pub fn transform_process_to_ui_element() -> u32 { + use objc2_application_services::{ + TransformProcessType, kCurrentProcess, kProcessTransformToUIElementApplication, + }; + use std::ptr; + + let psn = ProcessSerialNumber { + low: 0, + hi: kCurrentProcess, + }; + + unsafe { + TransformProcessType( + ptr::from_ref(&psn).cast(), + kProcessTransformToUIElementApplication, + ) + } +} + +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 { + open_command: AppCommand::Function(Function::OpenApp(path_str)), + desc: "Application".to_string(), + icons, + name_lc: name.to_lowercase(), + name, + }) + }) + .collect() +} + +pub fn get_installed_macos_apps(config: &Config) -> Vec { + 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 = paths + .par_iter() + .map(|path| get_installed_apps(path, store_icons)) + .flatten() + .collect(); + index_dirs_from_config(&mut apps); + + apps +} + +/// Opens a provided URL +pub fn open_url(url: &str) { + let url = url.to_owned(); + thread::spawn(move || { + NSWorkspace::new().openURL( + &NSURL::URLWithString_relativeToURL(&objc2_foundation::NSString::from_str(&url), None) + .unwrap(), + ); + }); +} + +/// Open the settings file with the system default editor +pub fn open_settings() { + thread::spawn(move || { + NSWorkspace::new().openURL(&NSURL::fileURLWithPath( + &objc2_foundation::NSString::from_str( + &(std::env::var("HOME").unwrap_or("".to_string()) + + "/.config/rustcast/config.toml"), + ), + )); + }); +} diff --git a/src/cross_platform/mod.rs b/src/cross_platform/mod.rs new file mode 100644 index 0000000..e84b658 --- /dev/null +++ b/src/cross_platform/mod.rs @@ -0,0 +1,15 @@ +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(target_os = "windows")] +pub mod windows; + +pub fn open_url(url: &str) { + #[cfg(target_os = "macos")] + macos::open_url(url) +} + +pub fn open_settings() { + #[cfg(target_os = "macos")] + macos::open_settings() +} diff --git a/src/cross_platform/windows.rs b/src/cross_platform/windows.rs new file mode 100644 index 0000000..cefd369 --- /dev/null +++ b/src/cross_platform/windows.rs @@ -0,0 +1,177 @@ +use { + crate::app::apps::App, + rayon::prelude::*, + windows::{ + Win32::{ + System::Com::CoTaskMemFree, + UI::{ + Shell::{ + FOLDERID_LocalAppData, FOLDERID_ProgramFiles, FOLDERID_ProgramFilesX86, + KF_FLAG_DEFAULT, SHGetKnownFolderPath, + }, + WindowsAndMessaging::GetCursorPos, + }, + }, + core::GUID, + }, +}; + +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(), + ]; + + // where we can find installed applications + // src: https://stackoverflow.com/questions/2864984/how-to-programatically-get-the-list-of-installed-programs/2892848#2892848 + 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); + + // 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_path = exe_path.to_string_lossy().to_string(); + let exe = exe_path.split(",").next().unwrap().to_string(); + + // make sure it ends with .exe + if !exe.ends_with(".exe") { + return; + } + + if !display_name.is_empty() { + use crate::{app::apps::AppCommand, commands::Function}; + + apps.push(App { + open_command: AppCommand::Function(Function::OpenApp(exe)), + name: display_name.clone().into_string().unwrap(), + name_lc: display_name.clone().into_string().unwrap().to_lowercase(), + icons: None, + desc: "Application".to_string(), + }) + } + }); + }); +} +fn get_apps_from_known_folder(apps: &mut Vec) { + let paths = get_known_paths(); + use crate::{app::apps::AppCommand, commands::Function}; + use walkdir::WalkDir; + + let found_apps: Vec = paths + .par_iter() + .flat_map(|path| { + WalkDir::new(path) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "exe")) + .map(|entry| { + let path = entry.path(); + let file_name = path.file_name().unwrap().to_string_lossy(); + let name = file_name.replace(".exe", ""); + + #[cfg(debug_assertions)] + tracing::trace!("Executable loaded [kfolder]: {:?}", path.to_str()); + + App { + open_command: AppCommand::Function(Function::OpenApp( + path.to_string_lossy().to_string(), + )), + name: name.clone(), + name_lc: name.to_lowercase(), + icons: None, + desc: "Application".to_string(), + } + }) + .collect::>() + }) + .collect(); + + apps.extend(found_apps); +} +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() + "\\Programs\\"), + ]; + paths +} +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 as *mut _)); + path + } else { + None + } + } +} +pub fn get_installed_windows_apps() -> Vec { + use crate::utils::index_dirs_from_config; + + let mut apps = Vec::new(); + + tracing::debug!("Getting apps from registry"); + get_apps_from_registry(&mut apps); + + tracing::debug!("Getting apps from known folder"); + get_apps_from_known_folder(&mut apps); + + tracing::debug!("Getting apps from config"); + index_dirs_from_config(&mut apps); + + tracing::debug!("Apps loaded ({} total count)", apps.len()); + + apps +} + +pub fn open_on_focused_monitor() -> iced::Point { + use windows::Win32::Foundation::POINT; + use windows::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromPoint, + }; + + use crate::app::{DEFAULT_WINDOW_HEIGHT, WINDOW_WIDTH}; + let mut point = POINT { x: 0, y: 0 }; + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + let _cursor = unsafe { GetCursorPos(&mut point) }; + let monitor = unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }; + let _monitor_infos = unsafe { GetMonitorInfoW(monitor, &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; + let window_width = WINDOW_WIDTH; + let window_height = DEFAULT_WINDOW_HEIGHT; + + let x = monitor_info.rcMonitor.left as f32 + (monitor_width as f32 - window_width) / 2.0; + let y = monitor_info.rcMonitor.top as f32 + (monitor_height as f32 - window_height) / 2.0; + + iced::Point { x, y } +} diff --git a/src/macos.rs b/src/macos.rs deleted file mode 100644 index 87f53dc..0000000 --- a/src/macos.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Macos specific logic, such as window settings, etc. -#[cfg(target_os = "macos")] -use iced::wgpu::rwh::WindowHandle; - -/// This sets the activation policy of the app to Accessory, allowing rustcast to be visible ontop -/// of fullscreen apps -#[cfg(target_os = "macos")] -pub fn set_activation_policy_accessory() { - use objc2::MainThreadMarker; - use objc2_app_kit::{NSApp, NSApplicationActivationPolicy}; - - let mtm = MainThreadMarker::new().expect("must be on main thread"); - let app = NSApp(mtm); - app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); -} - -/// This carries out the window configuration for the macos window (only things that are macos specific) -#[cfg(target_os = "macos")] -pub fn macos_window_config(handle: &WindowHandle) { - use iced::wgpu::rwh::RawWindowHandle; - use objc2::rc::Retained; - use objc2_app_kit::NSView; - - match handle.as_raw() { - RawWindowHandle::AppKit(handle) => { - let ns_view = handle.ns_view.as_ptr(); - let ns_view: Retained = unsafe { Retained::retain(ns_view.cast()) }.unwrap(); - let ns_window = ns_view - .window() - .expect("view was not installed in a window"); - - use objc2_app_kit::{NSFloatingWindowLevel, NSWindowCollectionBehavior}; - ns_window.setLevel(NSFloatingWindowLevel); - - ns_window.setCollectionBehavior(NSWindowCollectionBehavior::CanJoinAllSpaces); - } - _ => { - panic!( - "Why are you running this as a non-appkit window? this is a macos only app as of now" - ); - } - } -} - -/// This is the function that forces focus onto rustcast -#[allow(deprecated)] -#[cfg(target_os = "macos")] -pub fn focus_this_app() { - use objc2::MainThreadMarker; - use objc2_app_kit::NSApp; - - let mtm = MainThreadMarker::new().expect("must be on main thread"); - let app = NSApp(mtm); - - app.activateIgnoringOtherApps(true); -} - -/// This is the struct that represents the process serial number, allowing us to transform the process to a UI element -#[repr(C)] -struct ProcessSerialNumber { - low: u32, - hi: u32, -} - -/// This is the function that transforms the process to a UI element, and hides the dock icon -/// -/// see mostly -/// -/// returns ApplicationServices OSStatus (u32) -/// -/// doesn't seem to do anything if you haven't opened a window yet, so wait to call it until after that. -pub fn transform_process_to_ui_element() -> u32 { - use objc2_application_services::{ - TransformProcessType, kCurrentProcess, kProcessTransformToUIElementApplication, - }; - use std::ptr; - - let psn = ProcessSerialNumber { - low: 0, - hi: kCurrentProcess, - }; - - unsafe { - TransformProcessType( - ptr::from_ref(&psn).cast(), - kProcessTransformToUIElementApplication, - ) - } -} diff --git a/src/main.rs b/src/main.rs index 792367f..4d8f4a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,42 +3,64 @@ mod calculator; mod clipboard; mod commands; mod config; -mod haptics; -mod macos; mod styles; mod unit_conversion; mod utils; -use std::path::Path; +mod cross_platform; -use crate::{ - app::tile::{self, Tile}, - config::Config, -}; +use std::env::temp_dir; +use std::fs::File; + +// import from utils +use crate::utils::{create_config_file_if_not_exists, get_config_file_path, read_config_file}; + +use crate::app::tile::{self, Tile}; use global_hotkey::GlobalHotKeyManager; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::SubscriberExt; fn main() -> iced::Result { #[cfg(target_os = "macos")] + cross_platform::macos::set_activation_policy_accessory(); + + let file_path = get_config_file_path(); + let config = read_config_file(&file_path).unwrap(); + create_config_file_if_not_exists(&file_path, &config).unwrap(); + { - macos::set_activation_policy_accessory(); - } + let log_path = temp_dir().join("rustcast/log.log"); + let vv_log_path = temp_dir().join("rustcast/vv_log.log"); + + create_config_file_if_not_exists(&log_path, &config).unwrap(); + + let file = File::create(&log_path).expect("Failed to create logfile"); + let vv_file = File::create(&vv_log_path).expect("Failed to create logfile"); - let home = std::env::var("HOME").unwrap(); + let log_file = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(file) + .with_filter(LevelFilter::DEBUG); + let vv_log_file = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(vv_file); + let console_out = tracing_subscriber::fmt::layer().with_filter(LevelFilter::INFO); - let file_path = home.clone() + "/.config/rustcast/config.toml"; - if !Path::new(&file_path).exists() { - std::fs::create_dir_all(home + "/.config/rustcast").unwrap(); - std::fs::write( - &file_path, - toml::to_string(&Config::default()).unwrap_or_else(|x| x.to_string()), - ) - .unwrap(); + let subscriber = tracing_subscriber::registry() + .with(log_file) + .with(vv_log_file) + .with(console_out); + + tracing::subscriber::set_global_default(subscriber).expect("Error initing tracing"); + + tracing::info!("Main log file at : {}", &vv_log_path.display()); + tracing::info!("Verbose log file at : {}", &log_path.display()); + tracing::info!("Config file at : {}", &file_path.display()); } - let config: Config = match std::fs::read_to_string(&file_path) { - Ok(a) => toml::from_str(&a).unwrap_or(Config::default()), - Err(_) => Config::default(), - }; + + tracing::debug!("Loaded config data: {:#?}", &config); let manager = GlobalHotKeyManager::new().unwrap(); @@ -52,9 +74,20 @@ fn main() -> iced::Result { hotkeys.push(cb_page_hk); } - manager - .register_all(&hotkeys) - .expect("Unable to register hotkey"); + let result = manager.register_all(&hotkeys); + + 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) + } else { + tracing::warn!("Couldn't register hotkey {}", key) + } + } else if let Err(e) = result { + tracing::error!("{}", e.to_string()); + } + + tracing::info!("Starting."); iced::daemon( move || tile::elm::new(show_hide, &config), diff --git a/src/utils.rs b/src/utils.rs index 263ca11..ed1f5c1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,43 +1,33 @@ //! This has all the utility functions that rustcast uses use std::{ - fs::{self, File}, - io::Write, + fs::{self}, path::{Path, PathBuf}, - process::exit, thread, }; use iced::widget::image::Handle; +#[cfg(target_os = "macos")] use icns::IconFamily; -use image::RgbaImage; -use objc2_app_kit::NSWorkspace; -use objc2_foundation::NSURL; -use rayon::iter::{IntoParallelIterator, ParallelIterator}; + +#[cfg(target_os = "macos")] +use { + crate::cross_platform::macos::get_installed_macos_apps, objc2_app_kit::NSWorkspace, + objc2_foundation::NSURL, std::os::unix::fs::PermissionsExt, +}; + +#[cfg(target_os = "windows")] +use {crate::cross_platform::windows::get_installed_windows_apps, std::process::Command}; use crate::{ app::apps::{App, AppCommand}, commands::Function, }; -/// The default error log path (works only on unix systems, and must be changed for windows -/// support) -const ERR_LOG_PATH: &str = "/tmp/rustscan-err.log"; - -/// This logs an error to the error log file -pub(crate) fn log_error(msg: &str) { - if let Ok(mut file) = File::options().create(true).append(true).open(ERR_LOG_PATH) { - let _ = file.write_all(msg.as_bytes()).ok(); - } -} - -/// This logs an error to the error log file, and exits the program -pub(crate) fn log_error_and_exit(msg: &str) { - log_error(msg); - exit(-1) -} - /// This converts an icns file to an iced image handle +#[cfg(target_os = "macos")] pub(crate) fn handle_from_icns(path: &Path) -> Option { + use image::RgbaImage; + let data = std::fs::read(path).ok()?; let family = IconFamily::read(std::io::Cursor::new(&data)).ok()?; @@ -49,155 +39,165 @@ pub(crate) fn handle_from_icns(path: &Path) -> Option { icon.height() as u32, icon.data().to_vec(), )?; - Some(Handle::from_rgba( + return Some(Handle::from_rgba( image.width(), image.height(), image.into_raw(), - )) + )); } -/// This gets all the installed apps in the given directory -/// -/// the directories are defined in [`crate::app::tile::Tile::new`] -pub(crate) fn get_installed_apps(dir: impl AsRef, store_icons: bool) -> Vec { - let entries: Vec<_> = fs::read_dir(dir.as_ref()) - .unwrap_or_else(|x| { - log_error_and_exit(&x.to_string()); - 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| { - log_error(&e.to_string()); - exit(-1) - }); - if !file_type.is_dir() { - return None; - } +pub fn get_config_installation_dir() -> PathBuf { + if cfg!(target_os = "windows") { + std::env::var("LOCALAPPDATA").unwrap().into() + } else { + std::env::var("HOME").unwrap().into() + } +} - let file_name_os = x.file_name(); - let file_name = file_name_os.into_string().unwrap_or_else(|e| { - log_error(e.to_str().unwrap_or("")); - exit(-1) - }); - if !file_name.ends_with(".app") { - return None; - } +pub fn get_config_file_path() -> PathBuf { + let home = get_config_installation_dir(); - let path = x.path(); - let path_str = path.to_str().map(|x| x.to_string()).unwrap_or_else(|| { - log_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 - }; + if cfg!(target_os = "windows") { + home.join("rustcast/config.toml") + } else { + home.join(".config/rustcast/config.toml") + } +} +use crate::config::Config; + +pub fn read_config_file(file_path: &Path) -> Result { + let config: Config = match std::fs::read_to_string(file_path) { + Ok(a) => toml::from_str(&a).unwrap(), + Err(_) => Config::default(), + }; + + Ok(config) +} + +pub fn create_config_file_if_not_exists( + file_path: &Path, + config: &Config, +) -> Result<(), std::io::Error> { + // check if file exists + if let Ok(exists) = std::fs::metadata(file_path) + && exists.is_file() + { + return Ok(()); + } + + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } - let name = file_name.strip_suffix(".app").unwrap().to_string(); - Some(App { - open_command: AppCommand::Function(Function::OpenApp(path_str)), - desc: "Application".to_string(), - icons, - name_lc: name.to_lowercase(), - name, - }) - }) - .collect() + std::fs::write( + file_path, + toml::to_string(&config).unwrap_or_else(|x| x.to_string()), + ) + .unwrap(); + + Ok(()) } -/// Open the settings file with the system default editor -pub fn open_settings() { +pub fn open_application(path: &str) { + let path_string = path.to_string(); thread::spawn(move || { - NSWorkspace::new().openURL(&NSURL::fileURLWithPath( - &objc2_foundation::NSString::from_str( - &(std::env::var("HOME").unwrap_or("".to_string()) - + "/.config/rustcast/config.toml"), - ), - )); + let path = &path_string; + #[cfg(target_os = "windows")] + { + println!("Opening application: {}", path); + + Command::new("powershell") + .arg(format!("Start-Process '{}'", path)) + .status() + .ok(); + } + + #[cfg(target_os = "macos")] + { + NSWorkspace::new().openURL(&NSURL::fileURLWithPath( + &objc2_foundation::NSString::from_str(path), + )); + } + + #[cfg(target_os = "linux")] + { + Command::new("xdg-open").arg(path).status().ok(); + } }); } -/// Open a provided URL (Platform specific) -pub fn open_url(url: &str) { - let url = url.to_owned(); - thread::spawn(move || { - NSWorkspace::new().openURL( - &NSURL::URLWithString_relativeToURL(&objc2_foundation::NSString::from_str(&url), None) - .unwrap(), - ); +#[allow(unused)] +pub fn index_dirs_from_config(apps: &mut Vec) -> bool { + let path = get_config_file_path(); + let config = read_config_file(path.as_path()); + + // if config is not valid return false otherwise unwrap config so it is usable + let config = match config { + Ok(config) => config, + Err(err) => { + println!("Error reading config file: {}", err); + return false; + } + }; + + if config.index_dirs.is_empty() { + return false; + } + + config.index_dirs.clone().iter().for_each(|dir| { + // check if dir exists + if !Path::new(dir).exists() { + println!("Directory {} does not exist", dir); + return; + } + + let paths = fs::read_dir(dir).unwrap(); + + for path in paths { + let path = path.unwrap().path(); + let metadata = fs::metadata(&path).unwrap(); + + #[cfg(target_os = "windows")] + let is_executable = + metadata.is_file() && path.extension().and_then(|s| s.to_str()) == Some("exe"); + + #[cfg(target_os = "macos")] + let is_executable = { + (metadata.is_file() && (metadata.permissions().mode() & 0o111 != 0)) + || path.extension().and_then(|s| s.to_str()) == Some("app") + }; + + if is_executable { + let display_name = path.file_name().unwrap().to_string_lossy().to_string(); + apps.push(App { + open_command: AppCommand::Function(Function::OpenApp( + path.to_string_lossy().to_string(), + )), + name: display_name.clone(), + desc: "Application".to_string(), + name_lc: display_name.clone().to_lowercase(), + icons: None, + }); + } + } }); + + true +} + +/// Use this to get installed apps +pub fn get_installed_apps(config: &Config) -> Vec { + tracing::debug!("Indexing installed apps"); + + #[cfg(target_os = "macos")] + { + get_installed_macos_apps(config) + } + + #[cfg(target_os = "windows")] + { + get_installed_windows_apps() + } } /// Check if the provided string is a valid url