diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-06-20 12:04:10 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-09-01 23:47:19 -0700 |
| commit | 30b213601a4f71d65a2227fa68ffb1ab2a69f671 (patch) | |
| tree | e68d9db212c6a4ac610ec0f80bb3e5db83950a67 /niri-ipc/src | |
| parent | 8eb34b2e185aa0e0affea450226369cd3f9e6a78 (diff) | |
| download | niri-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.rs | 98 | ||||
| -rw-r--r-- | niri-ipc/src/socket.rs | 18 | ||||
| -rw-r--r-- | niri-ipc/src/state.rs | 188 |
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 + } +} |
