├── .gitignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.yaml └── workflows │ ├── ubuntu.dockerfile │ ├── set-vars.sh │ └── ci.yml ├── .cargo └── config.toml ├── OpenSans-Regular.ttf ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── wl_drm ├── Cargo.toml └── src │ ├── lib.rs │ └── drm.xml ├── testwl ├── Cargo.toml └── Cargo.lock ├── resources └── xwayland-satellite.service ├── src ├── main.rs ├── xstate │ ├── settings.rs │ └── selection.rs ├── lib.rs └── server │ ├── selection.rs │ ├── decoration.rs │ └── clientside.rs ├── flake.lock ├── Cargo.toml ├── flake.nix ├── README.md ├── ARCHITECTURE.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Supreeeme 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUST_TEST_THREADS = "1" 3 | -------------------------------------------------------------------------------- /OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Supreeeme/xwayland-satellite/HEAD/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Q & A 3 | url: https://github.com/Supreeeme/xwayland-satellite/discussions 4 | about: Need help troubleshooting, or have some other question? Ask here 5 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0.95" 14 | quote = "1.0.37" 15 | syn = { version = "2.0.79", features = ["full"] } 16 | -------------------------------------------------------------------------------- /wl_drm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wl_drm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | wayland-client.workspace = true 10 | wayland-scanner.workspace = true 11 | wayland-server.workspace = true 12 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ENV TZ=Etc/UTC 5 | RUN apt-get update \ 6 | && apt-get install -y xwayland libxcb1 clang libxcb-cursor0 libxcb-cursor-dev curl pkg-config libegl1 \ 7 | && rm -r /var/lib/apt/lists/* 8 | RUN mkdir /run/xwls-test 9 | ENV XDG_RUNTIME_DIR="/run/xwls-test" 10 | -------------------------------------------------------------------------------- /testwl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testwl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | wayland-protocols = { workspace = true, features = ["server", "staging", "unstable"] } 11 | wayland-server.workspace = true 12 | wl_drm = { path = "../wl_drm" } 13 | rustix = { workspace = true, features = ["pipe"] } 14 | rustversion = "1.0.22" 15 | -------------------------------------------------------------------------------- /resources/xwayland-satellite.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Xwayland outside your Wayland 3 | BindsTo=graphical-session.target 4 | PartOf=graphical-session.target 5 | After=graphical-session.target 6 | Requisite=graphical-session.target 7 | 8 | [Service] 9 | Type=notify 10 | NotifyAccess=all 11 | ExecStart=/usr/local/bin/xwayland-satellite 12 | StandardOutput=journal 13 | 14 | [Install] 15 | WantedBy=graphical-session.target 16 | -------------------------------------------------------------------------------- /wl_drm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, non_upper_case_globals)] 2 | pub mod client { 3 | use wayland_client::{self, protocol::*}; 4 | pub mod __interfaces { 5 | use wayland_client::backend as wayland_backend; 6 | use wayland_client::protocol::__interfaces::*; 7 | wayland_scanner::generate_interfaces!("src/drm.xml"); 8 | } 9 | use self::__interfaces::*; 10 | wayland_scanner::generate_client_code!("src/drm.xml"); 11 | } 12 | 13 | pub mod server { 14 | use self::__interfaces::*; 15 | pub use super::client::__interfaces; 16 | use wayland_server::{self, protocol::*}; 17 | wayland_scanner::generate_server_code!("src/drm.xml"); 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: input 6 | id: os 7 | attributes: 8 | label: Operating System 9 | placeholder: Arch Linux, Ubuntu 24.04, Gentoo, etc 10 | validations: 11 | required: true 12 | - type: input 13 | id: comp 14 | attributes: 15 | label: Wayland Compositor 16 | placeholder: Niri, Hyprland, Sway, etc 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: problem 21 | attributes: 22 | label: Describe the issue. 23 | value: | 24 | **Please attach a log if applicable.** 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: steps 29 | attributes: 30 | label: Steps to reproduce 31 | - type: checkboxes 32 | attributes: 33 | label: Please confirm you have reproduced this on the latest commit. 34 | options: 35 | - label: I have reproduced this on the latest commit 36 | required: true 37 | -------------------------------------------------------------------------------- /.github/workflows/set-vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Find the line specifying the version number for the Rust toolchain, then 4 | # extract that version number. Failure to do so aborts the CI run. 5 | msrv=$( 6 | awk '/^rust-version = "[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+"$/ { 7 | print substr($NF, 2, length($NF) - 2) 8 | }' Cargo.toml 9 | ) 10 | if [ -z $msrv ]; then 11 | printf "Could not determine Rust toolchain version\n" 12 | exit 1 13 | fi 14 | 15 | # Check for one of two conditions to regenerate the Docker image: 16 | # (These conditions must be met on `main` and not a PR branch, see ci.yml) 17 | # 1. An annotated tag was found when fetching the tags at depth 1 (tags on HEAD) 18 | # 2. "$UBUNTU_DOCKERFILE" was changed between HEAD and the prior commit (HEAD~1) 19 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 20 | if ! git diff HEAD~1 --quiet -- "$UBUNTU_DOCKERFILE" || 21 | git describe --candidates=0; then 22 | should_build=true 23 | else 24 | should_build=false 25 | fi 26 | 27 | echo "msrv=$msrv" >> "$GITHUB_OUTPUT" 28 | echo "should_build=$should_build" >> "$GITHUB_OUTPUT" 29 | echo "container_path=$REGISTRY/$1" >> "$GITHUB_OUTPUT" 30 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::{FromRawFd, OwnedFd, RawFd}; 2 | 3 | fn main() { 4 | pretty_env_logger::formatted_timed_builder() 5 | .filter_level(log::LevelFilter::Info) 6 | .parse_default_env() 7 | .init(); 8 | xwayland_satellite::main(parse_args()); 9 | } 10 | 11 | struct RealData { 12 | display: Option, 13 | listenfds: Vec, 14 | } 15 | impl xwayland_satellite::RunData for RealData { 16 | fn display(&self) -> Option<&str> { 17 | self.display.as_deref() 18 | } 19 | 20 | fn listenfds(&mut self) -> Vec { 21 | std::mem::take(&mut self.listenfds) 22 | } 23 | } 24 | 25 | fn parse_args() -> RealData { 26 | let mut data = RealData { 27 | display: None, 28 | listenfds: Vec::new(), 29 | }; 30 | 31 | let mut args: Vec<_> = std::env::args().collect(); 32 | if args.len() < 2 { 33 | return data; 34 | } 35 | 36 | // Argument at index 1 is our display name. The rest can be -listenfd. 37 | let mut i = 2; 38 | while i < args.len() { 39 | let arg = &args[i]; 40 | if arg == "-listenfd" { 41 | let next = i + 1; 42 | if next == args.len() { 43 | // Matches the Xwayland error message. 44 | panic!("Required argument to -listenfd not specified"); 45 | } 46 | 47 | let fd: RawFd = args[next].parse().expect("Error parsing -listenfd number"); 48 | // SAFETY: 49 | // - whoever runs the binary must ensure this fd is open and valid. 50 | // - parse_args() must only be called once to avoid double closing. 51 | let fd = unsafe { OwnedFd::from_raw_fd(fd) }; 52 | 53 | data.listenfds.push(fd); 54 | i += 2; 55 | } else if arg == "--test-listenfd-support" { 56 | std::process::exit(0); 57 | } else { 58 | panic!("Unrecognized argument: {arg}"); 59 | } 60 | } 61 | 62 | data.display = Some(args.swap_remove(1)); 63 | 64 | data 65 | } 66 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1764522689, 24 | "narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-25.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1739240901, 52 | "narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["macros", "testwl", "wl_drm"] 3 | 4 | [workspace.dependencies] 5 | wayland-client = "0.31.11" 6 | wayland-protocols = "0.32.9" 7 | wayland-scanner = "0.31.7" 8 | wayland-server = "0.31.10" 9 | rustix = "1.1.2" 10 | 11 | [workspace.lints.clippy] 12 | all = "deny" 13 | 14 | [workspace.package] 15 | rust-version = "1.83.0" 16 | 17 | [package] 18 | name = "xwayland-satellite" 19 | version = "0.8.0" 20 | authors = ["Shawn Wallace"] 21 | license = "MPL-2.0" 22 | description = "xwayland-satellite grants rootless Xwayland integration to any Wayland compositor implementing xdg_wm_base and viewporter. This is particularly useful for compositors that (understandably) do not want to go through implementing support for rootless Xwayland themselves." 23 | edition = "2021" 24 | rust-version.workspace = true 25 | 26 | [lints] 27 | workspace = true 28 | 29 | [lib] 30 | crate-type = ["lib"] 31 | 32 | [dependencies] 33 | bitflags = "2.5.0" 34 | rustix = { workspace = true, features = ["event"] } 35 | wayland-client.workspace = true 36 | wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] } 37 | wayland-server.workspace = true 38 | xcb = { version = "1.6.0", features = ["composite", "randr", "res"] } 39 | wl_drm = { path = "wl_drm" } 40 | log = "0.4.21" 41 | pretty_env_logger = "0.5.0" 42 | # xcb-util-cursor 0.4 uses Rust 2024, which would bump MSRV from 1.83 to 1.85, however it also has 43 | # no meaningful code changes, so we stick to the older version 44 | xcb-util-cursor = "0.3.5" 45 | smithay-client-toolkit = { version = "0.20.0", default-features = false } 46 | 47 | fontconfig = { version = "0.10", optional = true } 48 | sd-notify = { version = "0.4.2", optional = true } 49 | macros = { version = "0.1.0", path = "macros" } 50 | hecs = { version = "0.10.5", features = ["macros"] } 51 | num_enum = "0.7.4" 52 | tiny-skia = "0.11.4" 53 | ab_glyph = "0.2.32" 54 | fontdue = "0.9.3" 55 | 56 | [features] 57 | default = [] 58 | systemd = ["dep:sd-notify"] 59 | # load decoration font at runtime instead of embedding it into the executable 60 | fontconfig = ["dep:fontconfig"] 61 | 62 | [dev-dependencies] 63 | rustix = { workspace = true, features = ["fs"] } 64 | testwl = { path = "testwl" } 65 | 66 | [build-dependencies] 67 | anyhow = "1.0.98" 68 | vergen-gitcl = "1.0.8" 69 | 70 | [profile.ci] 71 | inherits = "dev" 72 | # Shrink bloat in ./target which negatively affects build caching 73 | incremental = false 74 | debug = false 75 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: [ '*' ] 7 | paths-ignore: [ '*.md' ] 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | UBUNTU_DOCKERFILE: .github/workflows/ubuntu.dockerfile 16 | 17 | permissions: 18 | pull-requests: read 19 | contents: read 20 | packages: read 21 | 22 | jobs: 23 | set-vars: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v5 27 | with: 28 | fetch-depth: 2 29 | sparse-checkout: | 30 | .github 31 | Cargo.toml 32 | sparse-checkout-cone-mode: false 33 | - id: set-vars 34 | run: ./.github/workflows/set-vars.sh ${GITHUB_REPOSITORY@L} 35 | outputs: 36 | msrv: ${{ steps.set-vars.outputs.msrv }} 37 | should-build: ${{ steps.set-vars.outputs.should_build == 'true' && github.event_name == 'push' }} 38 | container-path: ${{ steps.set-vars.outputs.container_path }} 39 | 40 | container-build: 41 | runs-on: ubuntu-latest 42 | needs: set-vars 43 | if: ${{ needs.set-vars.outputs.should-build == 'true' }} 44 | permissions: 45 | packages: write 46 | id-token: write 47 | 48 | steps: 49 | - uses: docker/setup-buildx-action@v3 50 | - uses: docker/login-action@v3 51 | with: 52 | registry: ${{ env.REGISTRY }} 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | - uses: docker/build-push-action@v6 56 | with: 57 | file: ${{ env.UBUNTU_DOCKERFILE }} 58 | push: true 59 | tags: ${{ needs.set-vars.outputs.container-path }}-ubuntu:latest 60 | labels: org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} 61 | cache-from: type=gha 62 | cache-to: type=gha,mode=max 63 | 64 | xwls-build-test: 65 | needs: [container-build, set-vars] 66 | if: ${{ always() && (needs.container-build.result == 'success' || needs.container-build.result == 'skipped') }} 67 | runs-on: ubuntu-latest 68 | container: 69 | image: ${{ needs.set-vars.outputs.container-path }}-ubuntu:latest 70 | credentials: 71 | username: ${{ github.actor }} 72 | password: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | steps: 75 | - uses: actions/checkout@v5 76 | - uses: dtolnay/rust-toolchain@master 77 | with: 78 | toolchain: ${{ needs.set-vars.outputs.msrv }} 79 | components: clippy, rustfmt 80 | - name: Build 81 | run: cargo build --all-targets --profile ci --locked --verbose 82 | - name: Run tests 83 | run: cargo test --profile ci --locked --verbose -- --test-threads 1 --nocapture 84 | - name: Format check 85 | run: cargo fmt --check 86 | - name: Clippy 87 | run: cargo clippy --all-targets --profile ci --locked --workspace 88 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | rust-overlay = { 6 | url = "github:oxalica/rust-overlay"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | 11 | outputs = { self, nixpkgs, rust-overlay, flake-utils }: 12 | let systems = [ "x86_64-linux" "aarch64-linux" ]; 13 | in flake-utils.lib.eachSystem systems (system: 14 | let 15 | overlays = [ (import rust-overlay) ]; 16 | pkgs = import nixpkgs { inherit system overlays; }; 17 | 18 | cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); 19 | cargoPackageVersion = cargoToml.package.version; 20 | 21 | commitHash = self.shortRev or self.dirtyShortRev or "unknown"; 22 | 23 | version = "${cargoPackageVersion}-${commitHash}"; 24 | 25 | buildXwaylandSatellite = 26 | { lib 27 | , rustPlatform 28 | , pkg-config 29 | , makeBinaryWrapper 30 | , libxcb 31 | , xcb-util-cursor 32 | , xwayland 33 | , withSystemd ? true 34 | }: 35 | 36 | rustPlatform.buildRustPackage rec { 37 | pname = "xwayland-satellite"; 38 | inherit version; 39 | 40 | src = self; 41 | 42 | cargoLock = { 43 | lockFile = "${src}/Cargo.lock"; 44 | allowBuiltinFetchGit = true; 45 | }; 46 | 47 | nativeBuildInputs = [ 48 | rustPlatform.bindgenHook 49 | pkg-config 50 | makeBinaryWrapper 51 | ]; 52 | 53 | buildInputs = [ 54 | libxcb 55 | xcb-util-cursor 56 | ]; 57 | 58 | buildNoDefaultFeatures = true; 59 | buildFeatures = lib.optionals withSystemd [ "systemd" ]; 60 | 61 | postPatch = '' 62 | substituteInPlace resources/xwayland-satellite.service \ 63 | --replace-fail '/usr/local/bin' "$out/bin" 64 | ''; 65 | 66 | postInstall = lib.optionalString withSystemd '' 67 | install -Dm0644 resources/xwayland-satellite.service -t $out/lib/systemd/user 68 | ''; 69 | 70 | postFixup = '' 71 | wrapProgram $out/bin/xwayland-satellite \ 72 | --prefix PATH : "${lib.makeBinPath [ xwayland ]}" 73 | ''; 74 | 75 | doCheck = false; 76 | 77 | meta = with lib; { 78 | description = "Xwayland outside your Wayland"; 79 | homepage = "https://github.com/Supreeeme/xwayland-satellite"; 80 | license = licenses.mpl20; 81 | mainProgram = "xwayland-satellite"; 82 | platforms = platforms.linux; 83 | }; 84 | }; 85 | 86 | xwayland-satellite = pkgs.callPackage buildXwaylandSatellite { }; 87 | in 88 | { 89 | devShell = (pkgs.mkShell.override { stdenv = pkgs.clangStdenv; }) { 90 | buildInputs = with pkgs; [ 91 | rustPlatform.bindgenHook 92 | rust-bin.stable.latest.default 93 | pkg-config 94 | 95 | xcb-util-cursor 96 | xorg.libxcb 97 | xwayland 98 | ]; 99 | }; 100 | 101 | packages = { 102 | xwayland-satellite = xwayland-satellite; 103 | default = xwayland-satellite; 104 | }; 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xwayland-satellite 2 | xwayland-satellite grants rootless Xwayland integration to any Wayland compositor implementing xdg_wm_base and viewporter. 3 | This is particularly useful for compositors that (understandably) do not want to go through implementing support for rootless Xwayland themselves. 4 | 5 | Found a bug? [Open a bug report.](https://github.com/Supreeeme/xwayland-satellite/issues/new?template=bug_report.yaml) 6 | 7 | Need help troubleshooting, or have some other general question? [Ask on GitHub Discussions.](https://github.com/Supreeeme/xwayland-satellite/discussions) 8 | 9 | ## Dependencies 10 | - Xwayland >=23.1 11 | - xcb 12 | - xcb-util-cursor 13 | - clang (building only) 14 | 15 | ## Usage 16 | Run `xwayland-satellite`. You can specify an X display to use (i.e. `:12`). Be sure to set the same `DISPLAY` environment variable for any X11 clients. 17 | Because xwayland-satellite is a Wayland client (in addition to being a Wayland compositor), it will need to launch after your compositor launches, but obviously before any X11 applications. 18 | 19 | ## Java applications 20 | Some (most?) Java applications may present themselves as a blank screen by default with satellite. To fix this, simply set the environment variable 21 | `_JAVA_AWT_WM_NONREPARENTING=1` before launching it to fix this. Unfortunately there is not a way for satellite to automatically do this. 22 | 23 | ## Building 24 | ``` 25 | # dev build 26 | cargo build 27 | # release build 28 | cargo build --release 29 | 30 | # run - will also build if not already built 31 | cargo run # --release 32 | ``` 33 | 34 | ## Systemd support 35 | xwayland-satellite can be built with systemd support - simply add `-F systemd` to your build command - i.e. `cargo build --release -F systemd`. 36 | 37 | With systemd support, satellite will send a state change notification when Xwayland has been initialized, allowing for having services dependent on satellite's startup. 38 | 39 | An example service file is located in `resources/xwayland-satellite.service` - be sure to replace the `ExecStart` line with the proper location before using it. 40 | It can be placed in a systemd user unit directory (i.e. `$XDG_CONFIG_HOME/systemd/user` or `/etc/systemd/user`), 41 | and be launched and enabled with `systemctl --user enable --now xwayland-satellite`. 42 | It will be started when the `graphical-session.target` is reached, 43 | which is likely after your compositor is started if it supports systemd. 44 | 45 | ## Scaling/HiDPI 46 | For most GTK and Qt apps, xwayland-satellite should automatically scale them properly. Note that for mixed DPI monitor setups, satellite will choose 47 | the smallest monitor's DPI, meaning apps may have small text on other monitors. 48 | 49 | Other miscellaneous apps (such as Wine apps) may have small text on HiDPI displays. It is application dependent on getting apps to scale properly with satellite, 50 | so you will have to figure out what app specific config needs to be set. See [the Arch Wiki on HiDPI](https://wiki.archlinux.org/title/HiDPI) for a good place start. 51 | 52 | Satellite acts as an Xsettings manager for setting scaling related settings, but will get out of the way of other Xsettings managers. 53 | To manually set these settings, try [xsettingsd](https://codeberg.org/derat/xsettingsd) or another Xsettings manager. 54 | 55 | ## Wayland protocols used 56 | The host compositor **must** implement the following protocols/interfaces for satellite to function: 57 | - Core interfaces (wl_output, wl_surface, wl_compositor, etc) 58 | - xdg_shell (xdg_wm_base, xdg_surface, xdg_popup, xdg_toplevel) 59 | - wp_viewporter - used for scaling 60 | 61 | Additionally, satellite can *optionally* take advantage of the following protocols: 62 | - Linux dmabuf 63 | - XDG activation 64 | - XDG foreign 65 | - Pointer constraints 66 | - Tablet input 67 | - Fractional scale 68 | 69 | ## Compositor integration 70 | Satellite supports passing through the `-listenfd` Xwayland argument. What this means is you can integrate satellite 71 | (and by extension Xwayland) into your compositor, and do things like on demand activation. Note that you *must* pass 72 | a display number to satellite as the first argument, and then the `-listenfd` argument. 73 | 74 | You can view [Niri's implementation of this integration](https://github.com/YaLTeR/niri/pull/1728/files) for understanding 75 | how it should work. 76 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | This is a high level overview of the architecture of xwayland-satellite. It has a 4 | few different roles that can be confusing to understand if you aren't familiar with 5 | how Wayland and X11 function. 6 | 7 | ## Overview 8 | 9 | ```mermaid 10 | flowchart TD 11 | host["Host compositor 12 | (niri, sway, etc)"] 13 | sat[xwayland-satellite] 14 | Xwayland 15 | client[X client] 16 | 17 | host --> sat 18 | sat -->|Wayland server| Xwayland 19 | Xwayland -->|X11 window manager| sat 20 | sat -->|X11 window manager| client 21 | Xwayland --> client 22 | ``` 23 | 24 | xwayland-satellite grants rootless Xwayland integration to any standard Wayland compositor through standard 25 | Wayland protocols. As such, this means satellite has to function as three different things: 26 | 27 | 1. An X11 window manager 28 | 2. A Wayland client 29 | 3. A Wayland server/compositor 30 | 31 | ### X11 window manager 32 | 33 | The code for the X11 portion of satellite lives in `src/xstate`. Satellite must function largely the same way 34 | as any other standard X11 window manager. This includes: 35 | 36 | - Setting SubstructureRedirect and SubstructureNotify on the root window, to get notifications for when new windows are being created 37 | - Follwing (most of) the [ICCCM](https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html) and [EWMH](https://specifications.freedesktop.org/wm-spec/latest/) specs 38 | 39 | In addition, satellite must do some other things that a normal X11 window manager wouldn't - but a compositor integrating 40 | Xwayland would - such as synchronize X11 and Wayland selections. This is explained further in the Wayland server section. 41 | 42 | The way that satellite manages windows from the X11 point of view is as follows: 43 | 44 | - All toplevels on a monitor are positioned at 0x0 on that monitor. So if you have one monitor at 0x0, 45 | all the windows are located at 0x0. If you have a monitor at 300x600, all the windows on that monitor are at 300x600. 46 | - This offset is needed because all monitors rest in the same coordinate plane in X11, so missing this offset would 47 | would lead to incorrect cursor behavior. 48 | - The current window that the mouse is hovering over is raised to the top of the stack. 49 | - Any window determined to be a popup (override redirect, EWMH properties, etc) has its position respected if there is 50 | an existing toplevel. If there is no existing toplevel, the window is treated as a toplevel. 51 | 52 | This approach seems to work well for most applications. The biggest issues will be applications that rely on creating windows 53 | at specific coordinates - for example Steam's notifications that slide in from the bottom of the screen. 54 | 55 | ### Wayland client 56 | 57 | Since satellite is intended to function on any Wayland compositor implementing the necessary protocols, 58 | it functions as a Wayland client. This is straightforward to think about. Client interfacing code lives in 59 | `src/clientside`, as well as being interspersed throughout `src/server`. 60 | 61 | ### Wayland server 62 | 63 | In order to interface with Xwayland, which itself is a Wayland client, satellite must function as a Wayland server. 64 | The code for this lives in `src/server`. Satellite will re-expose relevant Wayland interfaces from the host compositor 65 | (the compositor that satellite itself is a client to) back to Xwayland. 66 | 67 | A lot of the interfaces that Xwayland is interested in can be exposed directly from the host with no further changes, 68 | in which case satellite is just acting as an interface passthrough. However, some interfaces need to be manipulated 69 | or otherwise intercepted for proper functionality, such as: 70 | 71 | - `wl_surface` - Xwayland simply exposes all X11 windows as `wl_surface`s, but for standard desktop compositors to actually show something, 72 | these surfaces must have roles, such as `xdg_toplevel` and `xdg_popup` and friends 73 | - `xdg_output` - Xwayland will use `xdg_output`'s logical size for sizing the X11 screen, but this leads to the wrong size 74 | when the output is scaled, and blurry windows as you may have seen in other Xwayland integrations. 75 | - `wl_pointer` - For handling scaled mouse output, since we are changing the reported output/surface sizes. 76 | 77 | Note that there may be some interfaces that are only used from satellite's client side, such as `xdg_wm_base`. These 78 | are interfaces that Xwayland is not actually interested in, but satellite itself uses to provide some sort of functionality 79 | - satellite is a normal Wayland client after all. 80 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | use quote::{format_ident, quote}; 4 | use syn::{ 5 | braced, bracketed, parse::Parse, parse_macro_input, parse_quote, punctuated::Punctuated, Token, 6 | }; 7 | 8 | enum FieldOrClosure { 9 | Field(syn::Ident), 10 | Closure(syn::Ident, syn::Expr), 11 | } 12 | 13 | impl Parse for FieldOrClosure { 14 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 15 | input.parse().map(Self::Field).or_else(|_| { 16 | input.parse().map(|mut closure: syn::ExprClosure| { 17 | assert_eq!(closure.inputs.len(), 1); 18 | let syn::Pat::Ident(arg) = closure.inputs.pop().unwrap().into_value() else { 19 | panic!("expected ident for closure argument"); 20 | }; 21 | 22 | Self::Closure(arg.ident, *closure.body) 23 | }) 24 | }) 25 | } 26 | } 27 | 28 | struct EventVariant { 29 | name: syn::Ident, 30 | fields: Option>, 31 | } 32 | 33 | impl Parse for EventVariant { 34 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 35 | let name = input.parse()?; 36 | let fields = if input.peek(syn::token::Brace) { 37 | let f; 38 | braced!(f in input); 39 | let f = Punctuated::parse_terminated(&f)?; 40 | Some(f) 41 | } else { 42 | None 43 | }; 44 | 45 | Ok(Self { name, fields }) 46 | } 47 | } 48 | 49 | enum ShuntObject { 50 | Ident(syn::Ident), 51 | KSelf(Token![self]), 52 | } 53 | 54 | impl Parse for ShuntObject { 55 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 56 | if input.peek(Token![self]) { 57 | Ok(Self::KSelf(input.parse()?)) 58 | } else { 59 | Ok(Self::Ident(input.parse()?)) 60 | } 61 | } 62 | } 63 | 64 | impl quote::ToTokens for ShuntObject { 65 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 66 | match self { 67 | Self::Ident(i) => i.to_tokens(tokens), 68 | Self::KSelf(s) => s.to_tokens(tokens), 69 | } 70 | } 71 | } 72 | struct Input { 73 | object: syn::Expr, 74 | event_object: ShuntObject, 75 | event_type: syn::Type, 76 | events: Punctuated, 77 | } 78 | 79 | impl Parse for Input { 80 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 81 | let object = input.parse()?; 82 | input.parse::()?; 83 | let event_object = input.parse()?; 84 | let event_type = if matches!(event_object, ShuntObject::Ident(..)) { 85 | input.parse::()?; 86 | input.parse()? 87 | } else { 88 | parse_quote!(Self) 89 | }; 90 | input.parse::]>()?; 91 | let events; 92 | bracketed!(events in input); 93 | let events = Punctuated::parse_terminated(&events)?; 94 | Ok(Self { 95 | object, 96 | event_object, 97 | event_type, 98 | events, 99 | }) 100 | } 101 | } 102 | 103 | #[proc_macro] 104 | pub fn simple_event_shunt(tokens: TokenStream) -> TokenStream { 105 | let Input { 106 | object, 107 | event_object, 108 | event_type, 109 | events, 110 | } = parse_macro_input!(tokens as Input); 111 | 112 | let match_arms = events.into_iter().map(|e| { 113 | let mut field_names = Punctuated::<_, Token![,]>::new(); 114 | let mut fn_args = Punctuated::::new(); 115 | if let Some(fields) = e.fields { 116 | for field in fields { 117 | match field { 118 | FieldOrClosure::Field(name) => { 119 | fn_args.push(parse_quote! { #name }); 120 | field_names.push(name); 121 | } 122 | FieldOrClosure::Closure(name, expr) => { 123 | field_names.push(name); 124 | fn_args.push(expr); 125 | } 126 | } 127 | } 128 | } 129 | 130 | let name = e.name; 131 | let fn_name = String::from_utf8( 132 | name.to_string() 133 | .bytes() 134 | .enumerate() 135 | .flat_map(|(idx, c)| { 136 | if idx != 0 && c.is_ascii_uppercase() { 137 | vec![b'_', c.to_ascii_lowercase()] 138 | } else { 139 | vec![c.to_ascii_lowercase()] 140 | } 141 | }) 142 | .collect::>(), 143 | ) 144 | .unwrap(); 145 | let keyword_pfx = if fn_name == "type" { "_" } else { "" }; 146 | let fn_name = format_ident!("{keyword_pfx}{fn_name}"); 147 | 148 | quote! { 149 | #event_type::#name { #field_names } => { #object.#fn_name(#fn_args); } 150 | } 151 | }); 152 | quote! {{ 153 | match #event_object { 154 | #(#match_arms)* 155 | _ => log::warn!("unhandled {}: {:?}", std::any::type_name::<#event_type>(), #event_object) 156 | } 157 | }} 158 | .into() 159 | } 160 | -------------------------------------------------------------------------------- /src/xstate/settings.rs: -------------------------------------------------------------------------------- 1 | use super::XState; 2 | use log::warn; 3 | use std::collections::HashMap; 4 | use xcb::x; 5 | 6 | impl XState { 7 | pub(crate) fn set_xsettings_owner(&self) { 8 | self.connection 9 | .send_and_check_request(&x::SetSelectionOwner { 10 | owner: self.settings.window, 11 | selection: self.atoms.xsettings, 12 | time: x::CURRENT_TIME, 13 | }) 14 | .unwrap(); 15 | let reply = self 16 | .connection 17 | .wait_for_reply(self.connection.send_request(&x::GetSelectionOwner { 18 | selection: self.atoms.xsettings, 19 | })) 20 | .unwrap(); 21 | 22 | if reply.owner() != self.settings.window { 23 | warn!( 24 | "Could not get XSETTINGS selection (owned by {:?})", 25 | reply.owner() 26 | ); 27 | } 28 | } 29 | 30 | pub(crate) fn update_global_scale(&mut self, scale: f64) { 31 | self.settings.set_scale(scale); 32 | self.connection 33 | .send_and_check_request(&x::ChangeProperty { 34 | window: self.settings.window, 35 | mode: x::PropMode::Replace, 36 | property: self.atoms.xsettings_settings, 37 | r#type: self.atoms.xsettings_settings, 38 | data: &self.settings.as_data(), 39 | }) 40 | .unwrap(); 41 | } 42 | } 43 | 44 | /// The DPI consider 1x scale by X11. 45 | const DEFAULT_DPI: i32 = 96; 46 | /// I don't know why, but the DPI related xsettings seem to 47 | /// divide the DPI by 1024. 48 | const DPI_SCALE_FACTOR: i32 = 1024; 49 | 50 | const XFT_DPI: &str = "Xft/DPI"; 51 | const GDK_WINDOW_SCALE: &str = "Gdk/WindowScalingFactor"; 52 | const GDK_UNSCALED_DPI: &str = "Gdk/UnscaledDPI"; 53 | 54 | pub(super) struct Settings { 55 | window: x::Window, 56 | serial: u32, 57 | settings: HashMap<&'static str, IntSetting>, 58 | } 59 | 60 | #[derive(Copy, Clone)] 61 | struct IntSetting { 62 | value: i32, 63 | last_change_serial: u32, 64 | } 65 | 66 | mod setting_type { 67 | pub const INTEGER: u8 = 0; 68 | } 69 | 70 | impl Settings { 71 | pub(super) fn new(connection: &xcb::Connection, atoms: &super::Atoms, root: x::Window) -> Self { 72 | let window = connection.generate_id(); 73 | connection 74 | .send_and_check_request(&x::CreateWindow { 75 | wid: window, 76 | width: 1, 77 | height: 1, 78 | depth: 0, 79 | parent: root, 80 | x: 0, 81 | y: 0, 82 | border_width: 0, 83 | class: x::WindowClass::InputOnly, 84 | visual: x::COPY_FROM_PARENT, 85 | value_list: &[], 86 | }) 87 | .expect("Couldn't create window for settings"); 88 | 89 | let s = Settings { 90 | window, 91 | serial: 0, 92 | settings: HashMap::from([ 93 | ( 94 | XFT_DPI, 95 | IntSetting { 96 | value: DEFAULT_DPI * DPI_SCALE_FACTOR, 97 | last_change_serial: 0, 98 | }, 99 | ), 100 | ( 101 | GDK_WINDOW_SCALE, 102 | IntSetting { 103 | value: 1, 104 | last_change_serial: 0, 105 | }, 106 | ), 107 | ( 108 | GDK_UNSCALED_DPI, 109 | IntSetting { 110 | value: DEFAULT_DPI * DPI_SCALE_FACTOR, 111 | last_change_serial: 0, 112 | }, 113 | ), 114 | ]), 115 | }; 116 | 117 | connection 118 | .send_and_check_request(&x::ChangeProperty { 119 | window, 120 | mode: x::PropMode::Replace, 121 | property: atoms.xsettings_settings, 122 | r#type: atoms.xsettings_settings, 123 | data: &s.as_data(), 124 | }) 125 | .unwrap(); 126 | 127 | s 128 | } 129 | 130 | fn as_data(&self) -> Vec { 131 | // https://specifications.freedesktop.org/xsettings-spec/0.5/#format 132 | 133 | let mut data = vec![ 134 | // GTK seems to use this value for byte order from the X.h header, 135 | // so I assume I can use it too. 136 | x::ImageOrder::LsbFirst as u8, 137 | // unused 138 | 0, 139 | 0, 140 | 0, 141 | ]; 142 | 143 | data.extend_from_slice(&self.serial.to_le_bytes()); 144 | data.extend_from_slice(&(self.settings.len() as u32).to_le_bytes()); 145 | 146 | fn insert_with_padding(data: &[u8], out: &mut Vec) { 147 | out.extend_from_slice(data); 148 | // See https://x.org/releases/X11R7.7/doc/xproto/x11protocol.html#Syntactic_Conventions_b 149 | let num_padding_bytes = (4 - (data.len() % 4)) % 4; 150 | out.extend(std::iter::repeat_n(0, num_padding_bytes)); 151 | } 152 | 153 | for (name, setting) in &self.settings { 154 | data.extend_from_slice(&[setting_type::INTEGER, 0]); 155 | data.extend_from_slice(&(name.len() as u16).to_le_bytes()); 156 | insert_with_padding(name.as_bytes(), &mut data); 157 | data.extend_from_slice(&setting.last_change_serial.to_le_bytes()); 158 | data.extend_from_slice(&setting.value.to_le_bytes()); 159 | } 160 | 161 | data 162 | } 163 | 164 | fn set_scale(&mut self, scale: f64) { 165 | self.serial += 1; 166 | 167 | let scale = scale.max(1.0); 168 | let setting = IntSetting { 169 | value: (scale * DEFAULT_DPI as f64 * DPI_SCALE_FACTOR as f64).round() as i32, 170 | last_change_serial: self.serial, 171 | }; 172 | self.settings.entry(XFT_DPI).insert_entry(setting); 173 | // Gdk/WindowScalingFactor + Gdk/UnscaledDPI is identical to setting 174 | // GDK_SCALE = scale and then GDK_DPI_SCALE = 1 / scale. 175 | self.settings 176 | .entry(GDK_UNSCALED_DPI) 177 | .insert_entry(IntSetting { 178 | value: setting.value / scale as i32, 179 | last_change_serial: self.serial, 180 | }); 181 | self.settings 182 | .entry(GDK_WINDOW_SCALE) 183 | .insert_entry(IntSetting { 184 | value: scale as i32, 185 | last_change_serial: self.serial, 186 | }); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /wl_drm/src/drm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copyright © 2008-2011 Kristian Høgsberg 6 | Copyright © 2010-2011 Intel Corporation 7 | 8 | Permission to use, copy, modify, distribute, and sell this 9 | software and its documentation for any purpose is hereby granted 10 | without fee, provided that\n the above copyright notice appear in 11 | all copies and that both that copyright notice and this permission 12 | notice appear in supporting documentation, and that the name of 13 | the copyright holders not be used in advertising or publicity 14 | pertaining to distribution of the software without specific, 15 | written prior permission. The copyright holders make no 16 | representations about the suitability of this software for any 17 | purpose. It is provided "as is" without express or implied 18 | warranty. 19 | 20 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 21 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 22 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 24 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 25 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 26 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 27 | THIS SOFTWARE. 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | Bitmask of capabilities. 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /testwl/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.5.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.0.90" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "dlib" 25 | version = "0.5.2" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 28 | dependencies = [ 29 | "libloading", 30 | ] 31 | 32 | [[package]] 33 | name = "downcast-rs" 34 | version = "1.2.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 37 | 38 | [[package]] 39 | name = "errno" 40 | version = "0.3.8" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 43 | dependencies = [ 44 | "libc", 45 | "windows-sys", 46 | ] 47 | 48 | [[package]] 49 | name = "io-lifetimes" 50 | version = "2.0.3" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" 53 | 54 | [[package]] 55 | name = "libc" 56 | version = "0.2.153" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 59 | 60 | [[package]] 61 | name = "libloading" 62 | version = "0.8.3" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 65 | dependencies = [ 66 | "cfg-if", 67 | "windows-targets", 68 | ] 69 | 70 | [[package]] 71 | name = "linux-raw-sys" 72 | version = "0.4.13" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 75 | 76 | [[package]] 77 | name = "log" 78 | version = "0.4.21" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 81 | 82 | [[package]] 83 | name = "memchr" 84 | version = "2.7.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 87 | 88 | [[package]] 89 | name = "pkg-config" 90 | version = "0.3.30" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 93 | 94 | [[package]] 95 | name = "proc-macro2" 96 | version = "1.0.79" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 99 | dependencies = [ 100 | "unicode-ident", 101 | ] 102 | 103 | [[package]] 104 | name = "quick-xml" 105 | version = "0.31.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 108 | dependencies = [ 109 | "memchr", 110 | ] 111 | 112 | [[package]] 113 | name = "quote" 114 | version = "1.0.35" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 117 | dependencies = [ 118 | "proc-macro2", 119 | ] 120 | 121 | [[package]] 122 | name = "rustix" 123 | version = "0.38.32" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" 126 | dependencies = [ 127 | "bitflags", 128 | "errno", 129 | "libc", 130 | "linux-raw-sys", 131 | "windows-sys", 132 | ] 133 | 134 | [[package]] 135 | name = "scoped-tls" 136 | version = "1.0.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 139 | 140 | [[package]] 141 | name = "smallvec" 142 | version = "1.13.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 145 | 146 | [[package]] 147 | name = "testwl" 148 | version = "0.1.0" 149 | dependencies = [ 150 | "wayland-server", 151 | ] 152 | 153 | [[package]] 154 | name = "unicode-ident" 155 | version = "1.0.12" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 158 | 159 | [[package]] 160 | name = "wayland-backend" 161 | version = "0.3.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" 164 | dependencies = [ 165 | "cc", 166 | "downcast-rs", 167 | "rustix", 168 | "scoped-tls", 169 | "smallvec", 170 | "wayland-sys", 171 | ] 172 | 173 | [[package]] 174 | name = "wayland-scanner" 175 | version = "0.31.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" 178 | dependencies = [ 179 | "proc-macro2", 180 | "quick-xml", 181 | "quote", 182 | ] 183 | 184 | [[package]] 185 | name = "wayland-server" 186 | version = "0.31.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" 189 | dependencies = [ 190 | "bitflags", 191 | "downcast-rs", 192 | "io-lifetimes", 193 | "rustix", 194 | "wayland-backend", 195 | "wayland-scanner", 196 | ] 197 | 198 | [[package]] 199 | name = "wayland-sys" 200 | version = "0.31.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" 203 | dependencies = [ 204 | "dlib", 205 | "log", 206 | "pkg-config", 207 | ] 208 | 209 | [[package]] 210 | name = "windows-sys" 211 | version = "0.52.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 214 | dependencies = [ 215 | "windows-targets", 216 | ] 217 | 218 | [[package]] 219 | name = "windows-targets" 220 | version = "0.52.4" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 223 | dependencies = [ 224 | "windows_aarch64_gnullvm", 225 | "windows_aarch64_msvc", 226 | "windows_i686_gnu", 227 | "windows_i686_msvc", 228 | "windows_x86_64_gnu", 229 | "windows_x86_64_gnullvm", 230 | "windows_x86_64_msvc", 231 | ] 232 | 233 | [[package]] 234 | name = "windows_aarch64_gnullvm" 235 | version = "0.52.4" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 238 | 239 | [[package]] 240 | name = "windows_aarch64_msvc" 241 | version = "0.52.4" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 244 | 245 | [[package]] 246 | name = "windows_i686_gnu" 247 | version = "0.52.4" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 250 | 251 | [[package]] 252 | name = "windows_i686_msvc" 253 | version = "0.52.4" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 256 | 257 | [[package]] 258 | name = "windows_x86_64_gnu" 259 | version = "0.52.4" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 262 | 263 | [[package]] 264 | name = "windows_x86_64_gnullvm" 265 | version = "0.52.4" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 268 | 269 | [[package]] 270 | name = "windows_x86_64_msvc" 271 | version = "0.52.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 274 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | pub mod xstate; 3 | 4 | use crate::server::{NoConnection, PendingSurfaceState, ServerState}; 5 | use crate::xstate::{RealConnection, XState}; 6 | use log::{error, info}; 7 | use rustix::event::{poll, PollFd, PollFlags, Timespec}; 8 | use server::selection::{Clipboard, Primary}; 9 | use smithay_client_toolkit::data_device_manager::WritePipe; 10 | use std::io::{BufRead, BufReader, Read, Write}; 11 | use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; 12 | use std::os::unix::net::UnixStream; 13 | use std::process::{Command, ExitStatus, Stdio}; 14 | use wayland_server::{Display, ListeningSocket}; 15 | use xcb::x; 16 | 17 | pub trait XConnection: Sized + 'static { 18 | type X11Selection: X11Selection; 19 | 20 | fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState) -> bool; 21 | fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool); 22 | fn focus_window(&mut self, window: x::Window, output_name: Option); 23 | fn close_window(&mut self, window: x::Window); 24 | fn unmap_window(&mut self, window: x::Window); 25 | fn raise_to_top(&mut self, window: x::Window); 26 | } 27 | 28 | pub trait X11Selection { 29 | fn mime_types(&self) -> Vec<&str>; 30 | fn write_to(&self, mime: &str, pipe: WritePipe); 31 | } 32 | 33 | type EarlyServerState = ServerState::X11Selection>>; 34 | type RealServerState = ServerState; 35 | 36 | pub trait RunData { 37 | fn display(&self) -> Option<&str>; 38 | fn listenfds(&mut self) -> Vec; 39 | fn server(&self) -> Option { 40 | None 41 | } 42 | fn created_server(&self) {} 43 | fn connected_server(&self) {} 44 | fn quit_rx(&self) -> Option { 45 | None 46 | } 47 | fn xwayland_ready(&self, _display: String, _pid: u32) {} 48 | fn max_req_len_bytes(&self) -> Option { 49 | None 50 | } 51 | } 52 | 53 | pub const fn timespec_from_millis(millis: u64) -> Timespec { 54 | let d = std::time::Duration::from_millis(millis); 55 | Timespec { 56 | tv_sec: d.as_secs() as i64, 57 | tv_nsec: d.subsec_nanos() as i64, 58 | } 59 | } 60 | 61 | pub fn main(mut data: impl RunData) -> Option<()> { 62 | let mut version = env!("VERGEN_GIT_DESCRIBE"); 63 | if version == "VERGEN_IDEMPOTENT_OUTPUT" { 64 | version = env!("CARGO_PKG_VERSION"); 65 | } 66 | info!("Starting xwayland-satellite version {version}"); 67 | 68 | let socket = ListeningSocket::bind_auto("xwls", 1..=128).unwrap(); 69 | let mut display = Display::new().unwrap(); 70 | let dh = display.handle(); 71 | data.created_server(); 72 | 73 | let (xsock_wl, xsock_xwl) = UnixStream::pair().unwrap(); 74 | // Prevent creation of new Xwayland command from closing fd 75 | rustix::io::fcntl_setfd(&xsock_xwl, rustix::io::FdFlags::empty()).unwrap(); 76 | 77 | let (ready_tx, ready_rx) = UnixStream::pair().unwrap(); 78 | rustix::io::fcntl_setfd(&ready_tx, rustix::io::FdFlags::empty()).unwrap(); 79 | let mut xwayland = Command::new("Xwayland"); 80 | if let Some(display) = data.display() { 81 | xwayland.arg(display); 82 | } 83 | 84 | let fds = data.listenfds(); 85 | for fd in &fds { 86 | xwayland.args(["-listenfd", &fd.as_raw_fd().to_string()]); 87 | } 88 | 89 | let mut xwayland = xwayland 90 | .args([ 91 | "-rootless", 92 | "-force-xrandr-emulation", 93 | "-wm", 94 | &xsock_xwl.as_raw_fd().to_string(), 95 | "-displayfd", 96 | &ready_tx.as_raw_fd().to_string(), 97 | ]) 98 | .env("WAYLAND_DISPLAY", socket.socket_name().unwrap()) 99 | .stderr(Stdio::piped()) 100 | .spawn() 101 | .unwrap(); 102 | 103 | // Now that Xwayland spawned and got the listenfds, we can close them here. 104 | drop(fds); 105 | 106 | let xwl_pid = xwayland.id(); 107 | 108 | let (mut finish_tx, mut finish_rx) = UnixStream::pair().unwrap(); 109 | let stderr = xwayland.stderr.take().unwrap(); 110 | std::thread::spawn(move || { 111 | let reader = BufReader::new(stderr); 112 | for line in reader.lines() { 113 | let line = line.unwrap(); 114 | info!(target: "xwayland_process", "{line}"); 115 | } 116 | let status = Box::new(xwayland.wait().unwrap()); 117 | let status = Box::into_raw(status) as usize; 118 | finish_tx.write_all(&status.to_ne_bytes()).unwrap(); 119 | }); 120 | 121 | let mut ready_fds = [ 122 | PollFd::new(&socket, PollFlags::IN), 123 | PollFd::new(&finish_rx, PollFlags::IN), 124 | ]; 125 | 126 | fn xwayland_exit_code(rx: &mut UnixStream) -> Box { 127 | let mut data = [0; (usize::BITS / 8) as usize]; 128 | rx.read_exact(&mut data).unwrap(); 129 | let data = usize::from_ne_bytes(data); 130 | unsafe { Box::from_raw(data as *mut _) } 131 | } 132 | 133 | let connection = match poll(&mut ready_fds, None) { 134 | Ok(_) => { 135 | if !ready_fds[1].revents().is_empty() { 136 | let status = xwayland_exit_code(&mut finish_rx); 137 | error!("Xwayland exited early with {status}"); 138 | return None; 139 | } 140 | 141 | data.connected_server(); 142 | socket.accept().unwrap().unwrap() 143 | } 144 | Err(e) => { 145 | panic!("first poll failed: {e:?}") 146 | } 147 | }; 148 | 149 | let mut server_state = EarlyServerState::new(dh, data.server(), connection); 150 | server_state.run(); 151 | 152 | // Remove the lifetimes on our fds to avoid borrowing issues, since we know they will exist for 153 | // the rest of our program anyway 154 | let server_fd = unsafe { BorrowedFd::borrow_raw(server_state.clientside_fd().as_raw_fd()) }; 155 | let display_fd = unsafe { BorrowedFd::borrow_raw(display.backend().poll_fd().as_raw_fd()) }; 156 | 157 | // `finish_rx` only writes the status code of `Xwayland` exiting, so it is reasonable to use as 158 | // the UnixStream of choice when not running the integration tests. 159 | let mut quit_rx = data.quit_rx().unwrap_or(finish_rx); 160 | 161 | let mut fds = [ 162 | PollFd::from_borrowed_fd(server_fd, PollFlags::IN), 163 | PollFd::new(&xsock_wl, PollFlags::IN), 164 | PollFd::from_borrowed_fd(display_fd, PollFlags::IN), 165 | PollFd::new(&quit_rx, PollFlags::IN), 166 | PollFd::new(&ready_rx, PollFlags::IN), 167 | ]; 168 | 169 | loop { 170 | match poll(&mut fds, None) { 171 | Ok(_) => { 172 | if !fds[3].revents().is_empty() { 173 | let status = xwayland_exit_code(&mut quit_rx); 174 | if *status != ExitStatus::default() { 175 | error!("Xwayland exited early with {status}"); 176 | } 177 | return None; 178 | } 179 | if !fds[4].revents().is_empty() { 180 | break; 181 | } 182 | } 183 | Err(other) => panic!("Poll failed: {other:?}"), 184 | } 185 | 186 | display.dispatch_clients(&mut *server_state).unwrap(); 187 | server_state.run(); 188 | display.flush_clients().unwrap(); 189 | } 190 | 191 | let mut xstate = XState::new(xsock_wl.as_fd()); 192 | if let Some(bytes) = data.max_req_len_bytes() { 193 | xstate.set_max_req_bytes(bytes); 194 | } 195 | 196 | let mut reader = BufReader::new(&ready_rx); 197 | { 198 | let mut display = String::new(); 199 | reader.read_line(&mut display).unwrap(); 200 | display.pop(); 201 | display.insert(0, ':'); 202 | info!("Connected to Xwayland on {display}"); 203 | data.xwayland_ready(display, xwl_pid); 204 | } 205 | let mut server_state = xstate.server_state_setup(server_state); 206 | 207 | #[cfg(feature = "systemd")] 208 | { 209 | match sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) { 210 | Ok(()) => info!("Successfully notified systemd of ready state."), 211 | Err(e) => log::warn!("Systemd notify failed: {e:?}"), 212 | } 213 | } 214 | 215 | #[cfg(not(feature = "systemd"))] 216 | info!("Systemd support disabled."); 217 | 218 | loop { 219 | xstate.handle_events(&mut server_state); 220 | 221 | display.dispatch_clients(&mut *server_state).unwrap(); 222 | server_state.run(); 223 | display.flush_clients().unwrap(); 224 | 225 | if let Some(sel) = server_state.new_selection::() { 226 | xstate.set_clipboard(sel); 227 | } 228 | 229 | if let Some(sel) = server_state.new_selection::() { 230 | xstate.set_primary_selection(sel); 231 | } 232 | 233 | if let Some(scale) = server_state.new_global_scale() { 234 | xstate.update_global_scale(scale); 235 | } 236 | 237 | match poll(&mut fds, None) { 238 | Ok(_) => { 239 | if !fds[3].revents().is_empty() { 240 | let status = xwayland_exit_code(&mut quit_rx); 241 | if *status != ExitStatus::default() { 242 | error!("Xwayland exited early with {status}"); 243 | } 244 | return None; 245 | } 246 | } 247 | Err(other) => panic!("Poll failed: {other:?}"), 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/server/selection.rs: -------------------------------------------------------------------------------- 1 | use super::clientside::SelectionEvents; 2 | use super::{InnerServerState, MyWorld, ServerState}; 3 | use crate::{X11Selection, XConnection}; 4 | use log::{info, warn}; 5 | use smithay_client_toolkit::data_device_manager::ReadPipe; 6 | use wayland_client::globals::GlobalList; 7 | use wayland_client::protocol::wl_seat::WlSeat; 8 | use wayland_client::{Proxy, QueueHandle}; 9 | 10 | use smithay_client_toolkit::data_device_manager::{ 11 | data_device::DataDevice, data_offer::SelectionOffer as WlSelectionOffer, 12 | data_source::CopyPasteSource, DataDeviceManagerState, 13 | }; 14 | use smithay_client_toolkit::primary_selection::device::PrimarySelectionDevice; 15 | use smithay_client_toolkit::primary_selection::offer::PrimarySelectionOffer; 16 | use smithay_client_toolkit::primary_selection::selection::PrimarySelectionSource; 17 | use smithay_client_toolkit::primary_selection::PrimarySelectionManagerState; 18 | use std::io::Read; 19 | use std::rc::{Rc, Weak}; 20 | 21 | pub(super) struct SelectionStates { 22 | clipboard: Option>, 23 | primary: Option>, 24 | } 25 | 26 | impl SelectionStates { 27 | pub fn new(global_list: &GlobalList, qh: &QueueHandle) -> Self { 28 | Self { 29 | clipboard: DataDeviceManagerState::bind(global_list, qh) 30 | .inspect_err(|e| { 31 | warn!("Could not bind data device manager ({e:?}). Clipboard will not work.") 32 | }) 33 | .ok() 34 | .map(SelectionState::new), 35 | primary: PrimarySelectionManagerState::bind(global_list, qh) 36 | .inspect_err(|_| info!("Primary selection unsupported.")) 37 | .ok() 38 | .map(SelectionState::new), 39 | } 40 | } 41 | 42 | pub fn seat_created(&mut self, qh: &QueueHandle, seat: &WlSeat) { 43 | if let Some(c) = &mut self.clipboard { 44 | c.device = Some(c.manager.get_data_device(qh, seat)); 45 | } 46 | 47 | if let Some(d) = &mut self.primary { 48 | d.device = Some(d.manager.get_selection_device(qh, seat)); 49 | } 50 | } 51 | } 52 | 53 | enum SelectionData { 54 | X11 { inner: T::Source, data: Weak }, 55 | Foreign(ForeignSelection), 56 | } 57 | 58 | struct SelectionState { 59 | manager: T::Manager, 60 | device: Option, 61 | source: Option>, 62 | } 63 | 64 | impl SelectionState { 65 | fn new(manager: T::Manager) -> Self { 66 | Self { 67 | manager, 68 | device: None, 69 | source: None, 70 | } 71 | } 72 | } 73 | 74 | impl InnerServerState { 75 | pub(super) fn handle_selection_events(&mut self) { 76 | self.handle_impl::(); 77 | self.handle_impl::(); 78 | } 79 | 80 | fn handle_impl(&mut self) { 81 | let Some(state) = T::selection_state(&mut self.selection_states) else { 82 | return; 83 | }; 84 | 85 | let events = T::get_events(&mut self.world); 86 | 87 | for (mime_type, fd) in std::mem::take(&mut events.requests) { 88 | let SelectionData::X11 { data, .. } = state.source.as_ref().unwrap() else { 89 | unreachable!("Got selection request without having set the selection?") 90 | }; 91 | if let Some(data) = data.upgrade() { 92 | data.write_to(&mime_type, fd); 93 | } 94 | } 95 | 96 | if events.cancelled { 97 | state.source = None; 98 | events.cancelled = false; 99 | } 100 | 101 | if state.source.is_none() { 102 | if let Some(offer) = T::take_offer(&mut events.offer) { 103 | let mime_types = T::get_mimes(&offer); 104 | let foreign = ForeignSelection { 105 | mime_types, 106 | inner: offer, 107 | }; 108 | state.source = Some(SelectionData::Foreign(foreign)); 109 | } 110 | } 111 | } 112 | 113 | pub(crate) fn set_selection_source(&mut self, selection: &Rc) { 114 | if let Some(state) = T::selection_state(&mut self.selection_states) { 115 | let src = T::create_source(&state.manager, &self.qh, selection.mime_types()); 116 | let data = SelectionData::X11 { 117 | inner: src, 118 | data: Rc::downgrade(selection), 119 | }; 120 | let SelectionData::X11 { inner, .. } = state.source.insert(data) else { 121 | unreachable!(); 122 | }; 123 | if let Some(serial) = self 124 | .last_kb_serial 125 | .as_ref() 126 | .map(|(_seat, serial)| serial) 127 | .copied() 128 | { 129 | T::set_selection(inner, state.device.as_ref().unwrap(), serial); 130 | } 131 | } 132 | } 133 | 134 | pub(crate) fn new_selection(&mut self) -> Option> { 135 | T::selection_state(&mut self.selection_states) 136 | .as_mut() 137 | .and_then(|state| { 138 | state.source.take().and_then(|s| match s { 139 | SelectionData::Foreign(f) => Some(f), 140 | SelectionData::X11 { .. } => { 141 | state.source = Some(s); 142 | None 143 | } 144 | }) 145 | }) 146 | } 147 | } 148 | 149 | pub struct ForeignSelection { 150 | pub mime_types: Box<[String]>, 151 | inner: T::Offer, 152 | } 153 | 154 | #[allow(private_bounds)] 155 | impl ForeignSelection { 156 | pub(crate) fn receive( 157 | &self, 158 | mime_type: String, 159 | state: &ServerState, 160 | ) -> Vec { 161 | let mut pipe = T::receive_offer(&self.inner, mime_type).unwrap(); 162 | state.queue.flush().unwrap(); 163 | let mut data = Vec::new(); 164 | pipe.read_to_end(&mut data).unwrap(); 165 | data 166 | } 167 | } 168 | 169 | #[allow(private_bounds, private_interfaces)] 170 | pub trait SelectionType: Sized { 171 | type Source; 172 | type Offer; 173 | type Manager; 174 | type DataDevice; 175 | 176 | // The methods in this trait shouldn't be used outside of this file. 177 | 178 | fn selection_state( 179 | state: &mut SelectionStates, 180 | ) -> &mut Option>; 181 | 182 | fn create_source( 183 | manager: &Self::Manager, 184 | qh: &QueueHandle, 185 | mime_types: Vec<&str>, 186 | ) -> Self::Source; 187 | 188 | fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32); 189 | 190 | fn get_events(world: &mut MyWorld) -> &mut SelectionEvents; 191 | 192 | fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result; 193 | 194 | fn take_offer(offer: &mut Option) -> Option { 195 | offer.take() 196 | } 197 | 198 | fn get_mimes(offer: &Self::Offer) -> Box<[String]>; 199 | } 200 | 201 | pub enum Clipboard {} 202 | pub enum Primary {} 203 | 204 | #[allow(private_bounds, private_interfaces)] 205 | impl SelectionType for Clipboard { 206 | type Source = CopyPasteSource; 207 | type Offer = WlSelectionOffer; 208 | type Manager = DataDeviceManagerState; 209 | type DataDevice = DataDevice; 210 | 211 | fn selection_state( 212 | state: &mut SelectionStates, 213 | ) -> &mut Option> { 214 | &mut state.clipboard 215 | } 216 | 217 | fn create_source( 218 | manager: &Self::Manager, 219 | qh: &QueueHandle, 220 | mime_types: Vec<&str>, 221 | ) -> Self::Source { 222 | manager.create_copy_paste_source(qh, mime_types) 223 | } 224 | 225 | fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32) { 226 | source.set_selection(device, serial); 227 | } 228 | 229 | fn get_events(world: &mut MyWorld) -> &mut SelectionEvents { 230 | &mut world.clipboard 231 | } 232 | 233 | fn take_offer(offer: &mut Option) -> Option { 234 | offer.take().filter(|offer| offer.inner().is_alive()) 235 | } 236 | 237 | fn get_mimes(offer: &Self::Offer) -> Box<[String]> { 238 | offer.with_mime_types(|mimes| mimes.into()) 239 | } 240 | 241 | fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result { 242 | offer.receive(mime_type).map_err(|e| { 243 | use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError; 244 | match e { 245 | DataOfferError::InvalidReceive => std::io::Error::from(std::io::ErrorKind::Other), 246 | DataOfferError::Io(e) => e, 247 | } 248 | }) 249 | } 250 | } 251 | 252 | #[allow(private_bounds, private_interfaces)] 253 | impl SelectionType for Primary { 254 | type Source = PrimarySelectionSource; 255 | type Offer = PrimarySelectionOffer; 256 | type Manager = PrimarySelectionManagerState; 257 | type DataDevice = PrimarySelectionDevice; 258 | 259 | fn selection_state( 260 | state: &mut SelectionStates, 261 | ) -> &mut Option> { 262 | &mut state.primary 263 | } 264 | 265 | fn create_source( 266 | manager: &Self::Manager, 267 | qh: &QueueHandle, 268 | mime_types: Vec<&str>, 269 | ) -> Self::Source { 270 | manager.create_selection_source(qh, mime_types) 271 | } 272 | 273 | fn set_selection(source: &Self::Source, device: &Self::DataDevice, serial: u32) { 274 | source.set_selection(device, serial); 275 | } 276 | 277 | fn get_events(world: &mut MyWorld) -> &mut SelectionEvents { 278 | &mut world.primary 279 | } 280 | 281 | fn receive_offer(offer: &Self::Offer, mime_type: String) -> std::io::Result { 282 | offer.receive(mime_type) 283 | } 284 | 285 | fn get_mimes(offer: &Self::Offer) -> Box<[String]> { 286 | offer.with_mime_types(|mimes| mimes.into()) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/server/decoration.rs: -------------------------------------------------------------------------------- 1 | use crate::server::{InnerServerState, ServerState, SurfaceRole}; 2 | use crate::{X11Selection, XConnection}; 3 | 4 | use ab_glyph::{Font, FontRef, Glyph, PxScaleFont, ScaleFont}; 5 | use hecs::{CommandBuffer, Entity, World}; 6 | use log::{error, warn}; 7 | use smithay_client_toolkit::registry::SimpleGlobal; 8 | use smithay_client_toolkit::shm::slot::SlotPool; 9 | use std::borrow::Cow; 10 | use std::sync::LazyLock; 11 | use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Stroke, Transform}; 12 | use tiny_skia::{ColorU8, Rect}; 13 | use wayland_client::protocol::wl_seat::WlSeat; 14 | use wayland_client::protocol::wl_shm; 15 | use wayland_client::protocol::wl_subsurface::WlSubsurface; 16 | use wayland_client::protocol::wl_surface::WlSurface; 17 | use wayland_client::Proxy; 18 | use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; 19 | use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1; 20 | use wayland_protocols::xdg::shell::client::xdg_toplevel::XdgToplevel; 21 | use xcb::x; 22 | 23 | #[derive(Debug)] 24 | pub struct DecorationsData { 25 | pub wl: Option, 26 | // Boxed to avoid making ToplevelData so much bigger than PopupData 27 | pub satellite: Option>, 28 | } 29 | 30 | pub struct DecorationMarker { 31 | pub parent: Entity, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct DecorationsDataSatellite { 36 | surface: WlSurface, 37 | subsurface: WlSubsurface, 38 | pool: Entity, 39 | viewport: WpViewport, 40 | scale: f32, 41 | pixmap: Pixmap, 42 | x_data: DecorationsBox, 43 | title: Option, 44 | title_rect: Rect, 45 | should_draw: bool, 46 | remove_buffer: bool, 47 | } 48 | 49 | impl Drop for DecorationsDataSatellite { 50 | fn drop(&mut self) { 51 | self.subsurface.destroy(); 52 | self.surface.destroy(); 53 | self.viewport.destroy(); 54 | } 55 | } 56 | 57 | impl DecorationsDataSatellite { 58 | pub const TITLEBAR_HEIGHT: i32 = 25; 59 | 60 | pub fn try_new( 61 | state: &InnerServerState, 62 | parent: &WlSurface, 63 | title: Option<&str>, 64 | ) -> Option<(Box, Option)> { 65 | let mut new_pool = None; 66 | let mut query = state.world.query::<&SlotPool>(); 67 | let pool_entity = if let Some((pool_entity, _)) = query.into_iter().next() { 68 | pool_entity 69 | } else { 70 | new_pool = Some( 71 | SlotPool::new(1, &SimpleGlobal::from_bound(state.shm.clone())) 72 | .inspect_err(|e| { 73 | warn!("Couldn't create slot pool for decorations: {e:?}"); 74 | }) 75 | .ok()?, 76 | ); 77 | state.world.reserve_entity() 78 | }; 79 | 80 | let surface = state.compositor.create_surface( 81 | &state.qh, 82 | DecorationMarker { 83 | parent: parent.data().copied().unwrap(), 84 | }, 85 | ); 86 | let subsurface = { 87 | state 88 | .subcompositor 89 | .get_subsurface(&surface, parent, &state.qh, ()) 90 | }; 91 | subsurface.set_position(0, -Self::TITLEBAR_HEIGHT); 92 | let viewport = state.viewporter.get_viewport(&surface, &state.qh, ()); 93 | 94 | Some(( 95 | Self { 96 | surface, 97 | subsurface, 98 | pool: pool_entity, 99 | viewport, 100 | x_data: DecorationsBox::default(), 101 | pixmap: Pixmap::new(1, 1).unwrap(), 102 | scale: 1.0, 103 | title: title.map(str::to_string), 104 | title_rect: Rect::from_ltrb(0.0, 0.0, 0.0, 0.0).unwrap(), 105 | should_draw: true, 106 | remove_buffer: false, 107 | } 108 | .into(), 109 | new_pool.map(|p| { 110 | let mut buf = CommandBuffer::new(); 111 | buf.insert_one(pool_entity, p); 112 | buf 113 | }), 114 | )) 115 | } 116 | 117 | fn pool<'a>(&self, world: &'a World) -> hecs::RefMut<'a, SlotPool> { 118 | world.get::<&mut SlotPool>(self.pool).unwrap() 119 | } 120 | 121 | fn update_buffer(&mut self, world: &World) { 122 | let mut pool = self.pool(world); 123 | let (buffer, data) = match pool.create_buffer( 124 | self.pixmap.width() as i32, 125 | self.pixmap.height() as i32, 126 | self.pixmap.width() as i32 * 4, 127 | wl_shm::Format::Xrgb8888, 128 | ) { 129 | Ok(b) => b, 130 | Err(err) => { 131 | error!("Failed to create buffer for decorations: {err:?}"); 132 | return; 133 | } 134 | }; 135 | 136 | draw_pixmap_to_buffer(&self.pixmap, data); 137 | buffer.attach_to(&self.surface).unwrap(); 138 | self.surface.commit(); 139 | } 140 | 141 | #[must_use] 142 | pub fn will_draw_decorations(&self, width: i32) -> bool { 143 | width > 0 && self.should_draw 144 | } 145 | 146 | pub fn draw_decorations(&mut self, world: &World, width: i32, parent_scale_factor: f32) { 147 | if !self.will_draw_decorations(width) { 148 | if self.remove_buffer { 149 | self.surface.attach(None, 0, 0); 150 | self.surface.commit(); 151 | self.remove_buffer = false; 152 | } 153 | return; 154 | } 155 | 156 | self.scale = parent_scale_factor; 157 | let mut drawn_width = (width as f32 * self.scale).ceil() as i32; 158 | let drawn_height = (Self::TITLEBAR_HEIGHT as f32 * self.scale).ceil() as i32; 159 | 160 | let x = x_pixmap(drawn_height as u32, self.scale, self.x_data.hovered); 161 | if x.width() > drawn_width as u32 { 162 | drawn_width = x.width() as i32; 163 | } 164 | 165 | let title = self.title.as_ref().and_then(|t| { 166 | let width = (drawn_width as u32).saturating_sub(x.width()); 167 | if width > 0 { 168 | title_pixmap(t, width, drawn_height as u32, self.scale) 169 | } else { 170 | None 171 | } 172 | }); 173 | 174 | // Draw the bar and its components 175 | let mut bar = Pixmap::new(drawn_width as u32, drawn_height as u32).unwrap(); 176 | bar.fill(Color::WHITE); 177 | 178 | if let Some(title) = title { 179 | bar.draw_pixmap( 180 | 0, 181 | 0, 182 | title.as_ref(), 183 | &Default::default(), 184 | Transform::identity(), 185 | None, 186 | ); 187 | self.title_rect = 188 | Rect::from_xywh(0.0, 0.0, title.width() as f32, title.height() as f32).unwrap(); 189 | } 190 | 191 | bar.draw_pixmap( 192 | (bar.width() - x.width()) as i32, 193 | 0, 194 | x.as_ref(), 195 | &Default::default(), 196 | Transform::identity(), 197 | None, 198 | ); 199 | self.x_data = DecorationsBox { 200 | rect: Rect::from_ltrb( 201 | width as f32 - Self::TITLEBAR_HEIGHT as f32, 202 | 0.0, 203 | width as f32, 204 | Self::TITLEBAR_HEIGHT as f32, 205 | ) 206 | .unwrap(), 207 | hovered: false, 208 | }; 209 | 210 | self.pixmap = bar; 211 | self.viewport.set_destination(width, Self::TITLEBAR_HEIGHT); 212 | self.update_buffer(world); 213 | } 214 | 215 | fn redraw_x_pixmap(&mut self, world: &World) { 216 | let x = x_pixmap(self.pixmap.height(), self.scale, self.x_data.hovered); 217 | 218 | self.pixmap.draw_pixmap( 219 | (self.pixmap.width() - x.width()) as i32, 220 | 0, 221 | x.as_ref(), 222 | &Default::default(), 223 | Transform::identity(), 224 | None, 225 | ); 226 | 227 | self.surface.damage_buffer( 228 | (self.pixmap.width() - x.width()) as i32, 229 | 0, 230 | x.width() as i32, 231 | x.height() as i32, 232 | ); 233 | self.update_buffer(world); 234 | } 235 | 236 | pub fn set_title(&mut self, world: &World, title: &str) { 237 | self.title = Some(title.to_string()); 238 | 239 | // Don't draw title if there's not enough space 240 | let title_pixmap = title_pixmap( 241 | title, 242 | self.pixmap.width() - self.x_data.rect.width() as u32, 243 | self.pixmap.height(), 244 | self.scale, 245 | ); 246 | 247 | let new_title_rect = title_pixmap 248 | .as_ref() 249 | .map(|p| Rect::from_xywh(0.0, 0.0, p.width() as f32, p.height() as f32).unwrap()) 250 | .unwrap_or_else(|| Rect::from_ltrb(0.0, 0.0, 0.0, 0.0).unwrap()); 251 | 252 | let last_title_rect = std::mem::replace(&mut self.title_rect, new_title_rect); 253 | 254 | // Clear last title with white 255 | let mut paint = Paint::default(); 256 | paint.set_color(Color::WHITE); 257 | self.pixmap 258 | .fill_rect(last_title_rect, &paint, Transform::identity(), None); 259 | 260 | if let Some(p) = title_pixmap.as_ref() { 261 | self.pixmap.draw_pixmap( 262 | 0, 263 | 0, 264 | p.as_ref(), 265 | &Default::default(), 266 | Transform::identity(), 267 | None, 268 | ); 269 | } 270 | 271 | let damaged_width = last_title_rect 272 | .width() 273 | .max(title_pixmap.map(|p| p.width() as f32).unwrap_or(0.0)); 274 | 275 | self.surface 276 | .damage_buffer(0, 0, damaged_width as i32, last_title_rect.height() as i32); 277 | self.update_buffer(world); 278 | } 279 | 280 | pub fn handle_fullscreen(&mut self, fullscreen: bool) { 281 | if self.should_draw == fullscreen { 282 | self.should_draw = !fullscreen; 283 | self.remove_buffer = fullscreen; 284 | } 285 | } 286 | 287 | fn handle_motion(&mut self, world: &World, x: f64, y: f64) { 288 | if self.x_data.check_hovered(x as f32, y as f32) { 289 | self.redraw_x_pixmap(world); 290 | } 291 | } 292 | 293 | fn handle_leave(&mut self, world: &World) { 294 | if self.x_data.hovered { 295 | self.x_data.hovered = false; 296 | self.redraw_x_pixmap(world); 297 | } 298 | } 299 | 300 | /// Returns true if the toplevel should be closed 301 | fn handle_click(&self, toplevel: &XdgToplevel, seat: &WlSeat, serial: u32) -> bool { 302 | if self.x_data.hovered { 303 | true 304 | } else { 305 | toplevel._move(seat, serial); 306 | false 307 | } 308 | } 309 | } 310 | 311 | #[derive(Debug)] 312 | struct DecorationsBox { 313 | rect: Rect, 314 | hovered: bool, 315 | } 316 | 317 | impl Default for DecorationsBox { 318 | fn default() -> Self { 319 | Self { 320 | rect: Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(), 321 | hovered: false, 322 | } 323 | } 324 | } 325 | 326 | impl DecorationsBox { 327 | /// Returns true if hover state changed. 328 | fn check_hovered(&mut self, x: f32, y: f32) -> bool { 329 | let old_hovered = self.hovered; 330 | self.hovered = (self.rect.left()..=self.rect.right()).contains(&x) 331 | && (self.rect.top()..=self.rect.bottom()).contains(&y); 332 | 333 | old_hovered != self.hovered 334 | } 335 | } 336 | 337 | fn draw_pixmap_to_buffer(pixmap: &Pixmap, buffer: &mut [u8]) { 338 | // TODO: support big endian? 339 | for (data, pixel) in buffer.chunks_exact_mut(4).zip(pixmap.pixels()) { 340 | data[0] = pixel.blue(); 341 | data[1] = pixel.green(); 342 | data[2] = pixel.red(); 343 | data[3] = pixel.alpha(); 344 | } 345 | } 346 | 347 | fn x_pixmap(bar_height: u32, scale: f32, hovered: bool) -> Pixmap { 348 | let mut x = Pixmap::new(bar_height, bar_height).unwrap(); 349 | if hovered { 350 | x.fill(Color::from_rgba(1.0, 0.0, 0.0, 0.8).unwrap()); 351 | } else { 352 | x.fill(Color::WHITE); 353 | } 354 | let size = x.width() as f32; 355 | let margin = 8.4 * scale; 356 | 357 | let mut line = PathBuilder::new(); 358 | line.move_to(margin, margin); 359 | line.line_to(size - margin, size - margin); 360 | line.move_to(size - margin, margin); 361 | line.line_to(margin, size - margin); 362 | let line = line.finish().unwrap(); 363 | x.stroke_path( 364 | &line, 365 | &Default::default(), 366 | &Stroke { 367 | width: scale + 0.5, 368 | ..Default::default() 369 | }, 370 | Default::default(), 371 | None, 372 | ); 373 | 374 | x 375 | } 376 | 377 | fn title_pixmap(title: &str, max_width: u32, height: u32, scale: f32) -> Option { 378 | if title.is_empty() { 379 | return None; 380 | } 381 | 382 | let (glyphs, font) = layout_title_glyphs(title, max_width, height, scale); 383 | 384 | let width = glyphs 385 | .last() 386 | .map(|g| g.position.x + font.h_advance(g.id))? 387 | .ceil() as u32; 388 | 389 | let mut pixmap = Pixmap::new(width, height).unwrap(); 390 | let data = pixmap.pixels_mut(); 391 | 392 | for glyph in glyphs { 393 | if let Some(og) = font.outline_glyph(glyph) { 394 | let bounds = og.px_bounds(); 395 | og.draw(|x, y, coverage| { 396 | let pixel_idx = 397 | ((bounds.min.x as u32 + x) + (bounds.min.y as u32 + y) * width) as usize; 398 | 399 | data[pixel_idx] = 400 | ColorU8::from_rgba(0, 0, 0, (coverage * 255.0) as u8).premultiply(); 401 | }); 402 | } 403 | } 404 | 405 | Some(pixmap) 406 | } 407 | 408 | static FONT_DATA: LazyLock> = LazyLock::new(|| { 409 | #[cfg(feature = "fontconfig")] 410 | { 411 | let fc = fontconfig::Fontconfig::new().expect("Failed to initialize fontconfig."); 412 | let font = fc 413 | .find("opensans", None) 414 | .expect("Failed to load Open Sans Regular."); 415 | let data = std::fs::read(font.path).expect("Failed to read font from disk."); 416 | Cow::Owned(data) 417 | } 418 | #[cfg(not(feature = "fontconfig"))] 419 | { 420 | Cow::Borrowed(include_bytes!("../../OpenSans-Regular.ttf")) 421 | } 422 | }); 423 | 424 | static FONT: LazyLock> = 425 | LazyLock::new(|| FontRef::try_from_slice(FONT_DATA.as_ref()).unwrap()); 426 | 427 | fn layout_title_glyphs( 428 | text: &str, 429 | max_width: u32, 430 | height: u32, 431 | scale: f32, 432 | ) -> (Vec, PxScaleFont<&FontRef<'_>>) { 433 | const TEXT_SIZE: f32 = 10.0; 434 | const TEXT_MARGIN: f32 = 11.0; 435 | 436 | let mut ret = Vec::::new(); 437 | 438 | let px_scale = FONT.pt_to_px_scale(TEXT_SIZE * scale).unwrap(); 439 | let font = FONT.as_scaled(px_scale); 440 | for c in text.chars() { 441 | let mut glyph = font.scaled_glyph(c); 442 | // This centers the glyphs vertically 443 | glyph.position.y = (height as f32 / 2.0) - font.descent(); 444 | if let Some(previous) = ret.last() { 445 | glyph.position.x = previous.position.x 446 | + font.h_advance(previous.id) 447 | + font.kern(glyph.id, previous.id); 448 | } else { 449 | glyph.position.x = TEXT_MARGIN * scale; 450 | } 451 | if (glyph.position.x + font.h_advance(glyph.id)).ceil() as u32 > max_width { 452 | break; 453 | } 454 | 455 | ret.push(glyph); 456 | } 457 | 458 | (ret, font) 459 | } 460 | 461 | fn get_decoration( 462 | world: &World, 463 | parent: Entity, 464 | ) -> Option>> { 465 | let role = world.get::<&mut SurfaceRole>(parent).ok()?; 466 | Some(hecs::RefMut::map(role, |role| { 467 | let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role else { 468 | unreachable!() 469 | }; 470 | 471 | toplevel.decoration.satellite.as_mut().unwrap() 472 | })) 473 | } 474 | 475 | pub fn handle_pointer_leave(state: &InnerServerState, parent: Entity) { 476 | if let Some(mut decoration) = get_decoration(&state.world, parent) { 477 | decoration.handle_leave(&state.world); 478 | } 479 | } 480 | 481 | pub fn handle_pointer_motion( 482 | state: &InnerServerState, 483 | parent: Entity, 484 | surface_x: f64, 485 | surface_y: f64, 486 | ) { 487 | if let Some(mut decoration) = get_decoration(&state.world, parent) { 488 | decoration.handle_motion(&state.world, surface_x, surface_y); 489 | } 490 | } 491 | 492 | pub fn handle_pointer_click( 493 | state: &mut ServerState, 494 | parent: Entity, 495 | seat: &WlSeat, 496 | serial: u32, 497 | ) { 498 | let Ok(mut role) = state.world.get::<&mut SurfaceRole>(parent) else { 499 | return; 500 | }; 501 | let SurfaceRole::Toplevel(Some(toplevel)) = &mut *role else { 502 | unreachable!() 503 | }; 504 | 505 | if toplevel 506 | .decoration 507 | .satellite 508 | .as_mut() 509 | .unwrap() 510 | .handle_click(&toplevel.toplevel, seat, serial) 511 | { 512 | let window = *state.world.get::<&x::Window>(parent).unwrap(); 513 | drop(role); 514 | state.close_x_window(window); 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/server/clientside.rs: -------------------------------------------------------------------------------- 1 | use super::decoration::DecorationMarker; 2 | 3 | use super::ObjectEvent; 4 | use hecs::{Entity, World}; 5 | use smithay_client_toolkit::{ 6 | activation::{ActivationHandler, RequestData, RequestDataExt}, 7 | data_device_manager::{ 8 | data_device::{DataDeviceData, DataDeviceHandler}, 9 | data_offer::{DataOfferHandler, SelectionOffer}, 10 | data_source::DataSourceHandler, 11 | }, 12 | delegate_activation, delegate_data_device, delegate_primary_selection, 13 | primary_selection::{ 14 | device::{PrimarySelectionDeviceData, PrimarySelectionDeviceHandler}, 15 | offer::PrimarySelectionOffer, 16 | selection::PrimarySelectionSourceHandler, 17 | }, 18 | }; 19 | use std::sync::{mpsc, Mutex, OnceLock}; 20 | use wayland_client::protocol::{ 21 | wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, 22 | wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_region::WlRegion, 23 | wl_registry::WlRegistry, wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, 24 | wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface, 25 | wl_touch::WlTouch, 26 | }; 27 | use wayland_client::{ 28 | delegate_noop, event_created_child, 29 | globals::{Global, GlobalList, GlobalListContents}, 30 | Connection, Dispatch, Proxy, QueueHandle, 31 | }; 32 | use wayland_protocols::{ 33 | wp::relative_pointer::zv1::client::{ 34 | zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, 35 | zwp_relative_pointer_v1::ZwpRelativePointerV1, 36 | }, 37 | wp::{ 38 | fractional_scale::v1::client::{ 39 | wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, 40 | wp_fractional_scale_v1::WpFractionalScaleV1, 41 | }, 42 | linux_dmabuf::zv1::client::{ 43 | self as dmabuf, 44 | zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1 as DmabufFeedback, 45 | zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, 46 | }, 47 | pointer_constraints::zv1::client::{ 48 | zwp_confined_pointer_v1::ZwpConfinedPointerV1, 49 | zwp_locked_pointer_v1::ZwpLockedPointerV1, 50 | zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, 51 | }, 52 | primary_selection::zv1::client::{ 53 | zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1, 54 | zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, 55 | zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, 56 | }, 57 | tablet::zv2::client::{ 58 | zwp_tablet_manager_v2::ZwpTabletManagerV2, 59 | zwp_tablet_pad_group_v2::{ZwpTabletPadGroupV2, EVT_RING_OPCODE, EVT_STRIP_OPCODE}, 60 | zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, 61 | zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, 62 | zwp_tablet_pad_v2::{ZwpTabletPadV2, EVT_GROUP_OPCODE}, 63 | zwp_tablet_seat_v2::{ 64 | ZwpTabletSeatV2, EVT_PAD_ADDED_OPCODE, EVT_TABLET_ADDED_OPCODE, 65 | EVT_TOOL_ADDED_OPCODE, 66 | }, 67 | zwp_tablet_tool_v2::ZwpTabletToolV2, 68 | zwp_tablet_v2::ZwpTabletV2, 69 | }, 70 | viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter}, 71 | }, 72 | xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, 73 | xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, 74 | xdg::{ 75 | activation::v1::client::xdg_activation_v1::XdgActivationV1, 76 | shell::client::{ 77 | xdg_popup::XdgPopup, xdg_positioner::XdgPositioner, xdg_surface::XdgSurface, 78 | xdg_toplevel::XdgToplevel, xdg_wm_base::XdgWmBase, 79 | }, 80 | xdg_output::zv1::client::{ 81 | zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1 as XdgOutput, 82 | }, 83 | }, 84 | }; 85 | use wayland_server::protocol as server; 86 | use wl_drm::client::wl_drm::WlDrm; 87 | use xcb::x; 88 | 89 | pub(super) struct SelectionEvents { 90 | pub offer: Option, 91 | pub requests: Vec<( 92 | String, 93 | smithay_client_toolkit::data_device_manager::WritePipe, 94 | )>, 95 | pub cancelled: bool, 96 | } 97 | 98 | impl Default for SelectionEvents { 99 | fn default() -> Self { 100 | Self { 101 | offer: None, 102 | requests: Default::default(), 103 | cancelled: false, 104 | } 105 | } 106 | } 107 | 108 | pub(super) struct MyWorld { 109 | pub world: World, 110 | pub global_list: GlobalList, 111 | pub new_globals: Vec, 112 | events: Vec<(Entity, ObjectEvent)>, 113 | queued_events: Vec>, 114 | pub clipboard: SelectionEvents, 115 | pub primary: SelectionEvents, 116 | pub pending_activations: Vec<(xcb::x::Window, String)>, 117 | } 118 | 119 | impl MyWorld { 120 | pub fn new(global_list: GlobalList) -> Self { 121 | Self { 122 | world: World::new(), 123 | global_list, 124 | new_globals: Vec::new(), 125 | events: Vec::new(), 126 | queued_events: Vec::new(), 127 | clipboard: Default::default(), 128 | primary: Default::default(), 129 | pending_activations: Vec::new(), 130 | } 131 | } 132 | } 133 | 134 | impl std::ops::Deref for MyWorld { 135 | type Target = World; 136 | fn deref(&self) -> &Self::Target { 137 | &self.world 138 | } 139 | } 140 | 141 | impl std::ops::DerefMut for MyWorld { 142 | fn deref_mut(&mut self) -> &mut Self::Target { 143 | &mut self.world 144 | } 145 | } 146 | 147 | impl MyWorld { 148 | pub(crate) fn read_events(&mut self) -> Vec<(Entity, ObjectEvent)> { 149 | let mut events = std::mem::take(&mut self.events); 150 | self.queued_events.retain(|rx| { 151 | match rx.try_recv() { 152 | Ok(event) => { 153 | events.push(event); 154 | } 155 | Err(std::sync::mpsc::TryRecvError::Empty) => return true, 156 | 157 | Err(_) => unreachable!(), 158 | } 159 | 160 | events.extend(rx.try_iter()); 161 | false 162 | }); 163 | events 164 | } 165 | } 166 | 167 | pub type Event = ::Event; 168 | 169 | delegate_noop!(MyWorld: WlCompositor); 170 | delegate_noop!(MyWorld: WlSubcompositor); 171 | delegate_noop!(MyWorld: WlRegion); 172 | delegate_noop!(MyWorld: ignore WlShm); 173 | delegate_noop!(MyWorld: ignore ZwpLinuxDmabufV1); 174 | delegate_noop!(MyWorld: ZwpRelativePointerManagerV1); 175 | delegate_noop!(MyWorld: ignore dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1); 176 | delegate_noop!(MyWorld: XdgPositioner); 177 | delegate_noop!(MyWorld: WlShmPool); 178 | delegate_noop!(MyWorld: WpViewporter); 179 | delegate_noop!(MyWorld: WpViewport); 180 | delegate_noop!(MyWorld: ZxdgOutputManagerV1); 181 | delegate_noop!(MyWorld: ZwpPointerConstraintsV1); 182 | delegate_noop!(MyWorld: ZwpTabletManagerV2); 183 | delegate_noop!(MyWorld: XdgActivationV1); 184 | delegate_noop!(MyWorld: ZxdgDecorationManagerV1); 185 | delegate_noop!(MyWorld: WpFractionalScaleManagerV1); 186 | delegate_noop!(MyWorld: ZwpPrimarySelectionDeviceManagerV1); 187 | delegate_noop!(MyWorld: WlSubsurface); 188 | 189 | impl Dispatch for MyWorld { 190 | fn event( 191 | state: &mut Self, 192 | _: &WlRegistry, 193 | event: ::Event, 194 | _: &GlobalListContents, 195 | _: &wayland_client::Connection, 196 | _: &wayland_client::QueueHandle, 197 | ) { 198 | if let Event::::Global { 199 | name, 200 | interface, 201 | version, 202 | } = event 203 | { 204 | state.new_globals.push(Global { 205 | name, 206 | interface, 207 | version, 208 | }); 209 | }; 210 | } 211 | } 212 | 213 | impl Dispatch for MyWorld { 214 | fn event( 215 | _: &mut Self, 216 | base: &XdgWmBase, 217 | event: ::Event, 218 | _: &(), 219 | _: &wayland_client::Connection, 220 | _: &wayland_client::QueueHandle, 221 | ) { 222 | if let Event::::Ping { serial } = event { 223 | base.pong(serial); 224 | } 225 | } 226 | } 227 | 228 | impl Dispatch for MyWorld { 229 | fn event( 230 | _: &mut Self, 231 | _: &WlCallback, 232 | event: ::Event, 233 | s_callback: &server::wl_callback::WlCallback, 234 | _: &Connection, 235 | _: &QueueHandle, 236 | ) { 237 | if let Event::::Done { callback_data } = event { 238 | s_callback.done(callback_data); 239 | } 240 | } 241 | } 242 | 243 | impl Dispatch for MyWorld { 244 | fn event( 245 | _: &mut Self, 246 | _: &WlSurface, 247 | _: ::Event, 248 | _: &DecorationMarker, 249 | _: &Connection, 250 | _: &QueueHandle, 251 | ) { 252 | } 253 | } 254 | 255 | macro_rules! push_events { 256 | ($type:ident) => { 257 | impl Dispatch<$type, Entity> for MyWorld { 258 | fn event( 259 | state: &mut Self, 260 | _: &$type, 261 | event: <$type as Proxy>::Event, 262 | key: &Entity, 263 | _: &Connection, 264 | _: &QueueHandle, 265 | ) { 266 | state.events.push((*key, event.into())); 267 | } 268 | } 269 | }; 270 | } 271 | 272 | push_events!(WlSurface); 273 | push_events!(WlBuffer); 274 | push_events!(XdgSurface); 275 | push_events!(XdgToplevel); 276 | push_events!(XdgPopup); 277 | push_events!(WlSeat); 278 | push_events!(WlPointer); 279 | push_events!(WlOutput); 280 | push_events!(WlKeyboard); 281 | push_events!(ZwpRelativePointerV1); 282 | push_events!(WlDrm); 283 | push_events!(DmabufFeedback); 284 | push_events!(XdgOutput); 285 | push_events!(WlTouch); 286 | push_events!(ZwpConfinedPointerV1); 287 | push_events!(ZwpLockedPointerV1); 288 | push_events!(WpFractionalScaleV1); 289 | push_events!(ZxdgToplevelDecorationV1); 290 | 291 | pub(crate) struct LateInitObjectKey { 292 | key: OnceLock, 293 | queued_events: Mutex>, 294 | sender: Mutex>>, 295 | } 296 | 297 | impl LateInitObjectKey

