use std::sync::mpsc::Receiver;
use std::time::Duration;
use bevy::MinimalPlugins;
use bevy::app::App as BevyApp;
use bevy::app::{First, Last, PostUpdate, PreUpdate, Startup};
use bevy::ecs::hierarchy::ChildOf;
use bevy::ecs::lifecycle::RemovedComponents;
use bevy::ecs::query::{Added, Changed, With};
use bevy::ecs::resource::Resource;
use bevy::ecs::schedule::SystemCondition;
use bevy::ecs::schedule::common_conditions::{not, resource_exists};
use bevy::ecs::schedule::{ScheduleLabel as _, SingleThreadedExecutor};
use bevy::ecs::system::{Commands, EntityCommands, Query, Res, SystemId};
use bevy::prelude::Event as BevyEvent;
use bevy::tasks::Task;
use bevy::time::Timer;
use bevy::time::common_conditions::on_timer;
use bevy::time::{Time, Virtual};
use bevy::{
app::Update,
ecs::{component::Component, entity::Entity, schedule::IntoScheduleConfigs},
};
use derive_more::{Deref, DerefMut};
use tracing::{Level, error, instrument, warn};
use crate::commands::register_commands;
use crate::config::snippet::SnippetDialect;
use crate::config::{CONFIGURATION_FILE, Config, InitialWindowWidth, WindowParams};
use crate::ecs::layout::LayoutStrip;
use crate::ecs::state::PaneruState;
use crate::errors::Result;
use crate::events::{Event, EventSender, InputEvent};
#[cfg(feature = "lua")]
use crate::lua;
use crate::manager::{
Application, Origin, ProcessApi, Size, Window, WindowManager, WindowManagerApi, WindowManagerOS,
};
use crate::menubar::MenuBarManager;
use crate::overlay::{FlashMessageManager, OverlayManager};
use crate::platform::{Modifiers, PlatformCallbacks, WinID, WorkspaceId};
pub mod display;
pub mod focus;
pub mod layout;
#[cfg(feature = "lua")]
pub mod layout_ops;
pub mod mouse;
pub mod params;
pub(crate) mod restore;
pub mod script_state;
pub mod scroll;
pub mod state;
pub(crate) mod systems;
mod triggers;
pub mod workspace;
#[cfg(feature = "lua")]
pub(crate) use triggers::apply_config_side_effects;
#[allow(clippy::too_many_lines)]
pub fn register_systems(app: &mut bevy::app::App) {
const CLOSED_WINDOW_CHECK_FREQ: Duration = Duration::from_secs(1);
const LOW_POWER_MODE_CHECK: Duration = Duration::from_mins(1);
const APP_OBSERVABILITY_CHECK_FREQ: Duration = Duration::from_millis(200);
let not_swiping = |scrolling: Query<&Scrolling, With<ActiveWorkspaceMarker>>| {
scrolling
.iter()
.next()
.is_none_or(|marker| !marker.is_user_swiping)
};
let dimming_enabled = |config: Option<Res<Config>>| {
config
.is_some_and(|config| config.has_dim_inactive_color() || config.border_active_window())
};
let vw_indicator_dirty =
|strip_changed: Query<(), (With<ActiveWorkspaceMarker>, Changed<LayoutStrip>)>,
focus_gained: Query<(), Added<FocusedMarker>>,
workspace_changed: Query<(), Added<ActiveWorkspaceMarker>>,
focused_moved: Query<(), (With<FocusedMarker>, Changed<Position>)>| {
!strip_changed.is_empty()
|| !focus_gained.is_empty()
|| !workspace_changed.is_empty()
|| !focused_moved.is_empty()
};
let strip_count_changed =
|added: Query<(), Added<LayoutStrip>>, mut removed: RemovedComponents<LayoutStrip>| {
!added.is_empty() || removed.read().next().is_some()
};
let native_tabs_enabled =
|config: Option<Res<Config>>| config.is_none_or(|config| config.native_tabs_enabled());
app.add_systems(
Startup,
(
systems::gather_displays,
systems::gather_initial_processes,
systems::initialise_workspaces,
)
.chain(),
);
app.add_message::<InputEvent>();
app.add_systems(
PreUpdate,
(
systems::pump_events,
systems::window_creation_event.after(systems::pump_events),
systems::demux_input_events.after(systems::pump_events),
),
);
app.add_systems(
Update,
(
(
triggers::apply_window_defaults,
systems::detect_tabbed_windows.run_if(native_tabs_enabled),
triggers::apply_window_positions,
)
.chain()
.after(systems::finish_setup),
(
systems::add_existing_process,
systems::add_existing_application,
systems::finish_setup,
)
.chain()
.run_if(resource_exists::<Initializing>),
systems::add_launched_process.run_if(on_timer(APP_OBSERVABILITY_CHECK_FREQ)),
systems::add_launched_application.run_if(on_timer(APP_OBSERVABILITY_CHECK_FREQ)),
systems::fresh_marker_cleanup,
systems::timeout_ticker,
workspace::cleanup_unordered_windows
.run_if(not(resource_exists::<Initializing>))
.run_if(on_timer(CLOSED_WINDOW_CHECK_FREQ)),
systems::regroup_stray_native_tabs
.run_if(native_tabs_enabled)
.run_if(not(resource_exists::<Initializing>))
.run_if(on_timer(CLOSED_WINDOW_CHECK_FREQ)),
systems::auto_discover_unmanaged_focused_windows,
systems::retry_front_switch,
systems::update_low_power_state
.run_if(resource_exists::<LowPowerMode>)
.run_if(on_timer(LOW_POWER_MODE_CHECK)),
(
systems::window_resized_update_frame,
systems::window_moved_update_frame,
)
.chain()
.run_if(not_swiping),
systems::cleanup_on_exit,
restore::tick_restore_grace,
state::periodic_state_save.run_if(on_timer(Duration::from_mins(5))),
state::cleanup_on_exit,
script_state::periodic_script_state_save.run_if(on_timer(Duration::from_mins(5))),
script_state::script_state_cleanup_on_exit,
),
);
app.add_systems(
PostUpdate,
(
(
systems::animate_entities,
systems::commit_window_position.run_if(not(resource_exists::<Initializing>)),
systems::verify_window_position.run_if(not(resource_exists::<Initializing>)),
)
.chain(),
(
systems::animate_resize_entities,
systems::commit_window_size.run_if(not(resource_exists::<Initializing>)),
)
.chain(),
(
systems::update_overlays
.after(systems::animate_entities)
.after(systems::animate_resize_entities)
.run_if(dimming_enabled)
.run_if(vw_indicator_dirty),
systems::update_flash_messages,
)
.chain(),
crate::menubar::update_menu_bar
.run_if(vw_indicator_dirty.or_eager(strip_count_changed)),
),
);
}
pub fn register_triggers(app: &mut bevy::app::App) {
app.add_systems(
Update,
(
triggers::front_switched_trigger,
triggers::window_focused_trigger,
triggers::mission_control_trigger,
triggers::application_event_trigger,
triggers::dispatch_application_messages,
triggers::window_destroyed_trigger,
triggers::invalidate_window_title,
triggers::refresh_configuration_trigger,
triggers::theme_change_trigger,
triggers::window_resize_verifier,
),
);
app.add_observer(triggers::window_unmanaged_trigger)
.add_observer(triggers::window_managed_trigger)
.add_observer(triggers::window_minimized_trigger)
.add_observer(triggers::spawn_window_trigger)
.add_observer(triggers::send_message_trigger)
.add_observer(triggers::window_removal_trigger)
.add_observer(triggers::cleanup_timeout_trigger)
.add_observer(restore::restore_window_state);
}
#[derive(Component)]
pub struct FocusedMarker;
#[derive(Component)]
pub struct ActiveWorkspaceMarker;
#[derive(Component)]
pub struct SelectedVirtualMarker;
#[derive(Component)]
pub struct FlashMessage(pub String);
#[derive(Component)]
pub struct ActiveDisplayMarker;
#[derive(Component)]
pub struct FreshMarker;
#[derive(Component)]
pub struct ExistingMarker;
#[derive(Component, Debug, Deref, DerefMut)]
pub struct RepositionMarker(pub Origin);
#[derive(Component, Debug, Deref, DerefMut)]
pub struct ResizeMarker(pub Size);
#[derive(Component)]
pub struct ReshuffleAroundMarker;
#[derive(Component)]
pub struct EnsureVisibleMarker {
pub snap: bool,
}
#[derive(Component, Debug)]
pub struct ManualStripOffset {
pub signature: Vec<(Entity, i32)>,
}
#[derive(Component, Debug)]
pub struct Scrolling {
pub velocity: f64,
pub position: f64,
pub is_user_swiping: bool,
pub last_event: Duration,
}
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
pub struct LayoutPosition(pub Origin);
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct Position(pub Origin);
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct Bounds(pub Size);
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct WidthRatio(pub f64);
#[derive(Clone, Component, Debug)]
pub struct NativeFullscreenMarker {
pub layout_strip: Entity,
pub workspace_id: WorkspaceId,
pub index: usize,
}
#[derive(Component)]
pub struct FullWidthMarker {
pub width_ratio: f64,
}
#[derive(Component, Debug)]
pub enum Unmanaged {
Floating,
Minimized,
Hidden,
}
#[derive(Clone, Component, Copy, Debug)]
pub struct PreviousManagedStrip {
pub workspace_id: WorkspaceId,
pub virtual_index: u32,
pub index: usize,
}
#[derive(Component, Deref, DerefMut)]
pub struct BProcess(pub Box<dyn ProcessApi>);
#[derive(Component)]
pub struct Timeout {
pub timer: Timer,
pub system_id: Option<SystemId>,
pub on_expire: Option<fn(&mut EntityCommands)>,
}
impl Timeout {
pub fn new(duration: Duration, message: Option<String>, commands: &mut Commands) -> Self {
let timer = Timer::new(duration, bevy::time::TimerMode::Once);
let system_id = message.map(|message| {
commands.register_system(move || {
tracing::debug!("{message}");
})
});
Self {
timer,
system_id,
on_expire: None,
}
}
#[must_use]
pub fn for_component<C: Component>(duration: Duration) -> Self {
let timer = Timer::new(duration, bevy::time::TimerMode::Once);
Self {
timer,
system_id: None,
on_expire: Some(|entity_commands| {
entity_commands.try_remove::<(Self, C)>();
}),
}
}
pub fn callback(duration: Duration, system_id: SystemId, commands: &mut Commands) {
let timer = Timer::new(duration, bevy::time::TimerMode::Once);
commands.spawn(Self {
timer,
system_id: Some(system_id),
on_expire: None,
});
}
}
#[derive(Component)]
pub struct StrayFocusEvent(pub WinID);
#[derive(Component)]
pub struct RetryFrontSwitch(pub Entity);
#[derive(Component)]
pub struct VerifyFocus;
#[derive(Component)]
pub struct BruteforceWindows(Task<Vec<Window>>);
#[derive(Component, Debug)]
pub enum DockPosition {
Bottom(i32),
Left(i32),
Right(i32),
Hidden,
}
#[derive(Component)]
pub struct VerifyWindowPosition {
remaining: u8,
}
impl Default for VerifyWindowPosition {
fn default() -> Self {
Self { remaining: 3 }
}
}
impl VerifyWindowPosition {
pub fn tick(&mut self) -> bool {
self.remaining = self.remaining.saturating_sub(1);
self.remaining == 0
}
}
#[derive(Deref, DerefMut, Resource)]
pub struct LowPowerMode(pub bool);
#[derive(Resource)]
pub struct SystemTheme {
pub is_dark: bool,
}
#[derive(Resource)]
pub struct SkipReshuffle(pub bool);
#[derive(Component)]
pub struct MouseHeldMarker(pub Entity);
#[derive(Resource)]
pub struct MissionControlActive(pub bool);
#[derive(Resource)]
pub struct FocusFollowsMouse(pub Option<WinID>);
#[derive(Resource)]
pub struct Initializing;
#[derive(BevyEvent)]
pub struct SpawnWindowTrigger(pub Vec<Window>);
#[derive(BevyEvent)]
pub struct ReadDisplayProperties(pub Entity);
#[derive(BevyEvent)]
pub struct SendMessageTrigger(pub Event);
#[derive(BevyEvent)]
pub struct RestoreWindowState;
#[derive(BevyEvent)]
pub struct RaiseWindow {
pub entity: Entity,
pub with_strip: bool,
}
pub trait SpawnCommandsExt {
fn reposition_entity(&mut self, entity: Entity, origin: Origin);
fn resize_entity(&mut self, entity: Entity, size: Size);
fn reshuffle_around(&mut self, entity: Entity);
fn ensure_visible(&mut self, entity: Entity);
fn ensure_visible_snap(&mut self, entity: Entity, snap: bool);
fn focus_entity(&mut self, entity: Entity, raise: bool);
fn flash_message(&mut self, message: String, duration: f32);
fn spawn_layout_strip(
&mut self,
layout_strip: LayoutStrip,
origin: Origin,
display_entity: Entity,
active: bool,
) -> EntityCommands<'_>;
}
impl SpawnCommandsExt for Commands<'_, '_> {
#[instrument(level = Level::TRACE, skip(self))]
fn reposition_entity(&mut self, entity: Entity, origin: Origin) {
if let Ok(mut entity_commands) = self.get_entity(entity) {
entity_commands.try_insert(RepositionMarker(origin));
}
}
#[instrument(level = Level::TRACE, skip(self))]
fn resize_entity(&mut self, entity: Entity, size: Size) {
if size.x <= 0 || size.y <= 0 {
return;
}
if let Ok(mut entity_commands) = self.get_entity(entity) {
entity_commands.try_insert(ResizeMarker(size));
}
}
#[instrument(level = Level::TRACE, skip(self))]
fn reshuffle_around(&mut self, entity: Entity) {
if let Ok(mut entity_commands) = self.get_entity(entity) {
entity_commands.try_insert(ReshuffleAroundMarker);
}
}
#[instrument(level = Level::TRACE, skip(self))]
fn ensure_visible(&mut self, entity: Entity) {
self.ensure_visible_snap(entity, false);
}
#[instrument(level = Level::TRACE, skip(self))]
fn ensure_visible_snap(&mut self, entity: Entity, snap: bool) {
if let Ok(mut entity_commands) = self.get_entity(entity) {
entity_commands.try_insert(EnsureVisibleMarker { snap });
}
}
#[instrument(level = Level::TRACE, skip(self))]
fn focus_entity(&mut self, entity: Entity, raise: bool) {
if let Ok(mut entity_commands) = self.get_entity(entity) {
entity_commands.try_insert(FocusedMarker);
self.trigger(focus::FocusWindow { entity, raise });
}
}
#[instrument(level = Level::TRACE, skip(self))]
fn flash_message(&mut self, message: String, duration: f32) {
let timeout = Timeout::new(Duration::from_secs_f32(duration), None, self);
self.spawn((timeout, FlashMessage(message)));
}
#[instrument(level = Level::TRACE, skip(self))]
fn spawn_layout_strip(
&mut self,
layout_strip: LayoutStrip,
origin: Origin,
display_entity: Entity,
active: bool,
) -> EntityCommands<'_> {
let mut spawned = self.spawn((layout_strip, Position(origin), ChildOf(display_entity)));
if active {
spawned.insert(ActiveWorkspaceMarker);
} else {
spawned.insert(SelectedVirtualMarker);
}
spawned
}
}
pub(crate) fn rewatch_configs(
window_manager: &WindowManager,
changed: &std::path::Path,
) -> Option<Box<dyn notify::Watcher>> {
let mut watcher = window_manager
.setup_config_watcher(changed)
.inspect_err(|err| error!("watching the config '{}': {err}", changed.display()))
.ok()?;
let others = [
CONFIGURATION_FILE.clone(),
#[cfg(feature = "lua")]
crate::config::discover_lua_file(),
];
for other in others.into_iter().flatten() {
if other == changed {
continue;
}
if let Err(err) = watcher.watch(&other, notify::RecursiveMode::NonRecursive) {
warn!("re-watching config '{}': {err}", other.display());
}
}
Some(watcher)
}
fn autorelease_runner(mut app: BevyApp) -> bevy::app::AppExit {
if app.plugins_state() != bevy::app::PluginsState::Cleaned {
while app.plugins_state() == bevy::app::PluginsState::Adding {
bevy::tasks::tick_global_task_pools_on_main_thread();
}
objc2::rc::autoreleasepool(|_| {
app.finish();
app.cleanup();
});
}
loop {
objc2::rc::autoreleasepool(|_| {
app.update();
});
if let Some(exit) = app.should_exit() {
return exit;
}
}
}
pub fn setup_bevy_app(sender: EventSender, receiver: Receiver<Event>) -> Result<BevyApp> {
crate::manager::app::bound_ax_messaging_timeout()?;
let window_manager: Box<dyn WindowManagerApi> = Box::new(WindowManagerOS::new(sender.clone()));
#[cfg(feature = "lua")]
let lua_path = crate::config::ensure_lua_file()
.inspect_err(|err| warn!("preparing Lua script: {err}"))
.ok()
.flatten();
let toml_path = CONFIGURATION_FILE.as_deref();
#[cfg(feature = "lua")]
let primary = toml_path.or(lua_path.as_deref());
#[cfg(not(feature = "lua"))]
let primary = toml_path;
let primary = primary.ok_or_else(|| {
crate::errors::Error::InvalidConfig("no configuration file to watch".to_string())
})?;
#[cfg_attr(not(feature = "lua"), allow(unused_mut))]
let mut watcher = window_manager.setup_config_watcher(primary)?;
#[cfg(feature = "lua")]
if let Some(path) = &lua_path
&& path.as_path() != primary
&& let Err(err) = watcher.watch(path, notify::RecursiveMode::NonRecursive)
{
warn!("watching Lua script '{}': {err}", path.display());
}
let mut app = BevyApp::new();
app.add_plugins(MinimalPlugins)
.add_message::<Event>()
.insert_resource(Time::<Virtual>::from_max_delta(Duration::from_secs(10)))
.insert_resource(WindowManager(window_manager))
.insert_resource(SkipReshuffle(false))
.insert_resource(SystemTheme {
is_dark: crate::util::is_dark_mode(),
})
.insert_resource(MissionControlActive(false))
.insert_resource(FocusFollowsMouse(None))
.insert_resource(Initializing)
.insert_non_send(watcher)
.add_plugins(mouse::MouseEventsPlugin)
.add_plugins(scroll::ScrollEventsPlugin)
.add_plugins(workspace::WorkspaceEventsPlugin)
.add_plugins(layout::LayoutEventsPlugin)
.add_plugins(focus::FocusEventsPlugin)
.add_plugins(display::DisplayEventsPlugin)
.add_plugins((register_triggers, register_systems, register_commands));
for label in [
First.intern(),
PreUpdate.intern(),
Update.intern(),
PostUpdate.intern(),
Last.intern(),
] {
app.edit_schedule(label, |schedule| {
schedule.set_executor(SingleThreadedExecutor::new());
});
}
app.set_runner(autorelease_runner);
let menu_events = sender.clone();
let mut platform_callbacks = PlatformCallbacks::new(sender);
platform_callbacks.setup_handlers()?;
let mtm = platform_callbacks.main_thread_marker;
let overlay_manager = OverlayManager::new(mtm);
let flash_message_manager = FlashMessageManager::new(mtm);
let menu_bar_manager = MenuBarManager::new(mtm, menu_events);
app.insert_non_send(platform_callbacks)
.insert_non_send(overlay_manager)
.insert_non_send(flash_message_manager)
.insert_non_send(menu_bar_manager)
.insert_non_send(receiver);
app.insert_resource(if CONFIGURATION_FILE.is_none() {
SnippetDialect::Lua
} else {
SnippetDialect::Toml
});
if let Some(previous_state) =
PaneruState::load_from_file(&PaneruState::default_state_file_path())
{
app.insert_resource(previous_state);
}
app.insert_resource(script_state::ScriptStateStore::load());
app.insert_resource(LowPowerMode(false));
#[cfg(feature = "lua")]
if let Some(path) = lua_path {
crate::config::prime_virtual_keymap();
let revision = app
.world()
.resource::<script_state::ScriptStateStore>()
.revision_handle();
let worker = lua::LuaWorker::spawn(lua::LuaSource::Path(path.clone()), revision);
if let Some(config) = worker.built_config() {
app.insert_resource(config);
}
app.insert_resource(worker);
app.insert_resource(lua::LuaScriptPath(path));
app.add_plugins(lua::LuaPlugin {});
}
Ok(app)
}
struct WindowProperties {
params: Vec<WindowParams>,
}
impl WindowProperties {
pub fn new(app: &Application, window: &Window, config: &Config) -> Self {
let bundle_id = app.bundle_id().unwrap_or_default();
let title = window.title().unwrap_or_default();
let params = config.find_window_properties(&title, &bundle_id);
Self { params }
}
pub fn floating(&self) -> bool {
self.params
.iter()
.find_map(|props| props.floating)
.unwrap_or(false)
}
pub fn insertion(&self) -> Option<usize> {
self.params.iter().find_map(|props| props.index)
}
pub fn dont_focus(&self) -> bool {
self.params
.iter()
.find_map(|props| props.dont_focus)
.unwrap_or(false)
}
pub fn border_radius(&self) -> Option<f64> {
self.params.iter().find_map(|p| p.border_radius)
}
pub fn grid_ratios(&self) -> Option<(f64, f64, f64, f64)> {
self.params.iter().find_map(WindowParams::grid_ratios)
}
pub fn passthrough_keys(&self) -> Vec<(u8, Modifiers)> {
self.params
.iter()
.flat_map(|p| p.passthrough_keys().to_vec())
.collect::<Vec<_>>()
}
pub fn width_ratio(&self, focused_ratio: Option<f64>) -> Option<f64> {
self.params
.iter()
.find_map(|props| props.width)
.map(|width| width.ratio(focused_ratio))
}
pub fn fixed_width_ratio(&self) -> Option<f64> {
match self.params.iter().find_map(|props| props.width) {
Some(InitialWindowWidth::Ratio(ratio)) => Some(ratio),
_ => None,
}
}
pub fn vertical_padding(&self) -> i32 {
self.params
.iter()
.find_map(|props| props.vertical_padding)
.unwrap_or(0)
}
pub fn horizontal_padding(&self) -> i32 {
self.params
.iter()
.find_map(|props| props.horizontal_padding)
.unwrap_or(0)
}
}