From f519cbaf1edf295f9435b413e9aa48a857b1eb00 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sun, 18 Feb 2024 14:39:23 -0800 Subject: [PATCH] refactor(wm): split komorebi into bin and lib This commit splits the komorebi crate into a mixed binary and library crate. All types and logic related to window management have been moved under lib.rs, and are imported from there for use in main.rs, which is now only responsible for starting and running the window manager process. In preparation for exposing a new komorebi-client crate in the future, serde::Deserialize has been derived on Notification and any struct that may appear in a notification receievd by a process that has subscribed to event updates. re #659 --- komorebi/src/container.rs | 4 +- komorebi/src/lib.rs | 358 ++++++++++++++++++++++++++ komorebi/src/main.rs | 369 ++------------------------- komorebi/src/monitor.rs | 8 +- komorebi/src/ring.rs | 3 +- komorebi/src/window.rs | 3 +- komorebi/src/window_manager.rs | 3 +- komorebi/src/window_manager_event.rs | 3 +- komorebi/src/winevent.rs | 3 +- komorebi/src/workspace.rs | 10 +- 10 files changed, 396 insertions(+), 368 deletions(-) create mode 100644 komorebi/src/lib.rs diff --git a/komorebi/src/container.rs b/komorebi/src/container.rs index 448c56da..de496451 100644 --- a/komorebi/src/container.rs +++ b/komorebi/src/container.rs @@ -3,14 +3,14 @@ use std::collections::VecDeque; use getset::Getters; use nanoid::nanoid; use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; use crate::ring::Ring; use crate::window::Window; -#[derive(Debug, Clone, Serialize, Getters, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)] pub struct Container { - #[serde(skip_serializing)] #[getset(get = "pub")] id: String, windows: Ring, diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs new file mode 100644 index 00000000..29d8bdac --- /dev/null +++ b/komorebi/src/lib.rs @@ -0,0 +1,358 @@ +pub mod border; +pub mod com; +#[macro_use] +pub mod ring; +pub mod container; +pub mod hidden; +pub mod monitor; +pub mod process_command; +pub mod process_event; +pub mod process_movement; +pub mod set_window_position; +pub mod static_config; +pub mod styles; +pub mod window; +pub mod window_manager; +pub mod window_manager_event; +pub mod windows_api; +pub mod windows_callbacks; +pub mod winevent; +pub mod winevent_listener; +pub mod workspace; + +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::fs::File; +use std::io::Write; +use std::net::TcpStream; +use std::path::PathBuf; +use std::process::Command; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicI32; +use std::sync::atomic::AtomicIsize; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +pub use hidden::*; +pub use process_command::*; +pub use process_event::*; +pub use static_config::*; +pub use window_manager::*; +pub use window_manager_event::*; +pub use windows_api::WindowsApi; +pub use windows_api::*; + +use color_eyre::Result; +use komorebi_core::config_generation::IdWithIdentifier; +use komorebi_core::config_generation::MatchingStrategy; +use komorebi_core::ApplicationIdentifier; +use komorebi_core::HidingBehaviour; +use komorebi_core::Rect; +use komorebi_core::SocketMessage; +use os_info::Version; +use parking_lot::Mutex; +use regex::Regex; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use uds_windows::UnixStream; +use which::which; +use winreg::enums::HKEY_CURRENT_USER; +use winreg::RegKey; + +type WorkspaceRule = (usize, usize, bool); + +lazy_static! { + static ref HIDDEN_HWNDS: Arc>> = Arc::new(Mutex::new(vec![])); + static ref LAYERED_WHITELIST: Arc>> = Arc::new(Mutex::new(vec![ + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("steam.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + ])); + static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc>> = + Arc::new(Mutex::new(vec![ + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("explorer.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("firefox.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("chrome.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("idea64.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("ApplicationFrameHost.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("steam.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + } + ])); + static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc>> = Arc::new(Mutex::new(vec![ + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("firefox.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Exe, + id: String::from("idea64.exe"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + ])); + static ref MONITOR_INDEX_PREFERENCES: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref DISPLAY_INDEX_PREFERENCES: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref WORKSPACE_RULES: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref REGEX_IDENTIFIERS: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref MANAGE_IDENTIFIERS: Arc>> = Arc::new(Mutex::new(vec![])); + static ref FLOAT_IDENTIFIERS: Arc>> = Arc::new(Mutex::new(vec![ + // mstsc.exe creates these on Windows 11 when a WSL process is launched + // https://github.com/LGUG2Z/komorebi/issues/74 + IdWithIdentifier { + kind: ApplicationIdentifier::Class, + id: String::from("OPContainerClass"), + matching_strategy: Option::from(MatchingStrategy::Equals), + }, + IdWithIdentifier { + kind: ApplicationIdentifier::Class, + id: String::from("IHWindowClass"), + matching_strategy: Option::from(MatchingStrategy::Equals), + } + ])); + static ref PERMAIGNORE_CLASSES: Arc>> = Arc::new(Mutex::new(vec![ + "Chrome_RenderWidgetHostHWND".to_string(), + ])); + static ref BORDER_OVERFLOW_IDENTIFIERS: Arc>> = Arc::new(Mutex::new(vec![])); + static ref WSL2_UI_PROCESSES: Arc>> = Arc::new(Mutex::new(vec![ + "X410.exe".to_string(), + "vcxsrv.exe".to_string(), + ])); + static ref SUBSCRIPTION_PIPES: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref SUBSCRIPTION_SOCKETS: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref TCP_CONNECTIONS: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + static ref HIDING_BEHAVIOUR: Arc> = + Arc::new(Mutex::new(HidingBehaviour::Minimize)); + pub static ref HOME_DIR: PathBuf = { + std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| { + let home = PathBuf::from(&home_path); + + if home.as_path().is_dir() { + home + } else { + panic!( + "$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory", + ); + } + }) + }; + static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi"); + pub static ref AHK_EXE: String = { + let mut ahk: String = String::from("autohotkey.exe"); + + if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") { + if which(&komorebi_ahk_exe).is_ok() { + ahk = komorebi_ahk_exe; + } + } + + ahk + }; + static ref WINDOWS_11: bool = { + matches!( + os_info::get().version(), + Version::Semantic(_, _, x) if x >= &22000 + ) + }; + + static ref BORDER_RECT: Arc> = + Arc::new(Mutex::new(Rect::default())); + + static ref BORDER_OFFSET: Arc>> = + Arc::new(Mutex::new(None)); + + // Use app-specific titlebar removal options where possible + // eg. Windows Terminal, IntelliJ IDEA, Firefox + static ref NO_TITLEBAR: Arc>> = Arc::new(Mutex::new(vec![])); +} + +pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10); +pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10); + +pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false); +pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false); +pub static SESSION_ID: AtomicU32 = AtomicU32::new(0); +pub static ALT_FOCUS_HACK: AtomicBool = AtomicBool::new(false); +pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false); +pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0); +pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false); +pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0); +pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0); +pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0); +pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0); +pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20); +// 0 0 0 aka pure black, I doubt anyone will want this as a border colour +pub const TRANSPARENCY_COLOUR: u32 = 0; +pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false); + +pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); + +#[must_use] +pub fn current_virtual_desktop() -> Option> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // This is the path on Windows 10 + let mut current = hkcu + .open_subkey(format!( + r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#, + SESSION_ID.load(Ordering::SeqCst) + )) + .ok() + .and_then( + |desktops| match desktops.get_raw_value("CurrentVirtualDesktop") { + Ok(current) => Option::from(current.bytes), + Err(_) => None, + }, + ); + + // This is the path on Windows 11 + if current.is_none() { + current = hkcu + .open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops") + .ok() + .and_then( + |desktops| match desktops.get_raw_value("CurrentVirtualDesktop") { + Ok(current) => Option::from(current.bytes), + Err(_) => None, + }, + ); + } + + // For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not + // exist until one has been created in the task view + + // The registry value will also not exist on user login if virtual desktops have been created + // but the task view has not been initiated + + // In both of these cases, we return None, and the virtual desktop validation will never run. In + // the latter case, if the user desires this validation after initiating the task view, komorebi + // should be restarted, and then when this // fn runs again for the first time, it will pick up + // the value of CurrentVirtualDesktop and validate against it accordingly + current +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum NotificationEvent { + WindowManager(WindowManagerEvent), + Socket(SocketMessage), +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct Notification { + pub event: NotificationEvent, + pub state: State, +} + +pub fn notify_subscribers(notification: &str) -> Result<()> { + let mut stale_sockets = vec![]; + let mut sockets = SUBSCRIPTION_SOCKETS.lock(); + + for (socket, path) in &mut *sockets { + match UnixStream::connect(path) { + Ok(mut stream) => { + tracing::debug!("pushed notification to subscriber: {socket}"); + stream.write_all(notification.as_bytes())?; + } + Err(_) => { + stale_sockets.push(socket.clone()); + } + } + } + + for socket in stale_sockets { + tracing::warn!("removing stale subscription: {socket}"); + sockets.remove(&socket); + } + + let mut stale_pipes = vec![]; + let mut pipes = SUBSCRIPTION_PIPES.lock(); + for (subscriber, pipe) in &mut *pipes { + match writeln!(pipe, "{notification}") { + Ok(()) => { + tracing::debug!("pushed notification to subscriber: {subscriber}"); + } + Err(error) => { + // ERROR_FILE_NOT_FOUND + // 2 (0x2) + // The system cannot find the file specified. + + // ERROR_NO_DATA + // 232 (0xE8) + // The pipe is being closed. + + // Remove the subscription; the process will have to subscribe again + if let Some(2 | 232) = error.raw_os_error() { + stale_pipes.push(subscriber.clone()); + } + } + } + } + + for subscriber in stale_pipes { + tracing::warn!("removing stale subscription: {}", subscriber); + pipes.remove(&subscriber); + } + + Ok(()) +} + +pub fn load_configuration() -> Result<()> { + let config_pwsh = HOME_DIR.join("komorebi.ps1"); + let config_ahk = HOME_DIR.join("komorebi.ahk"); + + if config_pwsh.exists() { + let powershell_exe = if which("pwsh.exe").is_ok() { + "pwsh.exe" + } else { + "powershell.exe" + }; + + tracing::info!("loading configuration file: {}", config_pwsh.display()); + + Command::new(powershell_exe) + .arg(config_pwsh.as_os_str()) + .output()?; + } else if config_ahk.exists() && which(&*AHK_EXE).is_ok() { + tracing::info!("loading configuration file: {}", config_ahk.display()); + + Command::new(&*AHK_EXE) + .arg(config_ahk.as_os_str()) + .output()?; + } + + Ok(()) +} diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index 989310e6..c7b96cf0 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -6,16 +6,7 @@ clippy::significant_drop_in_scrutinee )] -use std::collections::HashMap; -use std::fs::File; -use std::io::Write; -use std::net::TcpStream; use std::path::PathBuf; -use std::process::Command; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::AtomicI32; -use std::sync::atomic::AtomicIsize; -use std::sync::atomic::AtomicU32; use std::sync::atomic::Ordering; use std::sync::Arc; #[cfg(feature = "deadlock_detection")] @@ -26,222 +17,29 @@ use color_eyre::Result; use crossbeam_channel::Receiver; use crossbeam_channel::Sender; use crossbeam_utils::Backoff; -use lazy_static::lazy_static; -use os_info::Version; #[cfg(feature = "deadlock_detection")] use parking_lot::deadlock; use parking_lot::Mutex; -use regex::Regex; -use schemars::JsonSchema; -use serde::Serialize; use sysinfo::Process; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::EnvFilter; -use uds_windows::UnixStream; -use which::which; -use winreg::enums::HKEY_CURRENT_USER; -use winreg::RegKey; -use crate::hidden::Hidden; -use komorebi_core::config_generation::IdWithIdentifier; -use komorebi_core::config_generation::MatchingStrategy; -use komorebi_core::ApplicationIdentifier; -use komorebi_core::HidingBehaviour; -use komorebi_core::Rect; -use komorebi_core::SocketMessage; - -use crate::process_command::listen_for_commands; -use crate::process_command::listen_for_commands_tcp; -use crate::process_event::listen_for_events; -use crate::process_movement::listen_for_movements; -use crate::static_config::StaticConfig; -use crate::window_manager::State; -use crate::window_manager::WindowManager; -use crate::window_manager_event::WindowManagerEvent; -use crate::windows_api::WindowsApi; - -#[macro_use] -mod ring; - -mod border; -mod com; -mod container; -mod hidden; -mod monitor; -mod process_command; -mod process_event; -mod process_movement; -mod set_window_position; -mod static_config; -mod styles; -mod window; -mod window_manager; -mod window_manager_event; -mod windows_api; -mod windows_callbacks; -mod winevent; -mod winevent_listener; -mod workspace; - -type WorkspaceRule = (usize, usize, bool); - -lazy_static! { - static ref HIDDEN_HWNDS: Arc>> = Arc::new(Mutex::new(vec![])); - static ref LAYERED_WHITELIST: Arc>> = Arc::new(Mutex::new(vec![ - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("steam.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - ])); - static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc>> = - Arc::new(Mutex::new(vec![ - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("explorer.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("firefox.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("chrome.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("idea64.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("ApplicationFrameHost.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("steam.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - } - ])); - static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc>> = Arc::new(Mutex::new(vec![ - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("firefox.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Exe, - id: String::from("idea64.exe"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - ])); - static ref MONITOR_INDEX_PREFERENCES: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref DISPLAY_INDEX_PREFERENCES: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref WORKSPACE_RULES: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref REGEX_IDENTIFIERS: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref MANAGE_IDENTIFIERS: Arc>> = Arc::new(Mutex::new(vec![])); - static ref FLOAT_IDENTIFIERS: Arc>> = Arc::new(Mutex::new(vec![ - // mstsc.exe creates these on Windows 11 when a WSL process is launched - // https://github.com/LGUG2Z/komorebi/issues/74 - IdWithIdentifier { - kind: ApplicationIdentifier::Class, - id: String::from("OPContainerClass"), - matching_strategy: Option::from(MatchingStrategy::Equals), - }, - IdWithIdentifier { - kind: ApplicationIdentifier::Class, - id: String::from("IHWindowClass"), - matching_strategy: Option::from(MatchingStrategy::Equals), - } - ])); - static ref PERMAIGNORE_CLASSES: Arc>> = Arc::new(Mutex::new(vec![ - "Chrome_RenderWidgetHostHWND".to_string(), - ])); - static ref BORDER_OVERFLOW_IDENTIFIERS: Arc>> = Arc::new(Mutex::new(vec![])); - static ref WSL2_UI_PROCESSES: Arc>> = Arc::new(Mutex::new(vec![ - "X410.exe".to_string(), - "vcxsrv.exe".to_string(), - ])); - static ref SUBSCRIPTION_PIPES: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref SUBSCRIPTION_SOCKETS: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref TCP_CONNECTIONS: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - static ref HIDING_BEHAVIOUR: Arc> = - Arc::new(Mutex::new(HidingBehaviour::Minimize)); - static ref HOME_DIR: PathBuf = { - std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| { - let home = PathBuf::from(&home_path); - - if home.as_path().is_dir() { - home - } else { - panic!( - "$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory", - ); - } - }) - }; - static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi"); - static ref AHK_EXE: String = { - let mut ahk: String = String::from("autohotkey.exe"); - - if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") { - if which(&komorebi_ahk_exe).is_ok() { - ahk = komorebi_ahk_exe; - } - } - - ahk - }; - static ref WINDOWS_11: bool = { - matches!( - os_info::get().version(), - Version::Semantic(_, _, x) if x >= &22000 - ) - }; - - static ref BORDER_RECT: Arc> = - Arc::new(Mutex::new(Rect::default())); - - static ref BORDER_OFFSET: Arc>> = - Arc::new(Mutex::new(None)); - - // Use app-specific titlebar removal options where possible - // eg. Windows Terminal, IntelliJ IDEA, Firefox - static ref NO_TITLEBAR: Arc>> = Arc::new(Mutex::new(vec![])); -} - -pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10); -pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10); - -pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false); -pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false); -pub static SESSION_ID: AtomicU32 = AtomicU32::new(0); -pub static ALT_FOCUS_HACK: AtomicBool = AtomicBool::new(false); -pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false); -pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0); -pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false); -pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0); -pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0); -pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0); -pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0); -pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20); -// 0 0 0 aka pure black, I doubt anyone will want this as a border colour -pub const TRANSPARENCY_COLOUR: u32 = 0; -pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false); - -pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); +use komorebi::hidden::Hidden; +use komorebi::load_configuration; +use komorebi::process_command::listen_for_commands; +use komorebi::process_command::listen_for_commands_tcp; +use komorebi::process_event::listen_for_events; +use komorebi::process_movement::listen_for_movements; +use komorebi::static_config::StaticConfig; +use komorebi::window_manager::WindowManager; +use komorebi::window_manager_event::WindowManagerEvent; +use komorebi::windows_api::WindowsApi; +use komorebi::winevent_listener; +use komorebi::CUSTOM_FFM; +use komorebi::HOME_DIR; +use komorebi::INITIAL_CONFIGURATION_LOADED; +use komorebi::SESSION_ID; fn setup() -> Result<(WorkerGuard, WorkerGuard)> { if std::env::var("RUST_LIB_BACKTRACE").is_err() { @@ -306,143 +104,6 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> { Ok((guard, color_guard)) } -pub fn load_configuration() -> Result<()> { - let config_pwsh = HOME_DIR.join("komorebi.ps1"); - let config_ahk = HOME_DIR.join("komorebi.ahk"); - - if config_pwsh.exists() { - let powershell_exe = if which("pwsh.exe").is_ok() { - "pwsh.exe" - } else { - "powershell.exe" - }; - - tracing::info!("loading configuration file: {}", config_pwsh.display()); - - Command::new(powershell_exe) - .arg(config_pwsh.as_os_str()) - .output()?; - } else if config_ahk.exists() && which(&*AHK_EXE).is_ok() { - tracing::info!("loading configuration file: {}", config_ahk.display()); - - Command::new(&*AHK_EXE) - .arg(config_ahk.as_os_str()) - .output()?; - } - - Ok(()) -} - -#[must_use] -pub fn current_virtual_desktop() -> Option> { - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - - // This is the path on Windows 10 - let mut current = hkcu - .open_subkey(format!( - r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#, - SESSION_ID.load(Ordering::SeqCst) - )) - .ok() - .and_then( - |desktops| match desktops.get_raw_value("CurrentVirtualDesktop") { - Ok(current) => Option::from(current.bytes), - Err(_) => None, - }, - ); - - // This is the path on Windows 11 - if current.is_none() { - current = hkcu - .open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops") - .ok() - .and_then( - |desktops| match desktops.get_raw_value("CurrentVirtualDesktop") { - Ok(current) => Option::from(current.bytes), - Err(_) => None, - }, - ); - } - - // For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not - // exist until one has been created in the task view - - // The registry value will also not exist on user login if virtual desktops have been created - // but the task view has not been initiated - - // In both of these cases, we return None, and the virtual desktop validation will never run. In - // the latter case, if the user desires this validation after initiating the task view, komorebi - // should be restarted, and then when this // fn runs again for the first time, it will pick up - // the value of CurrentVirtualDesktop and validate against it accordingly - current -} - -#[derive(Debug, Serialize, JsonSchema)] -#[serde(untagged)] -pub enum NotificationEvent { - WindowManager(WindowManagerEvent), - Socket(SocketMessage), -} - -#[derive(Debug, Serialize, JsonSchema)] -pub struct Notification { - pub event: NotificationEvent, - pub state: State, -} - -pub fn notify_subscribers(notification: &str) -> Result<()> { - let mut stale_sockets = vec![]; - let mut sockets = SUBSCRIPTION_SOCKETS.lock(); - - for (socket, path) in &mut *sockets { - match UnixStream::connect(path) { - Ok(mut stream) => { - tracing::debug!("pushed notification to subscriber: {socket}"); - stream.write_all(notification.as_bytes())?; - } - Err(_) => { - stale_sockets.push(socket.clone()); - } - } - } - - for socket in stale_sockets { - tracing::warn!("removing stale subscription: {socket}"); - sockets.remove(&socket); - } - - let mut stale_pipes = vec![]; - let mut pipes = SUBSCRIPTION_PIPES.lock(); - for (subscriber, pipe) in &mut *pipes { - match writeln!(pipe, "{notification}") { - Ok(()) => { - tracing::debug!("pushed notification to subscriber: {subscriber}"); - } - Err(error) => { - // ERROR_FILE_NOT_FOUND - // 2 (0x2) - // The system cannot find the file specified. - - // ERROR_NO_DATA - // 232 (0xE8) - // The pipe is being closed. - - // Remove the subscription; the process will have to subscribe again - if let Some(2 | 232) = error.raw_os_error() { - stale_pipes.push(subscriber.clone()); - } - } - } - } - - for subscriber in stale_pipes { - tracing::warn!("removing stale subscription: {}", subscriber); - pipes.remove(&subscriber); - } - - Ok(()) -} - #[cfg(feature = "deadlock_detection")] #[tracing::instrument] fn detect_deadlocks() { diff --git a/komorebi/src/monitor.rs b/komorebi/src/monitor.rs index 225264f9..e03eb46d 100644 --- a/komorebi/src/monitor.rs +++ b/komorebi/src/monitor.rs @@ -9,6 +9,7 @@ use getset::Getters; use getset::MutGetters; use getset::Setters; use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; use komorebi_core::Rect; @@ -17,7 +18,9 @@ use crate::container::Container; use crate::ring::Ring; use crate::workspace::Workspace; -#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)] +#[derive( + Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema, +)] pub struct Monitor { #[getset(get_copy = "pub", set = "pub")] id: isize, @@ -34,10 +37,9 @@ pub struct Monitor { #[getset(get_copy = "pub", set = "pub")] work_area_offset: Option, workspaces: Ring, - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "Option::is_none")] #[getset(get_copy = "pub", set = "pub")] last_focused_workspace: Option, - #[serde(skip_serializing)] #[getset(get_mut = "pub")] workspace_names: HashMap, } diff --git a/komorebi/src/ring.rs b/komorebi/src/ring.rs index e0c7cb4c..d5da1698 100644 --- a/komorebi/src/ring.rs +++ b/komorebi/src/ring.rs @@ -1,9 +1,10 @@ use std::collections::VecDeque; use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; -#[derive(Debug, Clone, Serialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct Ring { elements: VecDeque, focused: usize, diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index 47574a43..7b2f0b03 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -15,6 +15,7 @@ use regex::Regex; use schemars::JsonSchema; use serde::ser::Error; use serde::ser::SerializeStruct; +use serde::Deserialize; use serde::Serialize; use serde::Serializer; use windows::Win32::Foundation::HWND; @@ -42,7 +43,7 @@ use crate::PERMAIGNORE_CLASSES; use crate::REGEX_IDENTIFIERS; use crate::WSL2_UI_PROCESSES; -#[derive(Debug, Clone, Copy, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)] pub struct Window { pub(crate) hwnd: isize, } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 02a704fb..78bc5fa9 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -16,6 +16,7 @@ use hotwatch::notify::DebouncedEvent; use hotwatch::Hotwatch; use parking_lot::Mutex; use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; use uds_windows::UnixListener; @@ -84,7 +85,7 @@ pub struct WindowManager { } #[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct State { pub monitors: Ring, pub is_paused: bool, diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index ae6d19bd..215ff3d8 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use std::fmt::Formatter; use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; use crate::window::should_act; @@ -10,7 +11,7 @@ use crate::winevent::WinEvent; use crate::OBJECT_NAME_CHANGE_ON_LAUNCH; use crate::REGEX_IDENTIFIERS; -#[derive(Debug, Copy, Clone, Serialize, JsonSchema)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type", content = "content")] pub enum WindowManagerEvent { Destroy(WinEvent, Window), diff --git a/komorebi/src/winevent.rs b/komorebi/src/winevent.rs index 756a5580..d3f9c948 100644 --- a/komorebi/src/winevent.rs +++ b/komorebi/src/winevent.rs @@ -1,6 +1,7 @@ #![allow(clippy::use_self)] use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; use strum::Display; use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END; @@ -88,7 +89,7 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START; use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END; use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START; -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Display, JsonSchema)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Display, JsonSchema)] #[repr(u32)] #[allow(dead_code)] pub enum WinEvent { diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 61ee0eb3..c6fdfdf9 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -9,6 +9,7 @@ use getset::Getters; use getset::MutGetters; use getset::Setters; use schemars::JsonSchema; +use serde::Deserialize; use serde::Serialize; use komorebi_core::Axis; @@ -32,19 +33,21 @@ use crate::NO_TITLEBAR; use crate::REMOVE_TITLEBARS; #[allow(clippy::struct_field_names)] -#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)] +#[derive( + Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema, +)] pub struct Workspace { #[getset(get = "pub", set = "pub")] name: Option, containers: Ring, #[getset(get = "pub", get_mut = "pub", set = "pub")] monocle_container: Option, - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "Option::is_none")] #[getset(get_copy = "pub", set = "pub")] monocle_container_restore_idx: Option, #[getset(get = "pub", get_mut = "pub", set = "pub")] maximized_window: Option, - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "Option::is_none")] #[getset(get_copy = "pub", set = "pub")] maximized_window_restore_idx: Option, #[getset(get = "pub", get_mut = "pub")] @@ -59,7 +62,6 @@ pub struct Workspace { workspace_padding: Option, #[getset(get_copy = "pub", set = "pub")] container_padding: Option, - #[serde(skip_serializing)] #[getset(get = "pub", set = "pub")] latest_layout: Vec, #[getset(get = "pub", get_mut = "pub", set = "pub")]