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.
This commit is contained in:
LGUG2Z
2024-09-17 21:09:11 -07:00
parent 21a2138330
commit 2916256e79
12 changed files with 292 additions and 69 deletions

1
Cargo.lock generated
View File

@@ -2693,6 +2693,7 @@ dependencies = [
"color-eyre",
"crossbeam-channel",
"dirs",
"dunce",
"eframe",
"egui-phosphor",
"font-loader",

View File

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

View File

@@ -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);
}

View File

@@ -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<KomorebiLayoutConfig>,
/// Configure the Focused Window widget
pub focused_window: KomorebiFocusedWindowConfig,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -55,8 +60,37 @@ pub struct KomorebiFocusedWindowConfig {
pub show_icon: bool,
}
impl From<KomorebiConfig> 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<String, String>,
}
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<KomorebiConfig> 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<KomorebiConfig> for Komorebi {
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: KomorebiWorkspacesConfig,
pub layout: KomorebiLayoutConfig,
pub focused_window: KomorebiFocusedWindowConfig,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
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::<usize>().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<Rect>,
}
impl KomorebiNotificationState {
@@ -199,6 +277,9 @@ impl KomorebiNotificationState {
self.mouse_follows_focus = notification.state.mouse_follows_focus;
let monitor = &notification.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![];

View File

@@ -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)),

View File

@@ -133,6 +133,7 @@ pub enum SocketMessage {
ClearNamedWorkspaceLayoutRules(String),
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
ReloadStaticConfiguration(PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,

View File

@@ -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(

View File

@@ -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)?;
}

View File

@@ -1027,26 +1027,32 @@ impl StaticConfig {
pub fn preload(
path: &PathBuf,
incoming: Receiver<WindowManagerEvent>,
unix_listener: Option<UnixListener>,
) -> Result<WindowManager> {
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,

View File

@@ -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::<komorebi_client::StaticConfig>(&config_source)?;
let _ = serde_json::from_str::<StaticConfig>(&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)?;
}

View File

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

View File

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