298 | where 299 | P::Event: Into, 300 | { 301 | pub fn init(&self, key: Entity) { 302 | self.key.set(key).expect("Object key should not be set"); 303 | if let Some(sender) = self.sender.lock().unwrap().take() { 304 | for event in self.queued_events.lock().unwrap().drain(..) { 305 | sender.send((key, event.into())).unwrap(); 306 | } 307 | } 308 | } 309 | 310 | pub fn get(&self) -> Entity { 311 | self.key.get().copied().expect("Object key is not set") 312 | } 313 | 314 | fn new() -> Self { 315 | Self { 316 | key: OnceLock::new(), 317 | queued_events: Mutex::default(), 318 | sender: Mutex::default(), 319 | } 320 | } 321 | 322 | fn push_or_queue_event(&self, state: &mut MyWorld, event: P::Event) { 323 | if let Some(key) = self.key.get().copied() { 324 | state.events.push((key, event.into())); 325 | } else { 326 | let mut sender = self.sender.lock().unwrap(); 327 | if sender.is_none() { 328 | let (send, recv) = mpsc::channel(); 329 | *sender = Some(send); 330 | state.queued_events.push(recv); 331 | } 332 | self.queued_events.lock().unwrap().push(event); 333 | } 334 | } 335 | } 336 | 337 | impl std::ops::Deref for LateInitObjectKey

