pub mod capture;
pub mod hyprland;
pub mod pipewire;
pub mod protocols;
pub mod sway;
pub mod weston;
pub mod weston_input;
pub mod wlr_input;
use std::ffi::{OsStr, OsString};
use std::io;
use std::process::ExitStatus;
use std::time::Instant;
use crate::producer::{ProductIdentity, TerminalInjector};
use crate::cli::Config;
use crate::linux::app::AppLaunch;
use crate::linux::video::CaptureSource;
use capture::ScreencopyCapture;
use hyprland::HyprlandSession;
use pipewire::{PIPEWIRE_NODE, VideoCapture};
use sway::SwaySession;
use weston::{ActiveBackend, WestonSession};
pub use crate::cli::CompositorChoice;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolvedCompositor {
Weston,
Sway,
Hyprland,
}
impl ResolvedCompositor {
pub fn identity(self) -> ProductIdentity {
ProductIdentity {
slug: "vvland",
display_name: "Vvland",
compositor_name: self.display_name(),
}
}
pub fn display_name(self) -> &'static str {
match self {
Self::Weston => "Weston",
Self::Sway => "Sway",
Self::Hyprland => "Hyprland",
}
}
pub fn name(self) -> &'static str {
match self {
Self::Weston => "weston",
Self::Sway => "sway",
Self::Hyprland => "hyprland",
}
}
pub fn wire_name(self) -> &'static str {
match self {
Self::Weston => "veston",
Self::Sway => "vvsway",
Self::Hyprland => "vvland",
}
}
}
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)]
pub struct CompositorWindow {
pub id: u64,
pub title: Option<String>,
pub app_id: Option<String>,
pub xwayland_class: Option<String>,
pub pid: Option<u32>,
pub rect: WindowRect,
pub focused: bool,
pub fullscreen: bool,
}
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)]
pub struct WindowRect {
pub x: i64,
pub y: i64,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Debug)]
pub enum WindowIpc {
Sway(std::path::PathBuf),
Hyprland(std::path::PathBuf),
}
impl WindowIpc {
pub fn query(&self) -> io::Result<Vec<CompositorWindow>> {
match self {
Self::Sway(socket) => sway::query_windows(socket),
Self::Hyprland(socket) => hyprland::query_windows(socket),
}
}
pub fn compositor(&self) -> &'static str {
match self {
Self::Sway(_) => "sway",
Self::Hyprland(_) => "hyprland",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AppWindow {
pub app_id: &'static str,
pub fullscreen: bool,
}
pub struct CompositorEnvironment<'a> {
pub width: u32,
pub height: u32,
pub pulse_server: Option<&'a OsStr>,
pub pulse_sink: Option<&'a OsStr>,
pub app_window: Option<AppWindow>,
}
pub enum LiveInput<'a> {
Weston(&'a mut weston_input::InputChannel),
Wlr(&'a mut wlr_input::InputChannel),
}
impl LiveInput<'_> {
pub fn check_status(&mut self) -> io::Result<()> {
match self {
Self::Weston(input) => input.check_status(),
Self::Wlr(input) => input.check_status(),
}
}
}
pub fn check_pointer_bounds(
x: u32,
y: u32,
width: u32,
height: u32,
compositor: &str,
) -> io::Result<()> {
if x >= width || y >= height {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"absolute pointer position ({x}, {y}) is outside the {compositor} output \
({width}x{height})"
),
));
}
Ok(())
}
impl TerminalInjector for LiveInput<'_> {
fn key(&mut self, code: u32, pressed: bool) -> io::Result<()> {
match self {
Self::Weston(input) => input.key(code, pressed),
Self::Wlr(input) => input.key(code, pressed),
}
}
fn pointer_absolute(&mut self, x: u32, y: u32) -> io::Result<()> {
match self {
Self::Weston(input) => input.pointer_absolute(x, y),
Self::Wlr(input) => input.pointer_absolute(x, y),
}
}
fn pointer_button(&mut self, button: u32, pressed: bool) -> io::Result<()> {
match self {
Self::Weston(input) => input.pointer_button(button, pressed),
Self::Wlr(input) => input.pointer_button(button, pressed),
}
}
fn pointer_axis(&mut self, axis: u32, delta: i32) -> io::Result<()> {
match self {
Self::Weston(input) => input.pointer_axis(axis, delta),
Self::Wlr(input) => input.pointer_axis(axis, delta),
}
}
fn release_all(&mut self) -> io::Result<()> {
match self {
Self::Weston(input) => input.release_all(),
Self::Wlr(input) => input.release_all(),
}
}
}
pub enum Compositor {
Weston(WestonSession),
Sway(SwaySession),
Hyprland(HyprlandSession),
}
impl Compositor {
pub fn pid(&self) -> u32 {
match self {
Self::Weston(session) => session.pid(),
Self::Sway(session) => session.pid(),
Self::Hyprland(session) => session.pid(),
}
}
pub fn start(
compositor: ResolvedCompositor,
config: &Config,
environment: CompositorEnvironment<'_>,
) -> io::Result<Self> {
match compositor {
ResolvedCompositor::Weston => {
WestonSession::start(config, environment).map(Self::Weston)
}
ResolvedCompositor::Sway => SwaySession::start(config, environment).map(Self::Sway),
ResolvedCompositor::Hyprland => {
HyprlandSession::start(config, environment).map(Self::Hyprland)
}
}
}
pub fn start_capture(
&self,
width: u32,
height: u32,
fps: u32,
origin: Instant,
) -> io::Result<Box<dyn CaptureSource + Send + Sync>> {
match self {
Self::Weston(session) => {
VideoCapture::start(PIPEWIRE_NODE, session.pid(), width, height, fps, origin)
.map(|capture| Box::new(capture) as Box<dyn CaptureSource + Send + Sync>)
}
Self::Sway(session) => ScreencopyCapture::start(
session.wayland_socket(),
"Sway",
width,
height,
fps,
origin,
)
.map(|capture| Box::new(capture) as Box<dyn CaptureSource + Send + Sync>),
Self::Hyprland(session) => ScreencopyCapture::start(
session.wayland_socket(),
"Hyprland",
width,
height,
fps,
origin,
)
.map(|capture| Box::new(capture) as Box<dyn CaptureSource + Send + Sync>),
}
}
pub fn backend_name(&self) -> &'static str {
match self {
Self::Weston(session) => session.backend().name(),
Self::Sway(session) => session.backend_name(),
Self::Hyprland(session) => session.backend_name(),
}
}
pub fn weston_backend(&self) -> Option<ActiveBackend> {
match self {
Self::Weston(session) => Some(session.backend()),
Self::Sway(_) | Self::Hyprland(_) => None,
}
}
pub fn input_mut(&mut self) -> LiveInput<'_> {
match self {
Self::Weston(session) => LiveInput::Weston(session.input_mut()),
Self::Sway(session) => LiveInput::Wlr(session.input_mut()),
Self::Hyprland(session) => LiveInput::Wlr(session.input_mut()),
}
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
match self {
Self::Weston(session) => session.try_wait(),
Self::Sway(session) => session.try_wait(),
Self::Hyprland(session) => session.try_wait(),
}
}
pub fn launch_program(&mut self, program: &[OsString]) -> io::Result<()> {
match self {
Self::Weston(session) => session.launch_program(program),
Self::Sway(session) => session.launch_program(program),
Self::Hyprland(session) => session.launch_program(program),
}
}
pub fn launch_app(&mut self, launch: &AppLaunch) -> io::Result<()> {
match self {
Self::Weston(session) => session.launch_app(launch),
Self::Sway(session) => session.launch_app(launch),
Self::Hyprland(session) => session.launch_app(launch),
}
}
pub fn launch_shell_command(&mut self, command_text: &str) -> io::Result<()> {
match self {
Self::Weston(session) => session.launch_shell_command(command_text),
Self::Sway(session) => session.launch_shell_command(command_text),
Self::Hyprland(session) => session.launch_shell_command(command_text),
}
}
pub fn window_ipc(&self) -> Option<WindowIpc> {
match self {
Self::Weston(_) => None,
Self::Sway(session) => Some(WindowIpc::Sway(session.ipc_socket().to_owned())),
Self::Hyprland(session) => Some(WindowIpc::Hyprland(session.ipc_socket().to_owned())),
}
}
}
pub fn resolve(
choice: CompositorChoice,
config: &Config,
preferred: Option<CompositorChoice>,
) -> io::Result<ResolvedCompositor> {
match choice {
CompositorChoice::Weston => Ok(ResolvedCompositor::Weston),
CompositorChoice::Sway => Ok(ResolvedCompositor::Sway),
CompositorChoice::Hyprland => Ok(ResolvedCompositor::Hyprland),
CompositorChoice::Auto => {
if weston_flags_requested(config) {
return Ok(ResolvedCompositor::Weston);
}
if preferred == Some(CompositorChoice::Sway) && probe_sway(config).is_ok() {
return Ok(ResolvedCompositor::Sway);
}
if preferred == Some(CompositorChoice::Hyprland) && probe_hyprland(config).is_ok() {
return Ok(ResolvedCompositor::Hyprland);
}
let weston = probe_weston(config);
if weston.is_ok() {
return Ok(ResolvedCompositor::Weston);
}
let sway = probe_sway(config);
if sway.is_ok() {
return Ok(ResolvedCompositor::Sway);
}
let hyprland = probe_hyprland(config);
if hyprland.is_ok() {
return Ok(ResolvedCompositor::Hyprland);
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"no usable compositor: weston ({}); sway ({}); hyprland ({})",
weston.unwrap_err(),
sway.unwrap_err(),
hyprland.unwrap_err()
),
))
}
}
}
pub fn weston_flags_requested(config: &Config) -> bool {
config.backend == crate::cli::Backend::Drm
|| config.drm_device.is_some()
|| config.drm_output.is_some()
}
pub fn probe_weston(config: &Config) -> Result<String, String> {
if !weston::weston_input_compiled_in() {
return Err("built without libweston input support".into());
}
let version = crate::linux::doctor::weston_version(&config.weston)
.map_err(|error| format!("could not execute weston: {error}"))?;
if !crate::linux::doctor::weston_supported(&version) {
return Err(format!(
"weston 13-16 is required; found {}",
version.trim()
));
}
if !crate::linux::doctor::weston_advertises_pipewire(&config.weston) {
return Err("weston does not advertise its PipeWire backend".into());
}
Ok(version)
}
pub fn probe_hyprland(config: &Config) -> Result<String, String> {
let version = hyprland::hyprland_version(&config.hyprland)
.map_err(|error| format!("could not execute Hyprland: {error}"))?;
if hyprland::hyprland_supported(&version) {
Ok(version)
} else {
Err(format!(
"hyprland 0.53 or newer is required; found {}",
version.trim()
))
}
}
pub fn probe_sway(config: &Config) -> Result<String, String> {
let version = sway::sway_version(&config.sway)
.map_err(|error| format!("could not execute sway: {error}"))?;
if sway::sway_supported(&version) {
Ok(version)
} else {
Err(format!(
"sway 1.9 or newer is required; found {}",
version.trim()
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_is_vvland_with_a_per_compositor_display() {
let weston = ResolvedCompositor::Weston.identity();
assert_eq!(weston.slug, "vvland");
assert_eq!(weston.display_name, "Vvland");
assert_eq!(weston.compositor_name, "Weston");
assert_eq!(ResolvedCompositor::Sway.identity().compositor_name, "Sway");
assert_eq!(ResolvedCompositor::Sway.identity().slug, "vvland");
let hyprland = ResolvedCompositor::Hyprland.identity();
assert_eq!(hyprland.slug, "vvland");
assert_eq!(hyprland.compositor_name, "Hyprland");
}
#[test]
fn wire_identity_stays_per_compositor() {
assert_eq!(ResolvedCompositor::Weston.wire_name(), "veston");
assert_eq!(ResolvedCompositor::Sway.wire_name(), "vvsway");
assert_eq!(ResolvedCompositor::Hyprland.wire_name(), "vvland");
for compositor in [
ResolvedCompositor::Weston,
ResolvedCompositor::Sway,
ResolvedCompositor::Hyprland,
] {
assert_eq!(compositor.name(), compositor.display_name().to_lowercase());
}
}
#[test]
fn every_compositor_rejects_the_same_out_of_range_pointer_positions() {
for compositor in ["Weston", "Sway", "Hyprland"] {
assert!(check_pointer_bounds(1919, 1079, 1920, 1080, compositor).is_ok());
assert!(check_pointer_bounds(0, 0, 1920, 1080, compositor).is_ok());
for (x, y) in [(1920, 0), (0, 1080), (1920, 1080), (u32::MAX, 0)] {
let error = check_pointer_bounds(x, y, 1920, 1080, compositor).unwrap_err();
assert_eq!(
error.kind(),
io::ErrorKind::InvalidInput,
"{compositor} ({x}, {y})"
);
let message = error.to_string();
assert!(message.contains(compositor), "{message}");
assert!(message.contains("1920x1080"), "{message}");
assert!(message.contains(&format!("({x}, {y})")), "{message}");
}
}
}
#[test]
fn a_zero_sized_output_admits_no_pointer_position() {
assert!(check_pointer_bounds(0, 0, 0, 0, "Sway").is_err());
assert!(check_pointer_bounds(0, 0, 1920, 0, "Weston").is_err());
}
#[test]
fn explicit_choices_never_probe() {
let config = crate::cli::tests::parse(["vvland", "--doctor"]);
assert_eq!(
resolve(CompositorChoice::Weston, &config, None).unwrap(),
ResolvedCompositor::Weston
);
assert_eq!(
resolve(CompositorChoice::Sway, &config, None).unwrap(),
ResolvedCompositor::Sway
);
assert_eq!(
resolve(CompositorChoice::Hyprland, &config, None).unwrap(),
ResolvedCompositor::Hyprland
);
}
#[test]
fn drm_flags_force_weston_under_auto() {
let plain = crate::cli::tests::parse(["vvland", "--doctor"]);
assert!(!weston_flags_requested(&plain));
for flags in [
vec!["vvland", "--doctor", "--backend=drm"],
vec!["vvland", "--doctor", "--drm-device=/dev/dri/card0"],
vec!["vvland", "--doctor", "--drm-output=HDMI-A-1"],
] {
let config = crate::cli::tests::parse(flags.clone());
assert!(weston_flags_requested(&config), "{flags:?}");
assert_eq!(
resolve(CompositorChoice::Auto, &config, None).unwrap(),
ResolvedCompositor::Weston,
"{flags:?}"
);
}
}
}