diff --git a/src/app/apps.rs b/src/app/apps.rs index 676f339..d872a1c 100644 --- a/src/app/apps.rs +++ b/src/app/apps.rs @@ -108,6 +108,14 @@ impl App { display_name: "Quit RustCast".to_string(), search_name: "quit".to_string(), }, + App { + ranking: 0, + open_command: AppCommand::Function(Function::QuitAllApps), + desc: RUSTCAST_DESC_NAME.to_string(), + icons: icons.clone(), + display_name: "Quit All Apps".to_string(), + search_name: "quit all apps".to_string(), + }, App { ranking: 0, open_command: AppCommand::Message(Message::SwitchToPage(Page::Settings)), diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 4aa7ae3..60883e9 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -33,6 +33,7 @@ use crate::calculator::Expr; use crate::commands::Function; use crate::config::Config; use crate::debounce::DebouncePolicy; +use crate::quit::get_open_apps; use crate::unit_conversion; use crate::utils::is_valid_url; use crate::{app::ArrowKey, platform::focus_this_app}; @@ -698,12 +699,19 @@ fn open_window(height: f32) -> Task { /// A helper function for resizing rustcast when only one result is found fn single_item_resize_task(id: Id) -> Task { - Task::done(Message::ResizeWindow(id, 55. + DEFAULT_WINDOW_HEIGHT)) + resize_task(id, 1) } /// A helper function for resizing rustcast when zero results are found fn zero_item_resize_task(id: Id) -> Task { - Task::done(Message::ResizeWindow(id, DEFAULT_WINDOW_HEIGHT)) + resize_task(id, 0) +} + +fn resize_task(id: Id, count: u32) -> Task { + Task::done(Message::ResizeWindow( + id, + (55 * count) as f32 + DEFAULT_WINDOW_HEIGHT, + )) } fn resize_for_results_count(id: Id, count: usize) -> Task { @@ -787,6 +795,12 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task { return zero_item_resize_task(id); }; + let quittables = if tile.query_lc.starts_with("quit") { + get_open_apps(tile.config.theme.show_icons) + } else { + vec![] + }; + match tile.query_lc.as_str() { "randomvar" => { let rand_num = rand::random_range(0..100); @@ -864,6 +878,16 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task { } tile.handle_search_query_changed(); + if tile.query_lc.starts_with("quit") { + let query = tile.query_lc.clone(); + tile.results.extend(quittables.iter().filter_map(move |x| { + if x.search_name.starts_with(&query) { + Some(x.to_owned()) + } else { + None + } + })) + } if !tile.results.is_empty() { tile.results.par_sort_by_key(|x| -x.ranking); diff --git a/src/commands.rs b/src/commands.rs index d50e56f..a4d6d29 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -11,12 +11,15 @@ use crate::{ calculator::Expr, clipboard::ClipBoardContentType, config::Config, + quit::{terminate_all_apps, terminate_app}, }; /// The different functions that rustcast can perform #[derive(Debug, Clone, PartialEq)] pub enum Function { OpenApp(String), + QuitApp(String), + QuitAllApps, RunShellCommand(String), OpenWebsite(String), RandomVar(i32), // Easter egg function @@ -48,6 +51,14 @@ impl Function { .unwrap_or(()); } + Function::QuitAllApps => { + terminate_all_apps(); + } + + Function::QuitApp(name) => { + terminate_app(name.to_owned()); + } + Function::GoogleSearch(query_string) => { let query_args = query_string.replace(" ", "+"); let query = config.search_url.replace("%s", &query_args); diff --git a/src/main.rs b/src/main.rs index 0165cde..cf1adbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod commands; mod config; mod debounce; mod platform; +mod quit; mod styles; mod unit_conversion; mod utils; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 6e10e1f..38de61e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -4,9 +4,9 @@ use iced::wgpu::rwh::WindowHandle; pub use self::cross::default_app_paths; use crate::app::apps::App; -mod cross; +pub mod cross; #[cfg(target_os = "macos")] -mod macos; +pub mod macos; pub fn set_activation_policy_accessory() { #[cfg(target_os = "macos")] diff --git a/src/quit.rs b/src/quit.rs new file mode 100644 index 0000000..2678a15 --- /dev/null +++ b/src/quit.rs @@ -0,0 +1,76 @@ +use std::io::Cursor; + +use iced::widget::image::Handle; +use objc2_app_kit::{NSApplicationActivationPolicy, NSWorkspace}; +use objc2_foundation::NSString; + +use crate::{ + app::apps::{App, AppCommand}, + commands::Function, + platform::macos::discovery::icon_of_path_ns, +}; + +pub fn get_open_apps(store_icons: bool) -> Vec { + let open_apps = NSWorkspace::sharedWorkspace().runningApplications(); + + open_apps + .iter() + .filter_map(|app| { + if app.activationPolicy() != NSApplicationActivationPolicy::Regular { + return None; + } + + let name = app.localizedName().unwrap().to_string(); + + let icon = icon_of_path_ns( + &app.bundleURL() + .and_then(|x| x.path()) + .unwrap_or(NSString::new()) + .to_string(), + ) + .unwrap_or(vec![]); + let icons = if store_icons { + image::ImageReader::new(Cursor::new(icon)) + .with_guessed_format() + .unwrap() + .decode() + .ok() + .map(|img| Handle::from_rgba(img.width(), img.height(), img.into_bytes())) + } else { + None + }; + + Some(App { + ranking: 0, + open_command: AppCommand::Function(Function::QuitApp(name.clone())), + display_name: format!("Quit {}", name), + icons, + search_name: format!("quit {}", name.to_lowercase()), + desc: name.to_string(), + }) + }) + .collect() +} + +pub fn terminate_app(name: String) { + let open_apps = NSWorkspace::sharedWorkspace().runningApplications(); + + for app in open_apps { + let is_regular_app = app.activationPolicy() == NSApplicationActivationPolicy::Regular; + let name_matches = app.localizedName() == Some(NSString::from_str(&name)); + + if is_regular_app && name_matches { + app.terminate(); + break; + } + } +} + +pub fn terminate_all_apps() { + let open_apps = NSWorkspace::sharedWorkspace().runningApplications(); + for app in open_apps { + if app.activationPolicy() == NSApplicationActivationPolicy::Regular { + app.terminate(); + } + } +}