|
/*! |
|
Get truly visible applications - those with windows currently on top of the screen. |
|
This uses the Core Graphics framework's CGWindowListCopyWindowInfo to get only windows |
|
that are actually visible on screen, not just running applications. |
|
*/ |
|
|
|
use core_graphics::window::{ |
|
kCGWindowListExcludeDesktopElements, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo, |
|
}; |
|
use core_graphics::geometry::{CGRect, CGPoint, CGSize}; |
|
use core_foundation::array::CFArray; |
|
use core_foundation::dictionary::CFDictionary; |
|
use core_foundation::string::CFString; |
|
use core_foundation::base::{TCFType, CFTypeRef}; |
|
use core_foundation::number::CFNumber; |
|
use std::collections::HashSet; |
|
|
|
#[derive(Debug, Clone)] |
|
pub struct WindowInfo { |
|
pub app_name: String, |
|
pub window_title: String, |
|
pub pid: i64, |
|
pub window_id: u32, |
|
pub bounds: CGRect, |
|
pub layer: i64, |
|
} |
|
|
|
impl WindowInfo { |
|
fn from_cf_dict(dict: &CFDictionary<CFString, CFTypeRef>) -> Option<Self> { |
|
// Helper function to safely extract string values |
|
let get_string = |key: &str| -> String { |
|
let cf_key = CFString::new(key); |
|
dict.find(cf_key) |
|
.and_then(|value_ref| { |
|
let cf_string = unsafe { CFString::wrap_under_get_rule(*value_ref as *const _) }; |
|
Some(cf_string.to_string()) |
|
}) |
|
.unwrap_or_default() |
|
}; |
|
// Helper function to safely extract number values |
|
let get_number = |key: &str| -> i64 { |
|
let cf_key = CFString::new(key); |
|
dict.find(cf_key) |
|
.and_then(|value_ref| { |
|
let cf_number = unsafe { CFNumber::wrap_under_get_rule(*value_ref as *const _) }; |
|
cf_number.to_i64() |
|
}) |
|
.unwrap_or(0) |
|
}; |
|
|
|
// Helper function to safely extract float values |
|
let _get_float = |key: &str| -> f64 { |
|
let cf_key = CFString::new(key); |
|
dict.find(cf_key) |
|
.and_then(|value_ref| { |
|
let cf_number = unsafe { CFNumber::wrap_under_get_rule(*value_ref as *const _) }; |
|
cf_number.to_f64() |
|
}) |
|
.unwrap_or(0.0) |
|
}; |
|
|
|
let app_name = get_string("kCGWindowOwnerName"); |
|
let window_title = get_string("kCGWindowName"); |
|
let pid = get_number("kCGWindowOwnerPID"); |
|
let window_id = get_number("kCGWindowNumber") as u32; |
|
let layer = get_number("kCGWindowLayer"); |
|
|
|
// Extract bounds dictionary |
|
let bounds = { |
|
let cf_key = CFString::new("kCGWindowBounds"); |
|
dict.find(cf_key) |
|
.and_then(|value_ref| { |
|
let bounds_dict = unsafe { |
|
CFDictionary::<CFString, CFTypeRef>::wrap_under_get_rule(*value_ref as *const _) |
|
}; |
|
|
|
let x = { |
|
let x_key = CFString::new("X"); |
|
bounds_dict.find(x_key) |
|
.and_then(|v| { |
|
let cf_number = unsafe { CFNumber::wrap_under_get_rule(*v as *const _) }; |
|
cf_number.to_f64() |
|
}) |
|
.unwrap_or(0.0) |
|
}; |
|
|
|
let y = { |
|
let y_key = CFString::new("Y"); |
|
bounds_dict.find(y_key) |
|
.and_then(|v| { |
|
let cf_number = unsafe { CFNumber::wrap_under_get_rule(*v as *const _) }; |
|
cf_number.to_f64() |
|
}) |
|
.unwrap_or(0.0) |
|
}; |
|
|
|
let width = { |
|
let width_key = CFString::new("Width"); |
|
bounds_dict.find(width_key) |
|
.and_then(|v| { |
|
let cf_number = unsafe { CFNumber::wrap_under_get_rule(*v as *const _) }; |
|
cf_number.to_f64() |
|
}) |
|
.unwrap_or(0.0) |
|
}; |
|
|
|
let height = { |
|
let height_key = CFString::new("Height"); |
|
bounds_dict.find(height_key) |
|
.and_then(|v| { |
|
let cf_number = unsafe { CFNumber::wrap_under_get_rule(*v as *const _) }; |
|
cf_number.to_f64() |
|
}) |
|
.unwrap_or(0.0) |
|
}; |
|
|
|
Some(CGRect::new(&CGPoint::new(x, y), &CGSize::new(width, height))) |
|
}) |
|
.unwrap_or_else(|| CGRect::new(&CGPoint::new(0.0, 0.0), &CGSize::new(0.0, 0.0))) |
|
}; |
|
|
|
// Skip windows with zero area |
|
if bounds.size.width > 0.0 && bounds.size.height > 0.0 { |
|
Some(WindowInfo { |
|
app_name, |
|
window_title, |
|
pid, |
|
window_id, |
|
bounds, |
|
layer, |
|
}) |
|
} else { |
|
None |
|
} |
|
} |
|
} |
|
|
|
/// Get all visible windows that are currently on screen. |
|
pub fn get_visible_windows() -> Vec<WindowInfo> { |
|
let window_list_ref = unsafe { |
|
CGWindowListCopyWindowInfo( |
|
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, |
|
0, |
|
) |
|
}; |
|
|
|
let window_list = unsafe { CFArray::<CFDictionary<CFString, CFTypeRef>>::wrap_under_create_rule(window_list_ref) }; |
|
let mut visible_windows = Vec::new(); |
|
|
|
for i in 0..window_list.len() { |
|
if let Some(window_dict) = window_list.get(i) { |
|
if let Some(window_info) = WindowInfo::from_cf_dict(&window_dict) { |
|
visible_windows.push(window_info); |
|
} |
|
} |
|
} |
|
|
|
visible_windows |
|
} |
|
|
|
/// Get unique application names that have visible windows on screen. |
|
pub fn get_top_level_visible_apps() -> Vec<String> { |
|
let windows = get_visible_windows(); |
|
|
|
let mut app_names = HashSet::new(); |
|
for window in windows { |
|
let app_name = &window.app_name; |
|
if !app_name.is_empty() && app_name != "Window Server" && app_name != "Dock" && app_name != "Control Center" { |
|
app_names.insert(app_name.clone()); |
|
} |
|
} |
|
|
|
let mut sorted_apps: Vec<String> = app_names.into_iter().collect(); |
|
sorted_apps.sort(); |
|
sorted_apps |
|
} |
|
|
|
/// Get the frontmost (topmost) window. |
|
pub fn get_frontmost_window() -> Option<WindowInfo> { |
|
let windows = get_visible_windows(); |
|
|
|
// Filter out system windows and find the one with the highest layer |
|
let user_windows: Vec<_> = windows |
|
.into_iter() |
|
.filter(|w| w.app_name != "Window Server" && w.app_name != "Dock" && w.app_name != "Control Center") |
|
.collect(); |
|
|
|
user_windows |
|
.into_iter() |
|
.max_by_key(|w| w.layer) |
|
} |
|
|
|
/// Get JetBrains IDEs that are currently visible on screen. |
|
/// Returns a list of IDE names that have visible windows. |
|
pub fn get_visible_jetbrains_ides() -> Vec<String> { |
|
let jetbrains_ides = [ |
|
"pycharm", "clion", "webstorm", "phpstorm", "rubymine", "datagrip", |
|
"rider", "goland", "appcode", "android-studio", "dataspell", "fleet", |
|
"gateway", "space", "rustrover", "intellij", |
|
]; |
|
|
|
let visible_apps = get_top_level_visible_apps(); |
|
let mut found_ides = Vec::new(); |
|
|
|
for app_name in visible_apps { |
|
let app_lower = app_name.to_lowercase(); |
|
for &ide_name in &jetbrains_ides { |
|
if app_lower.contains(ide_name) { |
|
found_ides.push(ide_name.to_string()); |
|
break; // Found match for this app, move to next |
|
} |
|
} |
|
} |
|
|
|
found_ides.sort(); |
|
found_ides.dedup(); |
|
found_ides |
|
} |
|
|
|
/// Get the frontmost JetBrains IDE if one is currently focused. |
|
/// Returns the IDE name if a JetBrains IDE is the frontmost window. |
|
pub fn get_frontmost_jetbrains_ide() -> Option<String> { |
|
let jetbrains_ides = [ |
|
"pycharm", "clion", "webstorm", "phpstorm", "rubymine", "datagrip", |
|
"rider", "goland", "appcode", "android-studio", "dataspell", "fleet", |
|
"gateway", "space", "rustrover", "intellij", |
|
]; |
|
|
|
if let Some(frontmost) = get_frontmost_window() { |
|
let app_lower = frontmost.app_name.to_lowercase(); |
|
for &ide_name in &jetbrains_ides { |
|
if app_lower.contains(ide_name) { |
|
return Some(ide_name.to_string()); |
|
} |
|
} |
|
} |
|
|
|
None |
|
} |