use bevy::MinimalPlugins;
use bevy::app::{App as BevyApp, Startup};
use bevy::ecs::message::{Message, Messages};
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::world::World;
use bevy::time::{Time, Virtual};
use log::{debug, error, warn};
use objc2::rc::Retained;
use objc2_core_foundation::{CFRetained, CGPoint};
use objc2_core_graphics::CGDirectDisplayID;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use stdext::function_name;
use crate::commands::{Command, process_command_trigger};
use crate::config::{CONFIGURATION_FILE, Config};
use crate::ecs::{
BProcess, ExistingMarker, FocusFollowsMouse, MissionControlActive, PollForNotifications,
SkipReshuffle, gather_displays, register_systems, register_triggers,
run_initial_oneshot_systems,
};
use crate::errors::Result;
use crate::manager::{Process, WindowManager, WindowManagerApi, WindowManagerOS};
use crate::platform::{PlatformCallbacks, ProcessSerialNumber, WinID, WorkspaceObserver};
use crate::util::AXUIWrapper;
#[allow(dead_code)]
#[derive(Clone, Debug, Message)]
pub enum Event {
Exit,
ProcessesLoaded,
InitialConfig(Config),
ConfigRefresh(notify::Event),
ApplicationLaunched {
psn: ProcessSerialNumber,
observer: Retained<WorkspaceObserver>,
},
ApplicationTerminated { psn: ProcessSerialNumber },
ApplicationFrontSwitched { psn: ProcessSerialNumber },
ApplicationActivated,
ApplicationDeactivated,
ApplicationVisible { pid: i32 },
ApplicationHidden { pid: i32 },
WindowCreated { element: CFRetained<AXUIWrapper> },
WindowDestroyed { window_id: WinID },
WindowFocused { window_id: WinID },
WindowMoved { window_id: WinID },
WindowResized { window_id: WinID },
WindowMinimized { window_id: WinID },
WindowDeminimized { window_id: WinID },
WindowTitleChanged { window_id: WinID },
CurrentlyFocused,
MouseDown { point: CGPoint },
MouseUp { point: CGPoint },
MouseDragged { point: CGPoint },
MouseMoved { point: CGPoint },
Swipe { deltas: Vec<f64> },
SpaceCreated,
SpaceDestroyed,
SpaceChanged,
DisplayAdded { display_id: CGDirectDisplayID },
DisplayRemoved { display_id: CGDirectDisplayID },
DisplayMoved { display_id: CGDirectDisplayID },
DisplayResized { display_id: CGDirectDisplayID },
DisplayConfigured { display_id: CGDirectDisplayID },
DisplayChanged,
MissionControlShowAllWindows,
MissionControlShowFrontWindows,
MissionControlShowDesktop,
MissionControlExit,
DockDidChangePref { msg: String },
DockDidRestart { msg: String },
MenuOpened { window_id: WinID },
MenuClosed { window_id: WinID },
MenuBarHiddenChanged { msg: String },
SystemWoke { msg: String },
Command { command: Command },
PrintState,
}
#[derive(Clone, Debug)]
pub struct EventSender {
tx: Sender<Event>,
}
impl EventSender {
pub fn new() -> (Self, Receiver<Event>) {
let (tx, rx) = channel::<Event>();
(Self { tx }, rx)
}
pub fn send(&self, event: Event) -> Result<()> {
Ok(self.tx.send(event)?)
}
}
pub struct EventHandler;
impl EventHandler {
pub fn setup_bevy_app(sender: EventSender, receiver: Receiver<Event>) -> Result<BevyApp> {
let process_setup = move |world: &mut World| {
let Some((mut existing_processes, config)) = world
.get_non_send_resource::<Receiver<Event>>()
.and_then(|receiver| EventHandler::gather_initial_processes(receiver).ok())
else {
error!("{}: gathering initial processes.", function_name!());
return;
};
EventHandler::initial_setup(world, &mut existing_processes, config.as_ref());
};
let window_manager: Box<dyn WindowManagerApi> =
Box::new(WindowManagerOS::new(sender.clone()));
let watcher = window_manager.setup_config_watcher(CONFIGURATION_FILE.as_path())?;
let mut app = BevyApp::new();
app.add_plugins(MinimalPlugins)
.init_resource::<Messages<Event>>()
.insert_resource(Time::<Virtual>::from_max_delta(Duration::from_secs(10)))
.insert_resource(WindowManager(window_manager))
.insert_resource(SkipReshuffle(false))
.insert_resource(MissionControlActive(false))
.insert_resource(FocusFollowsMouse(None))
.insert_resource(PollForNotifications(true))
.add_observer(process_command_trigger)
.add_systems(Startup, (gather_displays, process_setup).chain());
app.insert_non_send_resource(watcher);
register_triggers(&mut app);
register_systems(&mut app);
let mut platform_callbacks = PlatformCallbacks::new(sender)?;
platform_callbacks.setup_handlers()?;
app.insert_non_send_resource(platform_callbacks);
app.insert_non_send_resource(receiver);
Ok(app)
}
fn gather_initial_processes(
receiver: &Receiver<Event>,
) -> Result<(Vec<BProcess>, Option<Config>)> {
let mut initial_processes = Vec::new();
let mut initial_config = None;
loop {
match receiver.recv()? {
Event::ProcessesLoaded | Event::Exit => break,
Event::ApplicationLaunched { psn, observer } => {
initial_processes.push(Process::new(&psn, observer.clone()).into());
}
Event::InitialConfig(config) => {
initial_config = Some(config);
}
event => warn!(
"{}: Stray event during initial process gathering: {event:?}",
function_name!()
),
}
}
Ok((initial_processes, initial_config))
}
fn initial_setup(
world: &mut World,
existing_processes: &mut Vec<BProcess>,
config: Option<&Config>,
) {
if let Some(config) = config {
world.insert_resource(config.clone());
}
while let Some(mut process) = existing_processes.pop() {
if process.is_observable() {
debug!(
"{}: Adding existing process {}",
function_name!(),
process.name()
);
world.spawn((ExistingMarker, process));
} else {
debug!(
"{}: Existing application '{}' is not observable, ignoring it.",
function_name!(),
process.name(),
);
}
}
run_initial_oneshot_systems(world);
}
}