use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::{ChildOf, Children};
use bevy::ecs::message::MessageWriter;
use bevy::ecs::observer::On;
use bevy::ecs::query::With;
use bevy::ecs::system::{Commands, Local, Query, Res, ResMut};
use bevy::time::{Time, Virtual};
use bevy::transform::commands::BuildChildrenTransformExt;
use core::ptr::NonNull;
use log::{debug, error, trace, warn};
use objc2_core_foundation::{
CFArray, CFMutableData, CFNumber, CFNumberType, CFRetained, CGPoint, CGRect,
};
use std::collections::HashMap;
use std::io::ErrorKind;
use std::mem::take;
use std::ops::{Add, Deref};
use std::slice::from_raw_parts_mut;
use std::time::Duration;
use stdext::function_name;
use crate::app::Application;
use crate::config::{Config, WindowParams};
use crate::errors::{Error, Result};
use crate::events::{
BProcess, Event, ExistingMarker, FocusFollowsMouse, FocusedMarker, FreshMarker, MainConnection,
MissionControlActive, OrphanedSpaces, RepositionMarker, ReshuffleAroundTrigger, SenderSocket,
SkipReshuffle, SpawnWindowTrigger, WMEventTrigger,
};
use crate::process::Process;
use crate::skylight::{
_AXUIElementCreateWithRemoteToken, ConnID, SLSCopyAssociatedWindows,
SLSCopyWindowsWithOptionsAndTags, SLSFindWindowAndOwner, SLSWindowIteratorAdvance,
SLSWindowIteratorGetAttributes, SLSWindowIteratorGetParentID, SLSWindowIteratorGetTags,
SLSWindowIteratorGetWindowID, SLSWindowQueryResultCopyWindows, SLSWindowQueryWindows, WinID,
};
use crate::util::{AXUIWrapper, create_array, get_array_values};
use crate::windows::{Display, Panel, Window, WindowPane, ax_window_id, ax_window_pid};
const THRESHOLD: f64 = 10.0;
#[derive(Default)]
pub struct WindowManager;
impl WindowManager {
pub fn register_triggers(app: &mut bevy::app::App) {
app.add_observer(WindowManager::mouse_moved_trigger)
.add_observer(WindowManager::mouse_down_trigger)
.add_observer(WindowManager::mouse_dragged_trigger)
.add_observer(WindowManager::display_change_trigger)
.add_observer(WindowManager::display_add_trigger)
.add_observer(WindowManager::display_remove_trigger)
.add_observer(WindowManager::display_moved_trigger)
.add_observer(WindowManager::front_switched_trigger)
.add_observer(WindowManager::window_focused_trigger)
.add_observer(WindowManager::reshuffle_around_trigger)
.add_observer(WindowManager::swipe_gesture_trigger)
.add_observer(WindowManager::mission_control_trigger)
.add_observer(WindowManager::application_event_trigger)
.add_observer(WindowManager::dispatch_application_messages)
.add_observer(WindowManager::window_resized_trigger)
.add_observer(WindowManager::window_destroyed_trigger)
.add_observer(WindowManager::spawn_window_trigger);
}
#[allow(clippy::needless_pass_by_value)]
pub fn application_event_trigger(
trigger: On<WMEventTrigger>,
processes: Query<(&BProcess, Entity)>,
mut commands: Commands,
) {
let find_process = |psn| {
processes
.iter()
.find(|(BProcess(process), _)| &process.psn == psn)
};
match &trigger.event().0 {
Event::ApplicationLaunched { psn, observer } => {
if find_process(psn).is_none() {
let process = Process::new(psn, observer.clone());
commands.spawn((FreshMarker::new(), BProcess(process)));
}
}
Event::ApplicationTerminated { psn } => {
if let Some((_, entity)) = find_process(psn) {
commands.entity(entity).despawn();
}
}
_ => (),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn dispatch_application_messages(
trigger: On<WMEventTrigger>,
mut windows: Query<(&mut Window, Entity)>,
mut displays: Query<&mut Display, With<FocusedMarker>>,
applications: Query<(&Application, &Children)>,
main_cid: Res<MainConnection>,
mut commands: Commands,
) {
let main_cid = main_cid.0;
let Ok(mut active_display) = displays.single_mut() else {
warn!("{}: Unable to get current display.", function_name!());
return;
};
match &trigger.event().0 {
Event::WindowCreated { element } => match Window::new(element) {
Ok(window) => {
commands.trigger(SpawnWindowTrigger(vec![window]));
}
Err(err) => debug!("{}: not adding window {element:?}: {err}", function_name!(),),
},
Event::WindowMinimized { window_id } => {
_ = window_minimized(
*window_id,
&mut windows,
&mut active_display,
main_cid,
&mut commands,
)
.inspect_err(|err| warn!("{}: Minimizing window: {err}", function_name!()));
}
Event::WindowDeminimized { window_id } => {
_ = window_unminimized(
*window_id,
&mut windows,
&mut active_display,
main_cid,
&mut commands,
)
.inspect_err(|err| warn!("{}: Unminimizing window: {err}", function_name!()));
}
Event::ApplicationHidden { pid } => {
let Some((_, children)) = applications.iter().find(|(app, _)| app.pid() == *pid)
else {
warn!("{}: Unable to find application {pid}", function_name!());
return;
};
let window_ids = children
.iter()
.filter_map(|entity| windows.get(*entity).map(|(window, _)| window.id()).ok())
.collect::<Vec<_>>();
for window_id in window_ids {
_ = window_minimized(
window_id,
&mut windows,
&mut active_display,
main_cid,
&mut commands,
)
.inspect_err(|err| warn!("{}: Minimizing window: {err}", function_name!()));
}
}
Event::ApplicationVisible { pid } => {
let Some((_, children)) = applications.iter().find(|(app, _)| app.pid() == *pid)
else {
warn!("{}: Unable to find application {pid}", function_name!());
return;
};
let window_ids = children
.iter()
.filter_map(|entity| windows.get(*entity).map(|(window, _)| window.id()).ok())
.collect::<Vec<_>>();
for window_id in window_ids {
_ = window_unminimized(
window_id,
&mut windows,
&mut active_display,
main_cid,
&mut commands,
)
.inspect_err(|err| warn!("{}: Unminimizing window: {err}", function_name!()));
}
}
_ => (),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn mission_control_trigger(
trigger: On<WMEventTrigger>,
mut mission_control_active: ResMut<MissionControlActive>,
) {
match trigger.event().0 {
Event::MissionControlShowAllWindows
| Event::MissionControlShowFrontWindows
| Event::MissionControlShowDesktop => {
mission_control_active.as_mut().0 = true;
}
Event::MissionControlExit => {
mission_control_active.as_mut().0 = false;
}
_ => (),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn swipe_gesture_trigger(
trigger: On<WMEventTrigger>,
active_display: Query<&Display, With<FocusedMarker>>,
mut focused_window: Query<(&mut Window, Entity), With<FocusedMarker>>,
main_cid: Res<MainConnection>,
config: Res<Config>,
mut commands: Commands,
) {
const SWIPE_THRESHOLD: f64 = 0.01;
let Event::Swipe { ref deltas } = trigger.event().0 else {
return;
};
if config
.options()
.swipe_gesture_fingers
.is_some_and(|fingers| deltas.len() == fingers)
{
let Ok(active_display) = active_display.single() else {
warn!("{}: Unable to get current display.", function_name!());
return;
};
let delta = deltas.iter().sum::<f64>();
if delta.abs() > SWIPE_THRESHOLD {
slide_window(
main_cid.0,
&mut focused_window,
active_display,
delta,
&mut commands,
);
}
}
}
pub fn refresh_display(
main_cid: ConnID,
display: &mut Display,
windows: &mut Query<(&mut Window, Entity)>,
) {
debug!(
"{}: Refreshing windows on display {}",
function_name!(),
display.id
);
let display_bounds = display.bounds;
for (space_id, pane) in &mut display.spaces {
let mut lens = windows.transmute_lens::<(&Window, Entity)>();
let new_windows = refresh_windows_space(main_cid, *space_id, &lens.query());
for window_entity in pane.all_windows() {
if !new_windows.contains(&window_entity) {
pane.remove(window_entity);
}
}
for window_entity in new_windows {
if windows
.get(window_entity)
.is_ok_and(|(window, _)| !window.managed())
{
continue;
}
if pane.index_of(window_entity).is_err() {
pane.append(window_entity);
if let Ok((mut window, _)) = windows.get_mut(window_entity) {
_ = window.update_frame(Some(&display_bounds));
}
}
}
debug!(
"{}: space {space_id}: after refresh {pane}",
function_name!()
);
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn front_switched_trigger(
trigger: On<WMEventTrigger>,
processes: Query<(&BProcess, &Children)>,
applications: Query<&Application>,
focused_window: Query<(&Window, Entity), With<FocusedMarker>>,
mut focus_follows_mouse_id: ResMut<FocusFollowsMouse>,
mut commands: Commands,
) {
let Event::ApplicationFrontSwitched { ref psn } = trigger.event().0 else {
return;
};
let Some((BProcess(process), children)) =
processes.iter().find(|process| &process.0.0.psn == psn)
else {
error!(
"{}: Unable to find process with PSN {psn:?}",
function_name!()
);
return;
};
if children.len() > 1 {
warn!(
"{}: Multiple apps registered to process {}.",
function_name!(),
process.name
);
}
let Some(app) = children
.first()
.and_then(|entity| applications.get(*entity).ok())
else {
error!(
"{}: No application for process {}.",
function_name!(),
process.name
);
return;
};
debug!("{}: {}", function_name!(), process.name);
match app.focused_window_id() {
Err(_) => {
let Ok((_, focused_entity)) = focused_window.single() else {
warn!("{}: window_manager_set_window_opacity", function_name!());
return;
};
focus_follows_mouse_id.as_mut().0 = None;
commands.entity(focused_entity).remove::<FocusedMarker>();
warn!("{}: reset focused window", function_name!());
}
Ok(focused_id) => commands.trigger(WMEventTrigger(Event::WindowFocused {
window_id: focused_id,
})),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn spawn_window_trigger(
mut trigger: On<SpawnWindowTrigger>,
windows: Query<(Entity, &Window, Option<&FocusedMarker>)>,
mut apps: Query<(Entity, &mut Application)>,
mut active_display: Query<&mut Display, With<FocusedMarker>>,
main_cid: Res<MainConnection>,
config: Res<Config>,
mut commands: Commands,
) {
let new_windows = &mut trigger.event_mut().0;
let Ok(mut active_display) = active_display.single_mut() else {
error!("{}: can not get current display.", function_name!());
return;
};
while let Some(mut window) = new_windows.pop() {
let window_id = window.id();
if windows
.iter()
.any(|(_, window, _)| window.id() == window_id)
{
continue;
}
debug!("{}: window {}", function_name!(), window_id);
let Ok(pid) = ax_window_pid(&window.element()) else {
warn!(
"{}: Unable to get window pid for {}",
function_name!(),
window_id,
);
continue;
};
let Some((app_entity, mut app)) = apps.iter_mut().find(|(_, app)| app.pid() == pid)
else {
warn!(
"{}: unable to find application with {pid}.",
function_name!()
);
continue;
};
debug!(
"{}: created {} title: {} role: {} subrole: {} element: {}",
function_name!(),
window_id,
window.title().unwrap_or_default(),
window.role().unwrap_or_default(),
window.subrole().unwrap_or_default(),
window.element(),
);
if app.observe_window(&window).is_err() {
warn!(
"{}: Error observing window {}.",
function_name!(),
window_id
);
}
window.psn = Some(app.psn());
window.eligible =
window.parent(app.connection().unwrap_or_default()).is_err() || window.is_root();
let bundle_id = app.bundle_id().map(String::as_str).unwrap_or_default();
debug!(
"{}: window {} isroot {} eligible {} bundle_id {}",
function_name!(),
window_id,
window.is_root(),
window.is_eligible(),
bundle_id,
);
let title = window.title().unwrap_or_default();
let properties = config
.find_window_properties(&title, bundle_id)
.inspect(|_| {
debug!(
"{}: Applying window properties for '{title}",
function_name!()
);
});
apply_window_properties(
window,
app_entity,
properties.as_ref(),
&mut active_display,
main_cid.0,
&windows,
&mut commands,
);
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn window_destroyed_trigger(
trigger: On<WMEventTrigger>,
mut windows: Query<(&Window, Entity, &ChildOf)>,
mut apps: Query<&mut Application>,
mut displays: Query<&mut Display>,
main_cid: Res<MainConnection>,
mut commands: Commands,
) {
let Event::WindowDestroyed { window_id } = trigger.event().0 else {
return;
};
let Some((window, entity, child)) = windows
.iter()
.find(|(window, _, _)| window.id() == window_id)
else {
error!(
"{}: Trying to destroy non-existing window {window_id}.",
function_name!()
);
return;
};
let Ok(mut app) = apps.get_mut(child.parent()) else {
error!(
"{}: Window {} has no parent!",
function_name!(),
window.id()
);
return;
};
app.unobserve_window(window);
commands.entity(entity).despawn();
let mut lens = windows.transmute_lens::<&Window>();
for mut display in &mut displays {
let Ok(panel) = display.active_panel(main_cid.0) else {
continue;
};
give_away_focus(entity, &lens.query(), panel, &mut commands);
display.remove_window(entity);
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn window_resized_trigger(
trigger: On<WMEventTrigger>,
mut windows: Query<(&mut Window, Entity)>,
displays: Query<&mut Display, With<FocusedMarker>>,
mut commands: Commands,
) {
let Event::WindowResized { window_id } = trigger.event().0 else {
return;
};
let Ok(active_display) = displays.single() else {
warn!("{}: Unable to get current display.", function_name!());
return;
};
let Some((mut window, _)) = windows
.iter_mut()
.find(|(window, _)| window.id() == window_id)
else {
return;
};
_ = window.update_frame(Some(&active_display.bounds));
commands.trigger(ReshuffleAroundTrigger(window.id()));
}
#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
pub fn window_focused_trigger(
trigger: On<WMEventTrigger>,
applications: Query<&Application>,
windows: Query<(&Window, Entity, &ChildOf, Option<&FocusedMarker>)>,
current_focus: Query<(&Window, Entity), With<FocusedMarker>>,
main_cid: Res<MainConnection>,
config: Res<Config>,
time: Res<Time<Virtual>>,
mut delayed_focus: Local<HashMap<WinID, Duration>>,
mut messages: MessageWriter<Event>,
mut focus_follows_mouse_id: ResMut<FocusFollowsMouse>,
mut skip_reshuffle: ResMut<SkipReshuffle>,
mut commands: Commands,
) {
let Event::WindowFocused { window_id } = trigger.event().0 else {
return;
};
let main_cid = main_cid.0;
let Some((window, entity, child, _)) = windows
.iter()
.find(|(window, _, _, _)| window.id() == window_id)
else {
stray_focus_event(window_id, &time, &mut delayed_focus, &mut messages);
return;
};
delayed_focus.remove(&window_id);
for (window, entity, _, focused) in windows {
if focused.is_some() && window.id() != window_id {
commands.entity(entity).remove::<FocusedMarker>();
}
if focused.is_none() && window.id() == window_id {
commands.entity(entity).insert(FocusedMarker);
}
}
debug!("{}: window id {}", function_name!(), window.id());
let Ok(app) = applications.get(child.parent()) else {
warn!(
"{}: Unable to get parent for window {}.",
function_name!(),
window.id()
);
return;
};
if !app.is_frontmost() {
return;
}
let previous_focus = current_focus.single().ok();
if let Some((_, previous_entity)) = previous_focus {
commands.entity(previous_entity).remove::<FocusedMarker>();
}
if mouse_follows_focus(&config)
&& previous_focus.is_none_or(|(previous, _)| previous.id() != window_id)
&& focus_follows_mouse_id.0.is_none_or(|id| id != window_id)
{
window.center_mouse(main_cid);
}
commands.entity(entity).insert(FocusedMarker);
focus_follows_mouse_id.as_mut().0 = None;
if skip_reshuffle.0 {
skip_reshuffle.as_mut().0 = false;
} else {
commands.trigger(ReshuffleAroundTrigger(window.id()));
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn reshuffle_around_trigger(
trigger: On<ReshuffleAroundTrigger>,
main_cid: Res<MainConnection>,
mut active_display: Query<&mut Display, With<FocusedMarker>>,
mut windows: Query<(&mut Window, Entity, Option<&RepositionMarker>)>,
mut commands: Commands,
) {
let Some((window, entity, _)) = windows
.iter()
.find(|(window, _, _)| window.id() == trigger.event().0)
else {
return;
};
if window.managed() {
_ = reshuffle_around(
main_cid.0,
&mut active_display,
entity,
&mut windows,
&mut commands,
)
.inspect_err(|err| {
error!("{}: failed with: {err}", function_name!());
});
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn add_existing_process(
cid: Res<MainConnection>,
events: Res<SenderSocket>,
process_query: Query<(Entity, &BProcess), With<ExistingMarker>>,
mut commands: Commands,
) {
for (entity, process) in process_query {
let app = Application::new(cid.0, &process.0, &events.0).unwrap();
commands
.spawn((app, ExistingMarker))
.set_parent_in_place(entity);
commands.entity(entity).remove::<ExistingMarker>();
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn add_existing_application(
cid: Res<MainConnection>,
displays: Query<&Display>,
app_query: Query<(&mut Application, Entity), With<ExistingMarker>>,
windows: Query<(&Window, Entity)>,
mut commands: Commands,
) {
let spaces = displays
.iter()
.flat_map(|display| display.spaces.keys().copied().collect::<Vec<_>>())
.collect::<Vec<_>>();
for (mut app, entity) in app_query {
if app.observe().is_ok_and(|result| result)
&& let Ok(windows) =
add_existing_application_windows(cid.0, &mut app, &spaces, 0, &windows)
.inspect_err(|err| warn!("{}: {err}", function_name!()))
{
commands.trigger(SpawnWindowTrigger(windows));
}
commands.entity(entity).remove::<ExistingMarker>();
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn add_launched_process(
cid: Res<MainConnection>,
events: Res<SenderSocket>,
process_query: Query<(Entity, &mut BProcess, Option<&Children>), With<FreshMarker>>,
mut commands: Commands,
) {
for (entity, mut process, children) in process_query {
let process = &mut *process.0;
if !process.ready() {
continue;
}
if children.is_some() {
commands.entity(entity).remove::<FreshMarker>();
continue;
}
let mut app = Application::new(cid.0, process, &events.0).unwrap();
if app.observe().is_ok_and(|good| good) {
commands
.spawn((app, FreshMarker::new()))
.set_parent_in_place(entity);
} else {
debug!(
"{}: failed to register some observers {}",
function_name!(),
process.name
);
}
}
}
pub fn add_launched_application(
app_query: Query<(&mut Application, Entity), With<FreshMarker>>,
windows: Query<&Window>,
mut commands: Commands,
) {
let find_window = |window_id| windows.iter().find(|window| window.id() == window_id);
for (app, entity) in app_query {
let Ok(array) = app.window_list() else {
continue;
};
let create_window = |element_ref: NonNull<_>| {
let element = AXUIWrapper::retain(element_ref.as_ptr()).ok();
element.and_then(|element| {
let window_id = ax_window_id(element.as_ptr())
.inspect_err(|err| {
warn!("{}: error adding window: {err}", function_name!());
})
.ok()?;
if find_window(window_id).is_none() {
Some(Window::new(&element).inspect_err(|err| {
warn!("{}: error adding window: {err}.", function_name!());
}))
} else {
None
}
})
};
let windows = get_array_values::<accessibility_sys::__AXUIElement>(&array)
.filter_map(create_window)
.flatten()
.collect::<Vec<_>>();
commands.entity(entity).remove::<FreshMarker>();
commands.trigger(SpawnWindowTrigger(windows));
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn display_add_trigger(
trigger: On<WMEventTrigger>,
mut windows: Query<&mut Window>,
main_cid: Res<MainConnection>,
mut orphaned_spaces: ResMut<OrphanedSpaces>,
mut commands: Commands,
) {
let Event::DisplayAdded { display_id } = trigger.event().0 else {
return;
};
let main_cid = main_cid.0;
debug!("{}: Display Added: {display_id:?}", function_name!());
let Some(mut display) = Display::present_displays(main_cid)
.into_iter()
.find(|display| display.id == display_id)
else {
error!("{}: Unable to find added display!", function_name!());
return;
};
find_orphaned_spaces(&mut orphaned_spaces.0, &mut display, &mut windows);
for (id, pane) in &display.spaces {
debug!("{}: Space {id} - {pane}", function_name!());
}
commands.spawn(display);
commands.trigger(WMEventTrigger(Event::DisplayChanged));
}
#[allow(clippy::needless_pass_by_value)]
pub fn display_remove_trigger(
trigger: On<WMEventTrigger>,
mut displays: Query<(&mut Display, Entity)>,
mut windows: Query<&mut Window>,
mut orphaned_spaces: ResMut<OrphanedSpaces>,
mut commands: Commands,
) {
let Event::DisplayRemoved { display_id } = trigger.event().0 else {
return;
};
debug!("{}: Display Removed: {display_id:?}", function_name!());
let Some((mut display, entity)) = displays
.iter_mut()
.find(|(display, _)| display.id == display_id)
else {
error!("{}: Unable to find removed display!", function_name!());
return;
};
let orphaned_spaces = &mut orphaned_spaces.0;
for (space_id, pane) in take(&mut display.spaces)
.into_iter()
.filter(|(_, pane)| pane.len() > 0)
{
debug!("{}: adding {pane} to orphaned list.", function_name!(),);
orphaned_spaces.insert(space_id, pane);
}
for (mut display, _) in displays {
find_orphaned_spaces(orphaned_spaces, &mut display, &mut windows);
}
commands.entity(entity).despawn();
commands.trigger(WMEventTrigger(Event::DisplayChanged));
}
#[allow(clippy::needless_pass_by_value)]
pub fn display_moved_trigger(
trigger: On<WMEventTrigger>,
mut displays: Query<(&mut Display, Entity)>,
mut windows: Query<&mut Window>,
main_cid: Res<MainConnection>,
mut orphaned_spaces: ResMut<OrphanedSpaces>,
mut commands: Commands,
) {
let Event::DisplayMoved { display_id } = trigger.event().0 else {
return;
};
let main_cid = main_cid.0;
debug!("{}: Display Moved: {display_id:?}", function_name!());
let Some((mut display, _)) = displays
.iter_mut()
.find(|(display, _)| display.id == display_id)
else {
error!("{}: Unable to find moved display!", function_name!());
return;
};
let Some(moved_display) = Display::present_displays(main_cid)
.into_iter()
.find(|display| display.id == display_id)
else {
return;
};
*display = moved_display;
find_orphaned_spaces(&mut orphaned_spaces.0, &mut display, &mut windows);
for (id, pane) in &display.spaces {
debug!("{}: Space {id} - {pane}", function_name!());
}
commands.trigger(WMEventTrigger(Event::DisplayChanged));
}
#[allow(clippy::needless_pass_by_value)]
pub fn display_change_trigger(
trigger: On<WMEventTrigger>,
focused_window: Query<(&Window, Entity), With<FocusedMarker>>,
mut displays: Query<(&mut Display, Entity, Option<&FocusedMarker>)>,
main_cid: Res<MainConnection>,
mut commands: Commands,
) {
if !matches!(trigger.event().0, Event::DisplayChanged) {
return;
}
let main_cid = main_cid.0;
let Ok(active_id) = Display::active_display_id(main_cid) else {
error!("{}: Unable to get active display id!", function_name!());
return;
};
if let Some((previous_display, previous_entity, _)) =
displays.iter().find(|(_, _, active)| active.is_some())
&& previous_display.id != active_id
{
commands.entity(previous_entity).remove::<FocusedMarker>();
}
_ = display_change(
active_id,
main_cid,
&mut displays,
&focused_window,
&mut commands,
)
.inspect_err(|err| warn!("{}: {err}", function_name!()));
}
#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
pub fn mouse_moved_trigger(
trigger: On<WMEventTrigger>,
windows: Query<&Window>,
focused_window: Query<(&Window, Entity), With<FocusedMarker>>,
main_cid: Res<MainConnection>,
config: Res<Config>,
mission_control_active: Res<MissionControlActive>,
mut focus_follows_mouse_id: ResMut<FocusFollowsMouse>,
mut skip_reshuffle: ResMut<SkipReshuffle>,
) {
let Event::MouseMoved { point } = trigger.event().0 else {
return;
};
let main_cid = main_cid.0;
if !focus_follows_mouse(config.as_ref()) {
return;
}
if mission_control_active.0 {
return;
}
if focus_follows_mouse_id.0.is_some() {
trace!("{}: ffm_window_id > 0", function_name!());
return;
}
let Ok(window_id) = find_window_at_point(main_cid, &point) else {
debug!(
"{}: can not find window at point {point:?}",
function_name!()
);
return;
};
let Ok((focused_window, _)) = focused_window.single() else {
debug!("{}: can not find focused widnow.", function_name!());
return;
};
if focused_window.id() == window_id {
trace!("{}: allready focused {}", function_name!(), window_id);
return;
}
let Some(window) = windows.iter().find(|window| window.id() == window_id) else {
trace!(
"{}: can not find focused window: {}",
function_name!(),
window_id
);
return;
};
if !window.is_eligible() {
trace!("{}: {} not eligible", function_name!(), window_id);
return;
}
let window_list = unsafe {
let arr_ref = SLSCopyAssociatedWindows(main_cid, window_id);
CFRetained::retain(arr_ref)
};
let mut window = window;
for item in get_array_values(&window_list) {
let mut child_wid: WinID = 0;
unsafe {
if !CFNumber::value(
item.as_ref(),
CFNumberType::SInt32Type,
NonNull::from(&mut child_wid).as_ptr().cast(),
) {
warn!(
"{}: Unable to find subwindows of window {}: {item:?}.",
function_name!(),
window_id
);
continue;
}
};
debug!(
"{}: checking {}'s childen: {}",
function_name!(),
window_id,
child_wid
);
let Some(child_window) = windows.iter().find(|window| window.id() == child_wid) else {
warn!(
"{}: Unable to find child window {child_wid}.",
function_name!()
);
continue;
};
let Ok(role) = window.role() else {
warn!("{}: finding role for {window_id}", function_name!(),);
continue;
};
let valid = ["AXSheet", "AXDrawer"]
.iter()
.any(|axrole| axrole.eq(&role));
if valid {
window = child_window;
break;
}
}
skip_reshuffle.as_mut().0 = true;
window.focus_without_raise(focused_window);
focus_follows_mouse_id.as_mut().0 = Some(window_id);
}
#[allow(clippy::needless_pass_by_value)]
pub fn mouse_down_trigger(
trigger: On<WMEventTrigger>,
windows: Query<&Window>,
active_display: Query<&Display, With<FocusedMarker>>,
main_cid: Res<MainConnection>,
mission_control_active: Res<MissionControlActive>,
mut commands: Commands,
) {
let Event::MouseDown { point } = trigger.event().0 else {
return;
};
debug!("{}: {point:?}", function_name!());
if mission_control_active.0 {
return;
}
let main_cid = main_cid.0;
let Ok(active_display) = active_display.single() else {
warn!("{}: Unable to get current display.", function_name!());
return;
};
let Some(window) = find_window_at_point(main_cid, &point)
.ok()
.and_then(|window_id| windows.iter().find(|window| window.id() == window_id))
else {
return;
};
if !window.fully_visible(&active_display.bounds) {
commands.trigger(ReshuffleAroundTrigger(window.id()));
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn mouse_dragged_trigger(
trigger: On<WMEventTrigger>,
mission_control_active: Res<MissionControlActive>,
) {
let Event::MouseDragged { point } = trigger.event().0 else {
return;
};
trace!("{}: {point:?}", function_name!());
if mission_control_active.0 {
#[warn(clippy::needless_return)]
return;
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn ready_timer_cleanup(
cleanup_query: Query<(Entity, &mut FreshMarker), With<FreshMarker>>,
processes: Query<&BProcess>,
applications: Query<&Application>,
time: Res<Time<Virtual>>,
mut commands: Commands,
) {
for (entity, mut ready_timer) in cleanup_query {
let timer = &mut ready_timer.0;
timer.tick(time.delta());
trace!(
"{}: Entity {entity} Timer {}",
function_name!(),
timer.elapsed().as_secs_f32()
);
if timer.is_finished() {
if let Ok(app) = applications.get(entity) {
debug!(
"{}: Application pid {} did not become observable in time. Removing entity {entity}.",
function_name!(),
app.pid()
);
} else if let Ok(process) = processes.get(entity) {
debug!(
"{}: Process '{}' did not become ready in time. Removing entity {entity}.",
function_name!(),
process.0.name,
);
}
commands.entity(entity).despawn();
}
}
}
}
fn find_orphaned_spaces(
orphaned_spaces: &mut HashMap<u64, WindowPane>,
display: &mut Display,
windows: &mut Query<&mut Window>,
) {
let mut relocated_windows = vec![];
for (space_id, pane) in &mut display.spaces {
debug!(
"{}: Checking space {space_id} for orphans: {pane}",
function_name!()
);
if let Some(space) = orphaned_spaces.remove(space_id) {
debug!(
"{}: Reinserting orphaned space {space_id} into display {}",
function_name!(),
display.id
);
for window_id in space.all_windows() {
pane.append(window_id);
relocated_windows.push((window_id, display.bounds));
}
}
}
for (entity, bounds) in relocated_windows {
let Ok(mut window) = windows.get_mut(entity) else {
continue;
};
_ = window.update_frame(Some(&bounds));
debug!(
"{}: Relocated window {} has ratio {:.02}",
function_name!(),
window.id(),
window.width_ratio,
);
}
}
fn refresh_windows_space(
main_cid: ConnID,
space_id: u64,
windows: &Query<(&Window, Entity)>,
) -> Vec<Entity> {
space_window_list_for_connection(main_cid, &[space_id], None, false, windows)
.inspect_err(|err| {
warn!(
"{}: getting windows for space {space_id}: {err}",
function_name!()
);
})
.unwrap_or_default()
.into_iter()
.filter_map(|window_id| windows.iter().find(|(window, _)| window.id() == window_id))
.filter_map(|(window, entity)| window.is_eligible().then_some(entity))
.collect()
}
fn space_window_list_for_connection(
main_cid: ConnID,
spaces: &[u64],
cid: Option<ConnID>,
also_minimized: bool,
windows: &Query<(&Window, Entity)>,
) -> Result<Vec<WinID>> {
unsafe {
let space_list_ref = create_array(spaces, CFNumberType::SInt64Type)?;
let mut set_tags = 0i64;
let mut clear_tags = 0i64;
let options = if also_minimized { 0x7 } else { 0x2 };
let ptr = NonNull::new(SLSCopyWindowsWithOptionsAndTags(
main_cid,
cid.unwrap_or(0),
&raw const *space_list_ref,
options,
&mut set_tags,
&mut clear_tags,
))
.ok_or(Error::new(
ErrorKind::InvalidInput,
format!(
"{}: nullptr returned from SLSCopyWindowsWithOptionsAndTags.",
function_name!()
),
))?;
let window_list_ref = CFRetained::from_raw(ptr);
let count = window_list_ref.count();
if count == 0 {
return Err(Error::new(
ErrorKind::InvalidData,
format!("{}: zero windows returned", function_name!()),
));
}
let query = CFRetained::from_raw(SLSWindowQueryWindows(
main_cid,
&raw const *window_list_ref,
count,
));
let iterator = CFRetained::from_raw(SLSWindowQueryResultCopyWindows(query.deref().into()));
let mut window_list = Vec::with_capacity(count.try_into().unwrap());
while SLSWindowIteratorAdvance(&raw const *iterator) {
let tags = SLSWindowIteratorGetTags(&raw const *iterator);
let attributes = SLSWindowIteratorGetAttributes(&raw const *iterator);
let parent_wid: WinID = SLSWindowIteratorGetParentID(&raw const *iterator);
let window_id: WinID = SLSWindowIteratorGetWindowID(&raw const *iterator);
trace!(
"{}: id: {window_id} parent: {parent_wid} tags: 0x{tags:x} attributes: 0x{attributes:x}",
function_name!()
);
match windows.iter().find(|(window, _)| window.id() == window_id) {
Some((window, _)) => {
if also_minimized || !window.minimized {
window_list.push(window.id());
}
}
None => {
if found_valid_window(parent_wid, attributes, tags) {
window_list.push(window_id);
}
}
}
}
Ok(window_list)
}
}
fn found_valid_window(parent_wid: WinID, attributes: i64, tags: i64) -> bool {
parent_wid == 0
&& ((0 != (attributes & 0x2) || 0 != (tags & 0x0400_0000_0000_0000))
&& (0 != (tags & 0x1) || (0 != (tags & 0x2) && 0 != (tags & 0x8000_0000))))
|| ((attributes == 0x0 || attributes == 0x1)
&& (0 != (tags & 0x1000_0000_0000_0000) || 0 != (tags & 0x0300_0000_0000_0000))
&& (0 != (tags & 0x1) || (0 != (tags & 0x2) && 0 != (tags & 0x8000_0000))))
}
fn existing_application_window_list(
cid: ConnID,
app: &Application,
spaces: &[u64],
windows: &Query<(&Window, Entity)>,
) -> Result<Vec<WinID>> {
if spaces.is_empty() {
return Err(Error::new(
ErrorKind::InvalidData,
format!("{}: no spaces returned", function_name!()),
));
}
space_window_list_for_connection(cid, spaces, app.connection(), true, windows)
}
fn bruteforce_windows(app: &mut Application, window_list: &mut Vec<WinID>) -> Vec<Window> {
const MAGIC: u32 = 0x636f_636f;
let mut found_windows = Vec::new();
debug!(
"{}: App {:?} has unresolved window on other desktops, bruteforcing them.",
function_name!(),
app.psn(),
);
unsafe {
const BUFSIZE: isize = 0x14;
let Some(data_ref) = CFMutableData::new(None, BUFSIZE) else {
error!("{}: error creating mutable data", function_name!());
return found_windows;
};
CFMutableData::increase_length(data_ref.deref().into(), BUFSIZE);
let data = from_raw_parts_mut(
CFMutableData::mutable_byte_ptr(data_ref.deref().into()),
BUFSIZE as usize,
);
let bytes = app.pid().to_ne_bytes();
data[0x0..bytes.len()].copy_from_slice(&bytes);
let bytes = MAGIC.to_ne_bytes();
data[0x8..0x8 + bytes.len()].copy_from_slice(&bytes);
for element_id in 0..0x7fffu64 {
let bytes = element_id.to_ne_bytes();
data[0xc..0xc + bytes.len()].copy_from_slice(&bytes);
let Ok(element_ref) =
AXUIWrapper::retain(_AXUIElementCreateWithRemoteToken(data_ref.as_ref()))
else {
continue;
};
let Ok(window_id) = ax_window_id(element_ref.as_ptr()) else {
continue;
};
if let Some(index) = window_list.iter().position(|&id| id == window_id) {
window_list.remove(index);
debug!("{}: Found window {window_id:?}", function_name!());
if let Ok(window) = Window::new(&element_ref)
.inspect_err(|err| warn!("{}: {err}", function_name!()))
{
found_windows.push(window);
}
}
}
}
found_windows
}
fn add_existing_application_windows(
cid: ConnID,
app: &mut Application,
spaces: &[u64],
refresh_index: i32,
windows: &Query<(&Window, Entity)>,
) -> Result<Vec<Window>> {
let global_window_list = existing_application_window_list(cid, app, spaces, windows)?;
if global_window_list.is_empty() {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"{}: No windows found for app {:?}",
function_name!(),
app.psn()
),
));
}
debug!(
"{}: App {:?} has global windows: {global_window_list:?}",
function_name!(),
app.psn(),
);
let window_list = app.window_list();
let window_count = window_list
.as_ref()
.map(|window_list| CFArray::count(window_list))
.unwrap_or(0);
let mut found_windows: Vec<Window> = Vec::new();
let mut empty_count = 0;
if let Ok(window_list) = window_list {
for window_ref in get_array_values(&window_list) {
let Ok(window_id) = ax_window_id(window_ref.as_ptr()) else {
empty_count += 1;
continue;
};
if !found_windows.iter().any(|window| window.id() == window_id) {
let window_ref = AXUIWrapper::retain(window_ref.as_ptr())?;
debug!(
"{}: Add window: {:?} {window_id}",
function_name!(),
app.psn()
);
if let Ok(window) = Window::new(&window_ref)
.inspect_err(|err| debug!("{}: {err}", function_name!()))
{
found_windows.push(window);
}
}
}
}
if isize::try_from(global_window_list.len())
.is_ok_and(|length| length == (window_count - empty_count))
{
if refresh_index != -1 {
debug!(
"{}: All windows for {:?} are now resolved",
function_name!(),
app.psn(),
);
}
} else {
let find_window = |window_id| found_windows.iter().find(|window| window.id() == window_id);
let mut app_window_list: Vec<WinID> = global_window_list
.iter()
.filter(|window_id| find_window(**window_id).is_none())
.copied()
.collect();
if !app_window_list.is_empty() {
debug!(
"{}: {:?} has windows that are not yet resolved",
function_name!(),
app.psn(),
);
found_windows.extend(bruteforce_windows(app, &mut app_window_list));
}
}
Ok(found_windows)
}
fn apply_window_properties(
mut window: Window,
app_entity: Entity,
properties: Option<&WindowParams>,
active_display: &mut Display,
main_cid: ConnID,
windows: &Query<(Entity, &Window, Option<&FocusedMarker>)>,
commands: &mut Commands,
) {
let window_id = window.id();
let floating = properties
.as_ref()
.and_then(|props| props.floating)
.unwrap_or(false);
let wanted_insertion = properties.as_ref().and_then(|props| props.index);
window.manage(!floating);
_ = window
.update_frame(Some(&active_display.bounds))
.inspect_err(|err| error!("{}: {err}", function_name!()));
let entity = commands.spawn(window).id();
commands.entity(entity).set_parent_in_place(app_entity);
if floating {
return;
}
let Ok(panel) = active_display.active_panel(main_cid) else {
return;
};
let insert_at = wanted_insertion.map_or_else(
|| {
let focused_window = windows
.iter()
.find_map(|(entity, _, focused)| focused.map(|_| entity));
focused_window
.and_then(|entity| panel.index_of(entity).ok())
.and_then(|insert_at| (insert_at + 1 < panel.len()).then_some(insert_at + 1))
},
Some,
);
debug!("{}: New window adding at {panel}", function_name!());
match insert_at {
Some(after) => {
debug!("{}: New window inserted at {after}", function_name!());
panel.insert_at(after, entity);
}
None => panel.append(entity),
}
commands.trigger(ReshuffleAroundTrigger(window_id));
}
#[allow(clippy::needless_pass_by_value)]
fn reshuffle_around(
main_cid: ConnID,
active_display: &mut Query<&mut Display, With<FocusedMarker>>,
entity: Entity,
windows: &mut Query<(&mut Window, Entity, Option<&RepositionMarker>)>,
commands: &mut Commands,
) -> Result<()> {
let mut active_display = active_display.single_mut()?;
let display_bounds = active_display.bounds;
let menubar_height = active_display.menubar_height;
let active_panel = active_display.active_panel(main_cid)?;
let (window, _, moving) = windows.get_mut(entity)?;
let frame = window.expose_window(&display_bounds, moving, entity, commands);
trace!(
"{}: Moving window {} to {:?}",
function_name!(),
window.id(),
frame.origin
);
let panel = active_panel
.index_of(entity)
.and_then(|index| active_panel.get(index))?;
reposition_stack(
frame.origin.x,
&panel,
frame.size.width,
&display_bounds,
menubar_height,
windows,
commands,
);
let mut upper_left = frame.origin.x + frame.size.width;
_ = active_panel.access_right_of(entity, |panel| {
let frame = panel
.top()
.and_then(|entity| windows.get(entity).ok())
.map(|(window, _, _)| (window.id(), window.frame));
if let Some((window_id, frame)) = frame {
trace!(
"{}: window {window_id} right: frame: {frame:?}",
function_name!()
);
if upper_left > display_bounds.size.width - THRESHOLD {
upper_left = display_bounds.size.width - THRESHOLD;
}
if (frame.origin.x - upper_left).abs() > 0.1 {
reposition_stack(
upper_left,
panel,
frame.size.width,
&display_bounds,
menubar_height,
windows,
commands,
);
}
upper_left += frame.size.width;
}
true });
let mut upper_left = frame.origin.x;
_ = active_panel.access_left_of(entity, |panel| {
let frame = panel
.top()
.and_then(|entity| windows.get(entity).ok())
.map(|(window, _, _)| (window.id(), window.frame));
if let Some((window_id, frame)) = frame {
trace!(
"{}: window {window_id} left: frame: {frame:?}",
function_name!()
);
if upper_left < THRESHOLD {
upper_left = THRESHOLD;
}
upper_left -= frame.size.width;
if (frame.origin.x - upper_left).abs() > 0.1 {
reposition_stack(
upper_left,
panel,
frame.size.width,
&display_bounds,
menubar_height,
windows,
commands,
);
}
}
true });
Ok(())
}
fn reposition_stack(
upper_left: f64,
panel: &Panel,
width: f64,
display_bounds: &CGRect,
menubar_height: f64,
windows: &mut Query<(&mut Window, Entity, Option<&RepositionMarker>)>,
commands: &mut Commands,
) {
const REMAINING_THERSHOLD: f64 = 200.0;
let display_height = display_bounds.size.height - menubar_height;
let entities = match panel {
Panel::Single(entity) => vec![*entity],
Panel::Stack(stack) => stack.clone(),
};
let count: f64 = u32::try_from(entities.len()).unwrap().into();
let mut fits = 0f64;
let mut height = menubar_height;
let mut remaining = display_height;
for entity in &entities[0..entities.len() - 1] {
remaining = display_height - height;
if let Ok((window, _, _)) = windows.get(*entity) {
if window.frame().size.height > remaining - REMAINING_THERSHOLD {
trace!(
"{}: height {height}, remaining {remaining}",
function_name!()
);
break;
}
height += window.frame().size.height;
fits += 1.0;
}
}
let avg_height = remaining / (count - fits);
trace!(
"{}: fits {fits:.0} avg_height {avg_height:.0}",
function_name!()
);
let mut y_pos = 0f64;
for entity in entities {
if let Ok((mut window, entity, _)) = windows.get_mut(entity) {
let window_height = window.frame().size.height;
commands.entity(entity).insert(RepositionMarker {
origin: CGPoint {
x: upper_left,
y: y_pos,
},
});
if fits > 0.0 {
y_pos += window_height;
fits -= 1.0;
} else {
window.resize(width, avg_height, display_bounds);
y_pos += avg_height;
}
}
}
}
fn focus_follows_mouse(config: &Config) -> bool {
config.options().focus_follows_mouse.is_none_or(|ffm| ffm)
}
fn mouse_follows_focus(config: &Config) -> bool {
config.options().mouse_follows_focus.is_none_or(|mff| mff)
}
fn display_change(
active_id: u32,
main_cid: ConnID,
displays: &mut Query<(&mut Display, Entity, Option<&FocusedMarker>)>,
focused_window: &Query<(&Window, Entity), With<FocusedMarker>>,
commands: &mut Commands,
) -> Result<()> {
let (mut active_display, entity, _) = displays
.iter_mut()
.find(|(display, _, _)| display.id == active_id)
.ok_or(Error::new(
ErrorKind::NotFound,
"Can not find active display {display_id}.",
))?;
commands.entity(entity).insert(FocusedMarker);
debug!(
"{}: Display ({active_id}) or Workspace changed, reorienting windows.",
function_name!(),
);
let (window, entity) = focused_window.single()?;
let panel = active_display.active_panel(main_cid)?;
debug!("{}: Active panel {panel}", function_name!());
if !window.managed() || panel.index_of(entity).is_ok() {
return Ok(());
}
debug!(
"{}: Window {} moved between displays or workspaces.",
function_name!(),
window.id(),
);
for (mut display, _, _) in displays {
display.remove_window(entity);
if display.id == active_id {
if let Ok(panel) = display.active_panel(main_cid) {
panel.append(entity);
}
}
}
commands.trigger(ReshuffleAroundTrigger(window.id()));
Ok(())
}
fn slide_window(
main_cid: ConnID,
focused_window: &mut Query<(&mut Window, Entity), With<FocusedMarker>>,
active_display: &Display,
delta_x: f64,
commands: &mut Commands,
) {
trace!("{}: Windows slide {delta_x}.", function_name!());
let Ok((mut window, _)) = focused_window.single_mut() else {
warn!("{}: No window focused.", function_name!());
return;
};
let frame = window.frame();
let x = frame.origin.x - (active_display.bounds.size.width / 2.0 * delta_x);
window.reposition(
x.min(active_display.bounds.size.width - frame.size.width)
.max(0.0),
frame.origin.y,
&active_display.bounds,
);
window.center_mouse(main_cid);
commands.trigger(ReshuffleAroundTrigger(window.id()));
}
fn find_window_at_point(main_cid: ConnID, point: &CGPoint) -> Result<WinID> {
let mut window_id: WinID = 0;
let mut window_conn_id: ConnID = 0;
let mut window_point = CGPoint { x: 0f64, y: 0f64 };
unsafe {
SLSFindWindowAndOwner(
main_cid,
0, 1,
0,
point,
&mut window_point,
&mut window_id,
&mut window_conn_id,
)
};
if main_cid == window_conn_id {
unsafe {
SLSFindWindowAndOwner(
main_cid,
window_id,
-1,
0,
point,
&mut window_point,
&mut window_id,
&mut window_conn_id,
)
};
}
if window_id == 0 {
Err(Error::invalid_window(&format!(
"{}: could not find a window at {point:?}",
function_name!()
)))
} else {
Ok(window_id)
}
}
fn give_away_focus(
entity: Entity,
windows: &Query<&Window>,
active_pane: &mut WindowPane,
commands: &mut Commands,
) {
if let Ok(index) = active_pane.index_of(entity)
&& active_pane.len() > 1
{
let neighbour = active_pane.get(index.saturating_sub(1)).ok();
if let Some(window) = neighbour
.and_then(|pane| pane.top())
.and_then(|entity| windows.get(entity).ok())
{
let window_id = window.id();
debug!(
"{}: window destroyed, moving focus to {window_id}",
function_name!()
);
commands.trigger(WMEventTrigger(Event::WindowFocused { window_id }));
commands.trigger(ReshuffleAroundTrigger(window_id));
}
}
}
fn window_minimized(
window_id: WinID,
windows: &mut Query<(&mut Window, Entity)>,
active_display: &mut Display,
main_cid: ConnID,
commands: &mut Commands,
) -> Result<()> {
let (mut window, entity) = windows
.iter_mut()
.find(|(window, _)| window.id() == window_id)
.ok_or(Error::InvalidWindow)?;
window.manage(false);
let mut lens = windows.transmute_lens::<&Window>();
let active_panel = active_display.active_panel(main_cid)?;
give_away_focus(entity, &lens.query(), active_panel, commands);
active_display.remove_window(entity);
Ok(())
}
fn window_unminimized(
window_id: WinID,
windows: &mut Query<(&mut Window, Entity)>,
active_display: &mut Display,
main_cid: ConnID,
commands: &mut Commands,
) -> Result<()> {
let active_panel = active_display.active_panel(main_cid)?;
let (mut window, entity) = windows
.iter_mut()
.find(|(window, _)| window.id() == window_id)
.ok_or(Error::InvalidWindow)?;
window.manage(true);
active_panel.append(entity);
commands.trigger(ReshuffleAroundTrigger(window_id));
Ok(())
}
fn stray_focus_event(
window_id: WinID,
time: &Time<Virtual>,
delayed_focus: &mut HashMap<WinID, Duration>,
messages: &mut MessageWriter<Event>,
) {
const STRAY_FOCUS_TIMEOUT_SECS: u64 = 1;
let focus_start = delayed_focus.entry(window_id).or_insert_with(|| {
debug!(
"{}: Early focus event for window id {window_id}. Re-queueing.",
function_name!()
);
time.elapsed()
});
if focus_start.add(Duration::new(STRAY_FOCUS_TIMEOUT_SECS, 0)) > time.elapsed() {
messages.write(Event::WindowFocused { window_id });
} else {
warn!(
"{}: Window {window_id} still missing after {STRAY_FOCUS_TIMEOUT_SECS}s.",
function_name!()
);
}
}