├── debian ├── source │ ├── format │ └── options ├── install ├── changelog ├── control └── rules ├── Cargo.toml ├── .gitignore ├── shell ├── Cargo.toml └── src │ └── lib.rs ├── cli ├── Cargo.toml └── src │ ├── align.rs │ └── main.rs ├── lib ├── Cargo.toml ├── examples │ └── async_dispatch.rs └── src │ ├── output_configuration_head.rs │ ├── output_configuration.rs │ ├── channel.rs │ ├── wl_registry.rs │ ├── output_mode.rs │ ├── lib.rs │ ├── output_manager.rs │ ├── output_head.rs │ └── context.rs ├── README.md ├── justfile ├── LICENSE └── Cargo.lock /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | /usr/bin/cosmic-randr -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | tar-ignore=.github 2 | tar-ignore=.vscode 3 | tar-ignore=vendor 4 | tar-ignore=target -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | cosmic-randr (0.1.0) jammy; urgency=medium 2 | 3 | * Initial release 4 | 5 | -- Michael Murphy Fri, 22 Dec 2023 02:41:12 +0100 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cli", "lib", "shell"] 3 | resolver = "3" 4 | 5 | [profile.release] 6 | package."*".opt-level = "s" 7 | opt-level = 3 8 | # rustflags = ["-C", "inline-threshold=1"] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | target 3 | vendor 4 | vendor.tar 5 | .cargo 6 | debian/* 7 | !debian/changelog 8 | !debian/control 9 | !debian/copyright 10 | !debian/rules 11 | !debian/source 12 | !debian/install 13 | 14 | -------------------------------------------------------------------------------- /shell/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-randr-shell" 3 | version = "0.1.0" 4 | description = "Wrapper for the cosmic-randr CLI" 5 | license = "MPL-2.0" 6 | edition = "2024" 7 | 8 | [dependencies] 9 | kdl = "6.5.0" 10 | slotmap = { version = "1.0.7" } 11 | thiserror = "2.0.17" 12 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: cosmic-randr 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Michael Murphy 5 | Build-Depends: 6 | cargo, 7 | debhelper-compat (=13), 8 | just, 9 | libwayland-dev, 10 | pkg-config, 11 | rustc, 12 | Standards-Version: 4.6.2 13 | Homepage: https://github.com/pop-os/cosmic-randr 14 | 15 | Package: cosmic-randr 16 | Architecture: amd64 arm64 17 | Depends: ${misc:Depends}, ${shlibs:Depends} 18 | Description: Display and configure wayland display outputs 19 | Display and configure wayland display outputs -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export DESTDIR = debian/tmp 4 | export VENDOR ?= 1 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_auto_clean: 10 | if ! ischroot && test "${VENDOR}" = "1"; then \ 11 | mkdir -p .cargo; \ 12 | cargo vendor | head -n -1 > .cargo/config; \ 13 | echo 'directory = "vendor"' >> .cargo/config; \ 14 | tar pcf vendor.tar vendor; \ 15 | rm -rf vendor; \ 16 | fi 17 | 18 | override_dh_auto_build: 19 | ifeq ($(VENDOR),1) 20 | just build-vendored 21 | else 22 | just 23 | endif 24 | 25 | override_dh_auto_install: 26 | just rootdir=$(DESTDIR) install -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-randr-cli" 3 | version = "0.1.0" 4 | description = "cosmic-randr command line interface" 5 | license = "MPL-2.0" 6 | edition = "2024" 7 | 8 | [[bin]] 9 | name = "cosmic-randr" 10 | path = "src/main.rs" 11 | 12 | [dependencies] 13 | clap = { version = "4.5.53", features = ["derive"] } 14 | cosmic-randr = { path = "../lib" } 15 | cosmic-randr-shell = { path = "../shell" } 16 | fomat-macros = "0.3.2" 17 | kdl = "6.5.0" 18 | nu-ansi-term = "0.50.3" 19 | tokio = { version = "1.48.0", features = ["macros", "rt", "io-std", "io-util"] } 20 | wayland-client = "0.31.11" 21 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-randr" 3 | version = "0.1.0" 4 | description = "high level library for wayland output configuration" 5 | license = "MPL-2.0" 6 | edition = "2024" 7 | 8 | [dependencies] 9 | cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols.git" } 10 | indexmap = "2.12.1" 11 | thiserror = "2.0.17" 12 | tokio = { version = "1.48.0", default-features = false, features = ["net", "sync"] } 13 | tracing = "0.1.43" 14 | wayland-client = "0.31.11" 15 | wayland-protocols-wlr = { version = "0.3.9", features = [ "client", "wayland-client" ] } 16 | 17 | [dev-dependencies] 18 | tokio = { version = "1.48.0", features = ["macros", "rt",] } 19 | -------------------------------------------------------------------------------- /lib/examples/async_dispatch.rs: -------------------------------------------------------------------------------- 1 | //! Watch for messages from the zwlr_output_manager_v1 protocol. 2 | //! 3 | //! May be used to check if the display configuration has changed. 4 | 5 | #[tokio::main(flavor = "current_thread")] 6 | async fn main() -> Result<(), Box> { 7 | let (tx, mut rx) = cosmic_randr::channel(); 8 | 9 | tokio::spawn(async move { 10 | let Ok((mut context, mut event_queue)) = cosmic_randr::connect(tx) else { 11 | return; 12 | }; 13 | 14 | loop { 15 | if dbg!(context.dispatch(&mut event_queue).await).is_err() { 16 | return; 17 | } 18 | } 19 | }); 20 | 21 | while let Some(event) = rx.recv().await { 22 | eprintln!("{event:?}"); 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cosmic-randr 2 | 3 | COSMIC RandR is both a library and command line utility for displaying and configuring Wayland outputs. Each display is represented as an "output head", whereas all supported configurations for each display is represented as "output modes". 4 | 5 | ## cosmic-randr cli 6 | 7 | All COSMIC installations have `cosmic-randr` preinstalled on the system. This can be used to list and configure outputs from the terminal. 8 | 9 | Those that want to integrate with this binary in their software can use `cosmic-randr list --kdl` to get a list of outputs and their modes in the [KDL syntax format](https://kdl.dev). Rust developers can use the `cosmic-randr-shell` crate provided here for the same integration. 10 | 11 | ## License 12 | 13 | Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0). 14 | 15 | ### Contribution 16 | 17 | Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the Mozilla Public License 2.0 (MPL-2.0). Each source file should have a SPDX copyright notice at the top of the file: 18 | 19 | ``` 20 | // SPDX-License-Identifier: MPL-2.0 21 | ``` 22 | -------------------------------------------------------------------------------- /lib/src/output_configuration_head.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::Context; 5 | 6 | use cosmic_protocols::output_management::v1::client::zcosmic_output_configuration_head_v1::ZcosmicOutputConfigurationHeadV1; 7 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 8 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1; 9 | 10 | impl Dispatch for Context { 11 | fn event( 12 | _state: &mut Self, 13 | _proxy: &ZwlrOutputConfigurationHeadV1, 14 | _event: ::Event, 15 | _data: &(), 16 | _conn: &Connection, 17 | _handle: &QueueHandle, 18 | ) { 19 | } 20 | } 21 | 22 | impl Dispatch for Context { 23 | fn event( 24 | _state: &mut Self, 25 | _proxy: &ZcosmicOutputConfigurationHeadV1, 26 | _event: ::Event, 27 | _data: &(), 28 | _conn: &Connection, 29 | _handle: &QueueHandle, 30 | ) { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | name := 'cosmic-randr' 2 | rootdir := '' 3 | prefix := '/usr' 4 | 5 | cargo-target-dir := env('CARGO_TARGET_DIR', 'target') 6 | 7 | bin-src := cargo-target-dir / 'release' / name 8 | bin-dst := clean(rootdir / prefix) / 'bin' / name 9 | 10 | default: build-release 11 | 12 | install: 13 | install -Dm0755 {{bin-src}} {{bin-dst}} 14 | 15 | # Remove Cargo build artifacts 16 | clean: 17 | cargo clean 18 | 19 | # Also remove .cargo and vendored dependencies 20 | clean-dist: clean 21 | rm -rf .cargo vendor vendor.tar target 22 | 23 | # Compile with debug profile 24 | build-debug *args: 25 | cargo build {{args}} 26 | 27 | # Compile with release profile 28 | build-release *args: (build-debug '--release' args) 29 | 30 | # Compile with a vendored tarball 31 | build-vendored *args: vendor-extract (build-release '--frozen --offline' args) 32 | 33 | # Check for errors and linter warnings 34 | check *args: 35 | cargo clippy --all-features {{args}} -- -W clippy::pedantic 36 | 37 | # Runs a check with JSON message format for IDE integration 38 | check-json: (check '--message-format=json') 39 | 40 | # Run the application for testing purposes 41 | run *args: 42 | env RUST_LOG=debug RUST_BACKTRACE=full cargo run --release {{args}} 43 | 44 | # Run `cargo test` 45 | test: 46 | cargo test 47 | 48 | # Vendor Cargo dependencies locally 49 | vendor: 50 | mkdir -p .cargo 51 | cargo vendor --sync Cargo.toml \ 52 | | head -n -1 > .cargo/config 53 | echo 'directory = "vendor"' >> .cargo/config 54 | tar pcf vendor.tar vendor 55 | rm -rf vendor 56 | 57 | # Extracts vendored dependencies 58 | [private] 59 | vendor-extract: 60 | rm -rf vendor 61 | tar pxf vendor.tar 62 | -------------------------------------------------------------------------------- /lib/src/output_configuration.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use super::{Context, Message}; 5 | use cosmic_protocols::output_management::v1::client::zcosmic_output_configuration_v1::ZcosmicOutputConfigurationV1; 6 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 7 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_v1::Event; 8 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1; 9 | 10 | impl Dispatch for Context { 11 | fn event( 12 | state: &mut Self, 13 | proxy: &ZwlrOutputConfigurationV1, 14 | event: ::Event, 15 | _data: &(), 16 | _conn: &Connection, 17 | _handle: &QueueHandle, 18 | ) { 19 | match event { 20 | Event::Succeeded => { 21 | let _res = state.send(Message::ConfigurationSucceeded); 22 | proxy.destroy(); 23 | } 24 | Event::Failed => { 25 | let _res = state.send(Message::ConfigurationFailed); 26 | proxy.destroy(); 27 | } 28 | Event::Cancelled => { 29 | let _res = state.send(Message::ConfigurationCancelled); 30 | proxy.destroy(); 31 | } 32 | _ => unreachable!(), 33 | } 34 | } 35 | } 36 | 37 | impl Dispatch for Context { 38 | fn event( 39 | _state: &mut Self, 40 | _proxy: &ZcosmicOutputConfigurationV1, 41 | _event: ::Event, 42 | _data: &(), 43 | _conn: &Connection, 44 | _handle: &QueueHandle, 45 | ) { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/channel.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use std::{ 5 | collections::VecDeque, 6 | sync::{ 7 | Arc, Mutex, 8 | atomic::{AtomicBool, Ordering}, 9 | }, 10 | }; 11 | 12 | use super::Message; 13 | 14 | /// Create a channel for receiving messages from cosmic-randr. 15 | pub fn channel() -> (Sender, Receiver) { 16 | let channel = Arc::new(Channel { 17 | queue: Mutex::new(VecDeque::default()), 18 | notify: tokio::sync::Notify::const_new(), 19 | closed: AtomicBool::new(false), 20 | }); 21 | 22 | (Sender(channel.clone()), Receiver(channel)) 23 | } 24 | 25 | /// A channel specifically for handling cosmic-randr messages. 26 | struct Channel { 27 | pub(self) queue: Mutex>, 28 | pub(self) notify: tokio::sync::Notify, 29 | pub(self) closed: AtomicBool, 30 | } 31 | 32 | pub struct Sender(Arc); 33 | 34 | impl Sender { 35 | pub fn send(&self, message: Message) { 36 | self.0.queue.lock().unwrap().push_back(message); 37 | self.0.notify.notify_one(); 38 | } 39 | } 40 | 41 | impl Drop for Sender { 42 | fn drop(&mut self) { 43 | self.0.closed.store(true, Ordering::SeqCst); 44 | self.0.notify.notify_one(); 45 | } 46 | } 47 | 48 | pub struct Receiver(Arc); 49 | 50 | impl Receiver { 51 | /// Returns a value until the sender is dropped. 52 | pub async fn recv(&self) -> Option { 53 | loop { 54 | if let Some(value) = self.0.queue.lock().unwrap().pop_front() { 55 | return Some(value); 56 | } 57 | 58 | if self.0.closed.load(Ordering::SeqCst) { 59 | return None; 60 | } 61 | 62 | self.0.notify.notified().await; 63 | } 64 | } 65 | 66 | pub fn try_recv(&self) -> Option { 67 | self.0.queue.lock().unwrap().pop_front() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/wl_registry.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::{Context, Message}; 5 | use cosmic_protocols::output_management::v1::client::zcosmic_output_manager_v1::ZcosmicOutputManagerV1; 6 | use wayland_client::{Connection, Dispatch, QueueHandle, protocol::wl_registry}; 7 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 8 | 9 | impl Dispatch for Context { 10 | fn event( 11 | state: &mut Self, 12 | registry: &wl_registry::WlRegistry, 13 | event: wl_registry::Event, 14 | _data: &(), 15 | _conn: &Connection, 16 | handle: &QueueHandle, 17 | ) { 18 | if let wl_registry::Event::Global { 19 | name, 20 | interface, 21 | version, 22 | } = event 23 | { 24 | if "zwlr_output_manager_v1" == &interface[..] { 25 | if version < 2 { 26 | tracing::error!( 27 | "wlr-output-management protocol version {version} < 2 is not supported" 28 | ); 29 | 30 | let _ = state.send(Message::Unsupported); 31 | 32 | return; 33 | } 34 | 35 | state.output_manager_version = version; 36 | state.output_manager = Some(registry.bind::( 37 | name, 38 | version.min(4), 39 | handle, 40 | (), 41 | )); 42 | } 43 | if "zcosmic_output_manager_v1" == &interface[..] { 44 | state.cosmic_output_manager = Some(registry.bind::( 45 | name, 46 | version.min(3), 47 | handle, 48 | (), 49 | )) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/output_mode.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use std::cmp::Ordering; 5 | use std::sync::Mutex; 6 | 7 | use crate::Context; 8 | use wayland_client::backend::ObjectId; 9 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 10 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::Event as ZwlrOutputModeEvent; 11 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::ZwlrOutputModeV1; 12 | 13 | #[derive(Clone, Debug, PartialEq, Eq)] 14 | pub struct OutputMode { 15 | pub width: i32, 16 | pub height: i32, 17 | pub refresh: i32, 18 | pub preferred: bool, 19 | pub wlr_mode: ZwlrOutputModeV1, 20 | } 21 | 22 | impl Dispatch>> for Context { 23 | fn event( 24 | state: &mut Self, 25 | proxy: &ZwlrOutputModeV1, 26 | event: ::Event, 27 | data: &Mutex>, 28 | _conn: &Connection, 29 | _handle: &QueueHandle, 30 | ) { 31 | let Some(head_id) = data.lock().unwrap().clone() else { 32 | return; 33 | }; 34 | let Some(head) = state.output_heads.get_mut(&head_id) else { 35 | return; 36 | }; 37 | let mode = head 38 | .modes 39 | .entry(proxy.id()) 40 | .or_insert_with(|| OutputMode::new(proxy.clone())); 41 | 42 | match event { 43 | ZwlrOutputModeEvent::Size { width, height } => { 44 | (mode.width, mode.height) = (width, height); 45 | } 46 | 47 | ZwlrOutputModeEvent::Refresh { refresh } => { 48 | mode.refresh = refresh; 49 | } 50 | 51 | ZwlrOutputModeEvent::Preferred => { 52 | mode.preferred = true; 53 | } 54 | 55 | ZwlrOutputModeEvent::Finished => { 56 | if proxy.version() >= 3 { 57 | proxy.release(); 58 | } 59 | 60 | head.modes.shift_remove(&proxy.id()); 61 | } 62 | 63 | _ => tracing::debug!(?event, "unknown event"), 64 | } 65 | } 66 | } 67 | 68 | impl OutputMode { 69 | #[must_use] 70 | pub fn new(wlr_mode: ZwlrOutputModeV1) -> Self { 71 | Self { 72 | width: 0, 73 | height: 0, 74 | refresh: 0, 75 | preferred: false, 76 | wlr_mode, 77 | } 78 | } 79 | } 80 | 81 | impl PartialOrd for OutputMode { 82 | fn partial_cmp(&self, other: &Self) -> Option { 83 | Some(self.cmp(other)) 84 | } 85 | } 86 | 87 | impl Ord for OutputMode { 88 | fn cmp(&self, other: &Self) -> Ordering { 89 | self.width 90 | .cmp(&other.width) 91 | .then(self.height.cmp(&other.height)) 92 | .then(self.refresh.cmp(&other.refresh)) 93 | .reverse() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | mod channel; 5 | pub use channel::{Receiver, Sender, channel}; 6 | 7 | pub mod context; 8 | pub use context::Context; 9 | 10 | pub mod output_configuration; 11 | pub mod output_configuration_head; 12 | pub mod output_head; 13 | pub mod output_manager; 14 | 15 | pub mod output_mode; 16 | pub use output_mode::OutputMode; 17 | 18 | pub use cosmic_protocols::output_management::v1::client::zcosmic_output_head_v1::{ 19 | AdaptiveSyncAvailability, AdaptiveSyncStateExt, 20 | }; 21 | pub mod wl_registry; 22 | 23 | use tokio::io::Interest; 24 | use wayland_client::backend::WaylandError; 25 | use wayland_client::{Connection, DispatchError, EventQueue}; 26 | 27 | /// Creates a wayland client connection with state for handling wlr outputs. 28 | /// 29 | /// # Errors 30 | /// 31 | /// Returns error if there are any wayland client connection errors. 32 | pub fn connect(sender: Sender) -> Result<(Context, EventQueue), Error> { 33 | Context::connect(sender) 34 | } 35 | 36 | #[allow(clippy::enum_variant_names)] 37 | #[derive(Clone, Copy, Debug)] 38 | pub enum Message { 39 | ConfigurationCancelled, 40 | ConfigurationFailed, 41 | ConfigurationSucceeded, 42 | ManagerDone, 43 | ManagerFinished, 44 | Unsupported, 45 | } 46 | 47 | #[derive(thiserror::Error, Debug)] 48 | pub enum Error { 49 | #[error("I/O error")] 50 | Io(#[from] std::io::Error), 51 | #[error("cannot free resources of destroyed output mode")] 52 | ReleaseOutputMode, 53 | #[error("wayland client context error")] 54 | WaylandContext(#[from] wayland_client::backend::WaylandError), 55 | #[error("wayland client dispatch error")] 56 | WaylandDispatch(#[from] wayland_client::DispatchError), 57 | #[error("wayland connection error")] 58 | WaylandConnection(#[from] wayland_client::ConnectError), 59 | #[error("wayland object ID invalid")] 60 | WaylandInvalidId(#[from] wayland_client::backend::InvalidId), 61 | } 62 | 63 | pub async fn async_dispatch( 64 | connection: &Connection, 65 | event_queue: &mut EventQueue, 66 | data: &mut Data, 67 | ) -> Result { 68 | let dispatched = event_queue.dispatch_pending(data)?; 69 | 70 | if dispatched > 0 { 71 | return Ok(dispatched); 72 | } 73 | 74 | connection.flush()?; 75 | 76 | if let Some(guard) = connection.prepare_read() { 77 | { 78 | let fd = guard.connection_fd(); 79 | let fd = tokio::io::unix::AsyncFd::new(fd).unwrap(); 80 | 81 | loop { 82 | match fd.ready(Interest::ERROR | Interest::READABLE).await { 83 | Ok(async_guard) => { 84 | if async_guard.ready().is_readable() { 85 | break; 86 | } 87 | } 88 | 89 | Err(why) if why.kind() == std::io::ErrorKind::Interrupted => continue, 90 | Err(why) => return Err(DispatchError::Backend(WaylandError::Io(why))), 91 | } 92 | } 93 | } 94 | 95 | if let Err(why) = guard.read() { 96 | if let WaylandError::Io(ref error) = why { 97 | if error.kind() != std::io::ErrorKind::WouldBlock { 98 | return Err(DispatchError::Backend(why)); 99 | } 100 | } else { 101 | return Err(DispatchError::Backend(why)); 102 | } 103 | } 104 | } 105 | 106 | event_queue.dispatch_pending(data) 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/output_manager.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use super::output_head::OutputHead; 5 | use crate::{Context, Message}; 6 | 7 | use cosmic_protocols::output_management::v1::client::zcosmic_output_manager_v1::ZcosmicOutputManagerV1; 8 | use wayland_client::event_created_child; 9 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 10 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::ZwlrOutputHeadV1; 11 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::EVT_HEAD_OPCODE; 12 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::Event as ZwlrOutputManagerEvent; 13 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 14 | 15 | impl Dispatch for Context { 16 | fn event( 17 | state: &mut Self, 18 | _proxy: &ZwlrOutputManagerV1, 19 | event: ::Event, 20 | _data: &(), 21 | conn: &Connection, 22 | handle: &QueueHandle, 23 | ) { 24 | match event { 25 | ZwlrOutputManagerEvent::Head { head } => { 26 | let cosmic_head = 27 | if let Some(cosmic_extension) = state.cosmic_output_manager.as_ref() { 28 | let cosmic_head = cosmic_extension.get_head(&head, handle, head.id()); 29 | 30 | // Use `sync` callback to wait until `get_head` is processed and 31 | // we also have cosmic extension events. 32 | let callback = conn.display().sync(handle, ()); 33 | state.cosmic_manager_sync_callback = Some(callback); 34 | Some(cosmic_head) 35 | } else { 36 | None 37 | }; 38 | state 39 | .output_heads 40 | .insert(head.id(), OutputHead::new(head, cosmic_head)); 41 | } 42 | 43 | ZwlrOutputManagerEvent::Done { serial } => { 44 | state.output_manager_serial = serial; 45 | if state.cosmic_manager_sync_callback.is_some() { 46 | // Potentally waiting for cosmic extension events after calling 47 | // `get_head`. Queue sending `ManagerDone` until sync callback. 48 | state.done_queued = true; 49 | } else { 50 | let _res = state.send(Message::ManagerDone); 51 | } 52 | } 53 | 54 | ZwlrOutputManagerEvent::Finished => { 55 | state.output_manager = None; 56 | state.output_manager_serial = 0; 57 | let _res = state.send(Message::ManagerFinished); 58 | } 59 | 60 | _ => tracing::debug!(?event, "unknown event"), 61 | } 62 | } 63 | 64 | event_created_child!(Context, ZwlrOutputManagerV1, [ 65 | EVT_HEAD_OPCODE=> (ZwlrOutputHeadV1, ()), 66 | ]); 67 | } 68 | 69 | impl Dispatch for Context { 70 | fn event( 71 | _state: &mut Self, 72 | _proxy: &ZcosmicOutputManagerV1, 73 | _event: ::Event, 74 | _data: &(), 75 | _conn: &Connection, 76 | _handle: &QueueHandle, 77 | ) { 78 | } 79 | } 80 | 81 | use wayland_client::protocol::wl_callback::{self, WlCallback}; 82 | 83 | impl Dispatch for Context { 84 | fn event( 85 | state: &mut Self, 86 | proxy: &WlCallback, 87 | event: wl_callback::Event, 88 | _data: &(), 89 | _conn: &Connection, 90 | _handle: &QueueHandle, 91 | ) { 92 | match event { 93 | wl_callback::Event::Done { callback_data: _ } => { 94 | if state.cosmic_manager_sync_callback.as_ref() == Some(proxy) { 95 | state.cosmic_manager_sync_callback = None; 96 | if state.done_queued { 97 | let _res = state.send(Message::ManagerDone); 98 | state.done_queued = false; 99 | } 100 | } 101 | } 102 | _ => unreachable!(), 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cli/src/align.rs: -------------------------------------------------------------------------------- 1 | pub fn display(new_region: &mut R, other_displays: impl Iterator) { 2 | let mut nearest = f32::MAX; 3 | let mut nearest_region = R::default(); 4 | let mut nearest_side = NearestSide::East; 5 | 6 | // Find the nearest adjacent display to the display. 7 | for other_display in other_displays { 8 | let center = new_region.center(); 9 | 10 | let eastward = distance(other_display.east_point(), center) * 1.25; 11 | let westward = distance(other_display.west_point(), center) * 1.25; 12 | let northward = distance(other_display.north_point(), center); 13 | let southward = distance(other_display.south_point(), center); 14 | 15 | let mut nearer = false; 16 | 17 | if nearest > eastward { 18 | (nearest, nearest_side, nearer) = (eastward, NearestSide::East, true); 19 | } 20 | 21 | if nearest > westward { 22 | (nearest, nearest_side, nearer) = (westward, NearestSide::West, true); 23 | } 24 | 25 | if nearest > northward { 26 | (nearest, nearest_side, nearer) = (northward, NearestSide::North, true); 27 | } 28 | 29 | if nearest > southward { 30 | (nearest, nearest_side, nearer) = (southward, NearestSide::South, true); 31 | } 32 | 33 | if nearer { 34 | nearest_region = other_display; 35 | } 36 | } 37 | 38 | // Attach display to nearest adjacent display. 39 | match nearest_side { 40 | NearestSide::East => { 41 | new_region.set_x(nearest_region.x() - new_region.width()); 42 | new_region.set_y( 43 | new_region 44 | .y() 45 | .max(nearest_region.y() - new_region.height() + 4.0) 46 | .min(nearest_region.y() + nearest_region.height() - 4.0), 47 | ); 48 | } 49 | 50 | NearestSide::North => { 51 | new_region.set_y(nearest_region.y() - new_region.height()); 52 | new_region.set_x( 53 | new_region 54 | .x() 55 | .max(nearest_region.x() - new_region.width() + 4.0) 56 | .min(nearest_region.x() + nearest_region.width() - 4.0), 57 | ); 58 | } 59 | 60 | NearestSide::West => { 61 | new_region.set_x(nearest_region.x() + nearest_region.width()); 62 | new_region.set_y( 63 | new_region 64 | .y() 65 | .max(nearest_region.y() - new_region.height() + 4.0) 66 | .min(nearest_region.y() + nearest_region.height() - 4.0), 67 | ); 68 | } 69 | 70 | NearestSide::South => { 71 | new_region.set_y(nearest_region.y() + nearest_region.height()); 72 | new_region.set_x( 73 | new_region 74 | .x() 75 | .max(nearest_region.x() - new_region.width() + 4.0) 76 | .min(nearest_region.x() + nearest_region.width() - 4.0), 77 | ); 78 | } 79 | } 80 | 81 | // Snap-align on x-axis when alignment is near. 82 | if (new_region.x() - nearest_region.x()).abs() <= 4.0 { 83 | new_region.set_x(nearest_region.x()); 84 | } 85 | 86 | // Snap-align on x-axis when alignment is near bottom edge. 87 | if ((new_region.x() + new_region.width()) - (nearest_region.x() + nearest_region.width())).abs() 88 | <= 4.0 89 | { 90 | new_region.set_x(nearest_region.x() + nearest_region.width() - new_region.width()); 91 | } 92 | 93 | // Snap-align on y-axis when alignment is near. 94 | if (new_region.y() - nearest_region.y()).abs() <= 4.0 { 95 | new_region.set_y(nearest_region.y()); 96 | } 97 | 98 | // Snap-align on y-axis when alignment is near bottom edge. 99 | if ((new_region.y() + new_region.height()) - (nearest_region.y() + nearest_region.height())) 100 | .abs() 101 | <= 4.0 102 | { 103 | new_region.set_y(nearest_region.y() + nearest_region.height() - new_region.height()); 104 | } 105 | } 106 | 107 | fn distance(a: Point, b: Point) -> f32 { 108 | ((b.x - a.x).powf(2.0) + (b.y - a.y).powf(2.0)).sqrt() 109 | } 110 | 111 | #[derive(Clone, Copy)] 112 | pub struct Point { 113 | pub x: f32, 114 | pub y: f32, 115 | } 116 | 117 | #[derive(Debug)] 118 | pub enum NearestSide { 119 | East, 120 | North, 121 | South, 122 | West, 123 | } 124 | 125 | #[derive(Default)] 126 | pub struct Rectangle { 127 | pub x: f32, 128 | pub y: f32, 129 | pub width: f32, 130 | pub height: f32, 131 | } 132 | 133 | impl Rectangular for Rectangle { 134 | fn x(&self) -> f32 { 135 | self.x 136 | } 137 | 138 | fn set_x(&mut self, x: f32) { 139 | self.x = x; 140 | } 141 | 142 | fn y(&self) -> f32 { 143 | self.y 144 | } 145 | 146 | fn set_y(&mut self, y: f32) { 147 | self.y = y; 148 | } 149 | 150 | fn width(&self) -> f32 { 151 | self.width 152 | } 153 | 154 | fn set_width(&mut self, width: f32) { 155 | self.width = width; 156 | } 157 | 158 | fn height(&self) -> f32 { 159 | self.height 160 | } 161 | 162 | fn set_height(&mut self, height: f32) { 163 | self.height = height; 164 | } 165 | } 166 | 167 | pub trait Rectangular: Default + Sized { 168 | fn x(&self) -> f32; 169 | 170 | fn set_x(&mut self, x: f32); 171 | 172 | fn y(&self) -> f32; 173 | 174 | fn set_y(&mut self, y: f32); 175 | 176 | fn width(&self) -> f32; 177 | 178 | fn set_width(&mut self, width: f32); 179 | 180 | fn height(&self) -> f32; 181 | 182 | fn set_height(&mut self, height: f32); 183 | 184 | fn center(&self) -> Point { 185 | Point { 186 | x: self.center_x(), 187 | y: self.center_y(), 188 | } 189 | } 190 | 191 | fn center_x(&self) -> f32 { 192 | self.x() + self.width() / 2.0 193 | } 194 | 195 | fn center_y(&self) -> f32 { 196 | self.y() + self.height() / 2.0 197 | } 198 | 199 | fn east_point(&self) -> Point { 200 | Point { 201 | x: self.x(), 202 | y: self.center_y(), 203 | } 204 | } 205 | 206 | fn north_point(&self) -> Point { 207 | Point { 208 | x: self.center_x(), 209 | y: self.y(), 210 | } 211 | } 212 | 213 | fn west_point(&self) -> Point { 214 | Point { 215 | x: self.x() + self.width(), 216 | y: self.center_y(), 217 | } 218 | } 219 | 220 | fn south_point(&self) -> Point { 221 | Point { 222 | x: self.center_x(), 223 | y: self.y() + self.height(), 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/src/output_head.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use std::sync::Mutex; 5 | 6 | use crate::{Context, OutputMode}; 7 | 8 | use cosmic_protocols::output_management::v1::client::zcosmic_output_head_v1::AdaptiveSyncAvailability; 9 | use cosmic_protocols::output_management::v1::client::zcosmic_output_head_v1::AdaptiveSyncStateExt; 10 | use cosmic_protocols::output_management::v1::client::zcosmic_output_head_v1::Event as ZcosmicOutputHeadEvent; 11 | use cosmic_protocols::output_management::v1::client::zcosmic_output_head_v1::ZcosmicOutputHeadV1; 12 | use indexmap::IndexMap; 13 | use wayland_client::backend::ObjectId; 14 | use wayland_client::event_created_child; 15 | use wayland_client::protocol::wl_output::Transform; 16 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 17 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::AdaptiveSyncState; 18 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::EVT_MODE_OPCODE; 19 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::Event as ZwlrOutputHeadEvent; 20 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::ZwlrOutputHeadV1; 21 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 22 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::ZwlrOutputModeV1; 23 | 24 | #[derive(Clone, Debug, PartialEq)] 25 | pub struct OutputHead { 26 | pub adaptive_sync: Option, 27 | pub adaptive_sync_support: Option, 28 | pub current_mode: Option, 29 | pub description: String, 30 | pub enabled: bool, 31 | pub make: String, 32 | pub model: String, 33 | pub modes: IndexMap, 34 | pub name: String, 35 | pub physical_height: i32, 36 | pub physical_width: i32, 37 | pub position_x: i32, 38 | pub position_y: i32, 39 | pub scale: f64, 40 | pub serial_number: String, 41 | pub transform: Option, 42 | pub mirroring: Option, 43 | pub xwayland_primary: Option, 44 | pub wlr_head: ZwlrOutputHeadV1, 45 | pub cosmic_head: Option, 46 | } 47 | 48 | impl Dispatch for Context { 49 | fn event( 50 | state: &mut Self, 51 | proxy: &ZwlrOutputHeadV1, 52 | event: ::Event, 53 | _: &(), 54 | _: &Connection, 55 | _handle: &QueueHandle, 56 | ) { 57 | let head = state 58 | .output_heads 59 | .get_mut(&proxy.id()) 60 | .expect("Inert WlrOutputHead"); 61 | 62 | match event { 63 | ZwlrOutputHeadEvent::Name { name } => { 64 | head.name = name; 65 | } 66 | 67 | ZwlrOutputHeadEvent::Description { description } => { 68 | head.description = description; 69 | } 70 | 71 | ZwlrOutputHeadEvent::PhysicalSize { width, height } => { 72 | (head.physical_width, head.physical_height) = (width, height); 73 | } 74 | 75 | ZwlrOutputHeadEvent::Mode { mode } => { 76 | *mode 77 | .data::>>() 78 | .unwrap() 79 | .lock() 80 | .unwrap() = Some(proxy.id()); 81 | head.modes.insert(mode.id(), OutputMode::new(mode)); 82 | } 83 | 84 | ZwlrOutputHeadEvent::Enabled { enabled } => { 85 | head.enabled = !matches!(enabled, 0); 86 | } 87 | 88 | ZwlrOutputHeadEvent::CurrentMode { mode } => { 89 | head.current_mode = Some(mode.id()); 90 | } 91 | 92 | ZwlrOutputHeadEvent::Position { x, y } => { 93 | (head.position_x, head.position_y) = (x, y); 94 | } 95 | 96 | ZwlrOutputHeadEvent::Transform { transform } => { 97 | head.transform = transform.into_result().ok(); 98 | } 99 | 100 | ZwlrOutputHeadEvent::Scale { scale } => { 101 | head.scale = scale; 102 | } 103 | 104 | ZwlrOutputHeadEvent::Finished => { 105 | if proxy.version() >= 3 { 106 | proxy.release(); 107 | } 108 | state.output_heads.remove(&proxy.id()); 109 | } 110 | 111 | ZwlrOutputHeadEvent::Make { make } => { 112 | head.make = make; 113 | } 114 | 115 | ZwlrOutputHeadEvent::Model { model } => { 116 | head.model = model; 117 | } 118 | 119 | ZwlrOutputHeadEvent::SerialNumber { serial_number } => { 120 | head.serial_number = serial_number; 121 | } 122 | 123 | ZwlrOutputHeadEvent::AdaptiveSync { state } => { 124 | head.adaptive_sync = match state.into_result().ok() { 125 | Some(AdaptiveSyncState::Enabled) => Some(AdaptiveSyncStateExt::Always), 126 | Some(AdaptiveSyncState::Disabled) => Some(AdaptiveSyncStateExt::Disabled), 127 | Some(_) | None => None, 128 | }; 129 | } 130 | 131 | _ => tracing::debug!(?event, "unknown event"), 132 | } 133 | } 134 | 135 | event_created_child!(Context, ZwlrOutputManagerV1, [ 136 | EVT_MODE_OPCODE => (ZwlrOutputModeV1, Mutex::new(None)), 137 | ]); 138 | } 139 | 140 | impl Dispatch for Context { 141 | fn event( 142 | state: &mut Self, 143 | _proxy: &ZcosmicOutputHeadV1, 144 | event: ::Event, 145 | data: &ObjectId, 146 | _conn: &Connection, 147 | _qhandle: &QueueHandle, 148 | ) { 149 | let head = state 150 | .output_heads 151 | .get_mut(data) 152 | .expect("Inert CosmicOutputHead"); 153 | 154 | match event { 155 | ZcosmicOutputHeadEvent::Scale1000 { scale_1000 } => { 156 | head.scale = (scale_1000 as f64) / 1000.0; 157 | } 158 | ZcosmicOutputHeadEvent::Mirroring { name } => { 159 | head.mirroring = name; 160 | } 161 | ZcosmicOutputHeadEvent::AdaptiveSyncAvailable { available } => { 162 | head.adaptive_sync_support = Some( 163 | available 164 | .into_result() 165 | .unwrap_or(AdaptiveSyncAvailability::Unsupported), 166 | ); 167 | } 168 | ZcosmicOutputHeadEvent::AdaptiveSyncExt { state } => { 169 | head.adaptive_sync = state.into_result().ok(); 170 | } 171 | ZcosmicOutputHeadEvent::XwaylandPrimary { state } => { 172 | head.xwayland_primary = Some(state != 0); 173 | } 174 | _ => tracing::debug!(?event, "unknown event"), 175 | } 176 | } 177 | } 178 | 179 | impl OutputHead { 180 | #[must_use] 181 | pub fn new(wlr_head: ZwlrOutputHeadV1, cosmic_head: Option) -> Self { 182 | Self { 183 | adaptive_sync: None, 184 | adaptive_sync_support: None, 185 | current_mode: None, 186 | description: String::new(), 187 | enabled: false, 188 | make: String::new(), 189 | model: String::new(), 190 | modes: IndexMap::new(), 191 | name: String::new(), 192 | physical_height: 0, 193 | physical_width: 0, 194 | position_x: 0, 195 | position_y: 0, 196 | scale: 1.0, 197 | serial_number: String::new(), 198 | transform: None, 199 | mirroring: None, 200 | xwayland_primary: None, 201 | wlr_head, 202 | cosmic_head, 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /lib/src/context.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::output_head::OutputHead; 5 | use crate::{Error, Message, Sender}; 6 | use cosmic_protocols::output_management::v1::client::zcosmic_output_configuration_head_v1::{ 7 | self, ZcosmicOutputConfigurationHeadV1, 8 | }; 9 | use cosmic_protocols::output_management::v1::client::zcosmic_output_configuration_v1::ZcosmicOutputConfigurationV1; 10 | use cosmic_protocols::output_management::v1::client::zcosmic_output_head_v1::AdaptiveSyncStateExt; 11 | use cosmic_protocols::output_management::v1::client::zcosmic_output_manager_v1::{ 12 | self, ZcosmicOutputManagerV1, 13 | }; 14 | use std::collections::HashMap; 15 | use std::fmt; 16 | use wayland_client::protocol::{ 17 | wl_callback::WlCallback, wl_output::Transform, wl_registry::WlRegistry, 18 | }; 19 | use wayland_client::{Connection, Proxy, QueueHandle, backend::ObjectId}; 20 | use wayland_client::{DispatchError, EventQueue}; 21 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1; 22 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1; 23 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::{ 24 | AdaptiveSyncState, ZwlrOutputHeadV1, 25 | }; 26 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 27 | 28 | pub struct Context { 29 | pub connection: Connection, 30 | pub handle: QueueHandle, 31 | sender: Sender, 32 | 33 | pub output_manager: Option, 34 | pub cosmic_output_manager: Option, 35 | pub output_manager_serial: u32, 36 | pub output_manager_version: u32, 37 | 38 | pub output_heads: HashMap, 39 | pub wl_registry: WlRegistry, 40 | 41 | pub cosmic_manager_sync_callback: Option, 42 | pub done_queued: bool, 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct Configuration { 47 | obj: ZwlrOutputConfigurationV1, 48 | cosmic_obj: Option, 49 | cosmic_output_manager: Option, 50 | handle: QueueHandle, 51 | 52 | known_heads: Vec, 53 | configured_heads: Vec, 54 | } 55 | 56 | #[derive(Debug, Default)] 57 | pub struct HeadConfiguration { 58 | /// Specifies the width and height of the output picture. 59 | pub size: Option<(u32, u32)>, 60 | /// Specifies the refresh rate to apply to the output. 61 | pub refresh: Option, 62 | /// Specifies the adaptive_sync mode to apply to the output. 63 | pub adaptive_sync: Option, 64 | /// Position the output within this x pixel coordinate. 65 | pub pos: Option<(i32, i32)>, 66 | /// Changes the dimensions of the output picture. 67 | pub scale: Option, 68 | /// Specifies a transformation matrix to apply to the output. 69 | pub transform: Option, 70 | } 71 | 72 | #[derive(Debug, Clone, Copy)] 73 | pub enum ConfigurationError { 74 | OutputAlreadyConfigured, 75 | UnknownOutput, 76 | ModeNotFound, 77 | NoCosmicExtension, 78 | PositionForMirroredOutput, 79 | MirroringItself, 80 | UnsupportedVrrState, 81 | UnsupportedXwaylandPrimary, 82 | } 83 | 84 | impl fmt::Display for ConfigurationError { 85 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 86 | match self { 87 | Self::OutputAlreadyConfigured => f.write_str("Output configured twice"), 88 | Self::UnknownOutput => f.write_str("Unknown output"), 89 | Self::ModeNotFound => f.write_str("Unknown or unsupported mode"), 90 | Self::NoCosmicExtension => f.write_str("Feature isn't available outside COSMIC"), 91 | Self::PositionForMirroredOutput => f.write_str("You cannot position a mirrored output"), 92 | Self::MirroringItself => f.write_str("Output mirroring itself"), 93 | Self::UnsupportedVrrState => { 94 | f.write_str("Automatic VRR state management isn't available outside COSMIC") 95 | } 96 | Self::UnsupportedXwaylandPrimary => f.write_str( 97 | "Xwayland compatibility options not available outside or on this version of COSMIC", 98 | ), 99 | } 100 | } 101 | } 102 | impl std::error::Error for ConfigurationError {} 103 | 104 | impl Configuration { 105 | pub fn disable_head(&mut self, output: &str) -> Result<(), ConfigurationError> { 106 | if self.configured_heads.iter().any(|o| o == output) { 107 | return Err(ConfigurationError::OutputAlreadyConfigured); 108 | } 109 | self.configured_heads.push(output.to_string()); 110 | 111 | let head = self 112 | .known_heads 113 | .iter() 114 | .find(|head| head.name == output) 115 | .ok_or(ConfigurationError::UnknownOutput)?; 116 | self.obj.disable_head(&head.wlr_head); 117 | 118 | Ok(()) 119 | } 120 | 121 | pub fn enable_head( 122 | &mut self, 123 | output: &str, 124 | mode: Option, 125 | ) -> Result<(), ConfigurationError> { 126 | if self.configured_heads.iter().any(|o| o == output) { 127 | return Err(ConfigurationError::OutputAlreadyConfigured); 128 | } 129 | self.configured_heads.push(output.to_string()); 130 | 131 | let head = self 132 | .known_heads 133 | .iter() 134 | .find(|head| head.name == output) 135 | .ok_or(ConfigurationError::UnknownOutput)?; 136 | let head_config = self.obj.enable_head(&head.wlr_head, &self.handle, ()); 137 | let cosmic_head_config = self 138 | .cosmic_output_manager 139 | .as_ref() 140 | .map(|extension| extension.get_configuration_head(&head_config, &self.handle, ())); 141 | 142 | if let Some(args) = mode { 143 | send_mode_to_config_head(head, head_config, cosmic_head_config, args)?; 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | pub fn mirror_head( 150 | &mut self, 151 | output: &str, 152 | mirrored: &str, 153 | mode: Option, 154 | ) -> Result<(), ConfigurationError> { 155 | if self.cosmic_obj.is_none() { 156 | return Err(ConfigurationError::NoCosmicExtension); 157 | } 158 | 159 | if self.configured_heads.iter().any(|o| o == output) { 160 | return Err(ConfigurationError::OutputAlreadyConfigured); 161 | } 162 | 163 | if output == mirrored { 164 | return Err(ConfigurationError::MirroringItself); 165 | } 166 | 167 | if mode.as_ref().is_some_and(|mode| mode.pos.is_some()) { 168 | return Err(ConfigurationError::PositionForMirroredOutput); 169 | } 170 | 171 | self.configured_heads.push(output.to_string()); 172 | 173 | let head = self 174 | .known_heads 175 | .iter() 176 | .find(|head| head.name == output) 177 | .ok_or(ConfigurationError::UnknownOutput)?; 178 | let mirror_head = self 179 | .known_heads 180 | .iter() 181 | .find(|head| head.name == mirrored) 182 | .ok_or(ConfigurationError::UnknownOutput)?; 183 | 184 | let cosmic_obj = self.cosmic_obj.as_ref().unwrap(); 185 | let head_config = 186 | cosmic_obj.mirror_head(&head.wlr_head, &mirror_head.wlr_head, &self.handle, ()); 187 | let cosmic_head_config = self 188 | .cosmic_output_manager 189 | .as_ref() 190 | .map(|extension| extension.get_configuration_head(&head_config, &self.handle, ())); 191 | 192 | if let Some(args) = mode { 193 | send_mode_to_config_head(head, head_config, cosmic_head_config, args)?; 194 | } 195 | 196 | Ok(()) 197 | } 198 | 199 | fn configure_remaining_heads(&mut self) { 200 | let known_heads = self.known_heads.clone(); 201 | let configured_heads = self.configured_heads.clone(); 202 | for output in known_heads 203 | .iter() 204 | .filter(|output| !configured_heads.contains(&output.name)) 205 | { 206 | if output.enabled { 207 | if let Some(from) = output.mirroring.as_ref() { 208 | self.mirror_head(&output.name, from, None).unwrap(); 209 | } else { 210 | self.enable_head(&output.name, None).unwrap(); 211 | } 212 | } else { 213 | self.disable_head(&output.name).unwrap(); 214 | } 215 | } 216 | } 217 | 218 | pub fn test(mut self) { 219 | self.configure_remaining_heads(); 220 | self.obj.test(); 221 | } 222 | 223 | pub fn apply(mut self) { 224 | self.configure_remaining_heads(); 225 | self.obj.apply(); 226 | } 227 | 228 | pub fn cancel(self) { 229 | self.obj.destroy() 230 | } 231 | } 232 | 233 | fn send_mode_to_config_head( 234 | head: &OutputHead, 235 | head_config: ZwlrOutputConfigurationHeadV1, 236 | cosmic_head_config: Option, 237 | args: HeadConfiguration, 238 | ) -> Result<(), ConfigurationError> { 239 | if let Some(scale) = args.scale { 240 | if let Some(cosmic_obj) = cosmic_head_config.as_ref() { 241 | cosmic_obj.set_scale_1000((scale * 1000.0) as i32); 242 | } else { 243 | head_config.set_scale(scale); 244 | } 245 | } 246 | 247 | if let Some(transform) = args.transform { 248 | head_config.set_transform(transform); 249 | } 250 | 251 | if let Some((x, y)) = args.pos { 252 | head_config.set_position(x, y); 253 | } 254 | 255 | let mode_iter = || { 256 | head.modes.values().filter(|mode| { 257 | if let Some((width, height)) = args.size { 258 | mode.width == width as i32 && mode.height == height as i32 259 | } else { 260 | head.current_mode 261 | .as_ref() 262 | .is_some_and(|current_mode| mode.wlr_mode.id() == *current_mode) 263 | } 264 | }) 265 | }; 266 | 267 | if let Some(vrr) = args.adaptive_sync { 268 | if let Some(cosmic_obj) = cosmic_head_config.as_ref().filter(|obj| { 269 | obj.version() >= zcosmic_output_configuration_head_v1::REQ_SET_ADAPTIVE_SYNC_EXT_SINCE 270 | }) { 271 | cosmic_obj.set_adaptive_sync_ext(vrr); 272 | } else { 273 | head_config.set_adaptive_sync(match vrr { 274 | AdaptiveSyncStateExt::Always => AdaptiveSyncState::Enabled, 275 | AdaptiveSyncStateExt::Disabled => AdaptiveSyncState::Disabled, 276 | AdaptiveSyncStateExt::Automatic => { 277 | return Err(ConfigurationError::UnsupportedVrrState); 278 | } 279 | _ => panic!("Unknown AdaptiveSyncStatExt variant"), 280 | }); 281 | } 282 | } 283 | 284 | if let Some(refresh) = args.refresh { 285 | #[allow(clippy::cast_possible_truncation)] 286 | let refresh = (refresh * 1000.0) as i32; 287 | 288 | let min = refresh - 501; 289 | let max = refresh + 501; 290 | 291 | let mode = mode_iter() 292 | .find(|mode| mode.refresh == refresh) 293 | .or_else(|| { 294 | mode_iter() 295 | .filter(|mode| min < mode.refresh && max > mode.refresh) 296 | .min_by_key(|mode| (mode.refresh - refresh).abs()) 297 | }); 298 | 299 | if let Some(mode) = mode { 300 | head_config.set_mode(&mode.wlr_mode); 301 | Ok(()) 302 | } else { 303 | Err(ConfigurationError::ModeNotFound) 304 | } 305 | } else if let Some(mode) = mode_iter().next() { 306 | head_config.set_mode(&mode.wlr_mode); 307 | Ok(()) 308 | } else { 309 | Err(ConfigurationError::ModeNotFound) 310 | } 311 | } 312 | 313 | impl Context { 314 | pub fn callback( 315 | &mut self, 316 | event_queue: &mut EventQueue, 317 | ) -> Result { 318 | event_queue.dispatch_pending(self) 319 | } 320 | 321 | pub async fn dispatch(&mut self, event_queue: &mut EventQueue) -> Result { 322 | crate::async_dispatch(&self.connection.clone(), event_queue, self) 323 | .await 324 | .map_err(Error::from) 325 | } 326 | 327 | pub fn send(&mut self, event: Message) { 328 | self.sender.send(event) 329 | } 330 | 331 | pub fn create_output_config(&mut self) -> Configuration { 332 | let configuration = self.output_manager.as_ref().unwrap().create_configuration( 333 | self.output_manager_serial, 334 | &self.handle, 335 | (), 336 | ); 337 | 338 | let cosmic_configuration = self 339 | .cosmic_output_manager 340 | .as_ref() 341 | .map(|extension| extension.get_configuration(&configuration, &self.handle, ())); 342 | 343 | Configuration { 344 | obj: configuration, 345 | cosmic_obj: cosmic_configuration, 346 | cosmic_output_manager: self.cosmic_output_manager.clone(), 347 | handle: self.handle.clone(), 348 | known_heads: self.output_heads.values().cloned().collect(), 349 | configured_heads: Vec::new(), 350 | } 351 | } 352 | 353 | pub fn set_xwayland_primary(&self, output: Option<&str>) -> Result<(), ConfigurationError> { 354 | let Some(cosmic_output_manager) = self.cosmic_output_manager.as_ref() else { 355 | return Err(ConfigurationError::NoCosmicExtension); 356 | }; 357 | if cosmic_output_manager.version() 358 | < zcosmic_output_manager_v1::REQ_SET_XWAYLAND_PRIMARY_SINCE 359 | { 360 | return Err(ConfigurationError::UnsupportedXwaylandPrimary); 361 | } 362 | 363 | match output { 364 | None => cosmic_output_manager.set_xwayland_primary(None), 365 | Some(name) => { 366 | let head = self 367 | .output_heads 368 | .values() 369 | .filter(|head| head.cosmic_head.is_some()) 370 | .find(|head| head.name == name) 371 | .ok_or(ConfigurationError::UnknownOutput)?; 372 | cosmic_output_manager.set_xwayland_primary(Some(head.cosmic_head.as_ref().unwrap())) 373 | } 374 | }; 375 | 376 | Ok(()) 377 | } 378 | 379 | pub fn connect(sender: Sender) -> Result<(Self, EventQueue), Error> { 380 | let connection = Connection::connect_to_env()?; 381 | 382 | let mut event_queue = connection.new_event_queue(); 383 | let handle = event_queue.handle(); 384 | 385 | let display = connection.display(); 386 | let wl_registry = display.get_registry(&handle, ()); 387 | 388 | let mut context = Self { 389 | connection, 390 | handle, 391 | output_manager_serial: Default::default(), 392 | output_manager: Default::default(), 393 | cosmic_output_manager: Default::default(), 394 | output_manager_version: Default::default(), 395 | output_heads: Default::default(), 396 | sender, 397 | wl_registry, 398 | cosmic_manager_sync_callback: None, 399 | done_queued: false, 400 | }; 401 | 402 | event_queue.roundtrip(&mut context)?; 403 | // second roundtrip for extension protocol 404 | if context.cosmic_output_manager.is_some() { 405 | event_queue.roundtrip(&mut context)?; 406 | } 407 | 408 | Ok((context, event_queue)) 409 | } 410 | 411 | /// Flushes the wayland client connection. 412 | /// 413 | /// # Errors 414 | /// 415 | /// Returns error if wayland client connection fails to flush. 416 | pub fn flush(&mut self) -> Result<(), Error> { 417 | Ok(self.connection.flush()?) 418 | } 419 | 420 | pub fn clear(&mut self) { 421 | for (id, _) in std::mem::take(&mut self.output_heads) { 422 | match ZwlrOutputHeadV1::from_id(&self.connection, id) { 423 | Ok(it) => it.release(), 424 | Err(err) => tracing::debug!("{}", err), 425 | } 426 | } 427 | 428 | if let Some(manager) = &self.output_manager { 429 | manager.stop(); 430 | } 431 | } 432 | 433 | pub async fn apply_current_config(&mut self) -> Result<(), ConfigurationError> { 434 | let Some(cosmic_output_manager) = self.cosmic_output_manager.as_ref() else { 435 | return Err(ConfigurationError::NoCosmicExtension); 436 | }; 437 | if cosmic_output_manager.version() 438 | < zcosmic_output_manager_v1::REQ_SET_XWAYLAND_PRIMARY_SINCE 439 | { 440 | return Err(ConfigurationError::UnsupportedXwaylandPrimary); 441 | } 442 | 443 | let configuration = self.output_manager.as_ref().unwrap().create_configuration( 444 | self.output_manager_serial, 445 | &self.handle, 446 | (), 447 | ); 448 | 449 | let cosmic_configuration = self 450 | .cosmic_output_manager 451 | .as_ref() 452 | .map(|extension| extension.get_configuration(&configuration, &self.handle, ())); 453 | 454 | let mut config_obj = Configuration { 455 | obj: configuration, 456 | cosmic_obj: cosmic_configuration, 457 | cosmic_output_manager: self.cosmic_output_manager.clone(), 458 | handle: self.handle.clone(), 459 | known_heads: self.output_heads.values().cloned().collect(), 460 | configured_heads: Vec::new(), 461 | }; 462 | 463 | let known_heads = config_obj.known_heads.clone(); 464 | let configured_heads = config_obj.configured_heads.clone(); 465 | for output in known_heads 466 | .iter() 467 | .filter(|output| !configured_heads.contains(&output.name)) 468 | { 469 | let head_configuration = HeadConfiguration { 470 | size: output.current_mode.as_ref().and_then(|mode_id| { 471 | output 472 | .modes 473 | .get(mode_id) 474 | .map(|mode| (mode.width as u32, mode.height as u32)) 475 | }), 476 | refresh: output.current_mode.as_ref().and_then(|mode_id| { 477 | output 478 | .modes 479 | .get(mode_id) 480 | .map(|mode| mode.refresh as f32 / 1000.0) 481 | }), 482 | adaptive_sync: output.adaptive_sync, 483 | pos: Some((output.position_x, output.position_y)), 484 | scale: Some(output.scale), 485 | transform: output.transform, 486 | }; 487 | if output.enabled { 488 | if let Some(from) = output.mirroring.as_ref() { 489 | config_obj 490 | .mirror_head(&output.name, from, Some(head_configuration)) 491 | .unwrap(); 492 | } else { 493 | config_obj 494 | .enable_head(&output.name, Some(head_configuration)) 495 | .unwrap(); 496 | } 497 | } else { 498 | config_obj.disable_head(&output.name).unwrap(); 499 | } 500 | } 501 | config_obj.apply(); 502 | 503 | Ok(()) 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.21" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.13" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.7" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 40 | dependencies = [ 41 | "windows-sys 0.61.2", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.11" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell_polyfill", 52 | "windows-sys 0.61.2", 53 | ] 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "1.5.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 60 | 61 | [[package]] 62 | name = "bitflags" 63 | version = "2.10.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 66 | 67 | [[package]] 68 | name = "bytes" 69 | version = "1.11.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 72 | 73 | [[package]] 74 | name = "cc" 75 | version = "1.2.46" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" 78 | dependencies = [ 79 | "find-msvc-tools", 80 | "shlex", 81 | ] 82 | 83 | [[package]] 84 | name = "cfg-if" 85 | version = "1.0.4" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 88 | 89 | [[package]] 90 | name = "clap" 91 | version = "4.5.53" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 94 | dependencies = [ 95 | "clap_builder", 96 | "clap_derive", 97 | ] 98 | 99 | [[package]] 100 | name = "clap_builder" 101 | version = "4.5.53" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 104 | dependencies = [ 105 | "anstream", 106 | "anstyle", 107 | "clap_lex", 108 | "strsim", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_derive" 113 | version = "4.5.49" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 116 | dependencies = [ 117 | "heck", 118 | "proc-macro2", 119 | "quote", 120 | "syn", 121 | ] 122 | 123 | [[package]] 124 | name = "clap_lex" 125 | version = "0.7.6" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 128 | 129 | [[package]] 130 | name = "colorchoice" 131 | version = "1.0.4" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 134 | 135 | [[package]] 136 | name = "cosmic-protocols" 137 | version = "0.1.0" 138 | source = "git+https://github.com/pop-os/cosmic-protocols.git#d0e95be25e423cfe523b11111a3666ed7aaf0dc4" 139 | dependencies = [ 140 | "bitflags", 141 | "wayland-backend", 142 | "wayland-client", 143 | "wayland-protocols", 144 | "wayland-protocols-wlr", 145 | "wayland-scanner", 146 | "wayland-server", 147 | ] 148 | 149 | [[package]] 150 | name = "cosmic-randr" 151 | version = "0.1.0" 152 | dependencies = [ 153 | "cosmic-protocols", 154 | "indexmap", 155 | "thiserror", 156 | "tokio", 157 | "tracing", 158 | "wayland-client", 159 | "wayland-protocols-wlr", 160 | ] 161 | 162 | [[package]] 163 | name = "cosmic-randr-cli" 164 | version = "0.1.0" 165 | dependencies = [ 166 | "clap", 167 | "cosmic-randr", 168 | "cosmic-randr-shell", 169 | "fomat-macros", 170 | "kdl", 171 | "nu-ansi-term", 172 | "tokio", 173 | "wayland-client", 174 | ] 175 | 176 | [[package]] 177 | name = "cosmic-randr-shell" 178 | version = "0.1.0" 179 | dependencies = [ 180 | "kdl", 181 | "slotmap", 182 | "thiserror", 183 | ] 184 | 185 | [[package]] 186 | name = "downcast-rs" 187 | version = "1.2.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 190 | 191 | [[package]] 192 | name = "equivalent" 193 | version = "1.0.2" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 196 | 197 | [[package]] 198 | name = "errno" 199 | version = "0.3.14" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 202 | dependencies = [ 203 | "libc", 204 | "windows-sys 0.61.2", 205 | ] 206 | 207 | [[package]] 208 | name = "find-msvc-tools" 209 | version = "0.1.5" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 212 | 213 | [[package]] 214 | name = "fomat-macros" 215 | version = "0.3.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "3f722aa875298d34a0ebb6004699f6f4ea830d36dec8ac2effdbbc840248a096" 218 | 219 | [[package]] 220 | name = "hashbrown" 221 | version = "0.16.1" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 224 | 225 | [[package]] 226 | name = "heck" 227 | version = "0.5.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 230 | 231 | [[package]] 232 | name = "indexmap" 233 | version = "2.12.1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 236 | dependencies = [ 237 | "equivalent", 238 | "hashbrown", 239 | ] 240 | 241 | [[package]] 242 | name = "is_terminal_polyfill" 243 | version = "1.70.2" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 246 | 247 | [[package]] 248 | name = "kdl" 249 | version = "6.5.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e" 252 | dependencies = [ 253 | "miette", 254 | "num", 255 | "winnow", 256 | ] 257 | 258 | [[package]] 259 | name = "libc" 260 | version = "0.2.177" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 263 | 264 | [[package]] 265 | name = "linux-raw-sys" 266 | version = "0.11.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 269 | 270 | [[package]] 271 | name = "memchr" 272 | version = "2.7.6" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 275 | 276 | [[package]] 277 | name = "miette" 278 | version = "7.6.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 281 | dependencies = [ 282 | "cfg-if", 283 | "unicode-width", 284 | ] 285 | 286 | [[package]] 287 | name = "mio" 288 | version = "1.1.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 291 | dependencies = [ 292 | "libc", 293 | "wasi", 294 | "windows-sys 0.61.2", 295 | ] 296 | 297 | [[package]] 298 | name = "nu-ansi-term" 299 | version = "0.50.3" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 302 | dependencies = [ 303 | "windows-sys 0.61.2", 304 | ] 305 | 306 | [[package]] 307 | name = "num" 308 | version = "0.4.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 311 | dependencies = [ 312 | "num-bigint", 313 | "num-complex", 314 | "num-integer", 315 | "num-iter", 316 | "num-rational", 317 | "num-traits", 318 | ] 319 | 320 | [[package]] 321 | name = "num-bigint" 322 | version = "0.4.6" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 325 | dependencies = [ 326 | "num-integer", 327 | "num-traits", 328 | ] 329 | 330 | [[package]] 331 | name = "num-complex" 332 | version = "0.4.6" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 335 | dependencies = [ 336 | "num-traits", 337 | ] 338 | 339 | [[package]] 340 | name = "num-integer" 341 | version = "0.1.46" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 344 | dependencies = [ 345 | "num-traits", 346 | ] 347 | 348 | [[package]] 349 | name = "num-iter" 350 | version = "0.1.45" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 353 | dependencies = [ 354 | "autocfg", 355 | "num-integer", 356 | "num-traits", 357 | ] 358 | 359 | [[package]] 360 | name = "num-rational" 361 | version = "0.4.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 364 | dependencies = [ 365 | "num-bigint", 366 | "num-integer", 367 | "num-traits", 368 | ] 369 | 370 | [[package]] 371 | name = "num-traits" 372 | version = "0.2.19" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 375 | dependencies = [ 376 | "autocfg", 377 | ] 378 | 379 | [[package]] 380 | name = "once_cell" 381 | version = "1.21.3" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 384 | 385 | [[package]] 386 | name = "once_cell_polyfill" 387 | version = "1.70.2" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 390 | 391 | [[package]] 392 | name = "pin-project-lite" 393 | version = "0.2.16" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 396 | 397 | [[package]] 398 | name = "pkg-config" 399 | version = "0.3.32" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 402 | 403 | [[package]] 404 | name = "proc-macro2" 405 | version = "1.0.103" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 408 | dependencies = [ 409 | "unicode-ident", 410 | ] 411 | 412 | [[package]] 413 | name = "quick-xml" 414 | version = "0.37.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 417 | dependencies = [ 418 | "memchr", 419 | ] 420 | 421 | [[package]] 422 | name = "quote" 423 | version = "1.0.42" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 426 | dependencies = [ 427 | "proc-macro2", 428 | ] 429 | 430 | [[package]] 431 | name = "rustix" 432 | version = "1.1.2" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 435 | dependencies = [ 436 | "bitflags", 437 | "errno", 438 | "libc", 439 | "linux-raw-sys", 440 | "windows-sys 0.61.2", 441 | ] 442 | 443 | [[package]] 444 | name = "shlex" 445 | version = "1.3.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 448 | 449 | [[package]] 450 | name = "slotmap" 451 | version = "1.0.7" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 454 | dependencies = [ 455 | "version_check", 456 | ] 457 | 458 | [[package]] 459 | name = "smallvec" 460 | version = "1.15.1" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 463 | 464 | [[package]] 465 | name = "socket2" 466 | version = "0.6.1" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 469 | dependencies = [ 470 | "libc", 471 | "windows-sys 0.60.2", 472 | ] 473 | 474 | [[package]] 475 | name = "strsim" 476 | version = "0.11.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 479 | 480 | [[package]] 481 | name = "syn" 482 | version = "2.0.110" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" 485 | dependencies = [ 486 | "proc-macro2", 487 | "quote", 488 | "unicode-ident", 489 | ] 490 | 491 | [[package]] 492 | name = "thiserror" 493 | version = "2.0.17" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 496 | dependencies = [ 497 | "thiserror-impl", 498 | ] 499 | 500 | [[package]] 501 | name = "thiserror-impl" 502 | version = "2.0.17" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 505 | dependencies = [ 506 | "proc-macro2", 507 | "quote", 508 | "syn", 509 | ] 510 | 511 | [[package]] 512 | name = "tokio" 513 | version = "1.48.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 516 | dependencies = [ 517 | "bytes", 518 | "libc", 519 | "mio", 520 | "pin-project-lite", 521 | "socket2", 522 | "tokio-macros", 523 | "windows-sys 0.61.2", 524 | ] 525 | 526 | [[package]] 527 | name = "tokio-macros" 528 | version = "2.6.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 531 | dependencies = [ 532 | "proc-macro2", 533 | "quote", 534 | "syn", 535 | ] 536 | 537 | [[package]] 538 | name = "tracing" 539 | version = "0.1.43" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" 542 | dependencies = [ 543 | "pin-project-lite", 544 | "tracing-attributes", 545 | "tracing-core", 546 | ] 547 | 548 | [[package]] 549 | name = "tracing-attributes" 550 | version = "0.1.31" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 553 | dependencies = [ 554 | "proc-macro2", 555 | "quote", 556 | "syn", 557 | ] 558 | 559 | [[package]] 560 | name = "tracing-core" 561 | version = "0.1.35" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" 564 | dependencies = [ 565 | "once_cell", 566 | ] 567 | 568 | [[package]] 569 | name = "unicode-ident" 570 | version = "1.0.22" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 573 | 574 | [[package]] 575 | name = "unicode-width" 576 | version = "0.1.14" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 579 | 580 | [[package]] 581 | name = "utf8parse" 582 | version = "0.2.2" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 585 | 586 | [[package]] 587 | name = "version_check" 588 | version = "0.9.5" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 591 | 592 | [[package]] 593 | name = "wasi" 594 | version = "0.11.1+wasi-snapshot-preview1" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 597 | 598 | [[package]] 599 | name = "wayland-backend" 600 | version = "0.3.11" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" 603 | dependencies = [ 604 | "cc", 605 | "downcast-rs", 606 | "rustix", 607 | "smallvec", 608 | "wayland-sys", 609 | ] 610 | 611 | [[package]] 612 | name = "wayland-client" 613 | version = "0.31.11" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" 616 | dependencies = [ 617 | "bitflags", 618 | "rustix", 619 | "wayland-backend", 620 | "wayland-scanner", 621 | ] 622 | 623 | [[package]] 624 | name = "wayland-protocols" 625 | version = "0.32.9" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" 628 | dependencies = [ 629 | "bitflags", 630 | "wayland-backend", 631 | "wayland-client", 632 | "wayland-scanner", 633 | "wayland-server", 634 | ] 635 | 636 | [[package]] 637 | name = "wayland-protocols-wlr" 638 | version = "0.3.9" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" 641 | dependencies = [ 642 | "bitflags", 643 | "wayland-backend", 644 | "wayland-client", 645 | "wayland-protocols", 646 | "wayland-scanner", 647 | "wayland-server", 648 | ] 649 | 650 | [[package]] 651 | name = "wayland-scanner" 652 | version = "0.31.7" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" 655 | dependencies = [ 656 | "proc-macro2", 657 | "quick-xml", 658 | "quote", 659 | ] 660 | 661 | [[package]] 662 | name = "wayland-server" 663 | version = "0.31.10" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "fcbd4f3aba6c9fba70445ad2a484c0ef0356c1a9459b1e8e435bedc1971a6222" 666 | dependencies = [ 667 | "bitflags", 668 | "downcast-rs", 669 | "rustix", 670 | "wayland-backend", 671 | "wayland-scanner", 672 | ] 673 | 674 | [[package]] 675 | name = "wayland-sys" 676 | version = "0.31.7" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" 679 | dependencies = [ 680 | "pkg-config", 681 | ] 682 | 683 | [[package]] 684 | name = "windows-link" 685 | version = "0.2.1" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 688 | 689 | [[package]] 690 | name = "windows-sys" 691 | version = "0.60.2" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 694 | dependencies = [ 695 | "windows-targets", 696 | ] 697 | 698 | [[package]] 699 | name = "windows-sys" 700 | version = "0.61.2" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 703 | dependencies = [ 704 | "windows-link", 705 | ] 706 | 707 | [[package]] 708 | name = "windows-targets" 709 | version = "0.53.5" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 712 | dependencies = [ 713 | "windows-link", 714 | "windows_aarch64_gnullvm", 715 | "windows_aarch64_msvc", 716 | "windows_i686_gnu", 717 | "windows_i686_gnullvm", 718 | "windows_i686_msvc", 719 | "windows_x86_64_gnu", 720 | "windows_x86_64_gnullvm", 721 | "windows_x86_64_msvc", 722 | ] 723 | 724 | [[package]] 725 | name = "windows_aarch64_gnullvm" 726 | version = "0.53.1" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 729 | 730 | [[package]] 731 | name = "windows_aarch64_msvc" 732 | version = "0.53.1" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 735 | 736 | [[package]] 737 | name = "windows_i686_gnu" 738 | version = "0.53.1" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 741 | 742 | [[package]] 743 | name = "windows_i686_gnullvm" 744 | version = "0.53.1" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 747 | 748 | [[package]] 749 | name = "windows_i686_msvc" 750 | version = "0.53.1" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 753 | 754 | [[package]] 755 | name = "windows_x86_64_gnu" 756 | version = "0.53.1" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 759 | 760 | [[package]] 761 | name = "windows_x86_64_gnullvm" 762 | version = "0.53.1" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 765 | 766 | [[package]] 767 | name = "windows_x86_64_msvc" 768 | version = "0.53.1" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 771 | 772 | [[package]] 773 | name = "winnow" 774 | version = "0.6.24" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" 777 | dependencies = [ 778 | "memchr", 779 | ] 780 | -------------------------------------------------------------------------------- /shell/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use std::fmt::Display; 5 | 6 | use kdl::{KdlDocument, KdlEntry, KdlError, KdlValue}; 7 | use slotmap::SlotMap; 8 | 9 | slotmap::new_key_type! { 10 | /// A unique slotmap key to an output. 11 | pub struct OutputKey; 12 | /// A unique slotmap key to a mode. 13 | pub struct ModeKey; 14 | } 15 | 16 | #[derive(Clone, Debug, PartialEq)] 17 | pub struct Mode { 18 | pub size: (u32, u32), 19 | pub refresh_rate: u32, 20 | pub preferred: bool, 21 | } 22 | 23 | impl Default for Mode { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl Mode { 30 | #[must_use] 31 | pub const fn new() -> Self { 32 | Self { 33 | size: (0, 0), 34 | refresh_rate: 0, 35 | preferred: false, 36 | } 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug, Default)] 41 | pub struct List { 42 | pub outputs: SlotMap, 43 | pub modes: SlotMap, 44 | } 45 | 46 | #[derive(Clone, Debug, PartialEq)] 47 | pub struct Output { 48 | pub serial_number: String, 49 | pub name: String, 50 | pub enabled: bool, 51 | pub mirroring: Option, 52 | pub make: Option, 53 | pub model: String, 54 | pub physical: (u32, u32), 55 | pub position: (i32, i32), 56 | pub scale: f64, 57 | pub transform: Option, 58 | pub modes: Vec, 59 | pub current: Option, 60 | pub adaptive_sync: Option, 61 | pub adaptive_sync_availability: Option, 62 | pub xwayland_primary: Option, 63 | } 64 | 65 | impl Default for Output { 66 | fn default() -> Self { 67 | Self::new() 68 | } 69 | } 70 | 71 | impl Output { 72 | #[must_use] 73 | pub const fn new() -> Self { 74 | Self { 75 | serial_number: String::new(), 76 | name: String::new(), 77 | enabled: false, 78 | mirroring: None, 79 | make: None, 80 | model: String::new(), 81 | physical: (0, 0), 82 | position: (0, 0), 83 | scale: 1.0, 84 | transform: None, 85 | modes: Vec::new(), 86 | current: None, 87 | adaptive_sync: None, 88 | adaptive_sync_availability: None, 89 | xwayland_primary: None, 90 | } 91 | } 92 | } 93 | 94 | #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] 95 | pub enum Transform { 96 | Normal, 97 | Rotate90, 98 | Rotate180, 99 | Rotate270, 100 | Flipped, 101 | Flipped90, 102 | Flipped180, 103 | Flipped270, 104 | } 105 | 106 | impl Display for Transform { 107 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 108 | f.write_str(match self { 109 | Transform::Normal => "normal", 110 | Transform::Rotate90 => "rotate90", 111 | Transform::Rotate180 => "rotate180", 112 | Transform::Rotate270 => "rotate270", 113 | Transform::Flipped => "flipped", 114 | Transform::Flipped90 => "flipped90", 115 | Transform::Flipped180 => "flipped180", 116 | Transform::Flipped270 => "flipped270", 117 | }) 118 | } 119 | } 120 | 121 | impl TryFrom<&str> for Transform { 122 | type Error = &'static str; 123 | 124 | fn try_from(value: &str) -> Result { 125 | Ok(match value { 126 | "normal" => Transform::Normal, 127 | "rotate90" => Transform::Rotate90, 128 | "rotate180" => Transform::Rotate180, 129 | "rotate270" => Transform::Rotate270, 130 | "flipped" => Transform::Flipped, 131 | "flipped90" => Transform::Flipped90, 132 | "flipped180" => Transform::Flipped180, 133 | "flipped270" => Transform::Flipped270, 134 | _ => return Err("unknown transform variant"), 135 | }) 136 | } 137 | } 138 | 139 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 140 | pub enum AdaptiveSyncState { 141 | Always, 142 | Auto, 143 | Disabled, 144 | } 145 | 146 | impl AdaptiveSyncState { 147 | fn try_from_kdl_value(value: &KdlValue) -> Option { 148 | value.as_bool().map_or_else( 149 | || { 150 | value 151 | .as_string() 152 | .and_then(|v| AdaptiveSyncState::try_from(v).ok()) 153 | }, 154 | |v| { 155 | Some(if v { 156 | AdaptiveSyncState::Always 157 | } else { 158 | AdaptiveSyncState::Disabled 159 | }) 160 | }, 161 | ) 162 | } 163 | } 164 | 165 | impl From for KdlValue { 166 | fn from(this: AdaptiveSyncState) -> Self { 167 | match this { 168 | AdaptiveSyncState::Disabled => KdlValue::Bool(false), 169 | AdaptiveSyncState::Always => KdlValue::Bool(true), 170 | AdaptiveSyncState::Auto => KdlValue::String("automatic".into()), 171 | } 172 | } 173 | } 174 | 175 | impl TryFrom<&str> for AdaptiveSyncState { 176 | type Error = &'static str; 177 | 178 | fn try_from(value: &str) -> Result { 179 | Ok(match value { 180 | "automatic" => AdaptiveSyncState::Auto, 181 | _ => return Err("unknown adaptive_sync state variant"), 182 | }) 183 | } 184 | } 185 | 186 | impl From for &'static str { 187 | fn from(this: AdaptiveSyncState) -> Self { 188 | match this { 189 | AdaptiveSyncState::Always => "true", 190 | AdaptiveSyncState::Auto => "automatic", 191 | AdaptiveSyncState::Disabled => "false", 192 | } 193 | } 194 | } 195 | 196 | impl Display for AdaptiveSyncState { 197 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 198 | f.write_str(<&'static str>::from(*self)) 199 | } 200 | } 201 | 202 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 203 | pub enum AdaptiveSyncAvailability { 204 | Supported, 205 | RequiresModeset, 206 | Unsupported, 207 | } 208 | 209 | impl AdaptiveSyncAvailability { 210 | pub fn try_from_kdl_value(value: &KdlValue) -> Option { 211 | value.as_bool().map_or_else( 212 | || { 213 | value 214 | .as_string() 215 | .and_then(|v| AdaptiveSyncAvailability::try_from(v).ok()) 216 | }, 217 | |v| { 218 | Some(if v { 219 | AdaptiveSyncAvailability::Supported 220 | } else { 221 | AdaptiveSyncAvailability::Unsupported 222 | }) 223 | }, 224 | ) 225 | } 226 | } 227 | 228 | impl From for KdlValue { 229 | fn from(this: AdaptiveSyncAvailability) -> Self { 230 | match this { 231 | AdaptiveSyncAvailability::Unsupported => KdlValue::Bool(false), 232 | AdaptiveSyncAvailability::Supported => KdlValue::Bool(true), 233 | AdaptiveSyncAvailability::RequiresModeset => { 234 | KdlValue::String("requires_modeset".into()) 235 | } 236 | } 237 | } 238 | } 239 | 240 | impl TryFrom<&str> for AdaptiveSyncAvailability { 241 | type Error = &'static str; 242 | 243 | fn try_from(value: &str) -> Result { 244 | Ok(match value { 245 | "requires_modeset" => AdaptiveSyncAvailability::RequiresModeset, 246 | _ => return Err("unknown adaptive_sync availability variant"), 247 | }) 248 | } 249 | } 250 | 251 | impl From for &'static str { 252 | fn from(this: AdaptiveSyncAvailability) -> Self { 253 | match this { 254 | AdaptiveSyncAvailability::Supported => "true", 255 | AdaptiveSyncAvailability::RequiresModeset => "requires_modeset", 256 | AdaptiveSyncAvailability::Unsupported => "false", 257 | } 258 | } 259 | } 260 | 261 | impl Display for AdaptiveSyncAvailability { 262 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 263 | f.write_str(<&'static str>::from(*self)) 264 | } 265 | } 266 | 267 | #[derive(thiserror::Error, Debug)] 268 | pub enum Error { 269 | #[error("`cosmic-randr` KDL format error")] 270 | Kdl(#[from] KdlError), 271 | #[error("could not exec `cosmic-randr`")] 272 | Spawn(#[source] std::io::Error), 273 | #[error("`cosmic-randr` output not UTF-8")] 274 | Utf(#[from] std::str::Utf8Error), 275 | } 276 | 277 | #[allow(clippy::too_many_lines)] 278 | pub async fn list() -> Result { 279 | // Get a list of outputs from `cosmic-randr` in KDL format. 280 | let stdout = std::process::Command::new("cosmic-randr") 281 | .args(["list", "--kdl"]) 282 | .stdin(std::process::Stdio::null()) 283 | .stdout(std::process::Stdio::piped()) 284 | .stderr(std::process::Stdio::null()) 285 | .output() 286 | .map_err(Error::Spawn)? 287 | .stdout; 288 | 289 | // Parse the output as a KDL document. 290 | let document = std::str::from_utf8(&stdout) 291 | .map_err(Error::Utf)? 292 | .parse::() 293 | .map_err(Error::Kdl)?; 294 | 295 | match List::try_from(document) { 296 | Ok(v) => Ok(v), 297 | Err(KdlParseWithError { list, errors }) => { 298 | for err in errors { 299 | eprintln!("{err:?}"); 300 | } 301 | Ok(list) 302 | } 303 | } 304 | } 305 | 306 | #[derive(Debug, Clone)] 307 | pub enum KdlParseError { 308 | InvalidRootNode(String), 309 | InvalidKey(String), 310 | InvalidValue { key: String, value: Vec }, 311 | MissingOutputName, 312 | MissingOutputChildren, 313 | MissingModeChildren, 314 | MissingEntryName, 315 | } 316 | 317 | #[derive(Debug, Clone)] 318 | pub struct KdlParseWithError { 319 | pub list: List, 320 | pub errors: Vec, 321 | } 322 | 323 | impl TryFrom for List { 324 | type Error = KdlParseWithError; 325 | 326 | fn try_from(document: KdlDocument) -> Result { 327 | let mut outputs = List { 328 | outputs: SlotMap::with_key(), 329 | modes: SlotMap::with_key(), 330 | }; 331 | let mut errors = Vec::new(); 332 | 333 | // Each node in the root of the document is an output. 334 | for node in document.nodes() { 335 | if node.name().value() != "output" { 336 | errors.push(KdlParseError::InvalidRootNode( 337 | node.name().value().to_string(), 338 | )); 339 | continue; 340 | } 341 | 342 | // Parse the properties of the output mode 343 | let mut entries = node.entries().iter(); 344 | 345 | // The first value is the name of the otuput 346 | let Some(name) = entries.next().and_then(|e| e.value().as_string()) else { 347 | errors.push(KdlParseError::MissingOutputName); 348 | continue; 349 | }; 350 | 351 | let mut output = Output::new(); 352 | 353 | // Check if the output contains the `enabled` attribute. 354 | for entry in entries { 355 | let Some(entry_name) = entry.name() else { 356 | errors.push(KdlParseError::MissingEntryName); 357 | continue; 358 | }; 359 | 360 | if entry_name.value() == "enabled" 361 | && let Some(enabled) = entry.value().as_bool() 362 | { 363 | output.enabled = enabled; 364 | } 365 | } 366 | 367 | // Gets the properties of the output. 368 | let Some(children) = node.children() else { 369 | errors.push(KdlParseError::MissingOutputChildren); 370 | continue; 371 | }; 372 | 373 | for node in children.nodes() { 374 | match node.name().value() { 375 | // Parse the serial number 376 | "serial_number" => { 377 | if let Some(entry) = node.entries().first() { 378 | output.serial_number = 379 | entry.value().as_string().unwrap_or("").to_owned(); 380 | } else { 381 | errors.push(KdlParseError::InvalidValue { 382 | key: "serial_number".to_string(), 383 | value: node.entries().to_vec(), 384 | }); 385 | } 386 | } 387 | 388 | // Parse the make and model of the display output. 389 | "description" => { 390 | for entry in node.entries() { 391 | let value = entry.value().as_string(); 392 | 393 | match entry.name().map(kdl::KdlIdentifier::value) { 394 | Some("make") => { 395 | output.make = value.map(String::from); 396 | } 397 | 398 | Some("model") => { 399 | if let Some(model) = value { 400 | output.model = String::from(model); 401 | } 402 | } 403 | 404 | v => errors.push(KdlParseError::InvalidKey( 405 | v.map_or(String::default(), |s| s.to_string()), 406 | )), 407 | } 408 | } 409 | } 410 | 411 | // Parse the physical width and height in millimeters 412 | "physical" => { 413 | if let [width, height, ..] = node.entries() { 414 | output.physical = ( 415 | width.value().as_integer().unwrap_or_default() as u32, 416 | height.value().as_integer().unwrap_or_default() as u32, 417 | ); 418 | } else { 419 | errors.push(KdlParseError::InvalidValue { 420 | key: "physical".to_string(), 421 | value: node.entries().to_vec(), 422 | }); 423 | } 424 | } 425 | 426 | // Parse the pixel coordinates of the output. 427 | "position" => { 428 | if let [x_pos, y_pos, ..] = node.entries() { 429 | output.position = ( 430 | x_pos.value().as_integer().unwrap_or_default() as i32, 431 | y_pos.value().as_integer().unwrap_or_default() as i32, 432 | ); 433 | } else { 434 | errors.push(KdlParseError::InvalidValue { 435 | key: "position".to_string(), 436 | value: node.entries().to_vec(), 437 | }); 438 | } 439 | } 440 | 441 | "scale" => { 442 | if let Some(entry) = node.entries().first() { 443 | if let Some(scale) = entry.value().as_float() { 444 | output.scale = scale; 445 | } 446 | } else { 447 | errors.push(KdlParseError::InvalidValue { 448 | key: "scale".to_string(), 449 | value: node.entries().to_vec(), 450 | }); 451 | } 452 | } 453 | 454 | // Parse the transform value of the output. 455 | "transform" => { 456 | if let Some(entry) = node.entries().first() { 457 | if let Some(string) = entry.value().as_string() { 458 | output.transform = Transform::try_from(string).ok(); 459 | } 460 | } else { 461 | errors.push(KdlParseError::InvalidValue { 462 | key: "transform".to_string(), 463 | value: node.entries().to_vec(), 464 | }); 465 | } 466 | } 467 | 468 | "adaptive_sync" => { 469 | if let Some(entry) = node.entries().first() { 470 | output.adaptive_sync = 471 | AdaptiveSyncState::try_from_kdl_value(entry.value()) 472 | } else { 473 | errors.push(KdlParseError::InvalidValue { 474 | key: "adaptive_sync".to_string(), 475 | value: node.entries().to_vec(), 476 | }); 477 | } 478 | } 479 | 480 | "adaptive_sync_support" => { 481 | if let Some(entry) = node.entries().first() { 482 | output.adaptive_sync_availability = 483 | AdaptiveSyncAvailability::try_from_kdl_value(entry.value()); 484 | } else { 485 | errors.push(KdlParseError::InvalidValue { 486 | key: "adaptive_sync_support".to_string(), 487 | value: node.entries().to_vec(), 488 | }); 489 | } 490 | } 491 | 492 | // Switch to parsing output modes. 493 | "modes" => { 494 | let Some(children) = node.children() else { 495 | errors.push(KdlParseError::MissingModeChildren); 496 | continue; 497 | }; 498 | 499 | for node in children.nodes() { 500 | if node.name().value() == "mode" { 501 | let mut current = false; 502 | let mut mode = Mode::new(); 503 | 504 | if let [width, height, refresh, ..] = node.entries() { 505 | mode.size = ( 506 | width.value().as_integer().unwrap_or_default() as u32, 507 | height.value().as_integer().unwrap_or_default() as u32, 508 | ); 509 | 510 | mode.refresh_rate = 511 | refresh.value().as_integer().unwrap_or_default() as u32; 512 | }; 513 | 514 | for entry in node.entries().iter().skip(3) { 515 | match entry.name().map(kdl::KdlIdentifier::value) { 516 | Some("current") => current = true, 517 | Some("preferred") => mode.preferred = true, 518 | _ => { 519 | errors.push(KdlParseError::InvalidKey( 520 | entry 521 | .name() 522 | .map_or(String::default(), |s| s.to_string()), 523 | )); 524 | } 525 | } 526 | } 527 | 528 | let mode_id = outputs.modes.insert(mode); 529 | 530 | if current { 531 | output.current = Some(mode_id); 532 | } 533 | 534 | output.modes.push(mode_id); 535 | } else { 536 | errors.push(KdlParseError::InvalidKey( 537 | node.name().value().to_string(), 538 | )); 539 | } 540 | } 541 | } 542 | 543 | "mirroring" => { 544 | let mut applied = false; 545 | 546 | if let Some(entry) = node.entries().first() 547 | && let Some(string) = entry.value().as_string() 548 | { 549 | applied = true; 550 | output.mirroring = Some(string.to_string()); 551 | } 552 | if !applied { 553 | errors.push(KdlParseError::InvalidValue { 554 | key: "mirroring".to_string(), 555 | value: node.entries().to_vec(), 556 | }); 557 | } 558 | } 559 | 560 | "xwayland_primary" => { 561 | let mut applied = false; 562 | if let Some(entry) = node.entries().first() 563 | && let Some(val) = entry.value().as_bool() 564 | { 565 | applied = true; 566 | output.xwayland_primary = Some(val); 567 | } 568 | if !applied { 569 | errors.push(KdlParseError::InvalidValue { 570 | key: "xwayland_primary".to_string(), 571 | value: node.entries().to_vec(), 572 | }); 573 | } 574 | } 575 | 576 | _ => errors.push(KdlParseError::InvalidKey(node.name().value().to_string())), 577 | }; 578 | } 579 | 580 | output.name = name.to_owned(); 581 | 582 | outputs.outputs.insert(output); 583 | } 584 | if errors.is_empty() { 585 | Ok(outputs) 586 | } else { 587 | Err(KdlParseWithError { 588 | list: outputs, 589 | errors, 590 | }) 591 | } 592 | } 593 | } 594 | 595 | impl From for KdlDocument { 596 | fn from(value: List) -> Self { 597 | let mut doc = KdlDocument::new(); 598 | 599 | for (_output_key, output) in value.outputs.iter() { 600 | let mut output_node = kdl::KdlNode::new("output"); 601 | 602 | // Serial number (if any) 603 | if !output.serial_number.is_empty() { 604 | output_node.push(output.serial_number.clone()); 605 | } 606 | 607 | // Display adapter name (unnamed) 608 | output_node.push(output.name.clone()); 609 | 610 | // Additional entries: enabled (named) 611 | output_node.push(("enabled", output.enabled)); 612 | 613 | // Children: description, physical, position, scale, transform, adaptive_sync, adaptive_sync_support, mirroring, xwayland_primary, modes 614 | let mut children = KdlDocument::new(); 615 | 616 | // description node 617 | if output.make.is_some() || !output.model.is_empty() { 618 | let mut desc_node = kdl::KdlNode::new("description"); 619 | if let Some(make) = &output.make { 620 | desc_node.push(("make", make.clone())); 621 | } 622 | if !output.model.is_empty() { 623 | desc_node.push(("model", output.model.clone())); 624 | } 625 | children.nodes_mut().push(desc_node); 626 | } 627 | 628 | // physical node 629 | children.nodes_mut().push({ 630 | let mut node = kdl::KdlNode::new("physical"); 631 | node.push(output.physical.0 as i128); 632 | node.push(output.physical.1 as i128); 633 | node 634 | }); 635 | 636 | // position node 637 | children.nodes_mut().push({ 638 | let mut node = kdl::KdlNode::new("position"); 639 | node.push(output.position.0 as i128); 640 | node.push(output.position.1 as i128); 641 | node 642 | }); 643 | 644 | // scale node 645 | children.nodes_mut().push({ 646 | let mut node = kdl::KdlNode::new("scale"); 647 | node.push(output.scale); 648 | node 649 | }); 650 | 651 | // transform node 652 | if let Some(transform) = output.transform { 653 | let mut node = kdl::KdlNode::new("transform"); 654 | node.push(transform.to_string()); 655 | children.nodes_mut().push(node); 656 | } 657 | 658 | // adaptive_sync node 659 | if let Some(adaptive_sync) = output.adaptive_sync { 660 | let mut node = kdl::KdlNode::new("adaptive_sync"); 661 | node.push(KdlEntry::new(adaptive_sync)); 662 | children.nodes_mut().push(node); 663 | } 664 | 665 | if let Some(adaptive_sync_availability) = output.adaptive_sync_availability { 666 | let mut node = kdl::KdlNode::new("adaptive_sync_support"); 667 | node.push(KdlEntry::new(adaptive_sync_availability)); 668 | children.nodes_mut().push(node); 669 | } 670 | 671 | // mirroring node 672 | if let Some(mirroring) = &output.mirroring { 673 | let mut node = kdl::KdlNode::new("mirroring"); 674 | node.push(mirroring.clone()); 675 | children.nodes_mut().push(node); 676 | } 677 | 678 | // xwayland_primary node 679 | if let Some(xwayland_primary) = output.xwayland_primary { 680 | let mut node = kdl::KdlNode::new("xwayland_primary"); 681 | node.push(xwayland_primary); 682 | children.nodes_mut().push(node); 683 | } 684 | 685 | // modes node 686 | let mut modes_node = kdl::KdlNode::new("modes"); 687 | let mut modes_children = KdlDocument::new(); 688 | 689 | for mode_key in &output.modes { 690 | if let Some(mode) = value.modes.get(*mode_key) { 691 | let mut mode_node = kdl::KdlNode::new("mode"); 692 | mode_node.push(mode.size.0 as i128); 693 | mode_node.push(mode.size.1 as i128); 694 | mode_node.push(mode.refresh_rate as i128); 695 | 696 | if output.current == Some(*mode_key) { 697 | mode_node.push(("current", true)); 698 | } 699 | if mode.preferred { 700 | mode_node.push(("preferred", true)); 701 | } 702 | modes_children.nodes_mut().push(mode_node); 703 | } 704 | } 705 | 706 | if !modes_children.nodes().is_empty() { 707 | modes_node.set_children(modes_children); 708 | children.nodes_mut().push(modes_node); 709 | } 710 | 711 | output_node.set_children(children); 712 | 713 | doc.nodes_mut().push(output_node); 714 | } 715 | 716 | doc 717 | } 718 | } 719 | #[cfg(test)] 720 | 721 | mod test { 722 | use super::*; 723 | use kdl::KdlDocument; 724 | 725 | #[test] 726 | fn test_kdl_serialization_deserialization() { 727 | let mut list = List::default(); 728 | 729 | let mode1 = Mode { 730 | size: (1920, 1080), 731 | refresh_rate: 60000, 732 | preferred: true, 733 | }; 734 | let mode2 = Mode { 735 | size: (1280, 720), 736 | refresh_rate: 60000, 737 | preferred: false, 738 | }; 739 | 740 | let mode1_key = list.modes.insert(mode1); 741 | let mode2_key = list.modes.insert(mode2); 742 | 743 | let output = Output { 744 | serial_number: String::new(), 745 | name: "HDMI-A-1".to_string(), 746 | enabled: true, 747 | mirroring: Some("eDP-1".to_string()), 748 | make: Some("Hello".to_string()), 749 | model: "Hi".to_string(), 750 | physical: (344, 194), 751 | position: (0, 0), 752 | scale: 1.0, 753 | transform: Some(Transform::Normal), 754 | modes: vec![mode1_key, mode2_key], 755 | current: Some(mode1_key), 756 | adaptive_sync: Some(AdaptiveSyncState::Auto), 757 | adaptive_sync_availability: Some(AdaptiveSyncAvailability::Supported), 758 | xwayland_primary: Some(true), 759 | }; 760 | 761 | list.outputs.insert(output); 762 | 763 | // Serialize to KDL 764 | let kdl_doc: KdlDocument = list.clone().into(); 765 | let kdl_string = kdl_doc.to_string(); 766 | 767 | // Parse back from KDL 768 | let parsed_doc: KdlDocument = kdl_string.parse().expect("KDL parse failed"); 769 | let parsed_list = List::try_from(parsed_doc) 770 | .map_err(|e| { 771 | for err in &e.errors { 772 | eprintln!("{:?}", err); 773 | } 774 | e 775 | }) 776 | .expect("KDL deserialization failed"); 777 | 778 | // Compare the original and parsed List 779 | // Since SlotMap keys are not preserved, compare the Output fields and Mode values 780 | let orig_output = list.outputs.values().next().unwrap(); 781 | let parsed_output = parsed_list.outputs.values().next().unwrap(); 782 | 783 | assert_eq!(orig_output.serial_number, parsed_output.serial_number); 784 | assert_eq!(orig_output.name, parsed_output.name); 785 | assert_eq!(orig_output.enabled, parsed_output.enabled); 786 | assert_eq!(orig_output.mirroring, parsed_output.mirroring); 787 | assert_eq!(orig_output.make, parsed_output.make); 788 | assert_eq!(orig_output.model, parsed_output.model); 789 | assert_eq!(orig_output.physical, parsed_output.physical); 790 | assert_eq!(orig_output.position, parsed_output.position); 791 | assert_eq!(orig_output.scale, parsed_output.scale); 792 | assert_eq!(orig_output.transform, parsed_output.transform); 793 | assert_eq!(orig_output.adaptive_sync, parsed_output.adaptive_sync); 794 | assert_eq!( 795 | orig_output.adaptive_sync_availability, 796 | parsed_output.adaptive_sync_availability 797 | ); 798 | assert_eq!(orig_output.xwayland_primary, parsed_output.xwayland_primary); 799 | 800 | // Compare modes by value (order should be preserved) 801 | let orig_modes: Vec<_> = orig_output.modes.iter().map(|k| &list.modes[*k]).collect(); 802 | let parsed_modes: Vec<_> = parsed_output 803 | .modes 804 | .iter() 805 | .map(|k| &parsed_list.modes[*k]) 806 | .collect(); 807 | assert_eq!(orig_modes.len(), parsed_modes.len()); 808 | for (a, b) in orig_modes.iter().zip(parsed_modes.iter()) { 809 | assert_eq!(a.size, b.size); 810 | assert_eq!(a.refresh_rate, b.refresh_rate); 811 | assert_eq!(a.preferred, b.preferred); 812 | } 813 | } 814 | } 815 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | pub mod align; 5 | 6 | use clap::{Parser, ValueEnum}; 7 | use cosmic_randr::Message; 8 | use cosmic_randr::context::HeadConfiguration; 9 | use cosmic_randr::{AdaptiveSyncAvailability, AdaptiveSyncStateExt, Context}; 10 | use cosmic_randr_shell::{KdlParseWithError, List}; 11 | use nu_ansi_term::{Color, Style}; 12 | use std::fmt::{Display, Write as FmtWrite}; 13 | use std::io::Write; 14 | use wayland_client::protocol::wl_output::Transform as WlTransform; 15 | use wayland_client::{EventQueue, Proxy}; 16 | 17 | /// Display and configure wayland outputs 18 | #[derive(clap::Parser, Debug)] 19 | #[command(author, version, about, long_about = None)] 20 | struct Cli { 21 | #[command(subcommand)] 22 | command: Commands, 23 | } 24 | 25 | #[derive(clap::Args, Debug)] 26 | struct Mode { 27 | /// Name of the output that the display is connected to. 28 | output: String, 29 | /// Specifies the width of the output picture. 30 | width: i32, 31 | /// Specifies the height of the output picture. 32 | height: i32, 33 | /// Specifies the refresh rate to apply to the output. 34 | #[arg(long)] 35 | refresh: Option, 36 | /// Specfies the adaptive sync mode to apply to the output. 37 | #[arg(long, value_enum)] 38 | adaptive_sync: Option, 39 | /// Position the output within this x pixel coordinate. 40 | #[arg(long, allow_hyphen_values(true))] 41 | pos_x: Option, 42 | /// Position the output within this y pixel coordinate. 43 | #[arg(long, allow_hyphen_values(true))] 44 | pos_y: Option, 45 | /// Changes the dimensions of the output picture. 46 | #[arg(long)] 47 | scale: Option, 48 | /// Tests the output configuration without applying it. 49 | #[arg(long)] 50 | test: bool, 51 | /// Specifies a transformation matrix to apply to the output. 52 | #[arg(long, value_enum)] 53 | transform: Option, 54 | } 55 | 56 | impl Mode { 57 | fn to_head_config(&self) -> HeadConfiguration { 58 | HeadConfiguration { 59 | size: Some((self.width as u32, self.height as u32)), 60 | refresh: self.refresh, 61 | adaptive_sync: self 62 | .adaptive_sync 63 | .map(|adaptive_sync| adaptive_sync.adaptive_sync_state_ext()), 64 | pos: (self.pos_x.is_some() || self.pos_y.is_some()).then(|| { 65 | ( 66 | self.pos_x.unwrap_or_default(), 67 | self.pos_y.unwrap_or_default(), 68 | ) 69 | }), 70 | scale: self.scale, 71 | transform: self.transform.map(|transform| transform.wl_transform()), 72 | } 73 | } 74 | } 75 | 76 | #[derive(clap::Subcommand, Debug)] 77 | enum Commands { 78 | /// Disable a display 79 | Disable { output: String }, 80 | 81 | /// Enable a display 82 | Enable { output: String }, 83 | 84 | /// Mirror a display 85 | Mirror { output: String, from: String }, 86 | 87 | /// List available output heads and modes. 88 | List { 89 | /// Display in KDL format. 90 | #[arg(long)] 91 | kdl: bool, 92 | }, 93 | 94 | /// Set a mode for a display. 95 | Mode(Mode), 96 | 97 | /// Set position of display. 98 | Position { 99 | output: String, 100 | x: i32, 101 | y: i32, 102 | #[arg(long)] 103 | test: bool, 104 | }, 105 | 106 | /// Xwayland compatibility options 107 | #[command(arg_required_else_help = true)] 108 | Xwayland { 109 | /// Set output as primary 110 | #[arg(long, value_name = "OUTPUT")] 111 | primary: Option, 112 | /// Unset primary output 113 | #[arg(long, conflicts_with = "primary")] 114 | no_primary: bool, 115 | }, 116 | 117 | /// List of output configurations to apply in KDL format 118 | /// Read via stdin 119 | Kdl, 120 | } 121 | 122 | #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, ValueEnum)] 123 | pub enum Transform { 124 | Normal, 125 | Rotate90, 126 | Rotate180, 127 | Rotate270, 128 | Flipped, 129 | Flipped90, 130 | Flipped180, 131 | Flipped270, 132 | } 133 | 134 | impl Display for Transform { 135 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 136 | f.write_str(match self { 137 | Transform::Normal => "normal", 138 | Transform::Rotate90 => "rotate90", 139 | Transform::Rotate180 => "rotate180", 140 | Transform::Rotate270 => "rotate270", 141 | Transform::Flipped => "flipped", 142 | Transform::Flipped90 => "flipped90", 143 | Transform::Flipped180 => "flipped180", 144 | Transform::Flipped270 => "flipped270", 145 | }) 146 | } 147 | } 148 | 149 | impl TryFrom for Transform { 150 | type Error = &'static str; 151 | 152 | fn try_from(transform: WlTransform) -> Result { 153 | Ok(match transform { 154 | WlTransform::Normal => Transform::Normal, 155 | WlTransform::_90 => Transform::Rotate90, 156 | WlTransform::_180 => Transform::Rotate180, 157 | WlTransform::_270 => Transform::Rotate270, 158 | WlTransform::Flipped => Transform::Flipped, 159 | WlTransform::Flipped90 => Transform::Flipped90, 160 | WlTransform::Flipped180 => Transform::Flipped180, 161 | WlTransform::Flipped270 => Transform::Flipped270, 162 | _ => return Err("unknown wl_transform variant"), 163 | }) 164 | } 165 | } 166 | 167 | impl Transform { 168 | #[must_use] 169 | pub fn wl_transform(self) -> WlTransform { 170 | match self { 171 | Transform::Normal => WlTransform::Normal, 172 | Transform::Rotate90 => WlTransform::_90, 173 | Transform::Rotate180 => WlTransform::_180, 174 | Transform::Rotate270 => WlTransform::_270, 175 | Transform::Flipped => WlTransform::Flipped, 176 | Transform::Flipped90 => WlTransform::Flipped90, 177 | Transform::Flipped180 => WlTransform::Flipped180, 178 | Transform::Flipped270 => WlTransform::Flipped270, 179 | } 180 | } 181 | } 182 | 183 | #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] 184 | pub enum AdaptiveSync { 185 | #[value(name = "true")] 186 | Always, 187 | #[value(name = "automatic")] 188 | Automatic, 189 | #[value(name = "false")] 190 | Disabled, 191 | } 192 | 193 | impl Display for AdaptiveSync { 194 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 195 | f.write_str(match self { 196 | AdaptiveSync::Always => "true", 197 | AdaptiveSync::Automatic => "automatic", 198 | AdaptiveSync::Disabled => "false", 199 | }) 200 | } 201 | } 202 | 203 | impl TryFrom for AdaptiveSync { 204 | type Error = &'static str; 205 | 206 | fn try_from(value: AdaptiveSyncStateExt) -> Result { 207 | Ok(match value { 208 | AdaptiveSyncStateExt::Always => AdaptiveSync::Always, 209 | AdaptiveSyncStateExt::Automatic => AdaptiveSync::Automatic, 210 | AdaptiveSyncStateExt::Disabled => AdaptiveSync::Disabled, 211 | _ => return Err("unknown adaptive_sync_state_ext variant"), 212 | }) 213 | } 214 | } 215 | 216 | impl AdaptiveSync { 217 | #[must_use] 218 | pub fn adaptive_sync_state_ext(self) -> AdaptiveSyncStateExt { 219 | match self { 220 | AdaptiveSync::Always => AdaptiveSyncStateExt::Always, 221 | AdaptiveSync::Automatic => AdaptiveSyncStateExt::Automatic, 222 | AdaptiveSync::Disabled => AdaptiveSyncStateExt::Disabled, 223 | } 224 | } 225 | } 226 | 227 | #[tokio::main(flavor = "current_thread")] 228 | async fn main() -> Result<(), Box> { 229 | let cli = Cli::parse(); 230 | 231 | let (message_tx, message_rx) = cosmic_randr::channel(); 232 | 233 | let (context, event_queue) = cosmic_randr::connect(message_tx)?; 234 | 235 | let mut app = App { 236 | context, 237 | event_queue, 238 | message_rx, 239 | }; 240 | 241 | match cli.command { 242 | Commands::Enable { output } => app.enable(&output).await, 243 | 244 | Commands::Mirror { output, from } => app.mirror(&output, &from).await, 245 | 246 | Commands::Disable { output } => app.disable(&output).await, 247 | 248 | Commands::List { kdl } => app.list(kdl).await, 249 | 250 | Commands::Mode(mode) => app.mode(mode).await, 251 | 252 | Commands::Position { output, x, y, test } => app.set_position(&output, x, y, test).await, 253 | 254 | Commands::Xwayland { primary, .. } => app.set_xwayland_primary(primary.as_deref()).await, 255 | 256 | Commands::Kdl => { 257 | let mut input = String::new(); 258 | use tokio::io::AsyncReadExt; 259 | tokio::io::stdin() 260 | .read_to_string(&mut input) 261 | .await 262 | .expect("Failed to read stdin"); 263 | let doc = kdl::KdlDocument::parse(&input).expect("Invalid KDL"); 264 | 265 | let list: List = match cosmic_randr_shell::List::try_from(doc) { 266 | Ok(l) => l, 267 | Err(KdlParseWithError { list, errors }) => { 268 | eprintln!("{errors:?}"); 269 | list 270 | } 271 | }; 272 | app.apply_list(list).await 273 | } 274 | } 275 | } 276 | 277 | struct App { 278 | context: Context, 279 | event_queue: EventQueue, 280 | message_rx: cosmic_randr::Receiver, 281 | } 282 | 283 | impl App { 284 | // Ignores any messages other than `ManagerDone` 285 | async fn dispatch_until_manager_done(&mut self) -> Result<(), cosmic_randr::Error> { 286 | loop { 287 | let watcher = async { 288 | while let Some(msg) = self.message_rx.recv().await { 289 | if matches!(msg, Message::ManagerDone) { 290 | return true; 291 | } 292 | } 293 | 294 | false 295 | }; 296 | 297 | tokio::select! { 298 | is_done = watcher => { 299 | if is_done { 300 | break 301 | } 302 | }, 303 | 304 | result = self.context.dispatch(&mut self.event_queue) => { 305 | result?; 306 | } 307 | }; 308 | } 309 | 310 | Ok(()) 311 | } 312 | 313 | /// # Errors 314 | /// 315 | /// Returns error if the message receiver fails, dispach fails, or a configuration failed. 316 | async fn receive_config_messages(&mut self) -> Result<(), Box> { 317 | loop { 318 | while let Some(message) = self.message_rx.try_recv() { 319 | if config_message(Some(message))? { 320 | return Ok(()); 321 | } 322 | } 323 | 324 | self.context.dispatch(&mut self.event_queue).await?; 325 | } 326 | } 327 | 328 | async fn enable(&mut self, output: &str) -> Result<(), Box> { 329 | self.dispatch_until_manager_done().await?; 330 | enable(&mut self.context, output)?; 331 | self.receive_config_messages().await?; 332 | 333 | Ok(()) 334 | } 335 | 336 | async fn mirror(&mut self, output: &str, from: &str) -> Result<(), Box> { 337 | self.dispatch_until_manager_done().await?; 338 | mirror(&mut self.context, output, from)?; 339 | self.receive_config_messages().await 340 | } 341 | 342 | async fn disable(&mut self, output: &str) -> Result<(), Box> { 343 | self.dispatch_until_manager_done().await?; 344 | disable(&mut self.context, output)?; 345 | self.receive_config_messages().await 346 | } 347 | 348 | async fn list(&mut self, kdl: bool) -> Result<(), Box> { 349 | self.dispatch_until_manager_done().await?; 350 | for head in self.context.output_heads.values_mut() { 351 | head.modes 352 | .sort_unstable_by(|_, either, _, or| either.cmp(or)); 353 | } 354 | 355 | if kdl { 356 | list_kdl(&self.context); 357 | } else { 358 | list(&self.context); 359 | } 360 | 361 | Ok(()) 362 | } 363 | 364 | async fn mode(&mut self, mode: Mode) -> Result<(), Box> { 365 | self.dispatch_until_manager_done().await?; 366 | set_mode(&mut self.context, &mode)?; 367 | self.receive_config_messages().await?; 368 | self.auto_correct_offsets(&mode.output, mode.test).await 369 | } 370 | 371 | async fn set_position( 372 | &mut self, 373 | output: &str, 374 | x: i32, 375 | y: i32, 376 | test: bool, 377 | ) -> Result<(), Box> { 378 | self.dispatch_until_manager_done().await?; 379 | set_position(&mut self.context, output, x, y, test)?; 380 | self.receive_config_messages().await?; 381 | self.auto_correct_offsets(output, test).await 382 | } 383 | 384 | async fn set_xwayland_primary( 385 | &mut self, 386 | output: Option<&str>, 387 | ) -> Result<(), Box> { 388 | self.dispatch_until_manager_done().await?; 389 | self.context.set_xwayland_primary(output)?; 390 | self.dispatch_until_manager_done().await?; 391 | Ok(()) 392 | } 393 | 394 | // Offset outputs in case of negative positioning. 395 | async fn auto_correct_offsets( 396 | &mut self, 397 | output: &str, 398 | test: bool, 399 | ) -> Result<(), Box> { 400 | // Get the position and dimensions of the moved display. 401 | let Some(ref mut active_output) = self 402 | .context 403 | .output_heads 404 | .values() 405 | .find(|head| head.name == output) 406 | .and_then(|head| { 407 | let mode = head.current_mode.as_ref()?; 408 | let mode = head.modes.get(mode)?; 409 | 410 | let (width, height) = if head.transform.is_none_or(|wl_transform| { 411 | Transform::try_from(wl_transform).map_or(true, is_landscape) 412 | }) { 413 | (mode.width, mode.height) 414 | } else { 415 | (mode.height, mode.width) 416 | }; 417 | 418 | Some(align::Rectangle { 419 | x: head.position_x as f32, 420 | y: head.position_y as f32, 421 | width: width as f32 / head.scale as f32, 422 | height: height as f32 / head.scale as f32, 423 | }) 424 | }) 425 | else { 426 | return Ok(()); 427 | }; 428 | 429 | // Create an iterator of other outputs and their positions and dimensions. 430 | let other_outputs = self.context.output_heads.values().filter_map(|head| { 431 | if head.name == output { 432 | None 433 | } else { 434 | let mode = head.current_mode.as_ref()?; 435 | let mode = head.modes.get(mode)?; 436 | 437 | if !head.enabled || head.mirroring.is_some() { 438 | return None; 439 | } 440 | 441 | let (width, height) = if head.transform.is_none_or(|wl_transform| { 442 | Transform::try_from(wl_transform).map_or(true, is_landscape) 443 | }) { 444 | (mode.width, mode.height) 445 | } else { 446 | (mode.height, mode.width) 447 | }; 448 | 449 | Some(align::Rectangle { 450 | x: head.position_x as f32, 451 | y: head.position_y as f32, 452 | width: width as f32 / head.scale as f32, 453 | height: height as f32 / head.scale as f32, 454 | }) 455 | } 456 | }); 457 | 458 | // Align outputs such that there are no gaps. 459 | align::display(active_output, other_outputs); 460 | 461 | // Calculate how much to offset the position of each display to be aligned against (0,0) 462 | let mut offset = self 463 | .context 464 | .output_heads 465 | .values() 466 | .filter(|head| head.enabled && head.mirroring.is_none()) 467 | .fold((i32::MAX, i32::MAX), |offset, head| { 468 | let (x, y) = if output == head.name { 469 | (active_output.x as i32, active_output.y as i32) 470 | } else { 471 | (head.position_x, head.position_y) 472 | }; 473 | 474 | (offset.0.min(x), offset.1.min(y)) 475 | }); 476 | 477 | // Reposition each display with that offset 478 | let updates = self 479 | .context 480 | .output_heads 481 | .values() 482 | .filter(|head| head.enabled && head.mirroring.is_none()) 483 | .map(|head| { 484 | let (x, y) = if output == head.name { 485 | (active_output.x as i32, active_output.y as i32) 486 | } else { 487 | (head.position_x, head.position_y) 488 | }; 489 | 490 | (head.name.clone(), x - offset.0, y - offset.1) 491 | }) 492 | .collect::>(); 493 | 494 | // Adjust again to (0,0) baseline 495 | offset = updates 496 | .iter() 497 | .fold((i32::MAX, i32::MAX), |offset, (_, x, y)| { 498 | (offset.0.min(*x), offset.1.min(*y)) 499 | }); 500 | 501 | // Apply new positions 502 | for (name, mut x, mut y) in updates { 503 | x -= offset.0; 504 | y -= offset.1; 505 | set_position(&mut self.context, &name, x, y, test)?; 506 | self.receive_config_messages().await?; 507 | } 508 | 509 | Ok(()) 510 | } 511 | 512 | /// Apply requested output configuration all at once using the protocol 513 | async fn apply_list(&mut self, mut list: List) -> Result<(), Box> { 514 | self.dispatch_until_manager_done().await?; 515 | 516 | // convert list to hashmap of output heads 517 | 518 | let mut current_heads: Vec<_> = self.context.output_heads.values_mut().collect(); 519 | 520 | for (_, head) in list.outputs.drain() { 521 | for current in &mut current_heads { 522 | if current.name == head.name 523 | && current.make == head.clone().make.unwrap_or_default() 524 | && current.model == head.model 525 | { 526 | current.adaptive_sync = head.adaptive_sync.map(|sync| match sync { 527 | cosmic_randr_shell::AdaptiveSyncState::Always => { 528 | AdaptiveSyncStateExt::Always 529 | } 530 | cosmic_randr_shell::AdaptiveSyncState::Auto => { 531 | AdaptiveSyncStateExt::Automatic 532 | } 533 | cosmic_randr_shell::AdaptiveSyncState::Disabled => { 534 | AdaptiveSyncStateExt::Disabled 535 | } 536 | }); 537 | current.enabled = head.enabled; 538 | current.position_x = head.position.0; 539 | current.position_y = head.position.1; 540 | current.scale = head.scale; 541 | current.transform = head.transform.map(|t| match t { 542 | cosmic_randr_shell::Transform::Normal => WlTransform::Normal, 543 | cosmic_randr_shell::Transform::Rotate90 => WlTransform::_90, 544 | cosmic_randr_shell::Transform::Rotate180 => WlTransform::_180, 545 | cosmic_randr_shell::Transform::Rotate270 => WlTransform::_270, 546 | cosmic_randr_shell::Transform::Flipped => WlTransform::Flipped, 547 | cosmic_randr_shell::Transform::Flipped90 => WlTransform::Flipped90, 548 | cosmic_randr_shell::Transform::Flipped180 => WlTransform::Flipped180, 549 | cosmic_randr_shell::Transform::Flipped270 => WlTransform::Flipped270, 550 | }); 551 | current.mirroring = head.mirroring.clone(); 552 | current.xwayland_primary = head.xwayland_primary; 553 | if let Some(cur_mode_id) = head 554 | .current 555 | .and_then(|k| list.modes.get(k)) 556 | .and_then(|mode_info| { 557 | current.modes.iter_mut().find_map(|(id, mode)| { 558 | if mode.width == mode_info.size.0 as i32 559 | && mode.height == mode_info.size.1 as i32 560 | { 561 | mode.refresh = mode_info.refresh_rate as i32; 562 | mode.preferred = mode_info.preferred; 563 | Some(id.clone()) 564 | } else { 565 | None 566 | } 567 | }) 568 | }) 569 | { 570 | current.current_mode = Some(cur_mode_id); 571 | } 572 | 573 | break; 574 | } 575 | } 576 | } 577 | 578 | self.context.apply_current_config().await?; 579 | self.receive_config_messages().await 580 | } 581 | } 582 | 583 | /// Handles output configuration messages. 584 | /// 585 | /// # Errors 586 | /// 587 | /// - Error if the output configuration returned an error. 588 | /// - Or if the channel is disconnected. 589 | pub fn config_message( 590 | message: Option, 591 | ) -> Result> { 592 | match message { 593 | Some(cosmic_randr::Message::ConfigurationCancelled) => { 594 | Err("configuration cancelled".into()) 595 | } 596 | 597 | Some(cosmic_randr::Message::ConfigurationFailed) => Err("configuration failed".into()), 598 | 599 | Some(cosmic_randr::Message::ConfigurationSucceeded) => Ok(true), 600 | _ => Ok(false), 601 | } 602 | } 603 | 604 | fn disable(context: &mut Context, output: &str) -> Result<(), Box> { 605 | let mut config = context.create_output_config(); 606 | config.disable_head(output)?; 607 | config.apply(); 608 | 609 | Ok(()) 610 | } 611 | 612 | fn enable(context: &mut Context, output: &str) -> Result<(), Box> { 613 | let mut config = context.create_output_config(); 614 | config.enable_head(output, None)?; 615 | config.apply(); 616 | 617 | Ok(()) 618 | } 619 | 620 | fn mirror( 621 | context: &mut Context, 622 | output: &str, 623 | from: &str, 624 | ) -> Result<(), Box> { 625 | let mut config = context.create_output_config(); 626 | config.mirror_head(output, from, None)?; 627 | config.apply(); 628 | 629 | Ok(()) 630 | } 631 | 632 | fn list(context: &Context) { 633 | let mut output = String::new(); 634 | let mut resolution = String::new(); 635 | 636 | for head in context.output_heads.values() { 637 | #[allow(clippy::ignored_unit_patterns)] 638 | let _res = fomat_macros::witeln!( 639 | &mut output, 640 | (Style::new().bold().paint(&head.name)) " " 641 | if head.enabled { 642 | if let Some(from) = head.mirroring.as_ref() { 643 | (Color::Blue.bold().paint(format!("(mirroring \"{}\")", from))) 644 | } else { 645 | (Color::Green.bold().paint("(enabled)")) 646 | } 647 | } else { 648 | (Color::Red.bold().paint("(disabled)")) 649 | } 650 | if !head.make.is_empty() { 651 | (Color::Yellow.bold().paint("\n Make: ")) (head.make) 652 | } 653 | (Color::Yellow.bold().paint("\n Model: ")) 654 | (head.model) 655 | (Color::Yellow.bold().paint("\n Physical Size: ")) 656 | (head.physical_width) " x " (head.physical_height) " mm" 657 | (Color::Yellow.bold().paint("\n Position: ")) 658 | (head.position_x) "," (head.position_y) 659 | (Color::Yellow.bold().paint("\n Scale: ")) ((head.scale * 100.0) as i32) "%" 660 | if let Some(wl_transform) = head.transform { 661 | if let Ok(transform) = Transform::try_from(wl_transform) { 662 | (Color::Yellow.bold().paint("\n Transform: ")) (transform) 663 | } 664 | } 665 | if let Some(available) = head.adaptive_sync_support { 666 | (Color::Yellow.bold().paint("\n Adaptive Sync Support: ")) 667 | (match available { 668 | AdaptiveSyncAvailability::Supported | AdaptiveSyncAvailability::RequiresModeset => Color::Green.paint("true"), 669 | _ => Color::Red.paint("false"), 670 | }) 671 | } 672 | if let Some(sync) = head.adaptive_sync { 673 | (Color::Yellow.bold().paint("\n Adaptive Sync: ")) 674 | (match sync { 675 | AdaptiveSyncStateExt::Always => { 676 | Color::Green.paint("true") 677 | }, 678 | AdaptiveSyncStateExt::Automatic => { 679 | Color::Green.paint("automatic") 680 | }, 681 | _ => { 682 | Color::Red.paint("false") 683 | } 684 | }) 685 | } 686 | if let Some(xwayland_primary) = head.xwayland_primary { 687 | (Color::Yellow.bold().paint("\n Xwayland primary: ")) 688 | (if xwayland_primary { 689 | Color::Green.paint("true") 690 | } else { 691 | Color::Red.paint("false") 692 | }) 693 | } 694 | (Color::Yellow.bold().paint("\n\n Modes:")) 695 | ); 696 | 697 | for mode in head.modes.values() { 698 | resolution.clear(); 699 | let _res = write!(&mut resolution, "{}x{}", mode.width, mode.height); 700 | 701 | let _res = writeln!( 702 | &mut output, 703 | " {:>9} @ {}{}{}", 704 | Color::Magenta.paint(format!("{resolution:>9}")), 705 | Color::Cyan.paint(format!( 706 | "{:>3}.{:03} Hz", 707 | mode.refresh / 1000, 708 | mode.refresh % 1000 709 | )), 710 | if head.current_mode.as_ref() == Some(&mode.wlr_mode.id()) { 711 | Color::Purple.bold().paint(" (current)") 712 | } else { 713 | Color::default().paint("") 714 | }, 715 | if mode.preferred { 716 | Color::Green.bold().paint(" (preferred)") 717 | } else { 718 | Color::default().paint("") 719 | } 720 | ); 721 | } 722 | } 723 | 724 | let mut stdout = std::io::stdout().lock(); 725 | let _res = stdout.write_all(output.as_bytes()); 726 | let _res = stdout.flush(); 727 | } 728 | 729 | fn list_kdl(context: &Context) { 730 | let mut output = String::new(); 731 | 732 | for head in context.output_heads.values() { 733 | #[allow(clippy::ignored_unit_patterns)] 734 | let _res = fomat_macros::witeln!( 735 | &mut output, 736 | "output \"" (head.name) "\" enabled=#" (head.enabled) " {\n" 737 | " description" 738 | if !head.make.is_empty() { " make=\"" (head.make) "\"" } 739 | " model=\"" (head.model) "\"\n" 740 | " physical " (head.physical_width) " " (head.physical_height) "\n" 741 | " position " (head.position_x) " " (head.position_y) "\n" 742 | " scale " (format!("{:.2}", head.scale)) "\n" 743 | if let Some(mirroring) = head.mirroring.as_ref() { 744 | " mirroring \"" (mirroring) "\"\n" 745 | } 746 | if let Some(wl_transform) = head.transform { 747 | if let Ok(transform) = Transform::try_from(wl_transform) { 748 | " transform \"" (transform) "\"\n" 749 | } 750 | } 751 | if let Some(available) = head.adaptive_sync_support { 752 | " adaptive_sync_support " 753 | (match available { 754 | AdaptiveSyncAvailability::Supported => "#true", 755 | AdaptiveSyncAvailability::RequiresModeset => "\"requires_modeset\"", 756 | _ => "#false", 757 | }) 758 | "\n" 759 | } 760 | if let Some(sync) = head.adaptive_sync { 761 | " adaptive_sync " 762 | (match sync { 763 | AdaptiveSyncStateExt::Always => "#true", 764 | AdaptiveSyncStateExt::Automatic => "\"automatic\"", 765 | _ => "#false", 766 | }) 767 | "\n" 768 | } 769 | if let Some(xwayland_primary) = head.xwayland_primary { 770 | " xwayland_primary " 771 | (if xwayland_primary { 772 | "#true" 773 | } else { 774 | "#false" 775 | }) 776 | "\n" 777 | } 778 | if !head.serial_number.is_empty() { 779 | " serial_number \"" (head.serial_number) "\"\n" 780 | } 781 | " modes {" 782 | ); 783 | 784 | for mode in head.modes.values() { 785 | let _res = writeln!( 786 | &mut output, 787 | " mode {} {} {}{}{}", 788 | mode.width, 789 | mode.height, 790 | mode.refresh, 791 | if head.current_mode.as_ref() == Some(&mode.wlr_mode.id()) { 792 | " current=#true" 793 | } else { 794 | "" 795 | }, 796 | if mode.preferred { 797 | " preferred=#true" 798 | } else { 799 | "" 800 | }, 801 | ); 802 | } 803 | 804 | let _res = writeln!(&mut output, " }}\n}}"); 805 | } 806 | 807 | let mut stdout = std::io::stdout().lock(); 808 | let _res = stdout.write_all(output.as_bytes()); 809 | let _res = stdout.flush(); 810 | } 811 | 812 | fn set_mode(context: &mut Context, args: &Mode) -> Result<(), Box> { 813 | let mirroring = context 814 | .output_heads 815 | .values() 816 | .find(|output| output.name == args.output) 817 | .and_then(|head| head.mirroring.clone()); 818 | 819 | let mut config = context.create_output_config(); 820 | let head_config = args.to_head_config(); 821 | 822 | if let Some(mirroring_from) = mirroring.filter(|_| head_config.pos.is_none()) { 823 | config.mirror_head(&args.output, &mirroring_from, Some(head_config))?; 824 | } else { 825 | config.enable_head(&args.output, Some(head_config))?; 826 | } 827 | 828 | if args.test { 829 | config.test(); 830 | } else { 831 | config.apply(); 832 | } 833 | 834 | Ok(()) 835 | } 836 | 837 | fn set_position( 838 | context: &mut Context, 839 | name: &str, 840 | x: i32, 841 | y: i32, 842 | test: bool, 843 | ) -> Result<(), Box> { 844 | let mut config = context.create_output_config(); 845 | config.enable_head( 846 | name, 847 | Some(HeadConfiguration { 848 | pos: Some((x, y)), 849 | ..Default::default() 850 | }), 851 | )?; 852 | 853 | if test { 854 | config.test(); 855 | } else { 856 | config.apply(); 857 | } 858 | 859 | Ok(()) 860 | } 861 | 862 | fn is_landscape(transform: Transform) -> bool { 863 | matches!( 864 | transform, 865 | Transform::Normal | Transform::Rotate180 | Transform::Flipped | Transform::Flipped180 866 | ) 867 | } 868 | --------------------------------------------------------------------------------