{ 338 | type Target = Entity; 339 | 340 | #[track_caller] 341 | fn deref(&self) -> &Self::Target { 342 | self.key.get().expect("object key has not been initialized") 343 | } 344 | } 345 | 346 | impl Dispatch for MyWorld { 347 | fn event( 348 | state: &mut Self, 349 | _: &ZwpTabletSeatV2, 350 | event: ::Event, 351 | key: &Entity, 352 | _: &Connection, 353 | _: &QueueHandle, 354 | ) { 355 | state.events.push((*key, event.into())); 356 | } 357 | 358 | event_created_child!(MyWorld, ZwpTabletSeatV2, [ 359 | EVT_TABLET_ADDED_OPCODE => (ZwpTabletV2, LateInitObjectKey::new()), 360 | EVT_PAD_ADDED_OPCODE => (ZwpTabletPadV2, LateInitObjectKey::new()), 361 | EVT_TOOL_ADDED_OPCODE => (ZwpTabletToolV2, LateInitObjectKey::new()) 362 | ]); 363 | } 364 | 365 | macro_rules! push_or_queue_events { 366 | ($type:ty) => { 367 | impl Dispatch<$type, LateInitObjectKey<$type>> for MyWorld { 368 | fn event( 369 | state: &mut Self, 370 | _: &$type, 371 | event: <$type as Proxy>::Event, 372 | key: &LateInitObjectKey<$type>, 373 | _: &Connection, 374 | _: &QueueHandle, 375 | ) { 376 | key.push_or_queue_event(state, event); 377 | } 378 | } 379 | }; 380 | } 381 | 382 | push_or_queue_events!(ZwpTabletV2); 383 | push_or_queue_events!(ZwpTabletToolV2); 384 | push_or_queue_events!(ZwpTabletPadRingV2); 385 | push_or_queue_events!(ZwpTabletPadStripV2); 386 | 387 | impl Dispatch> for MyWorld { 388 | fn event( 389 | state: &mut Self, 390 | _: &ZwpTabletPadV2, 391 | event: ::Event, 392 | key: &LateInitObjectKey, 393 | _: &Connection, 394 | _: &QueueHandle, 395 | ) { 396 | key.push_or_queue_event(state, event); 397 | } 398 | 399 | event_created_child!(MyWorld, ZwpTabletPadV2, [ 400 | EVT_GROUP_OPCODE => (ZwpTabletPadGroupV2, LateInitObjectKey::new()) 401 | ]); 402 | } 403 | 404 | impl Dispatch> for MyWorld { 405 | fn event( 406 | state: &mut Self, 407 | _: &ZwpTabletPadGroupV2, 408 | event: ::Event, 409 | key: &LateInitObjectKey, 410 | _: &Connection, 411 | _: &QueueHandle, 412 | ) { 413 | key.push_or_queue_event(state, event); 414 | } 415 | 416 | event_created_child!(MyWorld, ZwpTabletPadGroupV2, [ 417 | EVT_RING_OPCODE => (ZwpTabletPadRingV2, LateInitObjectKey::new()), 418 | EVT_STRIP_OPCODE => (ZwpTabletPadStripV2, LateInitObjectKey::new()) 419 | ]); 420 | } 421 | 422 | delegate_data_device!(MyWorld); 423 | 424 | impl DataDeviceHandler for MyWorld { 425 | fn selection( 426 | &mut self, 427 | _: &wayland_client::Connection, 428 | _: &wayland_client::QueueHandle, 429 | data_device: &wayland_client::protocol::wl_data_device::WlDataDevice, 430 | ) { 431 | let data: &DataDeviceData = data_device.data().unwrap(); 432 | self.clipboard.offer = data.selection_offer(); 433 | } 434 | 435 | fn drop_performed( 436 | &mut self, 437 | _: &wayland_client::Connection, 438 | _: &wayland_client::QueueHandle, 439 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 440 | ) { 441 | } 442 | 443 | fn motion( 444 | &mut self, 445 | _: &wayland_client::Connection, 446 | _: &wayland_client::QueueHandle, 447 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 448 | _: f64, 449 | _: f64, 450 | ) { 451 | } 452 | 453 | fn leave( 454 | &mut self, 455 | _: &wayland_client::Connection, 456 | _: &wayland_client::QueueHandle, 457 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 458 | ) { 459 | } 460 | 461 | fn enter( 462 | &mut self, 463 | _: &wayland_client::Connection, 464 | _: &wayland_client::QueueHandle, 465 | _: &wayland_client::protocol::wl_data_device::WlDataDevice, 466 | _: f64, 467 | _: f64, 468 | _: &wayland_client::protocol::wl_surface::WlSurface, 469 | ) { 470 | } 471 | } 472 | 473 | impl DataSourceHandler for MyWorld { 474 | fn send_request( 475 | &mut self, 476 | _: &wayland_client::Connection, 477 | _: &wayland_client::QueueHandle, 478 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 479 | mime: String, 480 | fd: smithay_client_toolkit::data_device_manager::WritePipe, 481 | ) { 482 | self.clipboard.requests.push((mime, fd)); 483 | } 484 | 485 | fn cancelled( 486 | &mut self, 487 | _: &wayland_client::Connection, 488 | _: &wayland_client::QueueHandle, 489 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 490 | ) { 491 | self.clipboard.cancelled = true; 492 | } 493 | 494 | fn action( 495 | &mut self, 496 | _: &wayland_client::Connection, 497 | _: &wayland_client::QueueHandle, 498 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 499 | _: wayland_client::protocol::wl_data_device_manager::DndAction, 500 | ) { 501 | } 502 | 503 | fn dnd_finished( 504 | &mut self, 505 | _: &wayland_client::Connection, 506 | _: &wayland_client::QueueHandle, 507 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 508 | ) { 509 | } 510 | 511 | fn dnd_dropped( 512 | &mut self, 513 | _: &wayland_client::Connection, 514 | _: &wayland_client::QueueHandle, 515 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 516 | ) { 517 | } 518 | 519 | fn accept_mime( 520 | &mut self, 521 | _: &wayland_client::Connection, 522 | _: &wayland_client::QueueHandle, 523 | _: &wayland_client::protocol::wl_data_source::WlDataSource, 524 | _: Option, 525 | ) { 526 | } 527 | } 528 | 529 | impl DataOfferHandler for MyWorld { 530 | fn selected_action( 531 | &mut self, 532 | _: &wayland_client::Connection, 533 | _: &wayland_client::QueueHandle, 534 | _: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer, 535 | _: wayland_client::protocol::wl_data_device_manager::DndAction, 536 | ) { 537 | } 538 | 539 | fn source_actions( 540 | &mut self, 541 | _: &wayland_client::Connection, 542 | _: &wayland_client::QueueHandle, 543 | _: &mut smithay_client_toolkit::data_device_manager::data_offer::DragOffer, 544 | _: wayland_client::protocol::wl_data_device_manager::DndAction, 545 | ) { 546 | } 547 | } 548 | 549 | delegate_activation!(MyWorld, ActivationData); 550 | 551 | pub struct ActivationData { 552 | window: x::Window, 553 | data: RequestData, 554 | } 555 | 556 | impl ActivationData { 557 | pub fn new(window: x::Window, data: RequestData) -> Self { 558 | Self { window, data } 559 | } 560 | } 561 | 562 | impl RequestDataExt for ActivationData { 563 | fn app_id(&self) -> Option<&str> { 564 | self.data.app_id() 565 | } 566 | 567 | fn seat_and_serial(&self) -> Option<(&wayland_client::protocol::wl_seat::WlSeat, u32)> { 568 | self.data.seat_and_serial() 569 | } 570 | 571 | fn surface(&self) -> Option<&wayland_client::protocol::wl_surface::WlSurface> { 572 | self.data.surface() 573 | } 574 | } 575 | 576 | impl ActivationHandler for MyWorld { 577 | type RequestData = ActivationData; 578 | 579 | fn new_token(&mut self, token: String, data: &Self::RequestData) { 580 | self.pending_activations.push((data.window, token)); 581 | } 582 | } 583 | 584 | delegate_primary_selection!(MyWorld); 585 | 586 | impl PrimarySelectionDeviceHandler for MyWorld { 587 | fn selection( 588 | &mut self, 589 | _: &Connection, 590 | _: &QueueHandle, 591 | primary_selection_device: &ZwpPrimarySelectionDeviceV1, 592 | ) { 593 | let Some(data) = primary_selection_device.data::() else { 594 | return; 595 | }; 596 | 597 | self.primary.offer = data.selection_offer(); 598 | } 599 | } 600 | 601 | impl PrimarySelectionSourceHandler for MyWorld { 602 | fn send_request( 603 | &mut self, 604 | _: &Connection, 605 | _: &QueueHandle, 606 | _: &ZwpPrimarySelectionSourceV1, 607 | mime: String, 608 | write_pipe: smithay_client_toolkit::data_device_manager::WritePipe, 609 | ) { 610 | self.primary.requests.push((mime, write_pipe)); 611 | } 612 | 613 | fn cancelled( 614 | &mut self, 615 | _: &Connection, 616 | _: &QueueHandle, 617 | _: &ZwpPrimarySelectionSourceV1, 618 | ) { 619 | self.primary.cancelled = true; 620 | } 621 | } 622 | -------------------------------------------------------------------------------- /src/xstate/selection.rs: -------------------------------------------------------------------------------- 1 | use super::{get_atom_name, XState}; 2 | use crate::server::selection::{Clipboard, ForeignSelection, Primary, SelectionType}; 3 | use crate::{RealServerState, X11Selection}; 4 | use log::{debug, error, warn}; 5 | use rustix::event::{poll, PollFd, PollFlags}; 6 | use smithay_client_toolkit::data_device_manager::WritePipe; 7 | use std::cell::RefCell; 8 | use std::collections::VecDeque; 9 | use std::io::{Error, ErrorKind, Result, Write}; 10 | use std::rc::Rc; 11 | use xcb::x; 12 | 13 | #[derive(Debug)] 14 | struct SelectionTargetId { 15 | name: String, 16 | target: x::Atom, 17 | property: x::Atom, 18 | source: Option, 19 | } 20 | 21 | struct PendingSelectionData { 22 | target: x::Atom, 23 | property: x::Atom, 24 | pipe: WritePipe, 25 | incr: bool, 26 | active: bool, 27 | } 28 | 29 | pub struct Selection { 30 | mimes: Vec, 31 | connection: Rc, 32 | window: x::Window, 33 | pending: RefCell>, 34 | selection: x::Atom, 35 | selection_time: u32, 36 | incr: x::Atom, 37 | } 38 | 39 | impl X11Selection for Selection { 40 | fn mime_types(&self) -> Vec<&str> { 41 | self.mimes 42 | .iter() 43 | .map(|target| target.name.as_str()) 44 | .collect() 45 | } 46 | 47 | fn write_to(&self, mime: &str, pipe: WritePipe) { 48 | if let Some(target) = self.mimes.iter().find(|target| target.name == mime) { 49 | // A lot of X applications do not anticipate the possibility of multiple requests for 50 | // its owned selection to need the INCR transfer mechanism and will stop sending the 51 | // necessary `PropertyNotify` events, hanging Wayland transfer receivers. 52 | // To remedy this, every target requested by Wayland is put into a FIFO queue and the 53 | // `ConvertSelection` starting the next request is not sent until `next_conversion` 54 | // closes the `WritePipe`, marking termination of that request. 55 | self.pending.borrow_mut().push_back(PendingSelectionData { 56 | target: target.target, 57 | property: target.property, 58 | pipe, 59 | incr: false, 60 | active: false, 61 | }); 62 | if self.pending.borrow().len() == 1 { 63 | self.next_conversion(); 64 | } 65 | } else { 66 | warn!("Could not find mime type {mime}"); 67 | } 68 | } 69 | } 70 | 71 | impl Selection { 72 | /// Finish handling the current pending selection (if any) and queue the next one (if any). 73 | /// 74 | /// Regardless of whether the selection conversion succeeds or fails, this function must be 75 | /// called in order to drop the `WritePipe` to signal to the Wayland program no more data is 76 | /// coming and to start processing the next `PendingSelectionData`. 77 | /// 78 | /// # Panics: 79 | /// This function will panic if the `pending` `RefCell` is actively being borrowed. 80 | fn next_conversion(&self) { 81 | let mut pending = self.pending.borrow_mut(); 82 | if pending.front().is_some_and(|p| p.active) { 83 | pending.pop_front(); 84 | } 85 | 86 | while let Some(psd) = pending.front_mut() { 87 | if let Err(e) = self 88 | .connection 89 | .send_and_check_request(&x::ConvertSelection { 90 | requestor: self.window, 91 | selection: self.selection, 92 | target: psd.target, 93 | property: psd.property, 94 | time: self.selection_time, 95 | }) 96 | { 97 | error!( 98 | "Failed to request selection data (target: {}, error: {e})", 99 | get_atom_name(&self.connection, psd.target), 100 | ); 101 | pending.pop_front(); 102 | } else { 103 | psd.active = true; 104 | break; 105 | } 106 | } 107 | } 108 | 109 | fn handle_notify(&self, target: x::Atom) { 110 | let mut pending = self.pending.borrow_mut(); 111 | let Some(psd) = pending.front_mut().filter(|t| t.target == target) else { 112 | warn!( 113 | "Got selection notify for unexpected target {}", 114 | get_atom_name(&self.connection, target), 115 | ); 116 | drop(pending); 117 | self.next_conversion(); 118 | return; 119 | }; 120 | 121 | let request = self.connection.send_request(&x::GetProperty { 122 | delete: true, 123 | window: self.window, 124 | property: psd.property, 125 | r#type: x::ATOM_ANY, 126 | long_offset: 0, 127 | long_length: u32::MAX, 128 | }); 129 | let reply = match self.connection.wait_for_reply(request) { 130 | Ok(reply) => reply, 131 | Err(e) => { 132 | warn!( 133 | "Couldn't get mime type for {}: {e:?}", 134 | get_atom_name(&self.connection, psd.target) 135 | ); 136 | drop(pending); 137 | self.next_conversion(); 138 | return; 139 | } 140 | }; 141 | 142 | debug!( 143 | "got type {} for mime type {}", 144 | get_atom_name(&self.connection, reply.r#type()), 145 | get_atom_name(&self.connection, psd.target), 146 | ); 147 | 148 | if reply.r#type() == self.incr { 149 | debug!( 150 | "beginning incr for {}", 151 | get_atom_name(&self.connection, psd.property) 152 | ); 153 | psd.incr = true; 154 | return; 155 | } 156 | 157 | let data = match reply.format() { 158 | 8 => reply.value::(), 159 | 32 => unsafe { reply.value::().align_to().1 }, 160 | other => { 161 | warn!("Unexpected format {other} in selection reply"); 162 | drop(pending); 163 | self.next_conversion(); 164 | return; 165 | } 166 | }; 167 | 168 | // Since the WritePipe given to us can have the O_NONBLOCK flag, we must respect that and 169 | // use `select` to monitor when the pipe is available to do more I/O. 170 | fn write_all(pipe: &mut WritePipe, mut buf: &[u8]) -> Result<()> { 171 | while !buf.is_empty() { 172 | match pipe.write(buf) { 173 | Ok(0) => return Err(Error::from(ErrorKind::WriteZero)), 174 | Ok(n) => buf = &buf[n..], 175 | Err(ref e) if e.kind() == ErrorKind::Interrupted => {} 176 | Err(ref e) if e.kind() == ErrorKind::WouldBlock => { 177 | let mut pollfds = [PollFd::new(pipe, PollFlags::OUT)]; 178 | poll(&mut pollfds, None)?; 179 | } 180 | Err(e) => return Err(e), 181 | } 182 | } 183 | Ok(()) 184 | } 185 | 186 | if !psd.incr || !data.is_empty() { 187 | if let Err(e) = write_all(&mut psd.pipe, data) { 188 | warn!("Failed to write selection data: {e:?}"); 189 | } else if psd.incr { 190 | debug!( 191 | "received some incr data for {}", 192 | get_atom_name(&self.connection, psd.target) 193 | ); 194 | return; 195 | } 196 | } else if psd.incr { 197 | // data is empty 198 | debug!( 199 | "completed incr for mime {}", 200 | get_atom_name(&self.connection, target) 201 | ); 202 | } 203 | 204 | drop(pending); 205 | self.next_conversion(); 206 | } 207 | 208 | fn check_for_incr(&self, event: &x::PropertyNotifyEvent) -> bool { 209 | debug_assert_eq!(event.state(), x::Property::NewValue); 210 | if event.window() != self.window { 211 | return false; 212 | } 213 | 214 | let target = self.pending.borrow().front().and_then(|pending| { 215 | (pending.property == event.atom() && pending.incr).then_some(pending.target) 216 | }); 217 | if let Some(target) = target { 218 | self.handle_notify(target); 219 | true 220 | } else { 221 | false 222 | } 223 | } 224 | } 225 | 226 | pub struct WaylandIncrInfo { 227 | data: Vec, 228 | start: usize, 229 | property: x::Atom, 230 | target_window: x::Window, 231 | target_type: x::Atom, 232 | max_req_bytes: usize, 233 | } 234 | 235 | pub struct WaylandSelection { 236 | mimes: Vec, 237 | inner: ForeignSelection, 238 | incr_data: Option, 239 | } 240 | 241 | impl WaylandSelection { 242 | fn check_for_incr( 243 | &mut self, 244 | event: &x::PropertyNotifyEvent, 245 | connection: &xcb::Connection, 246 | ) -> bool { 247 | let Some(incr_data) = self.incr_data.as_mut() else { 248 | return false; 249 | }; 250 | if incr_data.property != event.atom() { 251 | return false; 252 | } 253 | 254 | let incr_end = std::cmp::min( 255 | incr_data.max_req_bytes + incr_data.start, 256 | incr_data.data.len(), 257 | ); 258 | 259 | if let Err(e) = connection.send_and_check_request(&x::ChangeProperty { 260 | mode: x::PropMode::Append, 261 | window: incr_data.target_window, 262 | property: incr_data.property, 263 | r#type: incr_data.target_type, 264 | data: &incr_data.data[incr_data.start..incr_end], 265 | }) { 266 | warn!("failed to write selection data: {e:?}"); 267 | self.incr_data = None; 268 | return true; 269 | } 270 | 271 | if incr_data.start == incr_end { 272 | debug!( 273 | "completed incr for mime {}", 274 | get_atom_name(connection, incr_data.target_type) 275 | ); 276 | self.incr_data = None; 277 | } else { 278 | debug!( 279 | "received some incr data for {}", 280 | get_atom_name(connection, incr_data.target_type) 281 | ); 282 | incr_data.start = incr_end; 283 | } 284 | true 285 | } 286 | } 287 | 288 | enum CurrentSelection { 289 | X11(Rc), 290 | Wayland(WaylandSelection), 291 | } 292 | 293 | struct SelectionData { 294 | last_selection_timestamp: u32, 295 | atom: x::Atom, 296 | targets_atom: x::Atom, 297 | current_selection: Option>, 298 | } 299 | 300 | // This is a trait so that we can use &dyn 301 | trait SelectionDataImpl { 302 | fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window) -> bool; 303 | fn handle_new_owner( 304 | &mut self, 305 | connection: &xcb::Connection, 306 | wm_window: x::Window, 307 | atoms: &super::Atoms, 308 | owner: x::Window, 309 | timestamp: u32, 310 | ); 311 | fn handle_target_list( 312 | &mut self, 313 | connection: &Rc, 314 | wm_window: x::Window, 315 | atoms: &super::Atoms, 316 | target_window: x::Window, 317 | dest_property: x::Atom, 318 | server_state: &mut RealServerState, 319 | ); 320 | fn x11_selection(&self) -> Option<&Selection>; 321 | fn handle_selection_request( 322 | &mut self, 323 | connection: &xcb::Connection, 324 | atoms: &super::Atoms, 325 | request: &x::SelectionRequestEvent, 326 | max_req_bytes: usize, 327 | server_state: &mut RealServerState, 328 | ) -> bool; 329 | fn atom(&self) -> x::Atom; 330 | fn selection_clear(&mut self); 331 | } 332 | 333 | impl SelectionData { 334 | fn new(atom: x::Atom, targets_atom: x::Atom) -> Self { 335 | Self { 336 | last_selection_timestamp: x::CURRENT_TIME, 337 | atom, 338 | targets_atom, 339 | current_selection: None, 340 | } 341 | } 342 | fn wayland_selection_mut(&mut self) -> Option<&mut WaylandSelection> { 343 | match &mut self.current_selection { 344 | Some(CurrentSelection::Wayland(sel)) => Some(sel), 345 | _ => None, 346 | } 347 | } 348 | } 349 | 350 | impl SelectionDataImpl for SelectionData { 351 | fn atom(&self) -> x::Atom { 352 | self.atom 353 | } 354 | fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window) -> bool { 355 | if let Err(e) = connection.send_and_check_request(&x::SetSelectionOwner { 356 | owner: wm_window, 357 | selection: self.atom, 358 | time: self.last_selection_timestamp, 359 | }) { 360 | warn!( 361 | "Could not become owner of {}: {e:?}", 362 | get_atom_name(connection, self.atom) 363 | ); 364 | return false; 365 | }; 366 | 367 | match connection.wait_for_reply(connection.send_request(&x::GetSelectionOwner { 368 | selection: self.atom, 369 | })) { 370 | Ok(reply) if reply.owner() == wm_window => true, 371 | Ok(reply) => { 372 | warn!( 373 | "Could not become owner of {} (owned by {:?})", 374 | get_atom_name(connection, self.atom), 375 | reply.owner() 376 | ); 377 | false 378 | } 379 | Err(e) => { 380 | warn!( 381 | "Could not validate ownership of {}: {e:?}", 382 | get_atom_name(connection, self.atom) 383 | ); 384 | false 385 | } 386 | } 387 | } 388 | 389 | fn selection_clear(&mut self) { 390 | self.current_selection = None; 391 | } 392 | 393 | fn handle_new_owner( 394 | &mut self, 395 | connection: &xcb::Connection, 396 | wm_window: x::Window, 397 | atoms: &super::Atoms, 398 | owner: x::Window, 399 | timestamp: u32, 400 | ) { 401 | // Grab targets 402 | match connection.send_and_check_request(&x::ConvertSelection { 403 | requestor: wm_window, 404 | selection: self.atom, 405 | target: atoms.targets, 406 | property: self.targets_atom, 407 | time: timestamp, 408 | }) { 409 | Ok(_) => { 410 | debug!( 411 | "new {} owner: {owner:?}", 412 | get_atom_name(connection, self.atom) 413 | ); 414 | self.last_selection_timestamp = timestamp; 415 | } 416 | Err(e) => warn!( 417 | "could not set new {} owner: {e:?}", 418 | get_atom_name(connection, self.atom) 419 | ), 420 | } 421 | } 422 | 423 | fn handle_target_list( 424 | &mut self, 425 | connection: &Rc, 426 | wm_window: x::Window, 427 | atoms: &super::Atoms, 428 | target_window: x::Window, 429 | dest_property: x::Atom, 430 | server_state: &mut RealServerState, 431 | ) { 432 | let reply = match connection.wait_for_reply(connection.send_request(&x::GetProperty { 433 | delete: true, 434 | window: wm_window, 435 | property: dest_property, 436 | r#type: x::ATOM_ATOM, 437 | long_offset: 0, 438 | long_length: 20, 439 | })) { 440 | Ok(reply) => reply, 441 | Err(e) => { 442 | warn!("Could not obtain target list: {e:?}"); 443 | return; 444 | } 445 | }; 446 | 447 | let targets: &[x::Atom] = reply.value(); 448 | if targets.is_empty() { 449 | warn!("Got empty selection target list, trying again..."); 450 | match connection.wait_for_reply(connection.send_request(&x::GetSelectionOwner { 451 | selection: self.atom, 452 | })) { 453 | Ok(reply) if reply.owner() != wm_window => { 454 | self.handle_new_owner( 455 | connection, 456 | wm_window, 457 | atoms, 458 | reply.owner(), 459 | self.last_selection_timestamp, 460 | ); 461 | } 462 | Ok(_) => { 463 | warn!("We are unexpectedly the selection owner? Clipboard may be broken!"); 464 | } 465 | Err(e) => { 466 | error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!"); 467 | } 468 | } 469 | return; 470 | } 471 | if log::log_enabled!(log::Level::Debug) { 472 | let targets_str: Vec = targets 473 | .iter() 474 | .map(|t| get_atom_name(connection, *t)) 475 | .collect(); 476 | debug!("got targets: {targets_str:?}"); 477 | } 478 | 479 | let selection = get_atom_name(connection, self.atom); 480 | let mimes = targets 481 | .iter() 482 | .copied() 483 | .filter(|atom| ![atoms.targets, atoms.multiple, atoms.save_targets].contains(atom)) 484 | .map(|target| { 485 | let name = get_atom_name(connection, target); 486 | let property = connection 487 | .wait_for_reply(connection.send_request(&x::InternAtom { 488 | only_if_exists: false, 489 | name: &[name.as_bytes(), b"_", selection.as_bytes()].concat(), 490 | })) 491 | .unwrap() 492 | .atom(); 493 | SelectionTargetId { 494 | name, 495 | target, 496 | property, 497 | source: None, 498 | } 499 | }) 500 | .collect(); 501 | 502 | let selection = Rc::new(Selection { 503 | mimes, 504 | connection: connection.clone(), 505 | window: target_window, 506 | pending: RefCell::default(), 507 | selection: self.atom, 508 | selection_time: self.last_selection_timestamp, 509 | incr: atoms.incr, 510 | }); 511 | 512 | server_state.set_selection_source::(&selection); 513 | self.current_selection = Some(CurrentSelection::X11(selection)); 514 | debug!("{} set from X11", get_atom_name(connection, self.atom)); 515 | } 516 | 517 | fn x11_selection(&self) -> Option<&Selection> { 518 | match &self.current_selection { 519 | Some(CurrentSelection::X11(selection)) => Some(selection), 520 | _ => None, 521 | } 522 | } 523 | 524 | fn handle_selection_request( 525 | &mut self, 526 | connection: &xcb::Connection, 527 | atoms: &super::Atoms, 528 | request: &x::SelectionRequestEvent, 529 | max_req_bytes: usize, 530 | server_state: &mut RealServerState, 531 | ) -> bool { 532 | let Some(CurrentSelection::Wayland(WaylandSelection { 533 | mimes, 534 | inner, 535 | ref mut incr_data, 536 | })) = &mut self.current_selection 537 | else { 538 | warn!("Got selection request, but we don't seem to be the selection owner"); 539 | return false; 540 | }; 541 | 542 | let req_target = request.target(); 543 | if req_target == atoms.targets { 544 | let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.target).collect(); 545 | 546 | match connection.send_and_check_request(&x::ChangeProperty { 547 | mode: x::PropMode::Replace, 548 | window: request.requestor(), 549 | property: request.property(), 550 | r#type: x::ATOM_ATOM, 551 | data: &atoms, 552 | }) { 553 | Ok(_) => true, 554 | Err(e) => { 555 | warn!("Failed to set targets for selection request: {e:?}"); 556 | false 557 | } 558 | } 559 | } else { 560 | let Some(target) = mimes.iter().find(|t| t.target == req_target) else { 561 | if log::log_enabled!(log::Level::Debug) { 562 | let name = get_atom_name(connection, req_target); 563 | debug!( 564 | "refusing selection request because given atom could not be found ({name})" 565 | ); 566 | } 567 | return false; 568 | }; 569 | 570 | let mime_name = target 571 | .source 572 | .as_ref() 573 | .cloned() 574 | .unwrap_or_else(|| target.name.clone()); 575 | let data = inner.receive(mime_name, server_state); 576 | if data.len() > max_req_bytes { 577 | if let Err(e) = connection.send_and_check_request(&x::ChangeWindowAttributes { 578 | window: request.requestor(), 579 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 580 | }) { 581 | warn!("Failed to set up property change notifications: {e:?}"); 582 | return false; 583 | } 584 | if let Err(e) = connection.send_and_check_request(&x::ChangeProperty { 585 | mode: x::PropMode::Replace, 586 | window: request.requestor(), 587 | property: request.property(), 588 | r#type: atoms.incr, 589 | data: &[data.len() as u32], 590 | }) { 591 | warn!("Failed to set incr property for large transfer: {e:?}"); 592 | return false; 593 | } 594 | debug!( 595 | "beginning incr for {}", 596 | get_atom_name(connection, target.target) 597 | ); 598 | *incr_data = Some(WaylandIncrInfo { 599 | data, 600 | start: 0, 601 | target_window: request.requestor(), 602 | property: request.property(), 603 | target_type: target.target, 604 | max_req_bytes, 605 | }); 606 | true 607 | } else { 608 | match connection.send_and_check_request(&x::ChangeProperty { 609 | mode: x::PropMode::Replace, 610 | window: request.requestor(), 611 | property: request.property(), 612 | r#type: target.target, 613 | data: &data, 614 | }) { 615 | Ok(_) => true, 616 | Err(e) => { 617 | warn!("Failed setting selection property: {e:?}"); 618 | false 619 | } 620 | } 621 | } 622 | } 623 | } 624 | } 625 | 626 | pub(super) struct SelectionState { 627 | clipboard: SelectionData, 628 | primary: SelectionData, 629 | target_window: x::Window, 630 | } 631 | 632 | impl SelectionState { 633 | pub fn new(connection: &xcb::Connection, root: x::Window, atoms: &super::Atoms) -> Self { 634 | let target_window = connection.generate_id(); 635 | connection 636 | .send_and_check_request(&x::CreateWindow { 637 | wid: target_window, 638 | width: 1, 639 | height: 1, 640 | depth: 0, 641 | parent: root, 642 | x: 0, 643 | y: 0, 644 | border_width: 0, 645 | class: x::WindowClass::InputOnly, 646 | visual: x::COPY_FROM_PARENT, 647 | // Watch for INCR property changes. 648 | value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], 649 | }) 650 | .expect("Couldn't create window for selections"); 651 | Self { 652 | target_window, 653 | clipboard: SelectionData::new(atoms.clipboard, atoms.clipboard_targets), 654 | primary: SelectionData::new(atoms.primary, atoms.primary_targets), 655 | } 656 | } 657 | } 658 | 659 | impl XState { 660 | fn intern_target_property_atoms(&self, mime: &[u8], suffix: &[u8]) -> (x::Atom, x::Atom) { 661 | // A concatenation of the target and the selection type are used to create a distinct 662 | // property to write to 663 | let target = self 664 | .connection 665 | .wait_for_reply(self.connection.send_request(&x::InternAtom { 666 | only_if_exists: false, 667 | name: mime, 668 | })) 669 | .unwrap() 670 | .atom(); 671 | let property = self 672 | .connection 673 | .wait_for_reply(self.connection.send_request(&x::InternAtom { 674 | only_if_exists: false, 675 | name: &[mime, suffix].concat(), 676 | })) 677 | .unwrap() 678 | .atom(); 679 | (target, property) 680 | } 681 | 682 | pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) { 683 | let mut utf8_xwl = false; 684 | let mut utf8_wl = false; 685 | let mut mimes: Vec = selection 686 | .mime_types 687 | .iter() 688 | .map(|mime| { 689 | match mime.as_str() { 690 | "UTF8_STRING" => utf8_xwl = true, 691 | "text/plain;charset=utf-8" => utf8_wl = true, 692 | _ => {} 693 | } 694 | 695 | let (target, property) = 696 | self.intern_target_property_atoms(mime.as_bytes(), b"_CLIPBOARD"); 697 | SelectionTargetId { 698 | name: mime.clone(), 699 | target, 700 | property, 701 | source: None, 702 | } 703 | }) 704 | .collect(); 705 | 706 | if utf8_wl && !utf8_xwl { 707 | let name = "UTF8_STRING".to_string(); 708 | let (target, property) = 709 | self.intern_target_property_atoms(name.as_bytes(), b"_CLIPBOARD"); 710 | mimes.push(SelectionTargetId { 711 | name, 712 | target, 713 | property, 714 | source: Some("text/plain;charset=utf-8".to_string()), 715 | }); 716 | } 717 | 718 | if self 719 | .selection_state 720 | .clipboard 721 | .set_owner(&self.connection, self.wm_window) 722 | { 723 | self.selection_state.clipboard.current_selection = 724 | Some(CurrentSelection::Wayland(WaylandSelection { 725 | mimes, 726 | inner: selection, 727 | incr_data: None, 728 | })); 729 | 730 | debug!("Clipboard set from Wayland"); 731 | } 732 | } 733 | 734 | pub(crate) fn set_primary_selection(&mut self, selection: ForeignSelection) { 735 | let mut utf8_xwl = false; 736 | let mut utf8_wl = false; 737 | let mut mimes: Vec = selection 738 | .mime_types 739 | .iter() 740 | .map(|mime| { 741 | match mime.as_str() { 742 | "UTF8_STRING" => utf8_xwl = true, 743 | "text/plain;charset=utf-8" => utf8_wl = true, 744 | _ => {} 745 | } 746 | 747 | let (target, property) = 748 | self.intern_target_property_atoms(mime.as_bytes(), b"_PRIMARY"); 749 | SelectionTargetId { 750 | name: mime.clone(), 751 | target, 752 | property, 753 | source: None, 754 | } 755 | }) 756 | .collect(); 757 | 758 | if utf8_wl && !utf8_xwl { 759 | let name = "UTF8_STRING".to_string(); 760 | let (target, property) = 761 | self.intern_target_property_atoms(name.as_bytes(), b"_PRIMARY"); 762 | mimes.push(SelectionTargetId { 763 | name, 764 | target, 765 | property, 766 | source: Some("text/plain;charset=utf-8".to_string()), 767 | }); 768 | } 769 | 770 | if self 771 | .selection_state 772 | .primary 773 | .set_owner(&self.connection, self.wm_window) 774 | { 775 | self.selection_state.primary.current_selection = 776 | Some(CurrentSelection::Wayland(WaylandSelection { 777 | mimes, 778 | inner: selection, 779 | incr_data: None, 780 | })); 781 | debug!("Primary set from Wayland"); 782 | } 783 | } 784 | 785 | pub(super) fn handle_selection_event( 786 | &mut self, 787 | event: &xcb::Event, 788 | server_state: &mut RealServerState, 789 | ) -> bool { 790 | macro_rules! get_selection_data { 791 | ($selection:expr) => { 792 | match $selection { 793 | x if x == self.atoms.clipboard => { 794 | &mut self.selection_state.clipboard as &mut dyn SelectionDataImpl 795 | } 796 | x if x == self.atoms.primary => &mut self.selection_state.primary as _, 797 | _ => return true, 798 | } 799 | }; 800 | } 801 | match event { 802 | xcb::Event::X(x::Event::SelectionClear(e)) => { 803 | let data = get_selection_data!(e.selection()); 804 | data.selection_clear(); 805 | } 806 | xcb::Event::X(x::Event::SelectionNotify(e)) => { 807 | if e.property() == x::ATOM_NONE { 808 | // Since the requested conversion could not be made, the request is invalid and 809 | // should be removed, dropping the WritePipe to signal no data will be sent 810 | if e.requestor() == self.selection_state.target_window { 811 | let data = get_selection_data!(e.selection()); 812 | if let Some(selection) = data.x11_selection() { 813 | selection.next_conversion(); 814 | } 815 | } 816 | warn!( 817 | "selection notify fail? {}", 818 | get_atom_name(&self.connection, e.selection()) 819 | ); 820 | return true; 821 | } 822 | 823 | let data = get_selection_data!(e.selection()); 824 | debug!( 825 | "selection notify requestor: {:?} target: {} selection: {}", 826 | e.requestor(), 827 | get_atom_name(&self.connection, e.target()), 828 | get_atom_name(&self.connection, e.selection()), 829 | ); 830 | 831 | if e.requestor() == self.wm_window { 832 | match e.target() { 833 | x if x == self.atoms.targets => data.handle_target_list( 834 | &self.connection, 835 | self.wm_window, 836 | &self.atoms, 837 | self.selection_state.target_window, 838 | e.property(), 839 | server_state, 840 | ), 841 | other => warn!( 842 | "got unexpected selection notify for target {}", 843 | get_atom_name(&self.connection, other) 844 | ), 845 | } 846 | } else if e.requestor() == self.selection_state.target_window { 847 | if let Some(selection) = data.x11_selection() { 848 | selection.handle_notify(e.target()); 849 | } 850 | } else { 851 | warn!( 852 | "Got selection notify from unexpected requestor: {:?}", 853 | e.requestor() 854 | ); 855 | } 856 | } 857 | xcb::Event::X(x::Event::SelectionRequest(e)) => { 858 | let data = get_selection_data!(e.selection()); 859 | let send_notify = |property| { 860 | if let Err(e) = self.connection.send_and_check_request(&x::SendEvent { 861 | propagate: false, 862 | destination: x::SendEventDest::Window(e.requestor()), 863 | event_mask: x::EventMask::empty(), 864 | event: &x::SelectionNotifyEvent::new( 865 | e.time(), 866 | e.requestor(), 867 | e.selection(), 868 | e.target(), 869 | property, 870 | ), 871 | }) { 872 | warn!("Failed to send selection request notify: {e:?}"); 873 | }; 874 | }; 875 | let refuse = || send_notify(x::ATOM_NONE); 876 | let success = || send_notify(e.property()); 877 | 878 | if log::log_enabled!(log::Level::Debug) { 879 | let target = get_atom_name(&self.connection, e.target()); 880 | let selection = get_atom_name(&self.connection, data.atom()); 881 | debug!("Got selection request for target {target} (selection: {selection})"); 882 | } 883 | 884 | if e.property() == x::ATOM_NONE { 885 | debug!("refusing - property is set to none"); 886 | refuse(); 887 | return true; 888 | } 889 | 890 | if data.handle_selection_request( 891 | &self.connection, 892 | &self.atoms, 893 | e, 894 | self.max_req_bytes, 895 | server_state, 896 | ) { 897 | success() 898 | } else { 899 | refuse() 900 | } 901 | } 902 | 903 | xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => match e.selection() { 904 | x if x == self.atoms.clipboard || x == self.atoms.primary => match e.subtype() { 905 | xcb::xfixes::SelectionEvent::SetSelectionOwner => { 906 | if e.owner() == self.wm_window { 907 | return true; 908 | } 909 | 910 | let data = get_selection_data!(x); 911 | 912 | data.handle_new_owner( 913 | &self.connection, 914 | self.wm_window, 915 | &self.atoms, 916 | e.owner(), 917 | e.timestamp(), 918 | ); 919 | } 920 | xcb::xfixes::SelectionEvent::SelectionClientClose 921 | | xcb::xfixes::SelectionEvent::SelectionWindowDestroy => { 922 | debug!("Selection owner destroyed, selection will be unset"); 923 | self.selection_state.clipboard.current_selection = None; 924 | } 925 | }, 926 | x if x == self.atoms.xsettings => match e.subtype() { 927 | xcb::xfixes::SelectionEvent::SelectionClientClose 928 | | xcb::xfixes::SelectionEvent::SelectionWindowDestroy => { 929 | debug!("Xsettings owner disappeared, reacquiring"); 930 | self.set_xsettings_owner(); 931 | } 932 | _ => {} 933 | }, 934 | _ => {} 935 | }, 936 | _ => return false, 937 | } 938 | 939 | true 940 | } 941 | 942 | /// Check if a PropertyNotifyEvent refers to a supported selection property, then make progress 943 | /// on an incremental data transfer if that property is in that process. 944 | /// Returns `true` if an attempt at progressing the data transfer was made, whether or not it 945 | /// succeeded, and `false` otherwise (e.g. the event does not target a selection property or 946 | /// that property is not in the process of an incremental data transfer) 947 | pub(super) fn handle_selection_property_change( 948 | &mut self, 949 | event: &x::PropertyNotifyEvent, 950 | ) -> bool { 951 | fn inner( 952 | connection: &xcb::Connection, 953 | event: &x::PropertyNotifyEvent, 954 | data: &mut SelectionData, 955 | ) -> bool { 956 | match event.state() { 957 | x::Property::NewValue => { 958 | if let Some(selection) = &data.x11_selection() { 959 | return selection.check_for_incr(event); 960 | } 961 | } 962 | x::Property::Delete => { 963 | if let Some(selection) = data.wayland_selection_mut() { 964 | return selection.check_for_incr(event, connection); 965 | } 966 | } 967 | } 968 | false 969 | } 970 | inner(&self.connection, event, &mut self.selection_state.primary) 971 | || inner(&self.connection, event, &mut self.selection_state.clipboard) 972 | } 973 | } 974 | -------------------------------------------------------------------------------- /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 = "ab_glyph" 7 | version = "0.2.32" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" 10 | dependencies = [ 11 | "ab_glyph_rasterizer", 12 | "owned_ttf_parser", 13 | ] 14 | 15 | [[package]] 16 | name = "ab_glyph_rasterizer" 17 | version = "0.1.10" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" 20 | 21 | [[package]] 22 | name = "adler2" 23 | version = "2.0.1" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 26 | 27 | [[package]] 28 | name = "ahash" 29 | version = "0.8.12" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 32 | dependencies = [ 33 | "cfg-if", 34 | "once_cell", 35 | "version_check", 36 | "zerocopy", 37 | ] 38 | 39 | [[package]] 40 | name = "aho-corasick" 41 | version = "1.1.4" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 44 | dependencies = [ 45 | "memchr", 46 | ] 47 | 48 | [[package]] 49 | name = "allocator-api2" 50 | version = "0.2.21" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 53 | 54 | [[package]] 55 | name = "anyhow" 56 | version = "1.0.100" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 59 | 60 | [[package]] 61 | name = "arrayref" 62 | version = "0.3.9" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" 65 | 66 | [[package]] 67 | name = "arrayvec" 68 | version = "0.7.6" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 71 | 72 | [[package]] 73 | name = "bindgen" 74 | version = "0.72.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" 77 | dependencies = [ 78 | "bitflags 2.10.0", 79 | "cexpr", 80 | "clang-sys", 81 | "itertools", 82 | "log", 83 | "prettyplease", 84 | "proc-macro2", 85 | "quote", 86 | "regex", 87 | "rustc-hash", 88 | "shlex", 89 | "syn", 90 | ] 91 | 92 | [[package]] 93 | name = "bitflags" 94 | version = "1.3.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 97 | 98 | [[package]] 99 | name = "bitflags" 100 | version = "2.10.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 103 | 104 | [[package]] 105 | name = "bytemuck" 106 | version = "1.24.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 109 | 110 | [[package]] 111 | name = "cc" 112 | version = "1.2.48" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" 115 | dependencies = [ 116 | "find-msvc-tools", 117 | "shlex", 118 | ] 119 | 120 | [[package]] 121 | name = "cexpr" 122 | version = "0.6.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 125 | dependencies = [ 126 | "nom", 127 | ] 128 | 129 | [[package]] 130 | name = "cfg-if" 131 | version = "1.0.4" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 134 | 135 | [[package]] 136 | name = "clang-sys" 137 | version = "1.8.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 140 | dependencies = [ 141 | "glob", 142 | "libc", 143 | "libloading", 144 | ] 145 | 146 | [[package]] 147 | name = "crc32fast" 148 | version = "1.5.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 151 | dependencies = [ 152 | "cfg-if", 153 | ] 154 | 155 | [[package]] 156 | name = "cursor-icon" 157 | version = "1.2.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" 160 | 161 | [[package]] 162 | name = "darling" 163 | version = "0.20.11" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 166 | dependencies = [ 167 | "darling_core", 168 | "darling_macro", 169 | ] 170 | 171 | [[package]] 172 | name = "darling_core" 173 | version = "0.20.11" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 176 | dependencies = [ 177 | "fnv", 178 | "ident_case", 179 | "proc-macro2", 180 | "quote", 181 | "strsim", 182 | "syn", 183 | ] 184 | 185 | [[package]] 186 | name = "darling_macro" 187 | version = "0.20.11" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 190 | dependencies = [ 191 | "darling_core", 192 | "quote", 193 | "syn", 194 | ] 195 | 196 | [[package]] 197 | name = "deranged" 198 | version = "0.5.5" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 201 | dependencies = [ 202 | "powerfmt", 203 | ] 204 | 205 | [[package]] 206 | name = "derive_builder" 207 | version = "0.20.2" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 210 | dependencies = [ 211 | "derive_builder_macro", 212 | ] 213 | 214 | [[package]] 215 | name = "derive_builder_core" 216 | version = "0.20.2" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 219 | dependencies = [ 220 | "darling", 221 | "proc-macro2", 222 | "quote", 223 | "syn", 224 | ] 225 | 226 | [[package]] 227 | name = "derive_builder_macro" 228 | version = "0.20.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 231 | dependencies = [ 232 | "derive_builder_core", 233 | "syn", 234 | ] 235 | 236 | [[package]] 237 | name = "dlib" 238 | version = "0.5.2" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 241 | dependencies = [ 242 | "libloading", 243 | ] 244 | 245 | [[package]] 246 | name = "downcast-rs" 247 | version = "1.2.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 250 | 251 | [[package]] 252 | name = "either" 253 | version = "1.15.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 256 | 257 | [[package]] 258 | name = "env_logger" 259 | version = "0.10.2" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 262 | dependencies = [ 263 | "humantime", 264 | "is-terminal", 265 | "log", 266 | "regex", 267 | "termcolor", 268 | ] 269 | 270 | [[package]] 271 | name = "equivalent" 272 | version = "1.0.2" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 275 | 276 | [[package]] 277 | name = "errno" 278 | version = "0.3.14" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 281 | dependencies = [ 282 | "libc", 283 | "windows-sys", 284 | ] 285 | 286 | [[package]] 287 | name = "fdeflate" 288 | version = "0.3.7" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 291 | dependencies = [ 292 | "simd-adler32", 293 | ] 294 | 295 | [[package]] 296 | name = "find-msvc-tools" 297 | version = "0.1.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 300 | 301 | [[package]] 302 | name = "flate2" 303 | version = "1.1.5" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 306 | dependencies = [ 307 | "crc32fast", 308 | "miniz_oxide", 309 | ] 310 | 311 | [[package]] 312 | name = "fnv" 313 | version = "1.0.7" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 316 | 317 | [[package]] 318 | name = "foldhash" 319 | version = "0.1.5" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 322 | 323 | [[package]] 324 | name = "fontconfig" 325 | version = "0.10.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "b19c4bca8c705ea23bfb3e3403a9e699344d1ee3205b631f03fe4dbf1e52429f" 328 | dependencies = [ 329 | "yeslogic-fontconfig-sys", 330 | ] 331 | 332 | [[package]] 333 | name = "fontdue" 334 | version = "0.9.3" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" 337 | dependencies = [ 338 | "hashbrown 0.15.5", 339 | "ttf-parser 0.21.1", 340 | ] 341 | 342 | [[package]] 343 | name = "glob" 344 | version = "0.3.3" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 347 | 348 | [[package]] 349 | name = "hashbrown" 350 | version = "0.14.5" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 353 | dependencies = [ 354 | "ahash", 355 | ] 356 | 357 | [[package]] 358 | name = "hashbrown" 359 | version = "0.15.5" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 362 | dependencies = [ 363 | "allocator-api2", 364 | "equivalent", 365 | "foldhash", 366 | ] 367 | 368 | [[package]] 369 | name = "hashbrown" 370 | version = "0.16.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 373 | 374 | [[package]] 375 | name = "hecs" 376 | version = "0.10.5" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "e1cbc675ee8d97b4d206a985137f8ad59666538f56f906474f554467a63c776d" 379 | dependencies = [ 380 | "hashbrown 0.14.5", 381 | "hecs-macros", 382 | "spin", 383 | ] 384 | 385 | [[package]] 386 | name = "hecs-macros" 387 | version = "0.10.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "052fc25b12dc326082605cd2098eb76050a72fa0c0e9ea7faaa3f58b565fc970" 390 | dependencies = [ 391 | "proc-macro2", 392 | "quote", 393 | "syn", 394 | ] 395 | 396 | [[package]] 397 | name = "hermit-abi" 398 | version = "0.5.2" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 401 | 402 | [[package]] 403 | name = "humantime" 404 | version = "2.3.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" 407 | 408 | [[package]] 409 | name = "ident_case" 410 | version = "1.0.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 413 | 414 | [[package]] 415 | name = "indexmap" 416 | version = "2.12.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 419 | dependencies = [ 420 | "equivalent", 421 | "hashbrown 0.16.1", 422 | ] 423 | 424 | [[package]] 425 | name = "is-terminal" 426 | version = "0.4.17" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" 429 | dependencies = [ 430 | "hermit-abi", 431 | "libc", 432 | "windows-sys", 433 | ] 434 | 435 | [[package]] 436 | name = "itertools" 437 | version = "0.13.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 440 | dependencies = [ 441 | "either", 442 | ] 443 | 444 | [[package]] 445 | name = "itoa" 446 | version = "1.0.15" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 449 | 450 | [[package]] 451 | name = "libc" 452 | version = "0.2.177" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 455 | 456 | [[package]] 457 | name = "libloading" 458 | version = "0.8.9" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" 461 | dependencies = [ 462 | "cfg-if", 463 | "windows-link", 464 | ] 465 | 466 | [[package]] 467 | name = "linux-raw-sys" 468 | version = "0.11.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 471 | 472 | [[package]] 473 | name = "log" 474 | version = "0.4.28" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 477 | 478 | [[package]] 479 | name = "macros" 480 | version = "0.1.0" 481 | dependencies = [ 482 | "proc-macro2", 483 | "quote", 484 | "syn", 485 | ] 486 | 487 | [[package]] 488 | name = "memchr" 489 | version = "2.7.6" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 492 | 493 | [[package]] 494 | name = "memmap2" 495 | version = "0.9.9" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" 498 | dependencies = [ 499 | "libc", 500 | ] 501 | 502 | [[package]] 503 | name = "minimal-lexical" 504 | version = "0.2.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 507 | 508 | [[package]] 509 | name = "miniz_oxide" 510 | version = "0.8.9" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 513 | dependencies = [ 514 | "adler2", 515 | "simd-adler32", 516 | ] 517 | 518 | [[package]] 519 | name = "nom" 520 | version = "7.1.3" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 523 | dependencies = [ 524 | "memchr", 525 | "minimal-lexical", 526 | ] 527 | 528 | [[package]] 529 | name = "num-conv" 530 | version = "0.1.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 533 | 534 | [[package]] 535 | name = "num_enum" 536 | version = "0.7.5" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" 539 | dependencies = [ 540 | "num_enum_derive", 541 | "rustversion", 542 | ] 543 | 544 | [[package]] 545 | name = "num_enum_derive" 546 | version = "0.7.5" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" 549 | dependencies = [ 550 | "proc-macro-crate", 551 | "proc-macro2", 552 | "quote", 553 | "syn", 554 | ] 555 | 556 | [[package]] 557 | name = "num_threads" 558 | version = "0.1.7" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 561 | dependencies = [ 562 | "libc", 563 | ] 564 | 565 | [[package]] 566 | name = "once_cell" 567 | version = "1.21.3" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 570 | 571 | [[package]] 572 | name = "owned_ttf_parser" 573 | version = "0.25.1" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" 576 | dependencies = [ 577 | "ttf-parser 0.25.1", 578 | ] 579 | 580 | [[package]] 581 | name = "pkg-config" 582 | version = "0.3.32" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 585 | 586 | [[package]] 587 | name = "png" 588 | version = "0.17.16" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 591 | dependencies = [ 592 | "bitflags 1.3.2", 593 | "crc32fast", 594 | "fdeflate", 595 | "flate2", 596 | "miniz_oxide", 597 | ] 598 | 599 | [[package]] 600 | name = "powerfmt" 601 | version = "0.2.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 604 | 605 | [[package]] 606 | name = "pretty_env_logger" 607 | version = "0.5.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" 610 | dependencies = [ 611 | "env_logger", 612 | "log", 613 | ] 614 | 615 | [[package]] 616 | name = "prettyplease" 617 | version = "0.2.37" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 620 | dependencies = [ 621 | "proc-macro2", 622 | "syn", 623 | ] 624 | 625 | [[package]] 626 | name = "proc-macro-crate" 627 | version = "3.4.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" 630 | dependencies = [ 631 | "toml_edit", 632 | ] 633 | 634 | [[package]] 635 | name = "proc-macro2" 636 | version = "1.0.103" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 639 | dependencies = [ 640 | "unicode-ident", 641 | ] 642 | 643 | [[package]] 644 | name = "quick-xml" 645 | version = "0.30.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" 648 | dependencies = [ 649 | "memchr", 650 | ] 651 | 652 | [[package]] 653 | name = "quick-xml" 654 | version = "0.37.5" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 657 | dependencies = [ 658 | "memchr", 659 | ] 660 | 661 | [[package]] 662 | name = "quote" 663 | version = "1.0.42" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 666 | dependencies = [ 667 | "proc-macro2", 668 | ] 669 | 670 | [[package]] 671 | name = "regex" 672 | version = "1.12.2" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 675 | dependencies = [ 676 | "aho-corasick", 677 | "memchr", 678 | "regex-automata", 679 | "regex-syntax", 680 | ] 681 | 682 | [[package]] 683 | name = "regex-automata" 684 | version = "0.4.13" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 687 | dependencies = [ 688 | "aho-corasick", 689 | "memchr", 690 | "regex-syntax", 691 | ] 692 | 693 | [[package]] 694 | name = "regex-syntax" 695 | version = "0.8.8" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 698 | 699 | [[package]] 700 | name = "rustc-hash" 701 | version = "2.1.1" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 704 | 705 | [[package]] 706 | name = "rustix" 707 | version = "1.1.2" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 710 | dependencies = [ 711 | "bitflags 2.10.0", 712 | "errno", 713 | "libc", 714 | "linux-raw-sys", 715 | "windows-sys", 716 | ] 717 | 718 | [[package]] 719 | name = "rustversion" 720 | version = "1.0.22" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 723 | 724 | [[package]] 725 | name = "sd-notify" 726 | version = "0.4.5" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4" 729 | dependencies = [ 730 | "libc", 731 | ] 732 | 733 | [[package]] 734 | name = "serde" 735 | version = "1.0.228" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 738 | dependencies = [ 739 | "serde_core", 740 | ] 741 | 742 | [[package]] 743 | name = "serde_core" 744 | version = "1.0.228" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 747 | dependencies = [ 748 | "serde_derive", 749 | ] 750 | 751 | [[package]] 752 | name = "serde_derive" 753 | version = "1.0.228" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 756 | dependencies = [ 757 | "proc-macro2", 758 | "quote", 759 | "syn", 760 | ] 761 | 762 | [[package]] 763 | name = "shlex" 764 | version = "1.3.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 767 | 768 | [[package]] 769 | name = "simd-adler32" 770 | version = "0.3.7" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 773 | 774 | [[package]] 775 | name = "smallvec" 776 | version = "1.15.1" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 779 | 780 | [[package]] 781 | name = "smithay-client-toolkit" 782 | version = "0.20.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" 785 | dependencies = [ 786 | "bitflags 2.10.0", 787 | "cursor-icon", 788 | "libc", 789 | "log", 790 | "memmap2", 791 | "rustix", 792 | "thiserror", 793 | "wayland-backend", 794 | "wayland-client", 795 | "wayland-csd-frame", 796 | "wayland-cursor", 797 | "wayland-protocols", 798 | "wayland-protocols-experimental", 799 | "wayland-protocols-misc", 800 | "wayland-protocols-wlr", 801 | "wayland-scanner", 802 | "xkeysym", 803 | ] 804 | 805 | [[package]] 806 | name = "spin" 807 | version = "0.9.8" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 810 | 811 | [[package]] 812 | name = "strict-num" 813 | version = "0.1.1" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 816 | 817 | [[package]] 818 | name = "strsim" 819 | version = "0.11.1" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 822 | 823 | [[package]] 824 | name = "syn" 825 | version = "2.0.111" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 828 | dependencies = [ 829 | "proc-macro2", 830 | "quote", 831 | "unicode-ident", 832 | ] 833 | 834 | [[package]] 835 | name = "termcolor" 836 | version = "1.4.1" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 839 | dependencies = [ 840 | "winapi-util", 841 | ] 842 | 843 | [[package]] 844 | name = "testwl" 845 | version = "0.1.0" 846 | dependencies = [ 847 | "rustix", 848 | "rustversion", 849 | "wayland-protocols", 850 | "wayland-server", 851 | "wl_drm", 852 | ] 853 | 854 | [[package]] 855 | name = "thiserror" 856 | version = "2.0.17" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 859 | dependencies = [ 860 | "thiserror-impl", 861 | ] 862 | 863 | [[package]] 864 | name = "thiserror-impl" 865 | version = "2.0.17" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 868 | dependencies = [ 869 | "proc-macro2", 870 | "quote", 871 | "syn", 872 | ] 873 | 874 | [[package]] 875 | name = "time" 876 | version = "0.3.44" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 879 | dependencies = [ 880 | "deranged", 881 | "itoa", 882 | "libc", 883 | "num-conv", 884 | "num_threads", 885 | "powerfmt", 886 | "serde", 887 | "time-core", 888 | "time-macros", 889 | ] 890 | 891 | [[package]] 892 | name = "time-core" 893 | version = "0.1.6" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 896 | 897 | [[package]] 898 | name = "time-macros" 899 | version = "0.2.24" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 902 | dependencies = [ 903 | "num-conv", 904 | "time-core", 905 | ] 906 | 907 | [[package]] 908 | name = "tiny-skia" 909 | version = "0.11.4" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" 912 | dependencies = [ 913 | "arrayref", 914 | "arrayvec", 915 | "bytemuck", 916 | "cfg-if", 917 | "log", 918 | "png", 919 | "tiny-skia-path", 920 | ] 921 | 922 | [[package]] 923 | name = "tiny-skia-path" 924 | version = "0.11.4" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" 927 | dependencies = [ 928 | "arrayref", 929 | "bytemuck", 930 | "strict-num", 931 | ] 932 | 933 | [[package]] 934 | name = "toml_datetime" 935 | version = "0.7.3" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 938 | dependencies = [ 939 | "serde_core", 940 | ] 941 | 942 | [[package]] 943 | name = "toml_edit" 944 | version = "0.23.7" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" 947 | dependencies = [ 948 | "indexmap", 949 | "toml_datetime", 950 | "toml_parser", 951 | "winnow", 952 | ] 953 | 954 | [[package]] 955 | name = "toml_parser" 956 | version = "1.0.4" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 959 | dependencies = [ 960 | "winnow", 961 | ] 962 | 963 | [[package]] 964 | name = "ttf-parser" 965 | version = "0.21.1" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" 968 | 969 | [[package]] 970 | name = "ttf-parser" 971 | version = "0.25.1" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" 974 | 975 | [[package]] 976 | name = "unicode-ident" 977 | version = "1.0.22" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 980 | 981 | [[package]] 982 | name = "vergen" 983 | version = "9.0.6" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" 986 | dependencies = [ 987 | "anyhow", 988 | "derive_builder", 989 | "rustversion", 990 | "vergen-lib", 991 | ] 992 | 993 | [[package]] 994 | name = "vergen-gitcl" 995 | version = "1.0.8" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" 998 | dependencies = [ 999 | "anyhow", 1000 | "derive_builder", 1001 | "rustversion", 1002 | "time", 1003 | "vergen", 1004 | "vergen-lib", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "vergen-lib" 1009 | version = "0.1.6" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" 1012 | dependencies = [ 1013 | "anyhow", 1014 | "derive_builder", 1015 | "rustversion", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "version_check" 1020 | version = "0.9.5" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1023 | 1024 | [[package]] 1025 | name = "wayland-backend" 1026 | version = "0.3.11" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" 1029 | dependencies = [ 1030 | "cc", 1031 | "downcast-rs", 1032 | "rustix", 1033 | "smallvec", 1034 | "wayland-sys", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "wayland-client" 1039 | version = "0.31.11" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" 1042 | dependencies = [ 1043 | "bitflags 2.10.0", 1044 | "rustix", 1045 | "wayland-backend", 1046 | "wayland-scanner", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "wayland-csd-frame" 1051 | version = "0.3.0" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" 1054 | dependencies = [ 1055 | "bitflags 2.10.0", 1056 | "cursor-icon", 1057 | "wayland-backend", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "wayland-cursor" 1062 | version = "0.31.11" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" 1065 | dependencies = [ 1066 | "rustix", 1067 | "wayland-client", 1068 | "xcursor", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "wayland-protocols" 1073 | version = "0.32.9" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" 1076 | dependencies = [ 1077 | "bitflags 2.10.0", 1078 | "wayland-backend", 1079 | "wayland-client", 1080 | "wayland-scanner", 1081 | "wayland-server", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "wayland-protocols-experimental" 1086 | version = "20250721.0.1" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" 1089 | dependencies = [ 1090 | "bitflags 2.10.0", 1091 | "wayland-backend", 1092 | "wayland-client", 1093 | "wayland-protocols", 1094 | "wayland-scanner", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "wayland-protocols-misc" 1099 | version = "0.3.9" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" 1102 | dependencies = [ 1103 | "bitflags 2.10.0", 1104 | "wayland-backend", 1105 | "wayland-client", 1106 | "wayland-protocols", 1107 | "wayland-scanner", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "wayland-protocols-wlr" 1112 | version = "0.3.9" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" 1115 | dependencies = [ 1116 | "bitflags 2.10.0", 1117 | "wayland-backend", 1118 | "wayland-client", 1119 | "wayland-protocols", 1120 | "wayland-scanner", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "wayland-scanner" 1125 | version = "0.31.7" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" 1128 | dependencies = [ 1129 | "proc-macro2", 1130 | "quick-xml 0.37.5", 1131 | "quote", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "wayland-server" 1136 | version = "0.31.10" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "fcbd4f3aba6c9fba70445ad2a484c0ef0356c1a9459b1e8e435bedc1971a6222" 1139 | dependencies = [ 1140 | "bitflags 2.10.0", 1141 | "downcast-rs", 1142 | "rustix", 1143 | "wayland-backend", 1144 | "wayland-scanner", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "wayland-sys" 1149 | version = "0.31.7" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" 1152 | dependencies = [ 1153 | "pkg-config", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "winapi-util" 1158 | version = "0.1.11" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 1161 | dependencies = [ 1162 | "windows-sys", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "windows-link" 1167 | version = "0.2.1" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1170 | 1171 | [[package]] 1172 | name = "windows-sys" 1173 | version = "0.61.2" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1176 | dependencies = [ 1177 | "windows-link", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "winnow" 1182 | version = "0.7.14" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 1185 | dependencies = [ 1186 | "memchr", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "wl_drm" 1191 | version = "0.1.0" 1192 | dependencies = [ 1193 | "wayland-client", 1194 | "wayland-scanner", 1195 | "wayland-server", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "xcb" 1200 | version = "1.6.0" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" 1203 | dependencies = [ 1204 | "bitflags 1.3.2", 1205 | "libc", 1206 | "quick-xml 0.30.0", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "xcb-util-cursor" 1211 | version = "0.3.5" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "bf6417c51a1f5eda49156061175021bd3ccf0a759bc7c402bbea6a6a1ae14239" 1214 | dependencies = [ 1215 | "xcb", 1216 | "xcb-util-cursor-sys", 1217 | ] 1218 | 1219 | [[package]] 1220 | name = "xcb-util-cursor-sys" 1221 | version = "0.1.6" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "4c78acb131647687ee62f9e64c988457f23ecb8f3a078a37a312f989b320cb47" 1224 | dependencies = [ 1225 | "bindgen", 1226 | "pkg-config", 1227 | "xcb", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "xcursor" 1232 | version = "0.3.10" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" 1235 | 1236 | [[package]] 1237 | name = "xkeysym" 1238 | version = "0.2.1" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" 1241 | 1242 | [[package]] 1243 | name = "xwayland-satellite" 1244 | version = "0.8.0" 1245 | dependencies = [ 1246 | "ab_glyph", 1247 | "anyhow", 1248 | "bitflags 2.10.0", 1249 | "fontconfig", 1250 | "fontdue", 1251 | "hecs", 1252 | "log", 1253 | "macros", 1254 | "num_enum", 1255 | "pretty_env_logger", 1256 | "rustix", 1257 | "sd-notify", 1258 | "smithay-client-toolkit", 1259 | "testwl", 1260 | "tiny-skia", 1261 | "vergen-gitcl", 1262 | "wayland-client", 1263 | "wayland-protocols", 1264 | "wayland-server", 1265 | "wl_drm", 1266 | "xcb", 1267 | "xcb-util-cursor", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "yeslogic-fontconfig-sys" 1272 | version = "6.0.0" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" 1275 | dependencies = [ 1276 | "dlib", 1277 | "once_cell", 1278 | "pkg-config", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "zerocopy" 1283 | version = "0.8.31" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 1286 | dependencies = [ 1287 | "zerocopy-derive", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "zerocopy-derive" 1292 | version = "0.8.31" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 1295 | dependencies = [ 1296 | "proc-macro2", 1297 | "quote", 1298 | "syn", 1299 | ] 1300 | --------------------------------------------------------------------------------