diff --git a/komorebi/src/container.rs b/komorebi/src/container.rs index 448c56da..6819c5e3 100644 --- a/komorebi/src/container.rs +++ b/komorebi/src/container.rs @@ -3,12 +3,13 @@ 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")] 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..71d5139b 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, 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..e521a5d1 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,7 +33,9 @@ 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,