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",