mirror of
https://github.com/YaLTeR/niri.git
synced 2024-10-26 11:48:09 +03:00
Add an IPC socket and a niri msg outputs subcommand
This commit is contained in:
parent
768b326028
commit
40c85da102
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
|
||||
|
11
README.md
11
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 <kbd>Super</kbd>.
|
||||
|
@ -44,6 +44,7 @@
|
||||
include = [
|
||||
./src
|
||||
./niri-config
|
||||
./niri-ipc
|
||||
./Cargo.toml
|
||||
./Cargo.lock
|
||||
./resources
|
||||
|
11
niri-ipc/Cargo.toml
Normal file
11
niri-ipc/Cargo.toml
Normal file
@ -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
|
55
niri-ipc/src/lib.rs
Normal file
55
niri-ipc/src/lib.rs
Normal file
@ -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<String, Output>),
|
||||
}
|
||||
|
||||
/// 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<Mode>,
|
||||
/// Index of the current mode in [`Self::modes`].
|
||||
///
|
||||
/// `None` if the output is disabled.
|
||||
pub current_mode: Option<usize>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
@ -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.
|
||||
|
@ -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<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||
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<Mutex<HashMap<String, Output>>> {
|
||||
match self {
|
||||
|
@ -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<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
|
||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||
}
|
||||
|
||||
@ -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<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||
self.ipc_outputs.clone()
|
||||
}
|
||||
|
||||
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
||||
self.enabled_outputs.clone()
|
||||
}
|
||||
@ -1305,6 +1362,8 @@ impl Tty {
|
||||
warn!("error connecting connector: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_ipc_outputs();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ pub struct Winit {
|
||||
output: Output,
|
||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||
}
|
||||
|
||||
@ -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<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||
self.ipc_outputs.clone()
|
||||
}
|
||||
|
||||
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
||||
self.enabled_outputs.clone()
|
||||
}
|
||||
|
106
src/ipc/client.rs
Normal file
106
src/ipc/client.rs
Normal file
@ -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::<Vec<_>>();
|
||||
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(())
|
||||
}
|
2
src/ipc/mod.rs
Normal file
2
src/ipc/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
127
src/ipc/server.rs
Normal file
127
src/ipc/server.rs
Normal file
@ -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<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
pub fn start(
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
wayland_socket_name: &str,
|
||||
) -> anyhow::Result<Self> {
|
||||
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(())
|
||||
}
|
26
src/main.rs
26
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<PathBuf>,
|
||||
},
|
||||
/// 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<dyn std::error::Error>> {
|
||||
// Set backtrace defaults if not set.
|
||||
if env::var_os("RUST_BACKTRACE").is_none() {
|
||||
@ -120,6 +136,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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();
|
||||
|
13
src/niri.rs
13
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<zbus::zvariant::OwnedFd>,
|
||||
|
||||
pub ipc_server: Option<IpcServer>,
|
||||
|
||||
// Casts are dropped before PipeWire to prevent a double-free (yay).
|
||||
pub casts: Vec<Cast>,
|
||||
pub pipewire: Option<PipeWire>,
|
||||
@ -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![],
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user