Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ strum = { version = "0.26.2", features = ["derive"] }
swayipc = "3.0.2"
thiserror = "1.0.60"
toml = "0.8.13"
niri-ipc = "25.8.0"

# The profile that 'cargo dist' will build with
[profile.dist]
Expand Down
26 changes: 26 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
fenix.url = "github:nix-community/fenix";
};

nixConfig = {
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
extra-substituters = "https://devenv.cachix.org";
};

outputs = {
nixpkgs,
fenix,
...
}: let
pkgs = nixpkgs.legacyPackages."x86_64-linux";
rs-toolchain = with fenix.packages."x86_64-linux"; combine [complete.toolchain];
in {
devShells."x86_64-linux".default = pkgs.mkShell {
buildInputs = [
rs-toolchain
];
};
};
}
5 changes: 5 additions & 0 deletions src/comms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//! if there are signs present that the WM is running
//! in the current session

pub mod niri;
pub mod sway;

use std::{env, fmt};
Expand All @@ -25,6 +26,8 @@ pub type Name = String;
pub fn establish() -> Result<Box<dyn Comms>, Error> {
let comms = if env::var("SWAYSOCK").is_ok() {
sway::establish()?
} else if env::var("NIRI_SOCKET").is_ok() {
niri::establish()?
} else {
return Err(Error::NoWmRunning);
};
Expand All @@ -36,6 +39,8 @@ pub fn establish() -> Result<Box<dyn Comms>, Error> {
pub enum Error {
#[error("When communicating with sway: {0}")]
Sway(#[from] sway::Error),
#[error("When communicating with niri: {0}")]
Niri(#[from] niri::Error),
#[error("No known WM is running")]
NoWmRunning,
}
Expand Down
191 changes: 191 additions & 0 deletions src/comms/niri.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use niri_ipc::{
socket::Socket, LogicalOutput, OutputAction, OutputConfigChanged, Request, Response,
};

use crate::{
absolute::{self, Output},
geometry::{Interval, Rect, Rotation, Size, Transform},
};
use thiserror::Error;

use super::{sway::ParsePortError, Port, Result};

#[derive(Debug, Error)]
pub enum Error {
#[error("An IO Error occured: {0}")]
Io(#[from] std::io::Error),
#[error("Reply: {0}")]
Reply(String),
#[error("Could not parse output name `{raw}` into port: {err}")]
ParsePort { raw: String, err: ParsePortError },
#[error("Output {output} missing.")]
OutputWasMissing { output: Port },
#[error("Unexpected Response from window manager: {0:?}")]
UnexpectedResponseFromWm(Box<Response>),
}

pub struct Comms {
sock: Socket,
// so we dont have to send 20-30 (or more!!) niri messages for small changes on complex setups
}

pub fn establish() -> Result<Box<dyn super::Comms>> {
let sock = Socket::connect().map_err(Error::Io)?;
Ok(Box::new(Comms { sock }) as Box<dyn super::Comms>)
}

impl super::Comms for Comms {
fn layout(&mut self) -> super::Result<absolute::Layout> {
let mut sock = Socket::connect().map_err(Error::Io)?;

let Response::Outputs(outputs) = sock_send(&mut sock, Request::Outputs)? else {
unreachable!()
};

let res = outputs
.into_values()
.map(Output::try_from)
.collect::<Result<absolute::Layout, Error>>()?;

Ok(res)
}

fn set_layout(&mut self, layout: &absolute::Layout) -> super::Result<()> {
layout
.to_niri_output_actions()
.try_for_each(|(output, action)| {
sock_send(
&mut self.sock,
Request::Output {
output: output.to_string(),
action,
},
)
.and_then(|response| match response {
Response::OutputConfigChanged(OutputConfigChanged::Applied) => Ok(()),
Response::OutputConfigChanged(OutputConfigChanged::OutputWasMissing) => {
Err(super::Error::Niri(Error::OutputWasMissing { output }))
}
other => Err(super::Error::Niri(Error::UnexpectedResponseFromWm(
Box::new(other),
))),
})
})
}
}

fn sock_send(sock: &mut Socket, req: Request) -> super::Result<Response> {
sock.send(req)
.map_err(Error::Io)
.and_then(|val| val.map_err(Error::Reply))
.map_err(super::Error::Niri)
}

impl absolute::Layout {
pub fn to_niri_output_actions(&self) -> impl Iterator<Item = (Port, OutputAction)> + '_ {
self.outputs().flat_map(|output| {
vec![
OutputAction::Position {
position: niri_ipc::PositionToSet::Specific(niri_ipc::ConfiguredPosition {
x: output.cfg.bounds.x.start(),
y: output.cfg.bounds.y.start(),
}),
},
OutputAction::Scale {
scale: niri_ipc::ScaleToSet::Specific(output.cfg.scale),
},
OutputAction::Transform {
transform: output.cfg.transform.into(),
},
]
.into_iter()
.map(|it| (*output.port, it))
})
}
}

impl TryFrom<niri_ipc::Output> for Output {
type Error = Error;

fn try_from(output: niri_ipc::Output) -> std::result::Result<Self, Self::Error> {
let port = Port::parse_from_sway(&output.name).map_err(|err| Error::ParsePort {
raw: output.name.clone(),
err,
})?;

Ok(Self {
port,
cfg: if let Some(LogicalOutput {
x,
y,
width,
height,
scale,
transform,
}) = &output.logical
{
#[allow(
clippy::cast_possible_wrap,
reason = "realistically, a display will not be multiple billion pixels big"
)]
absolute::OutputConfig {
bounds: Rect {
x: Interval::new(*x, *x + *width as i32),
y: Interval::new(*y, *y + *height as i32),
},
resolution: Some(Size {
width: *width as i32,
height: *height as i32,
}),
scale: *scale,
transform: (*transform).into(),
active: output.current_mode.is_some(),
}
} else {
absolute::OutputConfig {
resolution: None,
bounds: Rect::default(),
scale: 1.,
transform: Transform::default(),
active: false,
}
},
})
}
}

impl From<niri_ipc::Transform> for Transform {
#[allow(
clippy::enum_glob_use,
clippy::just_underscores_and_digits,
reason = "readability"
)]
fn from(value: niri_ipc::Transform) -> Self {
use niri_ipc::Transform::*;

let flipped = matches!(value, Flipped | Flipped90 | Flipped180 | Flipped270);
let rotation = match value {
Normal | Flipped => Rotation::None,
_90 | Flipped90 => Rotation::Quarter,
_180 | Flipped180 => Rotation::Half,
_270 | Flipped270 => Rotation::ThreeQuarter,
};

Self { flipped, rotation }
}
}

impl From<Transform> for niri_ipc::Transform {
fn from(Transform { flipped, rotation }: Transform) -> Self {
match (flipped, rotation) {
(false, Rotation::None) => niri_ipc::Transform::Normal,
(false, Rotation::Quarter) => niri_ipc::Transform::_90,
(false, Rotation::Half) => niri_ipc::Transform::_180,
(false, Rotation::ThreeQuarter) => niri_ipc::Transform::_270,
(true, Rotation::None) => niri_ipc::Transform::Flipped,
(true, Rotation::Quarter) => niri_ipc::Transform::Flipped90,
(true, Rotation::Half) => niri_ipc::Transform::Flipped180,
(true, Rotation::ThreeQuarter) => niri_ipc::Transform::Flipped270,
}
}
}
2 changes: 1 addition & 1 deletion src/comms/sway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub enum ParsePortError {
}

impl Port {
fn parse_from_sway(name: &str) -> Result<Self, ParsePortError> {
pub(super) fn parse_from_sway(name: &str) -> Result<Self, ParsePortError> {
let (kind, idx) = name
.rsplit_once('-')
.ok_or_else(|| ParsePortError::NoDash {
Expand Down