pub mod capture;
pub mod pipewire;
pub mod protocols;
pub mod sway;
pub mod sway_input;
pub mod weston;
pub mod weston_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 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,
}
impl ResolvedCompositor {
pub fn identity(self) -> ProductIdentity {
ProductIdentity {
slug: "vvland",
display_name: "Vvland",
compositor_name: match self {
Self::Weston => "Weston",
Self::Sway => "Sway",
},
}
}
pub fn wire_name(self) -> &'static str {
match self {
Self::Weston => "veston",
Self::Sway => "vvsway",
}
}
}
#[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),
Sway(&'a mut sway_input::InputChannel),
}
impl LiveInput<'_> {
pub fn check_status(&mut self) -> io::Result<()> {
match self {
Self::Weston(input) => input.check_status(),
Self::Sway(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::Sway(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::Sway(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::Sway(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::Sway(input) => input.pointer_axis(axis, delta),
}
}
fn release_all(&mut self) -> io::Result<()> {
match self {
Self::Weston(input) => input.release_all(),
Self::Sway(input) => input.release_all(),
}
}
}
pub enum Compositor {
Weston(WestonSession),
Sway(SwaySession),
}
impl Compositor {
pub fn pid(&self) -> u32 {
match self {
Self::Weston(session) => session.pid(),
Self::Sway(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),
}
}
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(), 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(),
}
}
pub fn weston_backend(&self) -> Option<ActiveBackend> {
match self {
Self::Weston(session) => Some(session.backend()),
Self::Sway(_) => None,
}
}
pub fn input_mut(&mut self) -> LiveInput<'_> {
match self {
Self::Weston(session) => LiveInput::Weston(session.input_mut()),
Self::Sway(session) => LiveInput::Sway(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(),
}
}
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),
}
}
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),
}
}
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),
}
}
pub fn sway_ipc_socket(&self) -> Option<&std::path::Path> {
match self {
Self::Weston(_) => None,
Self::Sway(session) => Some(session.ipc_socket()),
}
}
}
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::Auto => {
if weston_flags_requested(config) {
return Ok(ResolvedCompositor::Weston);
}
if preferred == Some(CompositorChoice::Sway) && probe_sway(config).is_ok() {
return Ok(ResolvedCompositor::Sway);
}
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);
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"no usable compositor: weston ({}); sway ({})",
weston.unwrap_err(),
sway.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_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");
}
#[test]
fn wire_identity_stays_per_compositor() {
assert_eq!(ResolvedCompositor::Weston.wire_name(), "veston");
assert_eq!(ResolvedCompositor::Sway.wire_name(), "vvsway");
}
#[test]
fn both_compositors_reject_the_same_out_of_range_pointer_positions() {
for compositor in ["Weston", "Sway"] {
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
);
}
#[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:?}"
);
}
}
}