From 2916256e799a8fa9a846a5b3b4416247f92652be Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Tue, 17 Sep 2024 21:09:11 -0700 Subject: [PATCH] feat(wm): add replace configuration socket message This commit introduces a new SocketMessage, ReplaceConfiguration, which attempts to replace a running instance of WindowManager with another created from a (presumably) different komorebi.json file. This will likely be useful for people who have multiple different monitor setups that they connect and disconnect from throughout the day, but definitely needs more testing. An experimental sub-widget which calls this SocketMessage has been added to komorebi-bar to aid with initial testing. --- Cargo.lock | 1 + komorebi-bar/Cargo.toml | 1 + komorebi-bar/src/bar.rs | 4 +- komorebi-bar/src/komorebi.rs | 145 +++++++++++++++++++++++++------- komorebi-bar/src/widget.rs | 2 +- komorebi/src/core/mod.rs | 1 + komorebi/src/main.rs | 1 + komorebi/src/process_command.rs | 28 ++++++ komorebi/src/static_config.rs | 30 ++++--- komorebic/src/main.rs | 20 ++++- schema.bar.json | 46 +++++++++- schema.json | 82 +++++++++++++++--- 12 files changed, 292 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 978a322e..496e7560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2693,6 +2693,7 @@ dependencies = [ "color-eyre", "crossbeam-channel", "dirs", + "dunce", "eframe", "egui-phosphor", "font-loader", diff --git a/komorebi-bar/Cargo.toml b/komorebi-bar/Cargo.toml index 4c31aa80..a1a21ede 100644 --- a/komorebi-bar/Cargo.toml +++ b/komorebi-bar/Cargo.toml @@ -14,6 +14,7 @@ clap = { version = "4", features = ["derive", "wrap_help"] } color-eyre = "0.6" crossbeam-channel = "0.5" dirs = { workspace = true } +dunce = "1" eframe = "0.28" egui-phosphor = "0.6.0" font-loader = "0.11" diff --git a/komorebi-bar/src/bar.rs b/komorebi-bar/src/bar.rs index a6f1d6a4..7bba98ad 100644 --- a/komorebi-bar/src/bar.rs +++ b/komorebi-bar/src/bar.rs @@ -183,7 +183,7 @@ impl Komobar { for (idx, widget_config) in config.left_widgets.iter().enumerate() { if let WidgetConfig::Komorebi(config) = widget_config { - komorebi_widget = Some(Komorebi::from(*config)); + komorebi_widget = Some(Komorebi::from(config)); komorebi_widget_idx = Some(idx); side = Some(Side::Left); } @@ -191,7 +191,7 @@ impl Komobar { for (idx, widget_config) in config.right_widgets.iter().enumerate() { if let WidgetConfig::Komorebi(config) = widget_config { - komorebi_widget = Some(Komorebi::from(*config)); + komorebi_widget = Some(Komorebi::from(config)); komorebi_widget_idx = Some(idx); side = Some(Side::Right); } diff --git a/komorebi-bar/src/komorebi.rs b/komorebi-bar/src/komorebi.rs index c3b1aaf0..bd968c9c 100644 --- a/komorebi-bar/src/komorebi.rs +++ b/komorebi-bar/src/komorebi.rs @@ -16,21 +16,26 @@ use eframe::egui::Ui; use image::RgbaImage; use komorebi_client::CycleDirection; use komorebi_client::NotificationEvent; +use komorebi_client::Rect; use komorebi_client::SocketMessage; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::cell::RefCell; +use std::collections::BTreeMap; +use std::path::PathBuf; use std::rc::Rc; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiConfig { /// Configure the Workspaces widget pub workspaces: KomorebiWorkspacesConfig, /// Configure the Layout widget - pub layout: KomorebiLayoutConfig, + pub layout: Option, /// Configure the Focused Window widget - pub focused_window: KomorebiFocusedWindowConfig, + pub focused_window: Option, + /// Configure the Configuration Switcher widget + pub configuration_switcher: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -55,8 +60,37 @@ pub struct KomorebiFocusedWindowConfig { pub show_icon: bool, } -impl From for Komorebi { - fn from(value: KomorebiConfig) -> Self { +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct KomorebiConfigurationSwitcherConfig { + /// Enable the Komorebi Configurations widget + pub enable: bool, + /// A map of display friendly name => path to configuration.json + pub configurations: BTreeMap, +} + +impl From<&KomorebiConfig> for Komorebi { + fn from(value: &KomorebiConfig) -> Self { + let configuration_switcher = + if let Some(configuration_switcher) = &value.configuration_switcher { + let mut configuration_switcher = configuration_switcher.clone(); + for (_, location) in configuration_switcher.configurations.iter_mut() { + if let Ok(expanded) = std::env::var("KOMOREBI_CONFIG_HOME") { + *location = location.replace("$Env:KOMOREBI_CONFIG_HOME", &expanded); + } + + if let Ok(expanded) = std::env::var("USERPROFILE") { + *location = location.replace("$Env:USERPROFILE", &expanded); + } + + *location = dunce::simplified(&PathBuf::from(location.clone())) + .to_string_lossy() + .to_string(); + } + Some(configuration_switcher) + } else { + None + }; + Self { komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState { selected_workspace: String::new(), @@ -67,10 +101,12 @@ impl From for Komorebi { workspaces: vec![], hide_empty_workspaces: value.workspaces.hide_empty_workspaces, mouse_follows_focus: true, + work_area_offset: None, })), workspaces: value.workspaces, layout: value.layout, focused_window: value.focused_window, + configuration_switcher, } } } @@ -79,8 +115,9 @@ impl From for Komorebi { pub struct Komorebi { pub komorebi_notification_state: Rc>, pub workspaces: KomorebiWorkspacesConfig, - pub layout: KomorebiLayoutConfig, - pub focused_window: KomorebiFocusedWindowConfig, + pub layout: Option, + pub focused_window: Option, + pub configuration_switcher: Option, } impl BarWidget for Komorebi { @@ -103,9 +140,7 @@ impl BarWidget for Komorebi { .unwrap(); komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i)).unwrap(); komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( - self.komorebi_notification_state - .borrow() - .mouse_follows_focus, + komorebi_notification_state.mouse_follows_focus, )) .unwrap(); komorebi_client::send_message(&SocketMessage::Retile).unwrap(); @@ -119,36 +154,78 @@ impl BarWidget for Komorebi { ui.add_space(WIDGET_SPACING); } - if self.layout.enable { - if ui - .add( - Label::new(&komorebi_notification_state.layout) - .selectable(false) - .sense(Sense::click()), - ) - .clicked() - { - komorebi_client::send_message(&SocketMessage::CycleLayout(CycleDirection::Next)) + if let Some(layout) = self.layout { + if layout.enable { + if ui + .add( + Label::new(&komorebi_notification_state.layout) + .selectable(false) + .sense(Sense::click()), + ) + .clicked() + { + komorebi_client::send_message(&SocketMessage::CycleLayout( + CycleDirection::Next, + )) .unwrap(); - } + } - ui.add_space(WIDGET_SPACING); + ui.add_space(WIDGET_SPACING); + } } - if self.focused_window.enable { - if self.focused_window.show_icon { - if let Some(img) = &komorebi_notification_state.focused_window_icon { - ui.add( - Image::from(&img_to_texture(ctx, img)) - .maintain_aspect_ratio(true) - .max_height(15.0), - ); + if let Some(configuration_switcher) = &self.configuration_switcher { + if configuration_switcher.enable { + for (name, location) in configuration_switcher.configurations.iter() { + let path = PathBuf::from(location); + if path.is_file() + && ui + .add(Label::new(name).selectable(false).sense(Sense::click())) + .clicked() + { + let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path); + komorebi_client::send_message(&SocketMessage::ReplaceConfiguration( + canonicalized, + )) + .unwrap(); + + if let Some(rect) = komorebi_notification_state.work_area_offset { + let monitor_index = komorebi_client::send_query(&SocketMessage::Query( + komorebi_client::StateQuery::FocusedMonitorIndex, + )) + .unwrap(); + + komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset( + monitor_index.parse::().unwrap(), + rect, + )) + .unwrap(); + } + } } + + ui.add_space(WIDGET_SPACING); } + } - ui.add(Label::new(&komorebi_notification_state.focused_window_title).selectable(false)); + if let Some(focused_window) = self.focused_window { + if focused_window.enable { + if focused_window.show_icon { + if let Some(img) = &komorebi_notification_state.focused_window_icon { + ui.add( + Image::from(&img_to_texture(ctx, img)) + .maintain_aspect_ratio(true) + .max_height(15.0), + ); + } + } - ui.add_space(WIDGET_SPACING); + ui.add( + Label::new(&komorebi_notification_state.focused_window_title).selectable(false), + ); + + ui.add_space(WIDGET_SPACING); + } } } } @@ -170,6 +247,7 @@ pub struct KomorebiNotificationState { pub layout: String, pub hide_empty_workspaces: bool, pub mouse_follows_focus: bool, + pub work_area_offset: Option, } impl KomorebiNotificationState { @@ -199,6 +277,9 @@ impl KomorebiNotificationState { self.mouse_follows_focus = notification.state.mouse_follows_focus; let monitor = ¬ification.state.monitors.elements()[monitor_index]; + self.work_area_offset = + notification.state.monitors.elements()[monitor_index].work_area_offset(); + let focused_workspace_idx = monitor.focused_workspace_idx(); let mut workspaces = vec![]; diff --git a/komorebi-bar/src/widget.rs b/komorebi-bar/src/widget.rs index 33ffc6a1..c8ba21c5 100644 --- a/komorebi-bar/src/widget.rs +++ b/komorebi-bar/src/widget.rs @@ -41,7 +41,7 @@ impl WidgetConfig { match self { WidgetConfig::Battery(config) => Box::new(Battery::from(*config)), WidgetConfig::Date(config) => Box::new(Date::from(config.clone())), - WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(*config)), + WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)), WidgetConfig::Media(config) => Box::new(Media::from(*config)), WidgetConfig::Memory(config) => Box::new(Memory::from(*config)), WidgetConfig::Network(config) => Box::new(Network::from(*config)), diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 4633ae28..b84748e3 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -133,6 +133,7 @@ pub enum SocketMessage { ClearNamedWorkspaceLayoutRules(String), // Configuration ReloadConfiguration, + ReplaceConfiguration(PathBuf), ReloadStaticConfiguration(PathBuf), WatchConfiguration(bool), CompleteConfiguration, diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index 5f28bffc..269c81ae 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -220,6 +220,7 @@ fn main() -> Result<()> { Arc::new(Mutex::new(StaticConfig::preload( config, winevent_listener::event_rx(), + None, )?)) } else { Arc::new(Mutex::new(WindowManager::new( diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 60fd9e93..0c3ec459 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -56,6 +56,7 @@ use crate::window::Window; use crate::window_manager; use crate::window_manager::WindowManager; use crate::windows_api::WindowsApi; +use crate::winevent_listener; use crate::GlobalState; use crate::Notification; use crate::NotificationEvent; @@ -1097,6 +1098,33 @@ impl WindowManager { SocketMessage::ReloadConfiguration => { Self::reload_configuration(); } + SocketMessage::ReplaceConfiguration(ref config) => { + // Check that this is a valid static config file first + if StaticConfig::read(config).is_ok() { + // Clear workspace rules; these will need to be replaced + WORKSPACE_RULES.lock().clear(); + // Pause so that restored windows come to the foreground from all workspaces + self.is_paused = true; + // Bring all windows to the foreground + self.restore_all_windows()?; + + // Create a new wm from the config path + let mut wm = StaticConfig::preload( + config, + winevent_listener::event_rx(), + self.command_listener.try_clone().ok(), + )?; + + // Initialize the new wm + wm.init()?; + + // This is equivalent to StaticConfig::postload for this use case + StaticConfig::reload(config, &mut wm)?; + + // Set self to the new wm instance + *self = wm; + } + } SocketMessage::ReloadStaticConfiguration(ref pathbuf) => { self.reload_static_configuration(pathbuf)?; } diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 84ec1598..e02082c2 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -1027,26 +1027,32 @@ impl StaticConfig { pub fn preload( path: &PathBuf, incoming: Receiver, + unix_listener: Option, ) -> Result { let content = std::fs::read_to_string(path)?; let mut value: Self = serde_json::from_str(&content)?; value.apply_globals()?; - let socket = DATA_DIR.join("komorebi.sock"); + let listener = match unix_listener { + Some(listener) => listener, + None => { + let socket = DATA_DIR.join("komorebi.sock"); - match std::fs::remove_file(&socket) { - Ok(()) => {} - Err(error) => match error.kind() { - // Doing this because ::exists() doesn't work reliably on Windows via IntelliJ - ErrorKind::NotFound => {} - _ => { - return Err(error.into()); - } - }, + match std::fs::remove_file(&socket) { + Ok(()) => {} + Err(error) => match error.kind() { + // Doing this because ::exists() doesn't work reliably on Windows via IntelliJ + ErrorKind::NotFound => {} + _ => { + return Err(error.into()); + } + }, + }; + + UnixListener::bind(&socket)? + } }; - let listener = UnixListener::bind(&socket)?; - let mut wm = WindowManager { monitors: Ring::default(), incoming_events: incoming, diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index b5a45132..d3ccc646 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -861,6 +861,12 @@ struct EnableAutostart { ahk: bool, } +#[derive(Parser)] +struct ReplaceConfiguration { + /// Static configuration JSON file from which the configuration should be loaded + path: PathBuf, +} + #[derive(Parser)] #[clap(author, about, version = build::CLAP_LONG_VERSION)] struct Opts { @@ -1168,12 +1174,15 @@ enum SubCommand { Manage, /// Unmanage a window that was forcibly managed Unmanage, - /// Reload ~/komorebi.ahk (if it exists) + /// Replace the configuration of a running instance of komorebi from a static configuration file + #[clap(arg_required_else_help = true)] + ReplaceConfiguration(ReplaceConfiguration), + /// Reload legacy komorebi.ahk or komorebi.ps1 configurations (if they exist) ReloadConfiguration, - /// Enable or disable watching of ~/komorebi.ahk (if it exists) + /// Enable or disable watching of legacy komorebi.ahk or komorebi.ps1 configurations (if they exist) #[clap(arg_required_else_help = true)] WatchConfiguration(WatchConfiguration), - /// Signal that the final configuration option has been sent + /// For legacy komorebi.ahk or komorebi.ps1 configurations, signal that the final configuration option has been sent CompleteConfiguration, /// DEPRECATED since v0.1.22 #[clap(arg_required_else_help = true)] @@ -1518,7 +1527,7 @@ fn main() -> Result<()> { // Check that this file adheres to the schema static config schema as the last step, // so that more basic errors above can be shown to the error before schema-specific // errors - let _ = serde_json::from_str::(&config_source)?; + let _ = serde_json::from_str::(&config_source)?; if config_whkd.exists() { println!("Found {}; key bindings will be loaded from here when whkd is started, and you can start it automatically using the --whkd flag\n", config_whkd.to_string_lossy()); @@ -2283,6 +2292,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue arg.boolean_state.into(), ))?; } + SubCommand::ReplaceConfiguration(arg) => { + send_message(&SocketMessage::ReplaceConfiguration(arg.path))?; + } SubCommand::ReloadConfiguration => { send_message(&SocketMessage::ReloadConfiguration)?; } diff --git a/schema.bar.json b/schema.bar.json index 9f5502d1..cfa6d074 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -150,11 +150,30 @@ "Komorebi": { "type": "object", "required": [ - "focused_window", - "layout", "workspaces" ], "properties": { + "configuration_switcher": { + "description": "Configure the Configuration Switcher widget", + "type": "object", + "required": [ + "configurations", + "enable" + ], + "properties": { + "configurations": { + "description": "A map of display friendly name => path to configuration.json", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enable": { + "description": "Enable the Komorebi Configurations widget", + "type": "boolean" + } + } + }, "focused_window": { "description": "Configure the Focused Window widget", "type": "object", @@ -540,11 +559,30 @@ "Komorebi": { "type": "object", "required": [ - "focused_window", - "layout", "workspaces" ], "properties": { + "configuration_switcher": { + "description": "Configure the Configuration Switcher widget", + "type": "object", + "required": [ + "configurations", + "enable" + ], + "properties": { + "configurations": { + "description": "A map of display friendly name => path to configuration.json", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enable": { + "description": "Enable the Komorebi Configurations widget", + "type": "boolean" + } + } + }, "focused_window": { "description": "Configure the Focused Window widget", "type": "object", diff --git a/schema.json b/schema.json index faaaa2c6..20f81f59 100644 --- a/schema.json +++ b/schema.json @@ -1320,9 +1320,41 @@ "type": "object", "required": [ "name", - "type" + "palette" ], "properties": { + "bar_accent": { + "description": "Komorebi status bar accent (default: Blue)", + "type": "string", + "enum": [ + "Rosewater", + "Flamingo", + "Pink", + "Mauve", + "Red", + "Maroon", + "Peach", + "Yellow", + "Green", + "Teal", + "Sky", + "Sapphire", + "Blue", + "Lavender", + "Text", + "Subtext1", + "Subtext0", + "Overlay2", + "Overlay1", + "Overlay0", + "Surface2", + "Surface1", + "Surface0", + "Base", + "Mantle", + "Crust" + ] + }, "monocle_border": { "description": "Border colour when the container is in monocle mode (default: Pink)", "type": "string", @@ -1365,6 +1397,12 @@ "Mocha" ] }, + "palette": { + "type": "string", + "enum": [ + "Catppuccin" + ] + }, "single_border": { "description": "Border colour when the container contains a single window (default: Blue)", "type": "string", @@ -1525,12 +1563,6 @@ "Crust" ] }, - "type": { - "type": "string", - "enum": [ - "Catppuccin" - ] - }, "unfocused_border": { "description": "Border colour when the container is unfocused (default: Base)", "type": "string", @@ -1570,9 +1602,31 @@ "type": "object", "required": [ "name", - "type" + "palette" ], "properties": { + "bar_accent": { + "description": "Komorebi status bar accent (default: Base0D)", + "type": "string", + "enum": [ + "Base00", + "Base01", + "Base02", + "Base03", + "Base04", + "Base05", + "Base06", + "Base07", + "Base08", + "Base09", + "Base0A", + "Base0B", + "Base0C", + "Base0D", + "Base0E", + "Base0F" + ] + }, "monocle_border": { "description": "Border colour when the container is in monocle mode (default: Base0F)", "type": "string", @@ -1870,6 +1924,12 @@ "Zenburn" ] }, + "palette": { + "type": "string", + "enum": [ + "Base16" + ] + }, "single_border": { "description": "Border colour when the container contains a single window (default: Base0D)", "type": "string", @@ -1980,12 +2040,6 @@ "Base0F" ] }, - "type": { - "type": "string", - "enum": [ - "Base16" - ] - }, "unfocused_border": { "description": "Border colour when the container is unfocused (default: Base01)", "type": "string",