aboutsummaryrefslogtreecommitdiff
path: root/niri-ipc/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-06-20 12:04:10 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2024-09-01 23:47:19 -0700
commit30b213601a4f71d65a2227fa68ffb1ab2a69f671 (patch)
treee68d9db212c6a4ac610ec0f80bb3e5db83950a67 /niri-ipc/src
parent8eb34b2e185aa0e0affea450226369cd3f9e6a78 (diff)
downloadniri-30b213601a4f71d65a2227fa68ffb1ab2a69f671.tar.gz
niri-30b213601a4f71d65a2227fa68ffb1ab2a69f671.tar.bz2
niri-30b213601a4f71d65a2227fa68ffb1ab2a69f671.zip
Implement the event stream IPC
Diffstat (limited to 'niri-ipc/src')
-rw-r--r--niri-ipc/src/lib.rs98
-rw-r--r--niri-ipc/src/socket.rs18
-rw-r--r--niri-ipc/src/state.rs188
3 files changed, 301 insertions, 3 deletions
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index ecf202f2..e6058ca1 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -9,6 +9,8 @@ use serde::{Deserialize, Serialize};
mod socket;
pub use socket::{Socket, SOCKET_PATH_ENV};
+pub mod state;
+
/// Request from client to niri.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -38,6 +40,11 @@ pub enum Request {
FocusedOutput,
/// Request information about the keyboard layout.
KeyboardLayouts,
+ /// Start continuously receiving events from the compositor.
+ ///
+ /// The compositor should reply with `Reply::Ok(Response::Handled)`, then continuously send
+ /// [`Event`]s, one per line.
+ EventStream,
/// Respond with an error (for testing error handling).
ReturnError,
}
@@ -536,10 +543,18 @@ pub enum Transform {
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Window {
+ /// Unique id of this window.
+ pub id: u64,
/// Title, if set.
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
+ /// Id of the workspace this window is on, if any.
+ pub workspace_id: Option<u64>,
+ /// Whether this window is currently focused.
+ ///
+ /// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
+ pub is_focused: bool,
}
/// Output configuration change result.
@@ -556,6 +571,10 @@ pub enum OutputConfigChanged {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Workspace {
+ /// Unique id of this workspace.
+ ///
+ /// This id remains constant regardless of the workspace moving around and across monitors.
+ pub id: u64,
/// Index of the workspace on its monitor.
///
/// This is the same index you can use for requests like `niri msg action focus-workspace`.
@@ -567,7 +586,15 @@ pub struct Workspace {
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace is currently active on its output.
+ ///
+ /// Every output has one active workspace, the one that is currently visible on that output.
pub is_active: bool,
+ /// Whether the workspace is currently focused.
+ ///
+ /// There's only one focused workspace across all outputs.
+ pub is_focused: bool,
+ /// Id of the active window on this workspace, if any.
+ pub active_window_id: Option<u64>,
}
/// Configured keyboard layouts.
@@ -580,6 +607,77 @@ pub struct KeyboardLayouts {
pub current_idx: u8,
}
+/// A compositor event.
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
+pub enum Event {
+ /// The workspace configuration has changed.
+ WorkspacesChanged {
+ /// The new workspace configuration.
+ ///
+ /// This configuration completely replaces the previous configuration. I.e. if any
+ /// workspaces are missing from here, then they were deleted.
+ workspaces: Vec<Workspace>,
+ },
+ /// A workspace was activated on an output.
+ ///
+ /// This doesn't always mean the workspace became focused, just that it's now the active
+ /// workspace on its output. All other workspaces on the same output become inactive.
+ WorkspaceActivated {
+ /// Id of the newly active workspace.
+ id: u64,
+ /// Whether this workspace also became focused.
+ ///
+ /// If `true`, this is now the single focused workspace. All other workspaces are no longer
+ /// focused, but they may remain active on their respective outputs.
+ focused: bool,
+ },
+ /// An active window changed on a workspace.
+ WorkspaceActiveWindowChanged {
+ /// Id of the workspace on which the active window changed.
+ workspace_id: u64,
+ /// Id of the new active window, if any.
+ active_window_id: Option<u64>,
+ },
+ /// The window configuration has changed.
+ WindowsChanged {
+ /// The new window configuration.
+ ///
+ /// This configuration completely replaces the previous configuration. I.e. if any windows
+ /// are missing from here, then they were closed.
+ windows: Vec<Window>,
+ },
+ /// A new toplevel window was opened, or an existing toplevel window changed.
+ WindowOpenedOrChanged {
+ /// The new or updated window.
+ ///
+ /// If the window is focused, all other windows are no longer focused.
+ window: Window,
+ },
+ /// A toplevel window was closed.
+ WindowClosed {
+ /// Id of the removed window.
+ id: u64,
+ },
+ /// Window focus changed.
+ ///
+ /// All other windows are no longer focused.
+ WindowFocusChanged {
+ /// Id of the newly focused window, or `None` if no window is now focused.
+ id: Option<u64>,
+ },
+ /// The configured keyboard layouts have changed.
+ KeyboardLayoutsChanged {
+ /// The new keyboard layout configuration.
+ keyboard_layouts: KeyboardLayouts,
+ },
+ /// The keyboard layout switched.
+ KeyboardLayoutSwitched {
+ /// Index of the newly active layout.
+ idx: u8,
+ },
+}
+
impl FromStr for WorkspaceReferenceArg {
type Err = &'static str;
diff --git a/niri-ipc/src/socket.rs b/niri-ipc/src/socket.rs
index 3964f000..d629f1a4 100644
--- a/niri-ipc/src/socket.rs
+++ b/niri-ipc/src/socket.rs
@@ -6,7 +6,7 @@ use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::path::Path;
-use crate::{Reply, Request};
+use crate::{Event, Reply, Request};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
@@ -47,7 +47,11 @@ impl Socket {
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
- pub fn send(self, request: Request) -> io::Result<Reply> {
+ ///
+ /// This method also returns a blocking function that you can call to keep reading [`Event`]s
+ /// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
+ /// otherwise.
+ pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_string(&request).unwrap();
@@ -60,6 +64,14 @@ impl Socket {
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
- Ok(reply)
+
+ let events = move || {
+ buf.clear();
+ reader.read_line(&mut buf)?;
+ let event = serde_json::from_str(&buf)?;
+ Ok(event)
+ };
+
+ Ok((reply, events))
}
}
diff --git a/niri-ipc/src/state.rs b/niri-ipc/src/state.rs
new file mode 100644
index 00000000..8d2d1744
--- /dev/null
+++ b/niri-ipc/src/state.rs
@@ -0,0 +1,188 @@
+//! Helpers for keeping track of the event stream state.
+
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+
+use crate::{Event, KeyboardLayouts, Window, Workspace};
+
+/// Part of the state communicated via the event stream.
+pub trait EventStreamStatePart {
+ /// Returns a sequence of events that replicates this state from default initialization.
+ fn replicate(&self) -> Vec<Event>;
+
+ /// Applies the event to this state.
+ ///
+ /// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
+ /// part of the state.
+ fn apply(&mut self, event: Event) -> Option<Event>;
+}
+
+/// The full state communicated over the event stream.
+///
+/// Different parts of the state are not guaranteed to be consistent across every single event
+/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
+/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
+/// these two events, the workspace active window id refers to a window that does not yet exist in
+/// the windows state part.
+#[derive(Debug, Default)]
+pub struct EventStreamState {
+ /// State of workspaces.
+ pub workspaces: WorkspacesState,
+
+ /// State of workspaces.
+ pub windows: WindowsState,
+
+ /// State of the keyboard layouts.
+ pub keyboard_layouts: KeyboardLayoutsState,
+}
+
+/// The workspaces state communicated over the event stream.
+#[derive(Debug, Default)]
+pub struct WorkspacesState {
+ /// Map from a workspace id to the workspace.
+ pub workspaces: HashMap<u64, Workspace>,
+}
+
+/// The windows state communicated over the event stream.
+#[derive(Debug, Default)]
+pub struct WindowsState {
+ /// Map from a window id to the window.
+ pub windows: HashMap<u64, Window>,
+}
+
+/// The keyboard layout state communicated over the event stream.
+#[derive(Debug, Default)]
+pub struct KeyboardLayoutsState {
+ /// Configured keyboard layouts.
+ pub keyboard_layouts: Option<KeyboardLayouts>,
+}
+
+impl EventStreamStatePart for EventStreamState {
+ fn replicate(&self) -> Vec<Event> {
+ let mut events = Vec::new();
+ events.extend(self.workspaces.replicate());
+ events.extend(self.windows.replicate());
+ events.extend(self.keyboard_layouts.replicate());
+ events
+ }
+
+ fn apply(&mut self, event: Event) -> Option<Event> {
+ let event = self.workspaces.apply(event)?;
+ let event = self.windows.apply(event)?;
+ let event = self.keyboard_layouts.apply(event)?;
+ Some(event)
+ }
+}
+
+impl EventStreamStatePart for WorkspacesState {
+ fn replicate(&self) -> Vec<Event> {
+ let workspaces = self.workspaces.values().cloned().collect();
+ vec![Event::WorkspacesChanged { workspaces }]
+ }
+
+ fn apply(&mut self, event: Event) -> Option<Event> {
+ match event {
+ Event::WorkspacesChanged { workspaces } => {
+ self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
+ }
+ Event::WorkspaceActivated { id, focused } => {
+ let ws = self.workspaces.get(&id);
+ let ws = ws.expect("activated workspace was missing from the map");
+ let output = ws.output.clone();
+
+ for ws in self.workspaces.values_mut() {
+ let got_activated = ws.id == id;
+ if ws.output == output {
+ ws.is_active = got_activated;
+ }
+
+ if focused {
+ ws.is_focused = got_activated;
+ }
+ }
+ }
+ Event::WorkspaceActiveWindowChanged {
+ workspace_id,
+ active_window_id,
+ } => {
+ let ws = self.workspaces.get_mut(&workspace_id);
+ let ws = ws.expect("changed workspace was missing from the map");
+ ws.active_window_id = active_window_id;
+ }
+ event => return Some(event),
+ }
+ None
+ }
+}
+
+impl EventStreamStatePart for WindowsState {
+ fn replicate(&self) -> Vec<Event> {
+ let windows = self.windows.values().cloned().collect();
+ vec![Event::WindowsChanged { windows }]
+ }
+
+ fn apply(&mut self, event: Event) -> Option<Event> {
+ match event {
+ Event::WindowsChanged { windows } => {
+ self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
+ }
+ Event::WindowOpenedOrChanged { window } => {
+ let (id, is_focused) = match self.windows.entry(window.id) {
+ Entry::Occupied(mut entry) => {
+ let entry = entry.get_mut();
+ *entry = window;
+ (entry.id, entry.is_focused)
+ }
+ Entry::Vacant(entry) => {
+ let entry = entry.insert(window);
+ (entry.id, entry.is_focused)
+ }
+ };
+
+ if is_focused {
+ for win in self.windows.values_mut() {
+ if win.id != id {
+ win.is_focused = false;
+ }
+ }
+ }
+ }
+ Event::WindowClosed { id } => {
+ let win = self.windows.remove(&id);
+ win.expect("closed window was missing from the map");
+ }
+ Event::WindowFocusChanged { id } => {
+ for win in self.windows.values_mut() {
+ win.is_focused = Some(win.id) == id;
+ }
+ }
+ event => return Some(event),
+ }
+ None
+ }
+}
+
+impl EventStreamStatePart for KeyboardLayoutsState {
+ fn replicate(&self) -> Vec<Event> {
+ if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
+ vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
+ } else {
+ vec![]
+ }
+ }
+
+ fn apply(&mut self, event: Event) -> Option<Event> {
+ match event {
+ Event::KeyboardLayoutsChanged { keyboard_layouts } => {
+ self.keyboard_layouts = Some(keyboard_layouts);
+ }
+ Event::KeyboardLayoutSwitched { idx } => {
+ let kb = self.keyboard_layouts.as_mut();
+ let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
+ kb.current_idx = idx;
+ }
+ event => return Some(event),
+ }
+ None
+ }
+}