mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-23 02:05:33 +07:00
Implement the event stream IPC
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
+15
-3
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user