feat(client): add subscribe_with_options

This commit adds a new method, subscribe_with_options to
komorebi-client.

The first option introduced is to tell komorebi to only send
notifications when the window manager state has been changed during the
processing of an event.

This new subscription option is now used with komorebi-bar to improve
rendering and update performance.
This commit is contained in:
LGUG2Z
2024-10-11 10:32:01 -07:00
parent 7f42042322
commit 62047ed1ab
8 changed files with 176 additions and 29 deletions

View File

@@ -22,6 +22,7 @@ use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
@@ -328,7 +329,9 @@ fn main() -> color_eyre::Result<()> {
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let listener = komorebi_client::subscribe(&subscriber_name)
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,
})
.expect("could not subscribe to komorebi notifications");
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);

View File

@@ -44,6 +44,7 @@ pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
use komorebi::DATA_DIR;
@@ -96,3 +97,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
Ok(listener)
}
pub fn subscribe_with_options(
name: &str,
options: SubscribeOptions,
) -> std::io::Result<UnixListener> {
let socket = DATA_DIR.join(name);
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
return Err(error);
}
},
};
let listener = UnixListener::bind(&socket)?;
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
name.to_string(),
options,
))?;
Ok(listener)
}

View File

@@ -9,7 +9,7 @@ use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[getset(get = "pub")]
id: String,
@@ -27,12 +27,6 @@ impl Default for Container {
}
}
impl PartialEq for Container {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Container {
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {

View File

@@ -196,6 +196,7 @@ pub enum SocketMessage {
RemoveTitleBar(ApplicationIdentifier, String),
ToggleTitleBars,
AddSubscriberSocket(String),
AddSubscriberSocketWithOptions(String, SubscribeOptions),
RemoveSubscriberSocket(String),
AddSubscriberPipe(String),
RemoveSubscriberPipe(String),
@@ -221,6 +222,12 @@ impl FromStr for SocketMessage {
}
}
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SubscribeOptions {
/// Only emit notifications when the window manager state has changed
pub filter_state_changes: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
@@ -336,7 +343,16 @@ pub enum ApplicationIdentifier {
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Copy,
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
@@ -377,7 +393,16 @@ pub enum WindowContainerBehaviour {
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
@@ -411,7 +436,16 @@ pub enum HidingBehaviour {
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows

View File

@@ -177,6 +177,8 @@ lazy_static! {
Arc::new(Mutex::new(HashMap::new()));
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref SUBSCRIPTION_SOCKET_OPTIONS: Arc<Mutex<HashMap<String, SubscribeOptions>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
@@ -297,18 +299,34 @@ pub struct Notification {
pub state: State,
}
pub fn notify_subscribers(notification: &str) -> Result<()> {
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
let is_subscription_event = matches!(
notification.event,
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
);
let notification = &serde_json::to_string(&notification)?;
let mut stale_sockets = vec![];
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let options = SUBSCRIPTION_SOCKET_OPTIONS.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());
let apply_state_filter = (*options)
.get(socket)
.copied()
.unwrap_or_default()
.filter_state_changes;
if !apply_state_filter || state_has_been_modified || is_subscription_event {
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());
}
}
}
}

View File

@@ -61,6 +61,7 @@ use crate::winevent_listener;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
@@ -79,6 +80,7 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
@@ -187,6 +189,10 @@ impl WindowManager {
}
}
#[allow(clippy::useless_asref)]
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
match message {
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
if let Some(monitor) = self.focused_monitor_mut() {
@@ -1319,6 +1325,14 @@ impl WindowManager {
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
}
SocketMessage::AddSubscriberSocketWithOptions(ref socket, options) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
let mut socket_options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
socket_options.insert(socket.clone(), options);
}
SocketMessage::RemoveSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
sockets.remove(socket);
@@ -1574,12 +1588,14 @@ impl WindowManager {
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
let notification = Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
};
notify_subscribers(
Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
},
initial_state.has_been_modified(self.as_ref()),
)?;
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification(None);
transparency_manager::send_notification();
stackbar_manager::send_notification();

View File

@@ -32,6 +32,7 @@ use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::DATA_DIR;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
@@ -122,6 +123,10 @@ impl WindowManager {
}
}
#[allow(clippy::useless_asref)]
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
// Make sure we have the most recently focused monitor from any event
match event {
WindowManagerEvent::FocusChange(_, window)
@@ -670,12 +675,14 @@ impl WindowManager {
serde_json::to_writer_pretty(&file, &known_hwnds)?;
let notification = Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
};
notify_subscribers(
Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
},
initial_state.has_been_modified(self.as_ref()),
)?;
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification(Some(event.hwnd()));
transparency_manager::send_notification();
stackbar_manager::send_notification();

View File

@@ -122,6 +122,54 @@ pub struct State {
pub has_pending_raise_op: bool,
}
impl State {
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
let new = Self::from(wm);
if self.monitors != new.monitors {
return true;
}
if self.is_paused != new.is_paused {
return true;
}
if self.new_window_behaviour != new.new_window_behaviour {
return true;
}
if self.float_override != new.float_override {
return true;
}
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
return true;
}
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
return true;
}
if self.work_area_offset != new.work_area_offset {
return true;
}
if self.focus_follows_mouse != new.focus_follows_mouse {
return true;
}
if self.mouse_follows_focus != new.mouse_follows_focus {
return true;
}
if self.has_pending_raise_op != new.has_pending_raise_op {
return true;
}
false
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GlobalState {