Skip to content

Commit

Permalink
feat(subscriptions): add and remove subscribers
Browse files Browse the repository at this point in the history
This commit adds two new commands to add and remove subscribers to
WindowManagerEvent and SocketMessage notifications after they have been
handled by komorebi.

Interprocess communication is achieved using Named Pipes; the
subscribing process must first create the Named Pipe, and then run the
'add-subscriber' command, specifying the pipe name as the argument
(without the pipe filesystem path prepended).

Whenever a pipe is closing or has been closed, komorebi will flag this
as a stale subscription and remove it automatically.

resolve #54
  • Loading branch information
LGUG2Z committed Oct 25, 2021
1 parent f17bfe2 commit 6ae5967
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 8 deletions.
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ start Start komorebi.exe as a background process
stop Stop the komorebi.exe process and restore all hidden windows
state Show a JSON representation of the current window manager state
query Query the current window manager state
add-subscriber Subscribe to all komorebi events on a named pipe
remove-subscriber Subscribe to all komorebi events on a named pipe
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
quick-save Quicksave the current resize layout dimensions
quick-load Load the last quicksaved resize layout dimensions
Expand Down Expand Up @@ -393,6 +395,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
- [x] Helper library for AutoHotKey
- [x] View window manager state
- [x] Query window manager state
- [x] Subscribe to event and message notifications
## Development
Expand Down Expand Up @@ -450,3 +453,34 @@ representation of the `State` struct, which includes the current state of `Windo
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
## Window Manager Event Subscriptions
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
```powershell
komorebic.exe add-subscriber <your pipe name>
```

Note that you do not have to incldue the full path of the named pipe, just the name. If the named pipe
exists, `komorebi` will start pushing JSON data of successfully handled events and messages:

```json lines
{"type":"AddSubscriber","content":"test-pipe"}
{"type":"FocusWindow","content":"Up"}
{"type":"FocusChange","content":["SystemForeground",{"hwnd":1443930,"title":"komorebi – README.md","exe":"idea64.exe","class":"SunAwtFrame","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]}
{"type":"MonitorPoll","content":["ObjectCreate",{"hwnd":2624200,"title":"OLEChannelWnd","exe":"explorer.exe","class":"OleMainThreadWndClass","rect":{"left":0,"top":0,"right":0,"bottom":0}}]}
{"type":"FocusWindow","content":"Left"}
{"type":"FocusChange","content":["SystemForeground",{"hwnd":2558668,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":13,"top":60,"right":1520,"bottom":1655}}]}
{"type":"FocusWindow","content":"Right"}
{"type":"FocusChange","content":["SystemForeground",{"hwnd":1443930,"title":"komorebi – README.md","exe":"idea64.exe","class":"SunAwtFrame","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]}
{"type":"FocusWindow","content":"Down"}
{"type":"FocusChange","content":["SystemForeground",{"hwnd":67344,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":894,"right":757,"bottom":821}}]}
```

You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
in `komorebi-core`.
7 changes: 3 additions & 4 deletions komorebi-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod operation_direction;
pub mod rect;

#[derive(Clone, Debug, Serialize, Deserialize, Display)]
#[serde(tag = "type", content = "content")]
pub enum SocketMessage {
// Window / Container Commands
FocusWindow(OperationDirection),
Expand Down Expand Up @@ -92,16 +93,14 @@ pub enum SocketMessage {
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
AddSubscriber(String),
RemoveSubscriber(String),
}

impl SocketMessage {
pub fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
}

pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
Ok(serde_json::from_slice(bytes)?)
}
}

impl FromStr for SocketMessage {
Expand Down
1 change: 1 addition & 0 deletions komorebi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ uds_windows = "1"
which = "4"
winput = "0.2"
winvd = "0.0.20"
miow = "0.3"

[features]
deadlock_detection = []
38 changes: 38 additions & 0 deletions komorebi/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#![allow(clippy::missing_errors_doc)]

use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
Expand Down Expand Up @@ -79,6 +81,8 @@ lazy_static! {
"mstsc.exe".to_string(),
"vcxsrv.exe".to_string(),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
}

pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
Expand Down Expand Up @@ -182,6 +186,40 @@ pub fn load_configuration() -> Result<()> {
Ok(())
}

pub fn notify_subscribers(notification: &str) -> Result<()> {
let mut stale_subscriptions = vec![];
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
for (subscriber, pipe) in subscriptions.iter_mut() {
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() {
let subscriber_cl = subscriber.clone();
stale_subscriptions.push(subscriber_cl);
}
}
}
}

for subscriber in stale_subscriptions {
tracing::warn!("removing stale subscription: {}", subscriber);
subscriptions.remove(&subscriber);
}

Ok(())
}

#[cfg(feature = "deadlock_detection")]
#[tracing::instrument]
fn detect_deadlocks() {
Expand Down
19 changes: 18 additions & 1 deletion komorebi/src/process_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::thread;

use color_eyre::eyre::anyhow;
use color_eyre::Result;
use miow::pipe::connect;
use parking_lot::Mutex;
use uds_windows::UnixStream;

Expand All @@ -19,13 +20,15 @@ use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;

use crate::notify_subscribers;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::CUSTOM_FFM;
use crate::FLOAT_IDENTIFIERS;
use crate::MANAGE_IDENTIFIERS;
use crate::SUBSCRIPTION_PIPES;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;

Expand Down Expand Up @@ -435,6 +438,19 @@ impl WindowManager {
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
}
SocketMessage::AddSubscriber(subscriber) => {
let mut pipes = SUBSCRIPTION_PIPES.lock();
let pipe_path = format!(r"\\.\pipe\{}", subscriber);
let pipe = connect(&pipe_path).map_err(|_| {
anyhow!("the named pipe '{}' has not yet been created; please create it before running this command", pipe_path)
})?;

pipes.insert(subscriber, pipe);
}
SocketMessage::RemoveSubscriber(subscriber) => {
let mut pipes = SUBSCRIPTION_PIPES.lock();
pipes.remove(&subscriber);
}
};

tracing::info!("processed");
Expand All @@ -459,7 +475,8 @@ impl WindowManager {
};
}

self.process_command(message)?;
self.process_command(message.clone())?;
notify_subscribers(&serde_json::to_string(&message)?)?;
}

Ok(())
Expand Down
2 changes: 2 additions & 0 deletions komorebi/src/process_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;

use crate::notify_subscribers;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
Expand Down Expand Up @@ -316,6 +317,7 @@ impl WindowManager {
.open(hwnd_json)?;

serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(&serde_json::to_string(&event)?)?;

tracing::info!("processed: {}", event.window().to_string());
Ok(())
Expand Down
1 change: 1 addition & 0 deletions komorebi/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl Window {
return Ok(true);
}

#[allow(clippy::question_mark)]
if self.title().is_err() {
return Ok(false);
}
Expand Down
5 changes: 4 additions & 1 deletion komorebi/src/window_manager_event.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::fmt::Display;
use std::fmt::Formatter;

use serde::Serialize;

use crate::window::Window;
use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;

#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(tag = "type", content = "content")]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),
FocusChange(WinEvent, Window),
Expand Down
3 changes: 2 additions & 1 deletion komorebi/src/winevent.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use serde::Serialize;
use strum::Display;

use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
Expand Down Expand Up @@ -85,7 +86,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;

#[derive(Clone, Copy, PartialEq, Debug, Display)]
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Display)]
#[repr(u32)]
#[allow(dead_code)]
pub enum WinEvent {
Expand Down
8 changes: 8 additions & 0 deletions komorebic.lib.sample.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Query(state_query) {
Run, komorebic.exe query %state_query%, , Hide
}

AddSubscriber(named_pipe) {
Run, komorebic.exe add-subscriber %named_pipe%, , Hide
}

RemoveSubscriber(named_pipe) {
Run, komorebic.exe remove-subscriber %named_pipe%, , Hide
}

Log() {
Run, komorebic.exe log, , Hide
}
Expand Down
24 changes: 24 additions & 0 deletions komorebic/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,18 @@ struct LoadCustomLayout {
path: String,
}

#[derive(Clap, AhkFunction)]
struct AddSubscriber {
/// Name of the pipe to send notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}

#[derive(Clap, AhkFunction)]
struct RemoveSubscriber {
/// Name of the pipe to stop sending notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}

#[derive(Clap)]
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
struct Opts {
Expand All @@ -332,6 +344,12 @@ enum SubCommand {
/// Query the current window manager state
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Query(Query),
/// Subscribe to all komorebi events on a named pipe
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
AddSubscriber(AddSubscriber),
/// Subscribe to all komorebi events on a named pipe
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
RemoveSubscriber(RemoveSubscriber),
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
Log,
/// Quicksave the current resize layout dimensions
Expand Down Expand Up @@ -895,6 +913,12 @@ fn main() -> Result<()> {
SubCommand::Load(arg) => {
send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
}
SubCommand::AddSubscriber(arg) => {
send_message(&*SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
}
SubCommand::RemoveSubscriber(arg) => {
send_message(&*SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
}
}

Ok(())
Expand Down

0 comments on commit 6ae5967

Please sign in to comment.