From 40c85da102054caeb86b7905cd27c69e392c8f92 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Wed, 17 Jan 2024 10:38:32 +0400 Subject: [PATCH] Add an IPC socket and a niri msg outputs subcommand --- Cargo.lock | 33 +++++++++ Cargo.toml | 6 +- README.md | 11 +++ flake.nix | 1 + niri-ipc/Cargo.toml | 11 +++ niri-ipc/src/lib.rs | 55 +++++++++++++++ resources/default-config.kdl | 5 +- src/backend/mod.rs | 9 +++ src/backend/tty.rs | 77 ++++++++++++++++++--- src/backend/winit.rs | 29 ++++++++ src/ipc/client.rs | 106 +++++++++++++++++++++++++++++ src/ipc/mod.rs | 2 + src/ipc/server.rs | 127 +++++++++++++++++++++++++++++++++++ src/main.rs | 26 +++++++ src/niri.rs | 13 ++++ 15 files changed, 499 insertions(+), 12 deletions(-) create mode 100644 niri-ipc/Cargo.toml create mode 100644 niri-ipc/src/lib.rs create mode 100644 src/ipc/client.rs create mode 100644 src/ipc/mod.rs create mode 100644 src/ipc/server.rs diff --git a/Cargo.lock b/Cargo.lock index 9babdea..3ce9f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,6 +1308,12 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "jni" version = "0.21.1" @@ -1705,12 +1711,14 @@ dependencies = [ "calloop", "clap", "directories", + "futures-util", "git-version", "keyframe", "libc", "log", "logind-zbus", "niri-config", + "niri-ipc", "notify-rust", "pipewire", "png", @@ -1720,6 +1728,7 @@ dependencies = [ "proptest-derive", "sd-notify", "serde", + "serde_json", "smithay", "smithay-drm-extras", "tracing", @@ -1743,6 +1752,13 @@ dependencies = [ "tracy-client", ] +[[package]] +name = "niri-ipc" +version = "0.1.0-alpha.3" +dependencies = [ + "serde", +] + [[package]] name = "nix" version = "0.26.4" @@ -2331,6 +2347,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + [[package]] name = "same-file" version = "1.0.6" @@ -2378,6 +2400,17 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.18" diff --git a/Cargo.toml b/Cargo.toml index 25c106a..805f6e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/YaLTeR/niri" [workspace.dependencies] bitflags = "2.4.1" directories = "5.0.1" +serde = { version = "1.0.195", features = ["derive"] } tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] } tracy-client = { version = "0.16.5", default-features = false } @@ -42,20 +43,23 @@ bitflags = "2.4.1" calloop = { version = "0.12.3", features = ["executor", "futures-io"] } clap = { version = "4.4.13", features = ["derive"] } directories = "5.0.1" +futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] } git-version = "0.3.9" keyframe = { version = "1.1.1", default-features = false } libc = "0.2.151" logind-zbus = { version = "3.1.2", optional = true } log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] } niri-config = { version = "0.1.0-alpha.3", path = "niri-config" } +niri-ipc = { version = "0.1.0-alpha.3", path = "niri-ipc" } notify-rust = { version = "4.10.0", optional = true } pipewire = { version = "0.7.2", optional = true } png = "0.17.10" portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] } profiling = "1.0.13" sd-notify = "0.4.1" +serde.workspace = true +serde_json = "1.0.111" smithay-drm-extras.workspace = true -serde = { version = "1.0.195", features = ["derive"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing.workspace = true tracy-client.workspace = true diff --git a/README.md b/README.md index 115a239..91a2632 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,17 @@ In particular, it supports file choosers and monitor screencasting (e.g. to [OBS See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri. +### IPC + +You can communicate with the running niri instance over an IPC socket. +Check `niri msg --help` for available commands. + +The `--json` flag prints the response in JSON, rather than formatted. +For example, `niri msg --json outputs`. + +For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types. +The communication over the IPC socket happens in JSON. + ## Default Hotkeys When running on a TTY, the Mod key is Super. diff --git a/flake.nix b/flake.nix index 3dd3105..c97c2d9 100644 --- a/flake.nix +++ b/flake.nix @@ -44,6 +44,7 @@ include = [ ./src ./niri-config + ./niri-ipc ./Cargo.toml ./Cargo.lock ./resources diff --git a/niri-ipc/Cargo.toml b/niri-ipc/Cargo.toml new file mode 100644 index 0000000..21207ac --- /dev/null +++ b/niri-ipc/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "niri-ipc" +version.workspace = true +description.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs new file mode 100644 index 0000000..129e8f5 --- /dev/null +++ b/niri-ipc/src/lib.rs @@ -0,0 +1,55 @@ +//! Types for communicating with niri via IPC. +#![warn(missing_docs)] + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +/// Name of the environment variable containing the niri IPC socket path. +pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET"; + +/// Request from client to niri. +#[derive(Debug, Serialize, Deserialize)] +pub enum Request { + /// Request information about connected outputs. + Outputs, +} + +/// Response from niri to client. +#[derive(Debug, Serialize, Deserialize)] +pub enum Response { + /// Information about connected outputs. + /// + /// Map from connector name to output info. + Outputs(HashMap), +} + +/// Connected output. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Output { + /// Name of the output. + pub name: String, + /// Textual description of the manufacturer. + pub make: String, + /// Textual description of the model. + pub model: String, + /// Physical width and height of the output in millimeters, if known. + pub physical_size: Option<(u32, u32)>, + /// Available modes for the output. + pub modes: Vec, + /// Index of the current mode in [`Self::modes`]. + /// + /// `None` if the output is disabled. + pub current_mode: Option, +} + +/// Output mode. +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct Mode { + /// Width in physical pixels. + pub width: u16, + /// Height in physical pixels. + pub height: u16, + /// Refresh rate in millihertz. + pub refresh_rate: u32, +} diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 38a7848..c192467 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -54,7 +54,8 @@ input { // disable-power-key-handling } -// You can configure outputs by their name, which you can find with wayland-info(1). +// You can configure outputs by their name, which you can find +// by running `niri msg outputs` while inside a niri instance. // The built-in laptop monitor is usually called "eDP-1". // Remember to uncommend the node by removing "/-"! /-output "eDP-1" { @@ -69,7 +70,7 @@ input { // If the refresh rate is omitted, niri will pick the highest refresh rate // for the resolution. // If the mode is omitted altogether or is invalid, niri will pick one automatically. - // All valid modes are listed in niri's debug output when an output is connected. + // Run `niri msg outputs` while inside a niri instance to list all outputs and their modes. mode "1920x1080@144" // Position of the output in the global coordinate space. diff --git a/src/backend/mod.rs b/src/backend/mod.rs index a273f76..595b851 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,4 +1,6 @@ +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -110,6 +112,13 @@ impl Backend { } } + pub fn ipc_outputs(&self) -> Rc>> { + match self { + Backend::Tty(tty) => tty.ipc_outputs(), + Backend::Winit(winit) => winit.ipc_outputs(), + } + } + #[cfg_attr(not(feature = "dbus"), allow(unused))] pub fn enabled_outputs(&self) -> Arc>> { match self { diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 0dacf9b..f86851c 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -74,6 +74,7 @@ pub struct Tty { // The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to // the primary GPU. primary_allocator: Option>>, + ipc_outputs: Rc>>, enabled_outputs: Arc>>, } @@ -221,6 +222,7 @@ impl Tty { devices: HashMap::new(), dmabuf_global: None, primary_allocator: None, + ipc_outputs: Rc::new(RefCell::new(HashMap::new())), enabled_outputs: Arc::new(Mutex::new(HashMap::new())), } } @@ -367,6 +369,8 @@ impl Tty { warn!("error adding device: {err:?}"); } } + + self.refresh_ipc_outputs(); } } } @@ -512,6 +516,8 @@ impl Tty { _ => (), } } + + self.refresh_ipc_outputs(); } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { @@ -576,6 +582,8 @@ impl Tty { self.gpu_manager.as_mut().remove_node(&device.render_node); niri.event_loop.remove(device.token); + + self.refresh_ipc_outputs(); } fn connector_connected( @@ -608,16 +616,7 @@ impl Tty { let device = self.devices.get_mut(&node).context("missing device")?; - // FIXME: print modes here until we have a better way to list all modes. for m in connector.modes() { - let wl_mode = Mode::from(*m); - debug!( - "mode: {}x{}@{:.3}", - m.size().0, - m.size().1, - wl_mode.refresh as f64 / 1000., - ); - trace!("{m:?}"); } @@ -1157,6 +1156,64 @@ impl Tty { } } + fn refresh_ipc_outputs(&self) { + let _span = tracy_client::span!("Tty::refresh_ipc_outputs"); + + let mut ipc_outputs = HashMap::new(); + + for device in self.devices.values() { + for (connector, crtc) in device.drm_scanner.crtcs() { + let connector_name = format!( + "{}-{}", + connector.interface().as_str(), + connector.interface_id(), + ); + + let physical_size = connector.size(); + + let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle()) + .map(|info| (info.manufacturer, info.model)) + .unwrap_or_else(|| ("Unknown".into(), "Unknown".into())); + + let modes = connector + .modes() + .iter() + .map(|m| niri_ipc::Mode { + width: m.size().0, + height: m.size().1, + refresh_rate: Mode::from(*m).refresh as u32, + }) + .collect(); + + let mut output = niri_ipc::Output { + name: connector_name.clone(), + make, + model, + physical_size, + modes, + current_mode: None, + }; + + if let Some(surface) = device.surfaces.get(&crtc) { + let current = surface.compositor.pending_mode(); + if let Some(current) = connector.modes().iter().position(|m| *m == current) { + output.current_mode = Some(current); + } else { + error!("connector mode list missing current mode"); + } + } + + ipc_outputs.insert(connector_name, output); + } + } + + self.ipc_outputs.replace(ipc_outputs); + } + + pub fn ipc_outputs(&self) -> Rc>> { + self.ipc_outputs.clone() + } + pub fn enabled_outputs(&self) -> Arc>> { self.enabled_outputs.clone() } @@ -1305,6 +1362,8 @@ impl Tty { warn!("error connecting connector: {err:?}"); } } + + self.refresh_ipc_outputs(); } } diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 9b31d2c..3c4b9ad 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -28,6 +28,7 @@ pub struct Winit { output: Output, backend: WinitGraphicsBackend, damage_tracker: OutputDamageTracker, + ipc_outputs: Rc>>, enabled_outputs: Arc>>, } @@ -56,6 +57,23 @@ impl Winit { output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None); output.set_preferred(mode); + let physical_properties = output.physical_properties(); + let ipc_outputs = Rc::new(RefCell::new(HashMap::from([( + "winit".to_owned(), + niri_ipc::Output { + name: output.name(), + make: physical_properties.make, + model: physical_properties.model, + physical_size: None, + modes: vec![niri_ipc::Mode { + width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16, + height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16, + refresh_rate: 60_000, + }], + current_mode: Some(0), + }, + )]))); + let enabled_outputs = Arc::new(Mutex::new(HashMap::from([( "winit".to_owned(), output.clone(), @@ -76,6 +94,12 @@ impl Winit { None, None, ); + + let mut ipc_outputs = winit.ipc_outputs.borrow_mut(); + let mode = &mut ipc_outputs.get_mut("winit").unwrap().modes[0]; + mode.width = size.w.clamp(0, u16::MAX as i32) as u16; + mode.height = size.h.clamp(0, u16::MAX as i32) as u16; + state.niri.output_resized(winit.output.clone()); } WinitEvent::Input(event) => state.process_input_event(event), @@ -95,6 +119,7 @@ impl Winit { output, backend, damage_tracker, + ipc_outputs, enabled_outputs, } } @@ -198,6 +223,10 @@ impl Winit { } } + pub fn ipc_outputs(&self) -> Rc>> { + self.ipc_outputs.clone() + } + pub fn enabled_outputs(&self) -> Arc>> { self.enabled_outputs.clone() } diff --git a/src/ipc/client.rs b/src/ipc/client.rs new file mode 100644 index 0000000..c09ef84 --- /dev/null +++ b/src/ipc/client.rs @@ -0,0 +1,106 @@ +use std::env; +use std::io::{Read, Write}; +use std::net::Shutdown; +use std::os::unix::net::UnixStream; + +use anyhow::{bail, Context}; +use niri_ipc::{Mode, Output, Request, Response}; + +use crate::Msg; + +pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { + let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| { + format!( + "{} is not set, are you running this within niri?", + niri_ipc::SOCKET_PATH_ENV + ) + })?; + + let mut stream = + UnixStream::connect(socket_path).context("error connecting to {socket_path}")?; + + let request = match msg { + Msg::Outputs => Request::Outputs, + }; + let mut buf = serde_json::to_vec(&request).unwrap(); + stream + .write_all(&buf) + .context("error writing IPC request")?; + stream + .shutdown(Shutdown::Write) + .context("error closing IPC stream for writing")?; + + buf.clear(); + stream + .read_to_end(&mut buf) + .context("error reading IPC response")?; + + let response = serde_json::from_slice(&buf).context("error parsing IPC response")?; + match msg { + Msg::Outputs => { + #[allow(irrefutable_let_patterns)] + let Response::Outputs(outputs) = response + else { + bail!("unexpected response: expected Outputs, got {response:?}"); + }; + + if json { + let output = + serde_json::to_string(&outputs).context("error formatting response")?; + println!("{output}"); + return Ok(()); + } + + let mut outputs = outputs.into_iter().collect::>(); + outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + + for (connector, output) in outputs.into_iter() { + let Output { + name, + make, + model, + physical_size, + modes, + current_mode, + } = output; + + println!(r#"Output "{connector}" ({make} - {model} - {name})"#); + + if let Some(current) = current_mode { + let mode = *modes + .get(current) + .context("invalid response: current mode does not exist")?; + let Mode { + width, + height, + refresh_rate, + } = mode; + let refresh = refresh_rate as f64 / 1000.; + println!(" Current mode: {width}x{height} @ {refresh:.3} Hz"); + } else { + println!(" Disabled"); + } + + if let Some((width, height)) = physical_size { + println!(" Physical size: {width}x{height} mm"); + } else { + println!(" Physical size: unknown"); + } + + println!(" Available modes:"); + for mode in modes { + let Mode { + width, + height, + refresh_rate, + } = mode; + let refresh = refresh_rate as f64 / 1000.; + println!(" {width}x{height}@{refresh:.3}"); + } + println!(); + } + } + } + + Ok(()) +} diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs new file mode 100644 index 0000000..c07f47e --- /dev/null +++ b/src/ipc/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod server; diff --git a/src/ipc/server.rs b/src/ipc/server.rs new file mode 100644 index 0000000..d493e86 --- /dev/null +++ b/src/ipc/server.rs @@ -0,0 +1,127 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::PathBuf; +use std::rc::Rc; +use std::{env, io, process}; + +use anyhow::Context; +use calloop::io::Async; +use directories::BaseDirs; +use futures_util::io::{AsyncReadExt, BufReader}; +use futures_util::{AsyncBufReadExt, AsyncWriteExt}; +use niri_ipc::{Request, Response}; +use smithay::reexports::calloop::generic::Generic; +use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; +use smithay::reexports::rustix::fs::unlink; + +use crate::niri::State; + +pub struct IpcServer { + pub socket_path: PathBuf, +} + +struct ClientCtx { + ipc_outputs: Rc>>, +} + +impl IpcServer { + pub fn start( + event_loop: &LoopHandle<'static, State>, + wayland_socket_name: &str, + ) -> anyhow::Result { + let _span = tracy_client::span!("Ipc::start"); + + let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id()); + let mut socket_path = socket_dir(); + socket_path.push(socket_name); + + let listener = UnixListener::bind(&socket_path).context("error binding socket")?; + listener + .set_nonblocking(true) + .context("error setting socket to non-blocking")?; + + let source = Generic::new(listener, Interest::READ, Mode::Level); + event_loop + .insert_source(source, |_, socket, state| { + match socket.accept() { + Ok((stream, _)) => on_new_ipc_client(state, stream), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => (), + Err(e) => return Err(e), + } + + Ok(PostAction::Continue) + }) + .unwrap(); + + Ok(Self { socket_path }) + } +} + +impl Drop for IpcServer { + fn drop(&mut self) { + let _ = unlink(&self.socket_path); + } +} + +fn socket_dir() -> PathBuf { + BaseDirs::new() + .as_ref() + .and_then(|x| x.runtime_dir()) + .map(|x| x.to_owned()) + .unwrap_or_else(env::temp_dir) +} + +fn on_new_ipc_client(state: &mut State, stream: UnixStream) { + let _span = tracy_client::span!("on_new_ipc_client"); + trace!("new IPC client connected"); + + let stream = match state.niri.event_loop.adapt_io(stream) { + Ok(stream) => stream, + Err(err) => { + warn!("error making IPC stream async: {err:?}"); + return; + } + }; + + let ctx = ClientCtx { + ipc_outputs: state.backend.ipc_outputs(), + }; + + let future = async move { + if let Err(err) = handle_client(ctx, stream).await { + warn!("error handling IPC client: {err:?}"); + } + }; + if let Err(err) = state.niri.scheduler.schedule(future) { + warn!("error scheduling IPC stream future: {err:?}"); + } +} + +async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> { + let (read, mut write) = stream.split(); + let mut buf = String::new(); + + // Read a single line to allow extensibility in the future to keep reading. + BufReader::new(read) + .read_line(&mut buf) + .await + .context("error reading request")?; + + let request: Request = serde_json::from_str(&buf).context("error parsing request")?; + + let response = match request { + Request::Outputs => { + let ipc_outputs = ctx.ipc_outputs.borrow().clone(); + Response::Outputs(ipc_outputs) + } + }; + + let buf = serde_json::to_vec(&response).context("error formatting response")?; + write + .write_all(&buf) + .await + .context("error writing response")?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 427e75b..b4de465 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod dbus; mod frame_clock; mod handlers; mod input; +mod ipc; mod layout; mod niri; mod render_helpers; @@ -40,6 +41,7 @@ use tracing_subscriber::EnvFilter; use utils::spawn; use watcher::Watcher; +use crate::ipc::client::handle_msg; use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE}; #[derive(Parser)] @@ -67,10 +69,24 @@ enum Sub { #[arg(short, long)] config: Option, }, + /// Communicate with the running niri instance. + Msg { + #[command(subcommand)] + msg: Msg, + /// Format output as JSON. + #[arg(short, long)] + json: bool, + }, /// Cause a panic to check if the backtraces are good. Panic, } +#[derive(Subcommand)] +enum Msg { + /// List connected outputs. + Outputs, +} + fn main() -> Result<(), Box> { // Set backtrace defaults if not set. if env::var_os("RUST_BACKTRACE").is_none() { @@ -120,6 +136,10 @@ fn main() -> Result<(), Box> { info!("config is valid"); return Ok(()); } + Sub::Msg { msg, json } => { + handle_msg(msg, json)?; + return Ok(()); + } Sub::Panic => cause_panic(), } } @@ -159,6 +179,12 @@ fn main() -> Result<(), Box> { socket_name.to_string_lossy() ); + // Set NIRI_SOCKET for children. + if let Some(ipc) = &state.niri.ipc_server { + env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path); + info!("IPC listening on: {}", ipc.socket_path.to_string_lossy()); + } + if is_systemd_service { // We're starting as a systemd service. Export our variables. import_env_to_systemd(); diff --git a/src/niri.rs b/src/niri.rs index f22612e..d0a7490 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -98,6 +98,7 @@ use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri}; use crate::frame_clock::FrameClock; use crate::handlers::configure_lock_surface; use crate::input::{apply_libinput_settings, TabletData}; +use crate::ipc::server::IpcServer; use crate::layout::{Layout, MonitorRenderElement}; use crate::pw_utils::{Cast, PipeWire}; use crate::render_helpers::{NiriRenderer, PrimaryGpuTextureRenderElement}; @@ -190,6 +191,8 @@ pub struct Niri { #[cfg(feature = "dbus")] pub inhibit_power_key_fd: Option, + pub ipc_server: Option, + // Casts are dropped before PipeWire to prevent a double-free (yay). pub casts: Vec, pub pipewire: Option, @@ -865,6 +868,14 @@ impl Niri { }) .unwrap(); + let ipc_server = match IpcServer::start(&event_loop, &socket_name.to_string_lossy()) { + Ok(server) => Some(server), + Err(err) => { + warn!("error starting IPC server: {err:?}"); + None + } + }; + let pipewire = match PipeWire::new(&event_loop) { Ok(pipewire) => Some(pipewire), Err(err) => { @@ -949,6 +960,8 @@ impl Niri { #[cfg(feature = "dbus")] inhibit_power_key_fd: None, + ipc_server, + pipewire, casts: vec![], }