feat: wayland connection, niri IPC and valw doctor
This commit is contained in:
1 parent
c6094307f5
commit
272f2d30ab
4 files changed
+799
-4
No files matched your search
+263
@@ -0,0 +1,263 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
use crate::niri;
|
||||||
|
use crate::wayland::Wayland;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Level {
|
||||||
|
Ok,
|
||||||
|
Warn,
|
||||||
|
Fail,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Check {
|
||||||
|
pub level: Level,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Check {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let tag = match self.level {
|
||||||
|
Level::Ok => "ok",
|
||||||
|
Level::Warn => "warn",
|
||||||
|
Level::Fail => "fail",
|
||||||
|
};
|
||||||
|
write!(f, "{tag:<5} {}", self.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(level: Level, text: impl Into<String>) -> Check {
|
||||||
|
Check {
|
||||||
|
level,
|
||||||
|
text: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Globals valw can't work without: (interface, minimum version, used for).
|
||||||
|
const REQUIRED: [(&str, u32, &str); 7] = [
|
||||||
|
("wl_compositor", 1, "surfaces"),
|
||||||
|
("wl_shm", 1, "pixel buffers"),
|
||||||
|
("wl_seat", 1, "mouse and keyboard"),
|
||||||
|
("zxdg_output_manager_v1", 2, "output names and positions"),
|
||||||
|
("zwlr_layer_shell_v1", 1, "the selection overlay"),
|
||||||
|
("zwlr_screencopy_manager_v1", 1, "screen capture"),
|
||||||
|
("wp_viewporter", 1, "sharp overlays at fractional scale"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Globals that enable a feature: (any of these interfaces, feature).
|
||||||
|
const OPTIONAL: [(&[&str], &str); 2] = [
|
||||||
|
(
|
||||||
|
&["wp_cursor_shape_manager_v1"],
|
||||||
|
"crosshair cursor in region mode",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ext_data_control_manager_v1",
|
||||||
|
"zwlr_data_control_manager_v1",
|
||||||
|
],
|
||||||
|
"clipboard copy",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn check_globals(globals: &[(String, u32)]) -> Vec<Check> {
|
||||||
|
let version = |name: &str| globals.iter().find(|(i, _)| i == name).map(|&(_, v)| v);
|
||||||
|
let mut checks = Vec::new();
|
||||||
|
for (name, min, purpose) in REQUIRED {
|
||||||
|
checks.push(match version(name) {
|
||||||
|
Some(v) if v >= min => check(Level::Ok, format!("{name} v{v} ({purpose})")),
|
||||||
|
Some(v) => check(
|
||||||
|
Level::Fail,
|
||||||
|
format!("{name} v{v} is too old, need v{min} ({purpose})"),
|
||||||
|
),
|
||||||
|
None => check(Level::Fail, format!("{name} missing ({purpose})")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (names, feature) in OPTIONAL {
|
||||||
|
checks.push(
|
||||||
|
match names.iter().find_map(|&n| version(n).map(|v| (n, v))) {
|
||||||
|
Some((n, v)) => check(Level::Ok, format!("{n} v{v} ({feature})")),
|
||||||
|
None => check(
|
||||||
|
Level::Warn,
|
||||||
|
format!("{} missing ({feature} disabled)", names.join(" / ")),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
checks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_niri(version: Result<String>) -> Check {
|
||||||
|
let (y, m) = niri::IPC_VERSION;
|
||||||
|
match version {
|
||||||
|
Err(e) => check(
|
||||||
|
Level::Warn,
|
||||||
|
format!("niri IPC unavailable ({e:#}); using the first output for `screen`"),
|
||||||
|
),
|
||||||
|
Ok(v) => match niri::parse_version(&v) {
|
||||||
|
Some(found) if found == niri::IPC_VERSION => check(Level::Ok, format!("niri {v}")),
|
||||||
|
_ => check(
|
||||||
|
Level::Warn,
|
||||||
|
format!(
|
||||||
|
"niri {v}, but valw was built against niri-ipc {y}.{m}; IPC replies may not parse"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_config(path: &Path) -> Check {
|
||||||
|
match config::load(path) {
|
||||||
|
Ok(_) if path.exists() => check(Level::Ok, format!("config {}", path.display())),
|
||||||
|
Ok(_) => check(
|
||||||
|
Level::Ok,
|
||||||
|
format!("config {} not found, using defaults", path.display()),
|
||||||
|
),
|
||||||
|
Err(e) => check(Level::Fail, format!("config: {e:#}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_save_dir(dir: &Path) -> Check {
|
||||||
|
let probe = dir.join(".valw-doctor");
|
||||||
|
let result = std::fs::create_dir_all(dir)
|
||||||
|
.and_then(|()| std::fs::write(&probe, b""))
|
||||||
|
.and_then(|()| std::fs::remove_file(&probe));
|
||||||
|
match result {
|
||||||
|
Ok(()) => check(
|
||||||
|
Level::Ok,
|
||||||
|
format!("save directory {} is writable", dir.display()),
|
||||||
|
),
|
||||||
|
Err(e) => check(
|
||||||
|
Level::Fail,
|
||||||
|
format!("save directory {}: {e}", dir.display()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs every check and prints one line each. Fails if any check failed.
|
||||||
|
pub fn run() -> Result<()> {
|
||||||
|
let mut checks = Vec::new();
|
||||||
|
match Wayland::connect() {
|
||||||
|
Ok(wl) => {
|
||||||
|
checks.extend(check_globals(&wl.global_list()));
|
||||||
|
for o in wl.outputs() {
|
||||||
|
let g = &o.geom;
|
||||||
|
checks.push(check(
|
||||||
|
Level::Ok,
|
||||||
|
format!(
|
||||||
|
"output {}: {}x{} logical at {},{}, transform {:?}",
|
||||||
|
g.name, g.width, g.height, g.x, g.y, o.transform
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => checks.push(check(Level::Fail, format!("wayland: {e:#}"))),
|
||||||
|
}
|
||||||
|
checks.push(check_niri(niri::version()));
|
||||||
|
let config_path = config::default_path();
|
||||||
|
checks.push(check_config(&config_path));
|
||||||
|
if let Ok(config) = config::load(&config_path) {
|
||||||
|
checks.push(check_save_dir(&config.save_dir()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in &checks {
|
||||||
|
println!("{c}");
|
||||||
|
}
|
||||||
|
let failed = checks.iter().filter(|c| c.level == Level::Fail).count();
|
||||||
|
if failed > 0 {
|
||||||
|
bail!("{failed} check(s) failed");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn globals(list: &[(&str, u32)]) -> Vec<(String, u32)> {
|
||||||
|
list.iter().map(|&(n, v)| (n.to_string(), v)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn niri_26_04() -> Vec<(String, u32)> {
|
||||||
|
globals(&[
|
||||||
|
("wl_compositor", 6),
|
||||||
|
("wl_shm", 2),
|
||||||
|
("wl_seat", 9),
|
||||||
|
("zxdg_output_manager_v1", 3),
|
||||||
|
("zwlr_layer_shell_v1", 5),
|
||||||
|
("zwlr_screencopy_manager_v1", 3),
|
||||||
|
("wp_viewporter", 1),
|
||||||
|
("wp_cursor_shape_manager_v1", 2),
|
||||||
|
("zwlr_data_control_manager_v1", 2),
|
||||||
|
("ext_data_control_manager_v1", 1),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_present_is_all_ok() {
|
||||||
|
let checks = check_globals(&niri_26_04());
|
||||||
|
assert!(checks.iter().all(|c| c.level == Level::Ok), "{checks:#?}");
|
||||||
|
assert!(
|
||||||
|
checks
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.text == "ext_data_control_manager_v1 v1 (clipboard copy)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_required_fails_missing_optional_warns() {
|
||||||
|
let list: Vec<_> = niri_26_04()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(n, _)| n != "zwlr_screencopy_manager_v1" && n != "wp_cursor_shape_manager_v1")
|
||||||
|
.collect();
|
||||||
|
let checks = check_globals(&list);
|
||||||
|
assert!(checks.contains(&check(
|
||||||
|
Level::Fail,
|
||||||
|
"zwlr_screencopy_manager_v1 missing (screen capture)"
|
||||||
|
)));
|
||||||
|
assert!(checks.contains(&check(
|
||||||
|
Level::Warn,
|
||||||
|
"wp_cursor_shape_manager_v1 missing (crosshair cursor in region mode disabled)"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn old_version_fails() {
|
||||||
|
let mut list = niri_26_04();
|
||||||
|
list.retain(|(n, _)| n != "zxdg_output_manager_v1");
|
||||||
|
list.push(("zxdg_output_manager_v1".into(), 1));
|
||||||
|
assert!(check_globals(&list).contains(&check(
|
||||||
|
Level::Fail,
|
||||||
|
"zxdg_output_manager_v1 v1 is too old, need v2 (output names and positions)"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn niri_versions() {
|
||||||
|
assert_eq!(check_niri(Ok("26.04 (Nixpkgs)".into())).level, Level::Ok);
|
||||||
|
assert_eq!(check_niri(Ok("25.11".into())).level, Level::Warn);
|
||||||
|
assert_eq!(
|
||||||
|
check_niri(Err(anyhow::anyhow!("no socket"))).level,
|
||||||
|
Level::Warn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_dir_checks() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
assert_eq!(check_save_dir(&dir.path().join("new")).level, Level::Ok);
|
||||||
|
let file = dir.path().join("file");
|
||||||
|
std::fs::write(&file, b"").unwrap();
|
||||||
|
assert_eq!(check_save_dir(&file).level, Level::Fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_format() {
|
||||||
|
assert_eq!(check(Level::Warn, "x").to_string(), "warn x");
|
||||||
|
assert_eq!(check(Level::Ok, "x").to_string(), "ok x");
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-4
@@ -3,20 +3,64 @@
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod detach;
|
mod detach;
|
||||||
|
mod doctor;
|
||||||
mod error;
|
mod error;
|
||||||
mod frame;
|
mod frame;
|
||||||
mod lock;
|
mod lock;
|
||||||
mod log;
|
mod log;
|
||||||
|
mod niri;
|
||||||
mod output;
|
mod output;
|
||||||
mod render;
|
mod render;
|
||||||
mod selection;
|
mod selection;
|
||||||
|
mod wayland;
|
||||||
|
|
||||||
use clap::Parser;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::Local;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::error::Cancelled;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about = "macOS-style screenshots for niri")]
|
#[command(version, about = "macOS-style screenshots for niri")]
|
||||||
struct Cli {}
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
#[derive(Subcommand)]
|
||||||
Cli::parse();
|
enum Command {
|
||||||
|
/// Report what the compositor and system support.
|
||||||
|
Doctor,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let log = log::init(&log::default_dir(), Local::now());
|
||||||
|
log::install_panic_hook(log.clone());
|
||||||
|
tracing::info!(
|
||||||
|
"valw {} {:?}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
std::env::args().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
match run(cli) {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(e) if e.downcast_ref::<Cancelled>().is_some() => {
|
||||||
|
tracing::info!("cancelled by the user");
|
||||||
|
ExitCode::from(3)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("{e:#}");
|
||||||
|
eprint!("{}", error::render(&e, log.as_deref()));
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(cli: Cli) -> Result<()> {
|
||||||
|
match cli.command {
|
||||||
|
Command::Doctor => doctor::run(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use niri_ipc::{Request, Response, socket::Socket};
|
||||||
|
|
||||||
|
/// The niri release the pinned `niri-ipc` crate belongs to. Keep in sync
|
||||||
|
/// with the `niri-ipc = "=X.Y.0"` line in Cargo.toml (a test checks this).
|
||||||
|
pub const IPC_VERSION: (u32, u32) = (26, 4);
|
||||||
|
|
||||||
|
fn request(request: Request) -> Result<Response> {
|
||||||
|
let mut socket =
|
||||||
|
Socket::connect().context("could not connect to niri (is NIRI_SOCKET set?)")?;
|
||||||
|
socket
|
||||||
|
.send(request)
|
||||||
|
.context("niri IPC request failed")?
|
||||||
|
.map_err(|e| anyhow!("niri replied with an error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name of the output niri has focused, if any.
|
||||||
|
pub fn focused_output() -> Result<Option<String>> {
|
||||||
|
match request(Request::FocusedOutput)? {
|
||||||
|
Response::FocusedOutput(output) => Ok(output.map(|o| o.name)),
|
||||||
|
other => Err(anyhow!("unexpected niri reply: {other:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// niri's version string, e.g. "26.04 (Nixpkgs)".
|
||||||
|
pub fn version() -> Result<String> {
|
||||||
|
match request(Request::Version)? {
|
||||||
|
Response::Version(v) => Ok(v),
|
||||||
|
other => Err(anyhow!("unexpected niri reply: {other:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `(year, month)` from a niri version string like "26.04 (Nixpkgs)".
|
||||||
|
pub fn parse_version(v: &str) -> Option<(u32, u32)> {
|
||||||
|
let (year, rest) = v.split_once('.')?;
|
||||||
|
let month: String = rest.chars().take_while(char::is_ascii_digit).collect();
|
||||||
|
Some((year.parse().ok()?, month.parse().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_versions() {
|
||||||
|
assert_eq!(parse_version("26.04 (Nixpkgs)"), Some((26, 4)));
|
||||||
|
assert_eq!(parse_version("25.11"), Some((25, 11)));
|
||||||
|
assert_eq!(parse_version("26.04.1-dev"), Some((26, 4)));
|
||||||
|
assert_eq!(parse_version("unknown"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ipc_version_matches_cargo_toml() {
|
||||||
|
let (y, m) = IPC_VERSION;
|
||||||
|
let pin = format!("niri-ipc = \"={y}.{m}.0\"");
|
||||||
|
assert!(
|
||||||
|
include_str!("../Cargo.toml").contains(&pin),
|
||||||
|
"Cargo.toml should pin {pin}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+427
@@ -0,0 +1,427 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
compositor::{CompositorHandler, CompositorState},
|
||||||
|
delegate_registry,
|
||||||
|
output::{OutputHandler, OutputState},
|
||||||
|
registry::{ProvidesRegistryState, RegistryState},
|
||||||
|
registry_handlers,
|
||||||
|
seat::{
|
||||||
|
Capability, SeatHandler, SeatState,
|
||||||
|
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
|
||||||
|
pointer::{PointerEvent, PointerHandler, cursor_shape::CursorShapeManager},
|
||||||
|
},
|
||||||
|
shell::wlr_layer::{LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure},
|
||||||
|
shm::{Shm, ShmHandler},
|
||||||
|
};
|
||||||
|
use wayland_client::{
|
||||||
|
Connection, EventQueue, QueueHandle,
|
||||||
|
globals::{GlobalList, registry_queue_init},
|
||||||
|
protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
|
||||||
|
};
|
||||||
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1;
|
||||||
|
use wayland_protocols::wp::viewporter::client::{
|
||||||
|
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
|
||||||
|
};
|
||||||
|
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||||
|
|
||||||
|
use crate::error::HintExt;
|
||||||
|
use crate::frame::OutputGeom;
|
||||||
|
|
||||||
|
/// An output as valw sees it.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Output {
|
||||||
|
pub wl: wl_output::WlOutput,
|
||||||
|
pub geom: OutputGeom,
|
||||||
|
pub transform: wl_output::Transform,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Wayland {
|
||||||
|
pub conn: Connection,
|
||||||
|
pub queue: EventQueue<State>,
|
||||||
|
pub globals: GlobalList,
|
||||||
|
pub state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
registry: RegistryState,
|
||||||
|
pub outputs: OutputState,
|
||||||
|
pub seat: SeatState,
|
||||||
|
pub shm: Shm,
|
||||||
|
pub compositor: CompositorState,
|
||||||
|
pub layer_shell: Option<LayerShell>,
|
||||||
|
pub screencopy: Option<ZwlrScreencopyManagerV1>,
|
||||||
|
pub viewporter: Option<WpViewporter>,
|
||||||
|
pub cursor_shape: Option<CursorShapeManager>,
|
||||||
|
pub cursor_device: Option<WpCursorShapeDeviceV1>,
|
||||||
|
pub keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||||
|
pub pointer: Option<wl_pointer::WlPointer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wayland {
|
||||||
|
pub fn connect() -> Result<Wayland> {
|
||||||
|
let conn = Connection::connect_to_env()
|
||||||
|
.context("could not connect to the Wayland compositor")
|
||||||
|
.hint("valw needs a Wayland session; is WAYLAND_DISPLAY set?")?;
|
||||||
|
let (globals, mut queue) = registry_queue_init::<State>(&conn)
|
||||||
|
.context("could not read the compositor's globals")?;
|
||||||
|
let qh = queue.handle();
|
||||||
|
|
||||||
|
let mut state = State {
|
||||||
|
registry: RegistryState::new(&globals),
|
||||||
|
outputs: OutputState::new(&globals, &qh),
|
||||||
|
seat: SeatState::new(&globals, &qh),
|
||||||
|
shm: Shm::bind(&globals, &qh).context("compositor has no wl_shm")?,
|
||||||
|
compositor: CompositorState::bind(&globals, &qh)
|
||||||
|
.context("compositor has no wl_compositor")?,
|
||||||
|
layer_shell: LayerShell::bind(&globals, &qh).ok(),
|
||||||
|
screencopy: globals.bind(&qh, 1..=3, ()).ok(),
|
||||||
|
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||||
|
cursor_shape: CursorShapeManager::bind(&globals, &qh).ok(),
|
||||||
|
cursor_device: None,
|
||||||
|
keyboard: None,
|
||||||
|
pointer: None,
|
||||||
|
};
|
||||||
|
// Two round trips: one for wl_output, one for the xdg-output details.
|
||||||
|
queue
|
||||||
|
.roundtrip(&mut state)
|
||||||
|
.context("Wayland round trip failed")?;
|
||||||
|
queue
|
||||||
|
.roundtrip(&mut state)
|
||||||
|
.context("Wayland round trip failed")?;
|
||||||
|
|
||||||
|
let wl = Wayland {
|
||||||
|
conn,
|
||||||
|
queue,
|
||||||
|
globals,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
wl.log_environment();
|
||||||
|
Ok(wl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All outputs with a known name and logical geometry.
|
||||||
|
pub fn outputs(&self) -> Vec<Output> {
|
||||||
|
self.state
|
||||||
|
.outputs
|
||||||
|
.outputs()
|
||||||
|
.filter_map(|wl| {
|
||||||
|
let info = self.state.outputs.info(&wl)?;
|
||||||
|
let (x, y) = info.logical_position?;
|
||||||
|
let (width, height) = info.logical_size?;
|
||||||
|
Some(Output {
|
||||||
|
geom: OutputGeom {
|
||||||
|
name: info
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("output-{}", info.id)),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
transform: info.transform,
|
||||||
|
wl,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `(interface, version)` of every global, sorted.
|
||||||
|
pub fn global_list(&self) -> Vec<(String, u32)> {
|
||||||
|
let mut list: Vec<_> = self
|
||||||
|
.globals
|
||||||
|
.contents()
|
||||||
|
.clone_list()
|
||||||
|
.into_iter()
|
||||||
|
.map(|g| (g.interface, g.version))
|
||||||
|
.collect();
|
||||||
|
list.sort();
|
||||||
|
list.dedup();
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_environment(&self) {
|
||||||
|
let globals: Vec<String> = self
|
||||||
|
.global_list()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(interface, version)| format!("{interface} v{version}"))
|
||||||
|
.collect();
|
||||||
|
tracing::info!("globals: {}", globals.join(", "));
|
||||||
|
for o in self.outputs() {
|
||||||
|
let g = &o.geom;
|
||||||
|
tracing::info!(
|
||||||
|
"output {} at {},{} size {}x{} (logical) transform {:?}",
|
||||||
|
g.name,
|
||||||
|
g.x,
|
||||||
|
g.y,
|
||||||
|
g.width,
|
||||||
|
g.height,
|
||||||
|
o.transform
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches events until `done` returns true or `deadline` passes.
|
||||||
|
pub fn dispatch_until(
|
||||||
|
&mut self,
|
||||||
|
deadline: Instant,
|
||||||
|
mut done: impl FnMut(&State) -> bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
self.queue
|
||||||
|
.dispatch_pending(&mut self.state)
|
||||||
|
.context("Wayland dispatch failed")?;
|
||||||
|
if done(&self.state) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.queue.flush().context("Wayland flush failed")?;
|
||||||
|
let Some(guard) = self.queue.prepare_read() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let left = deadline.saturating_duration_since(Instant::now());
|
||||||
|
if left.is_zero() {
|
||||||
|
bail!("the compositor did not answer in time");
|
||||||
|
}
|
||||||
|
let timeout = Timespec::try_from(left).expect("duration fits");
|
||||||
|
let fd = guard.connection_fd();
|
||||||
|
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
|
||||||
|
match poll(&mut fds, Some(&timeout)) {
|
||||||
|
Ok(0) => continue,
|
||||||
|
Ok(_) => {
|
||||||
|
guard.read().context("Wayland read failed")?;
|
||||||
|
}
|
||||||
|
Err(rustix::io::Errno::INTR) => continue,
|
||||||
|
Err(e) => return Err(e).context("poll failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches events until `done` returns true. Waits as long as it takes.
|
||||||
|
pub fn dispatch_blocking(&mut self, mut done: impl FnMut(&State) -> bool) -> Result<()> {
|
||||||
|
while !done(&self.state) {
|
||||||
|
self.queue
|
||||||
|
.blocking_dispatch(&mut self.state)
|
||||||
|
.context("Wayland dispatch failed")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositorHandler for State {
|
||||||
|
fn scale_factor_changed(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_surface::WlSurface,
|
||||||
|
_: i32,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_changed(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_surface::WlSurface,
|
||||||
|
_: wl_output::Transform,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {}
|
||||||
|
|
||||||
|
fn surface_enter(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_surface::WlSurface,
|
||||||
|
_: &wl_output::WlOutput,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_leave(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_surface::WlSurface,
|
||||||
|
_: &wl_output::WlOutput,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputHandler for State {
|
||||||
|
fn output_state(&mut self) -> &mut OutputState {
|
||||||
|
&mut self.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
|
||||||
|
|
||||||
|
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
|
||||||
|
|
||||||
|
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerShellHandler for State {
|
||||||
|
fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {}
|
||||||
|
|
||||||
|
fn configure(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &LayerSurface,
|
||||||
|
_: LayerSurfaceConfigure,
|
||||||
|
_: u32,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SeatHandler for State {
|
||||||
|
fn seat_state(&mut self) -> &mut SeatState {
|
||||||
|
&mut self.seat
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||||
|
|
||||||
|
fn new_capability(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
seat: wl_seat::WlSeat,
|
||||||
|
capability: Capability,
|
||||||
|
) {
|
||||||
|
if capability == Capability::Keyboard && self.keyboard.is_none() {
|
||||||
|
self.keyboard = self.seat.get_keyboard(qh, &seat, None).ok();
|
||||||
|
}
|
||||||
|
if capability == Capability::Pointer
|
||||||
|
&& self.pointer.is_none()
|
||||||
|
&& let Ok(pointer) = self.seat.get_pointer(qh, &seat)
|
||||||
|
{
|
||||||
|
self.cursor_device = self
|
||||||
|
.cursor_shape
|
||||||
|
.as_ref()
|
||||||
|
.map(|m| m.get_shape_device(&pointer, qh));
|
||||||
|
self.pointer = Some(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_capability(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: wl_seat::WlSeat,
|
||||||
|
capability: Capability,
|
||||||
|
) {
|
||||||
|
if capability == Capability::Keyboard
|
||||||
|
&& let Some(k) = self.keyboard.take()
|
||||||
|
{
|
||||||
|
k.release();
|
||||||
|
}
|
||||||
|
if capability == Capability::Pointer {
|
||||||
|
if let Some(d) = self.cursor_device.take() {
|
||||||
|
d.destroy();
|
||||||
|
}
|
||||||
|
if let Some(p) = self.pointer.take() {
|
||||||
|
p.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardHandler for State {
|
||||||
|
fn enter(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
_: &wl_surface::WlSurface,
|
||||||
|
_: u32,
|
||||||
|
_: &[u32],
|
||||||
|
_: &[Keysym],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leave(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
_: &wl_surface::WlSurface,
|
||||||
|
_: u32,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press_key(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
_: u32,
|
||||||
|
_: KeyEvent,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repeat_key(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
_: u32,
|
||||||
|
_: KeyEvent,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_key(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
_: u32,
|
||||||
|
_: KeyEvent,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_modifiers(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
_: u32,
|
||||||
|
_: Modifiers,
|
||||||
|
_: RawModifiers,
|
||||||
|
_: u32,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerHandler for State {
|
||||||
|
fn pointer_frame(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_pointer::WlPointer,
|
||||||
|
_: &[PointerEvent],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShmHandler for State {
|
||||||
|
fn shm_state(&mut self) -> &mut Shm {
|
||||||
|
&mut self.shm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProvidesRegistryState for State {
|
||||||
|
fn registry(&mut self) -> &mut RegistryState {
|
||||||
|
&mut self.registry
|
||||||
|
}
|
||||||
|
registry_handlers![OutputState, SeatState];
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_registry!(State);
|
||||||
|
smithay_client_toolkit::delegate_dispatch2!(State);
|
||||||
|
wayland_client::delegate_noop!(State: ZwlrScreencopyManagerV1);
|
||||||
|
wayland_client::delegate_noop!(State: WpViewporter);
|
||||||
|
wayland_client::delegate_noop!(State: WpViewport);
|
||||||
Reference in new issue
Block a user