diff --git a/Cargo.lock b/Cargo.lock index 93c18cd..d10a198 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "async-broadcast" @@ -151,7 +151,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -180,13 +180,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -197,21 +197,21 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "blocking" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" dependencies = [ "async-channel", "async-task", @@ -222,9 +222,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cairo-rs" @@ -251,9 +251,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "shlex", @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.7" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +checksum = "fe4ece8474b5f766c63426647e7b4b316b67431ade1036a8313cee24a03ae917" dependencies = [ "smallvec", "target-lexicon", @@ -277,9 +277,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -299,14 +299,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -332,9 +332,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "derive_more" @@ -354,7 +354,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.119", "unicode-xid", ] @@ -382,7 +382,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -403,11 +403,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -424,9 +423,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "field-offset" @@ -440,21 +439,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -467,9 +460,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -477,15 +470,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -494,9 +487,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -513,32 +506,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -610,15 +603,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", - "wasip3", ] [[package]] @@ -682,7 +673,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -790,7 +781,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -814,18 +805,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -835,9 +817,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -845,22 +827,14 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -869,33 +843,22 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.184" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libusb1-sys" @@ -915,23 +878,17 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -1022,9 +979,9 @@ checksum = "a1a0483e89e81d7915defe83c51f23f6800594d64f6f4a21253ce87fd8444ada" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "polling" @@ -1040,16 +997,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1061,9 +1008,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1112,9 +1059,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1159,9 +1106,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "semver" @@ -1171,9 +1118,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1191,46 +1138,33 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "syn 3.0.4", ] [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -1244,9 +1178,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -1266,9 +1200,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "strsim" @@ -1278,9 +1212,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -1302,9 +1247,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -1321,9 +1266,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.51.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "pin-project-lite", "tokio-macros", @@ -1331,20 +1276,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -1352,7 +1297,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.1", + "winnow", ] [[package]] @@ -1366,30 +1311,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow 1.0.1", + "winnow", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.1", + "winnow", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -1410,7 +1355,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1495,9 +1440,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom", "js-sys", @@ -1517,29 +1462,11 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -1550,9 +1477,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1560,60 +1487,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "windows" version = "0.61.3" @@ -1668,7 +1561,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1679,7 +1572,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1742,115 +1635,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "zbus" -version = "5.14.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" dependencies = [ "async-broadcast", "async-executor", @@ -1875,7 +1671,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys", - "winnow 0.7.15", + "winnow", "zbus_macros", "zbus_names", "zvariant", @@ -1883,14 +1679,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 3.0.4", "zbus_names", "zvariant", "zvariant_utils", @@ -1898,58 +1694,62 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 0.7.15", + "winnow", "zvariant", ] [[package]] -name = "zmij" -version = "1.0.21" +name = "zcheapstr" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] [[package]] name = "zvariant" -version = "5.10.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" dependencies = [ "endi", "enumflags2", "serde", "serde_bytes", - "winnow 0.7.15", + "winnow", + "zcheapstr", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 3.0.4", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" dependencies = [ "proc-macro2", "quote", "serde", - "syn", - "winnow 0.7.15", + "syn 3.0.4", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index c7e9419..df43f09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ homepage = "https://github.com/thelicato/qd2" anyhow = "1.0.102" async-trait = "0.1.89" clap = { version = "4.6.0", features = ["derive"] } -gtk4 = { version = "0.10.0", features = ["v4_14"] } +gtk4 = { version = "0.10.0", features = ["v4_16"] } pixman-sys = "0.1.0" qemu-display = "0.2.1" serde_bytes = "0.11.19" diff --git a/README.md b/README.md index b49f659..f5874dc 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ QD2 is built for people who want the flexibility of QEMU's D-Bus display stack w | `--undecorated` | Open the GTK4 viewer without normal window decorations for `connect`. | `qd2 connect --undecorated` | | `--dpu` | Use QEMU-provided DMABUF damage rectangles instead of the default full-surface DMABUF refresh path. | `qd2 connect --dpu` | | `--hotkeys ...` | Override viewer shortcuts in a virt-viewer-style format for `connect`. | `qd2 connect --hotkeys "toggle-fullscreen=ctrl+enter,release-cursor=ctrl+alt"` | +| `--no-fullscreen-bar` | Hide the floating fullscreen toolbar and its top-edge hover hotspot for `connect`. Useful when the guest has panels at the screen edges. | `qd2 connect --fullscreen --no-fullscreen-bar` | +| `--name ` | Set the viewer's application identity (Wayland app_id / X11 WM class) so taskbars and window rules can match it, like spicy's `--name`. | `qd2 connect --name my-vm-launcher` | ## 🖥️ Viewer Highlights @@ -55,6 +57,10 @@ QD2 is built for people who want the flexibility of QEMU's D-Bus display stack w - Guest audio playback through the QEMU D-Bus audio interface. - Floating fullscreen controls inspired by virt-viewer. - Direct fullscreen launch with `qd2 connect --fullscreen`. +- Guest display resolution follows the viewer window size (including fullscreen). +- Sharp guest cursor on HiDPI displays: the cursor is rendered inside the scene at content scale instead of through the GTK cursor API. +- DMABUF frames are offloaded to a compositor subsurface (`GtkGraphicsOffload`), keeping large guest resolutions smooth in fullscreen. +- Keyboard forwarding starts when the viewer window gains focus (once a display is presented), with host shortcuts restored on focus loss. - Optional undecorated launch mode for tiling compositor workflows. - Top-bar actions for taking screenshots and sending guest shortcuts like `Ctrl+Alt+Delete`. - Configurable hotkeys for fullscreen, grab release, and DMABUF transforms. @@ -69,7 +75,7 @@ Linux releases also ship native `.deb` and `.rpm` packages. Those packages insta Building from source currently requires: - Rust stable with Cargo -- GTK4 development files +- GTK4 development files (GTK 4.16 or newer) - pixman development files - usbredir 0.13+ libraries visible to `pkg-config` (`libusbredirhost.pc` and `libusbredirparser-0.5.pc`) - `pkg-config` or `pkgconf` diff --git a/src/cli.rs b/src/cli.rs index ed095d5..6d9ed9e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -94,11 +94,22 @@ pub struct ConnectArgs { #[arg(long)] pub undecorated: bool, + /// Do not show the floating toolbar (or its top-edge hover hotspot) in + /// fullscreen. Useful when the guest has panels at the screen edges. + #[arg(long)] + pub no_fullscreen_bar: bool, + /// Use QEMU-provided DMABUF damage rectangles instead of full-surface /// refreshes. This can be faster, but some guest/driver combinations may /// flicker. #[arg(long = "dpu")] pub dmabuf_partial_updates: bool, + + /// Set the viewer's application/window identity (Wayland app_id, X11 WM + /// class), like spicy's --name. Lets taskbars and window rules match the + /// viewer to a specific launcher. + #[arg(long, value_name = "APP_ID")] + pub name: Option, } impl ConnectArgs { diff --git a/src/main.rs b/src/main.rs index fd06864..ff11c0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,12 @@ async fn main() -> Result<()> { } async fn run_connect_command(args: ConnectArgs) -> Result<()> { + // Must happen before GTK initializes a display connection: the Wayland + // app_id / X11 WM class are derived from the program name. + if let Some(name) = args.name.as_deref() { + gtk4::glib::set_prgname(Some(name)); + } + let target = if let Some(selector) = args.vm.as_deref() { qemu::resolve_connect_target(args.address(), Some(selector), args.console).await? } else { @@ -68,6 +74,7 @@ async fn run_connect_command(args: ConnectArgs) -> Result<()> { args.hotkeys.as_deref(), args.fullscreen, args.undecorated, + args.no_fullscreen_bar, args.dmabuf_partial_updates, ) } diff --git a/src/viewer/chrome.rs b/src/viewer/chrome.rs index 1a590f9..a9aa6a6 100644 --- a/src/viewer/chrome.rs +++ b/src/viewer/chrome.rs @@ -341,8 +341,11 @@ pub(super) fn sync_fullscreen_chrome( update_fullscreen_button(button, is_fullscreen); } + // GTK4 forbids set_titlebar() on a realized window (warning + segfault). + // Keep the titlebar set once at construction and only toggle its visibility; + // GTK hides titlebars in fullscreen anyway. if is_fullscreen { - window.set_titlebar(None::<>k::Widget>); + header_bar.set_visible(false); fullscreen_hotspot.set_visible(true); reveal_fullscreen_bar(fullscreen_revealer, fullscreen_state); schedule_hide_fullscreen_bar(window, fullscreen_revealer, fullscreen_state); @@ -351,10 +354,6 @@ pub(super) fn sync_fullscreen_chrome( fullscreen_revealer.set_reveal_child(false); fullscreen_revealer.set_visible(false); fullscreen_hotspot.set_visible(false); - if decorated_window { - window.set_titlebar(Some(header_bar)); - } else { - window.set_titlebar(None::<>k::Widget>); - } + header_bar.set_visible(decorated_window); } } diff --git a/src/viewer/cursor.rs b/src/viewer/cursor.rs index 013ae9e..93175ff 100644 --- a/src/viewer/cursor.rs +++ b/src/viewer/cursor.rs @@ -55,7 +55,8 @@ impl GuestCursor { })) } - fn to_gdk_cursor(&self) -> gdk::Cursor { + /// The cursor texture plus hotspot, for rendering inside the scene. + pub(super) fn to_texture(&self) -> (gdk::Texture, i32, i32) { let bytes = glib::Bytes::from_owned(self.rgba.clone()); let texture = gdk::MemoryTexture::new( self.width, @@ -64,8 +65,7 @@ impl GuestCursor { &bytes, self.stride(), ); - let fallback = gdk::Cursor::from_name("default", None); - gdk::Cursor::from_texture(&texture, self.hotspot_x, self.hotspot_y, fallback.as_ref()) + (texture.upcast(), self.hotspot_x, self.hotspot_y) } fn stride(&self) -> usize { @@ -106,8 +106,14 @@ impl Default for CursorState { } impl CursorState { - pub(super) fn set_shape(&mut self, shape: Option) { - self.active_cursor = shape.as_ref().map(GuestCursor::to_gdk_cursor); + /// While a guest shape is defined the scene draws it (see `cursor_scene`), + /// so the widget cursor only needs to keep the host cursor out of the way. + pub(super) fn set_shape(&mut self, has_guest_shape: bool) { + self.active_cursor = if has_guest_shape { + Some(self.hidden_cursor()) + } else { + None + }; } pub(super) fn set_visible(&mut self, visible: bool) { diff --git a/src/viewer/cursor_scene.rs b/src/viewer/cursor_scene.rs new file mode 100644 index 0000000..fe08869 --- /dev/null +++ b/src/viewer/cursor_scene.rs @@ -0,0 +1,176 @@ +//! Renders the guest cursor inside the viewer scene instead of relying on +//! `gdk::Cursor`. GTK rasterizes pointer cursors at logical scale (scale=1 +//! even on HiDPI outputs), so a cursor set through the GTK cursor API is +//! always blurry on scaled displays. Drawing the cursor texture as part of +//! the scene maps guest pixels 1:1 to physical pixels whenever the display +//! itself does, which is what makes it sharp. + +use std::{cell::RefCell, rc::Rc}; + +use gtk::{gdk, glib, graphene, prelude::*, subclass::prelude::*}; +use gtk4 as gtk; + +use super::{UiState, mouse::MouseMode}; + +pub(super) struct SceneState { + texture: Option, + hotspot: (i32, i32), + pointer: Option<(f64, f64)>, + cursor_visible: bool, + picture: Option, + ui_state: Option>>, + mouse_mode: Option>>, +} + +impl Default for SceneState { + fn default() -> Self { + Self { + texture: None, + hotspot: (0, 0), + pointer: None, + cursor_visible: true, + picture: None, + ui_state: None, + mouse_mode: None, + } + } +} + +mod cursor_scene_imp { + use super::*; + + #[derive(Default)] + pub struct CursorScene { + pub(super) state: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for CursorScene { + const NAME: &'static str = "Qd2CursorScene"; + type Type = super::CursorScene; + type ParentType = gtk::Widget; + } + + impl ObjectImpl for CursorScene {} + + impl WidgetImpl for CursorScene { + fn snapshot(&self, snapshot: >k::Snapshot) { + let state = self.state.borrow(); + + if !state.cursor_visible { + return; + } + let Some(texture) = &state.texture else { + return; + }; + let Some((pointer_x, pointer_y)) = state.pointer else { + return; + }; + let (Some(picture), Some(ui_state), Some(mouse_mode)) = + (&state.picture, &state.ui_state, &state.mouse_mode) + else { + return; + }; + // In relative mode the local pointer position says nothing about + // where the guest keeps its cursor, so don't pretend otherwise. + if *mouse_mode.borrow() != MouseMode::Absolute { + return; + } + let Some((frame_width, frame_height)) = ui_state.borrow().frame_size else { + return; + }; + let Some(bounds) = picture.compute_bounds(self.obj().upcast_ref::()) + else { + return; + }; + + // The same guest-pixel -> logical-pixel factor ContentFit::Contain + // applies to the framebuffer, so the cursor scales with the scene. + let scale = (f64::from(bounds.width()) / f64::from(frame_width)) + .min(f64::from(bounds.height()) / f64::from(frame_height)); + if !scale.is_finite() || scale <= 0.0 { + return; + } + + let x = f64::from(bounds.x()) + pointer_x - f64::from(state.hotspot.0) * scale; + let y = f64::from(bounds.y()) + pointer_y - f64::from(state.hotspot.1) * scale; + snapshot.append_texture( + texture, + &graphene::Rect::new( + x as f32, + y as f32, + (f64::from(texture.width()) * scale) as f32, + (f64::from(texture.height()) * scale) as f32, + ), + ); + } + } +} + +glib::wrapper! { + pub struct CursorScene(ObjectSubclass) + @extends gtk::Widget, + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; +} + +impl CursorScene { + pub(super) fn new( + picture: >k::Picture, + ui_state: Rc>, + mouse_mode: Rc>, + ) -> Self { + let scene: Self = glib::Object::builder().build(); + { + let mut state = scene.imp().state.borrow_mut(); + state.picture = Some(picture.clone()); + state.ui_state = Some(ui_state); + state.mouse_mode = Some(mouse_mode); + } + scene.set_can_target(false); + scene + } + + pub(super) fn set_shape(&self, shape: Option<(gdk::Texture, i32, i32)>) { + { + let mut state = self.imp().state.borrow_mut(); + match shape { + Some((texture, hotspot_x, hotspot_y)) => { + state.texture = Some(texture); + state.hotspot = (hotspot_x, hotspot_y); + } + None => state.texture = None, + } + } + self.queue_draw(); + } + + pub(super) fn set_cursor_visible(&self, visible: bool) { + self.imp().state.borrow_mut().cursor_visible = visible; + self.queue_draw(); + } + + pub(super) fn set_pointer(&self, pointer: Option<(f64, f64)>) { + self.imp().state.borrow_mut().pointer = pointer; + self.queue_draw(); + } +} + +/// Follow the local pointer over the picture. Unlike the input controllers in +/// `mouse.rs` this is not gated on the input grab: the drawn cursor replaces +/// the host cursor, which also follows the pointer unconditionally. +pub(super) fn track_pointer(picture: >k::Picture, scene: &CursorScene) { + let motion = gtk::EventControllerMotion::new(); + motion.connect_enter({ + let scene = scene.clone(); + move |_, x, y| scene.set_pointer(Some((x, y))) + }); + motion.connect_motion({ + let scene = scene.clone(); + move |_, x, y| scene.set_pointer(Some((x, y))) + }); + motion.connect_leave({ + let scene = scene.clone(); + move |_| scene.set_pointer(None) + }); + picture.add_controller(motion); +} diff --git a/src/viewer/framebuffer.rs b/src/viewer/framebuffer.rs index 6bd33ca..2d63cd5 100644 --- a/src/viewer/framebuffer.rs +++ b/src/viewer/framebuffer.rs @@ -703,6 +703,7 @@ fn premultiply(channel: u8, alpha: u8) -> u8 { pub(super) struct FrameStreamHandler { event_tx: EventSender, framebuffer: Option, + disable_notice_pending: bool, } impl FrameStreamHandler { @@ -710,6 +711,14 @@ impl FrameStreamHandler { Self { event_tx, framebuffer: None, + disable_notice_pending: false, + } + } + + fn clear_disable_notice(&mut self) { + if self.disable_notice_pending { + self.disable_notice_pending = false; + self.send_status(""); } } @@ -794,16 +803,19 @@ impl FrameStreamHandler { #[cfg(unix)] pub(super) fn emit_dmabuf_scanout(&mut self, scanout: DmabufFrame) { self.framebuffer = None; + self.clear_disable_notice(); let _ = self.event_tx.send(ViewerEvent::Dmabuf(scanout)); } #[cfg(unix)] pub(super) fn update_dmabuf(&mut self, update: UpdateDMABUF) { + self.clear_disable_notice(); let _ = self.event_tx.send(ViewerEvent::DmabufUpdate(update)); } pub(super) fn disable(&mut self) { self.framebuffer = None; + self.disable_notice_pending = true; self.send_status("The guest display was disabled."); } diff --git a/src/viewer/hotkeys.rs b/src/viewer/hotkeys.rs index ddbef7b..4abc85b 100644 --- a/src/viewer/hotkeys.rs +++ b/src/viewer/hotkeys.rs @@ -208,6 +208,25 @@ impl Hotkey { } } + /// Whether the binding is a modifier-only chord (like the default + /// `Ctrl+Alt`). Those must not fire on press: half of every + /// `Ctrl+Alt+` guest shortcut would trigger them. The keyboard + /// controller arms them on press and fires on a clean release instead. + pub(super) fn is_modifier_chord(&self) -> bool { + matches!(self, Self::Modifiers { .. }) + } + + /// Whether this key is one of the modifiers making up a modifier-only + /// binding (used for release-time chord detection). + pub(super) fn chord_member(&self, keyval: gdk::Key) -> bool { + match self { + Self::Modifiers { modifiers, .. } => { + modifier_mask_for_key(keyval).is_some_and(|mask| modifiers.contains(mask)) + } + _ => false, + } + } + pub(super) fn accelerator(&self) -> Option<&str> { match self { Self::Disabled => None, diff --git a/src/viewer/keyboard.rs b/src/viewer/keyboard.rs index 6b54efe..30433e4 100644 --- a/src/viewer/keyboard.rs +++ b/src/viewer/keyboard.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, collections::HashSet, rc::Rc}; -use gtk::{glib, prelude::*}; +use gtk::{gdk, glib, prelude::*}; use gtk4 as gtk; use tokio::sync::mpsc as tokio_mpsc; @@ -91,6 +91,7 @@ pub(super) fn install_keyboard_controller( release_grab: impl Fn() + 'static, ) -> KeyboardControllerHandle { let state = Rc::new(RefCell::new(PressedKeyState::default())); + let release_grab = Rc::new(release_grab); let key_controller = gtk::EventControllerKey::new(); key_controller.set_propagation_phase(gtk::PropagationPhase::Capture); @@ -98,6 +99,7 @@ pub(super) fn install_keyboard_controller( let input_tx = input_tx.clone(); let input_grab = input_grab.clone(); let release_hotkey = release_hotkey.clone(); + let release_grab = release_grab.clone(); let state = state.clone(); move |_, keyval, keycode, modifiers| { let Some(qnum) = gdk_keycode_to_qnum(keycode) else { @@ -109,6 +111,18 @@ pub(super) fn install_keyboard_controller( } if release_hotkey.matches(keyval, modifiers) { + if release_hotkey.is_modifier_chord() { + // Don't fire on press: the chord is the prefix of every + // `+` guest shortcut. Arm it, keep forwarding, + // and fire only if it is released without another key. + let mut state = state.borrow_mut(); + state.release_chord_armed = true; + if state.press(qnum) { + let _ = input_tx.send(InputEvent::KeyPress(qnum)); + } + return glib::Propagation::Stop; + } + let mut state = state.borrow_mut(); state.release_all(&input_tx); state.suppress_next_release(qnum); @@ -117,6 +131,7 @@ pub(super) fn install_keyboard_controller( } let mut state = state.borrow_mut(); + state.release_chord_armed = false; if state.press(qnum) { let _ = input_tx.send(InputEvent::KeyPress(qnum)); } @@ -125,13 +140,24 @@ pub(super) fn install_keyboard_controller( }); key_controller.connect_key_released({ let input_tx = input_tx.clone(); + let release_hotkey = release_hotkey.clone(); + let release_grab = release_grab.clone(); let state = state.clone(); - move |_, _, keycode, _| { + move |_, keyval, keycode, _| { let Some(qnum) = gdk_keycode_to_qnum(keycode) else { return; }; let mut state = state.borrow_mut(); + if state.release_chord_armed && release_hotkey.chord_member(keyval) { + // Clean chord release: no other key was pressed in between. + state.release_chord_armed = false; + state.release_all(&input_tx); + state.take_suppressed_release(qnum); + release_grab(); + return; + } + if state.take_suppressed_release(qnum) { return; } @@ -141,6 +167,18 @@ pub(super) fn install_keyboard_controller( } } }); + // Safety net for release events GTK never delivers (grab churn, focus + // flicker, compositor shortcuts): when the modifier state says a modifier + // is up but we still track it as pressed, release it in the guest so it + // can never get stuck repeating a chord. + key_controller.connect_modifiers({ + let input_tx = input_tx.clone(); + let state = state.clone(); + move |_, modifiers| { + state.borrow_mut().reconcile_modifiers(modifiers, &input_tx); + glib::Propagation::Proceed + } + }); picture.add_controller(key_controller); KeyboardControllerHandle { state, input_tx } @@ -159,10 +197,24 @@ pub(super) fn send_guest_shortcut( } } +/// Modifier keys by qnum, paired with the GDK modifier bit they raise. +/// Used to reconcile our pressed-key tracking against GTK's modifier state. +const MODIFIER_QNUMS: &[(u32, gdk::ModifierType)] = &[ + (29, gdk::ModifierType::CONTROL_MASK), // Ctrl_L + (157, gdk::ModifierType::CONTROL_MASK), // Ctrl_R + (42, gdk::ModifierType::SHIFT_MASK), // Shift_L + (54, gdk::ModifierType::SHIFT_MASK), // Shift_R + (56, gdk::ModifierType::ALT_MASK), // Alt_L + (184, gdk::ModifierType::ALT_MASK), // Alt_R / AltGr + (219, gdk::ModifierType::SUPER_MASK), // Super_L + (220, gdk::ModifierType::SUPER_MASK), // Super_R +]; + #[derive(Default)] struct PressedKeyState { pressed: HashSet, suppressed_releases: HashSet, + release_chord_armed: bool, } impl PressedKeyState { @@ -170,6 +222,18 @@ impl PressedKeyState { self.pressed.insert(qnum) } + fn reconcile_modifiers( + &mut self, + current: gdk::ModifierType, + input_tx: &tokio_mpsc::UnboundedSender, + ) { + for &(qnum, mask) in MODIFIER_QNUMS { + if !current.contains(mask) && self.pressed.remove(&qnum) { + let _ = input_tx.send(InputEvent::KeyRelease(qnum)); + } + } + } + fn release(&mut self, qnum: u32) -> bool { self.pressed.remove(&qnum) } @@ -410,6 +474,37 @@ mod tests { assert!(!state.take_suppressed_release(29)); } + #[test] + fn reconcile_modifiers_releases_stuck_modifiers_only() { + let (input_tx, mut input_rx) = tokio_mpsc::unbounded_channel(); + let mut state = PressedKeyState::default(); + + assert!(state.press(29)); // Ctrl_L, release will be "lost" + assert!(state.press(34)); // G, still physically held + + state.reconcile_modifiers(gdk::ModifierType::empty(), &input_tx); + + assert_eq!( + input_rx.try_recv().ok(), + Some(super::InputEvent::KeyRelease(29)) + ); + assert!(input_rx.try_recv().is_err()); + assert!(state.pressed.contains(&34)); + assert!(!state.pressed.contains(&29)); + } + + #[test] + fn reconcile_modifiers_keeps_held_modifiers() { + let (input_tx, mut input_rx) = tokio_mpsc::unbounded_channel(); + let mut state = PressedKeyState::default(); + + assert!(state.press(29)); // Ctrl_L + state.reconcile_modifiers(gdk::ModifierType::CONTROL_MASK, &input_tx); + + assert!(input_rx.try_recv().is_err()); + assert!(state.pressed.contains(&29)); + } + #[test] fn guest_shortcut_sends_press_and_reverse_release_sequence() { let (input_tx, mut input_rx) = tokio_mpsc::unbounded_channel(); diff --git a/src/viewer/listener/remote.rs b/src/viewer/listener/remote.rs index 5a52505..332f419 100644 --- a/src/viewer/listener/remote.rs +++ b/src/viewer/listener/remote.rs @@ -71,6 +71,13 @@ impl RemoteConsole { .context("failed to query the mouse mode") } + pub(super) async fn set_ui_info(&self, width: u32, height: u32) -> Result<()> { + self.proxy + .set_ui_info(0, 0, 0, 0, width, height) + .await + .with_context(|| format!("failed to request a guest resize to {width}x{height}")) + } + pub(super) async fn check_alive(&self) -> Result<()> { self.proxy .label() @@ -91,9 +98,9 @@ impl RemoteConsole { .release(keycode) .await .with_context(|| format!("failed to send key release for qnum {keycode}")), - InputEvent::ClipboardViewerFocused(_) | InputEvent::ClipboardHostChanged(_, _) => { - Ok(()) - } + InputEvent::ClipboardViewerFocused(_) + | InputEvent::ClipboardHostChanged(_, _) + | InputEvent::UiInfo { .. } => Ok(()), InputEvent::MousePress(button) => self .mouse .press(button) diff --git a/src/viewer/listener/session.rs b/src/viewer/listener/session.rs index 9ad45cd..bd55e4c 100644 --- a/src/viewer/listener/session.rs +++ b/src/viewer/listener/session.rs @@ -137,6 +137,15 @@ pub(super) async fn listener_session( continue; } + if let InputEvent::UiInfo { width, height } = &input { + if let Err(error) = console.set_ui_info(*width, *height).await { + let _ = event_tx.send(ViewerEvent::Status(format!( + "Guest resize request failed: {error:#}" + ))); + } + continue; + } + let needs_mouse_mode = mouse::input_needs_mouse_mode(&input); if let Err(error) = console.handle_input(input).await { let recovered = if needs_mouse_mode { diff --git a/src/viewer/mod.rs b/src/viewer/mod.rs index 104779a..28e5f14 100644 --- a/src/viewer/mod.rs +++ b/src/viewer/mod.rs @@ -3,6 +3,7 @@ mod chooser; mod chrome; mod clipboard; mod cursor; +mod cursor_scene; mod dmabuf; mod events; mod framebuffer; @@ -125,6 +126,7 @@ pub fn connect( hotkeys_spec: Option<&str>, start_fullscreen: bool, undecorated: bool, + no_fullscreen_bar: bool, dmabuf_partial_updates: bool, ) -> Result<()> { let hotkeys = hotkeys::ViewerHotkeys::parse(hotkeys_spec) @@ -156,6 +158,7 @@ pub fn connect( hotkeys, start_fullscreen, undecorated, + no_fullscreen_bar, dmabuf_partial_updates, ); @@ -176,6 +179,7 @@ fn run_window( hotkeys: hotkeys::ViewerHotkeys, start_fullscreen: bool, undecorated: bool, + no_fullscreen_bar: bool, dmabuf_partial_updates: bool, ) -> Result<()> { gtk::init().context("failed to initialize GTK4")?; @@ -200,9 +204,19 @@ fn run_window( status_label.set_margin_start(12); status_label.set_margin_end(12); + // Hand dmabuf frames straight to the compositor as a subsurface instead of + // compositing them through GTK's GL renderer — at large guest resolutions + // that compositing pass dominates the frame budget. Black-background mode + // keeps the subsurface below the UI so overlays (in-scene cursor, + // fullscreen bar) can still draw on top. + let offload = gtk::GraphicsOffload::new(Some(&picture)); + offload.set_hexpand(true); + offload.set_vexpand(true); + offload.set_black_background(true); + let container = gtk::Box::new(gtk::Orientation::Vertical, 0); container.append(&status_label); - container.append(&picture); + container.append(&offload); let overlay = gtk::Overlay::new(); overlay.set_child(Some(&container)); @@ -290,9 +304,13 @@ fn run_window( .build(); fullscreen_revealer.set_child(Some(&floating_controls.container)); fullscreen_revealer.set_visible(false); - overlay.add_overlay(&fullscreen_revealer); - overlay.set_measure_overlay(&fullscreen_revealer, false); - overlay.set_clip_overlay(&fullscreen_revealer, false); + // --no-fullscreen-bar: leave the floating bar and its hotspot unparented so + // fullscreen has no screen-edge chrome at all. + if !no_fullscreen_bar { + overlay.add_overlay(&fullscreen_revealer); + overlay.set_measure_overlay(&fullscreen_revealer, false); + overlay.set_clip_overlay(&fullscreen_revealer, false); + } let fullscreen_hotspot = gtk::Box::builder() .halign(gtk::Align::Center) @@ -302,9 +320,11 @@ fn run_window( .build(); fullscreen_hotspot.set_opacity(0.0); fullscreen_hotspot.set_visible(false); - overlay.add_overlay(&fullscreen_hotspot); - overlay.set_measure_overlay(&fullscreen_hotspot, false); - overlay.set_clip_overlay(&fullscreen_hotspot, false); + if !no_fullscreen_bar { + overlay.add_overlay(&fullscreen_hotspot); + overlay.set_measure_overlay(&fullscreen_hotspot, false); + overlay.set_clip_overlay(&fullscreen_hotspot, false); + } let fullscreen_state = Rc::new(RefCell::new(chrome::FullscreenChromeState::default())); let titlebar_widget = header_bar.clone().upcast::(); @@ -341,6 +361,91 @@ fn run_window( } }); + // Auto-resize: forward viewer size changes to the guest via SetUIInfo, + // the same mechanism the SPICE vdagent uses. Debounced so an interactive + // resize sends one final size instead of a flood. + { + let input_tx = input_tx.clone(); + let picture_for_send = picture.clone(); + let last_sent: Rc> = Rc::new(RefCell::new((0, 0))); + let pending: Rc>> = Rc::new(RefCell::new(None)); + let send_ui_info: Rc = Rc::new(move || { + let scale = picture_for_send.scale_factor(); + let width = picture_for_send.width() * scale; + let height = picture_for_send.height() * scale; + if width > 0 && height > 0 && *last_sent.borrow() != (width, height) { + *last_sent.borrow_mut() = (width, height); + let _ = input_tx.send(InputEvent::UiInfo { + width: width as u32, + height: height as u32, + }); + } + }); + let schedule: Rc = Rc::new({ + let pending = pending.clone(); + move || { + if let Some(source) = pending.borrow_mut().take() { + source.remove(); + } + let send_ui_info = send_ui_info.clone(); + let pending_inner = pending.clone(); + let source = glib::timeout_add_local_once( + std::time::Duration::from_millis(350), + move || { + *pending_inner.borrow_mut() = None; + send_ui_info(); + }, + ); + *pending.borrow_mut() = Some(source); + } + }); + window.connect_default_width_notify({ + let schedule = schedule.clone(); + move |_| schedule() + }); + window.connect_default_height_notify({ + let schedule = schedule.clone(); + move |_| schedule() + }); + window.connect_fullscreened_notify({ + let schedule = schedule.clone(); + move |_| schedule() + }); + window.connect_maximized_notify({ + let schedule = schedule.clone(); + move |_| schedule() + }); + // First map: the window may already be fullscreen (--fullscreen) before + // the picture gets an allocation, so no notify above will fire — request + // once the widgets are actually laid out. + window.connect_map({ + let schedule = schedule.clone(); + move |_| schedule() + }); + // The compositor can resize the surface without any of the window + // properties above changing — e.g. the monitor's resolution changes + // while the viewer is fullscreen. The surface `layout` signal is the + // ground truth for actual size changes, so hook it on every realize + // (each realize creates a fresh surface). + window.connect_realize({ + let schedule = schedule.clone(); + move |window| { + if let Some(surface) = window.surface() { + surface.connect_layout({ + let schedule = schedule.clone(); + move |_, _, _| schedule() + }); + } + } + }); + // Moving to a monitor with a different scale changes the physical + // pixel count without a logical resize. + window.connect_scale_factor_notify({ + let schedule = schedule.clone(); + move |_| schedule() + }); + } + let hotspot_motion = gtk::EventControllerMotion::new(); hotspot_motion.connect_enter({ let window = window.clone(); @@ -433,6 +538,10 @@ fn run_window( let clipboard_state = Rc::new(RefCell::new(clipboard::ClipboardUiState::default())); let cursor_state = Rc::new(RefCell::new(cursor::CursorState::default())); let mouse_mode = Rc::new(RefCell::new(ready.mouse_mode)); + let cursor_scene = + cursor_scene::CursorScene::new(&picture, ui_state.clone(), mouse_mode.clone()); + overlay.add_overlay(&cursor_scene); + cursor_scene::track_pointer(&picture, &cursor_scene); let input_grab = grab::new_state(); let keyboard_controller = ready.keyboard_available.then(|| { keyboard::install_keyboard_controller( @@ -502,6 +611,15 @@ fn run_window( let keyboard_controller = keyboard_controller.clone(); move |window| { if window.is_active() { + // Focus means input: start forwarding keys immediately, like + // other viewers do, instead of demanding a first sacrificial + // click to activate the grab. Only once a display is actually + // presented — inhibiting host shortcuts while showing a blank + // window or an error dialog holds alt-tab hostage. + if picture.is_visible() && grab::activate(window, &picture, &input_grab, None) { + ui_state.borrow_mut().last_pointer_guest_position = None; + grab::sync_cursor_capture(&picture, &cursor_state, &input_grab, &mouse_mode); + } return; } @@ -569,6 +687,7 @@ fn run_window( let picture = picture.clone(); let status_label = status_label.clone(); let cursor_state = cursor_state.clone(); + let cursor_scene = cursor_scene.clone(); let clipboard_state = clipboard_state.clone(); let input_grab = input_grab.clone(); let ui_state = ui_state.clone(); @@ -656,10 +775,12 @@ fn run_window( if cursor_dirty { let mut current_cursor = cursor_state.borrow_mut(); if let Some(shape) = latest_cursor_shape { - current_cursor.set_shape(shape); + cursor_scene.set_shape(shape.as_ref().map(cursor::GuestCursor::to_texture)); + current_cursor.set_shape(shape.is_some()); } if let Some(visible) = latest_cursor_visible { current_cursor.set_visible(visible); + cursor_scene.set_cursor_visible(visible); } drop(current_cursor); grab::sync_cursor_capture(&picture, &cursor_state, &input_grab, &mouse_mode); @@ -842,8 +963,12 @@ fn run_window( } if let Some(message) = latest_status { - status_label.set_label(&message); - status_label.set_visible(true); + if message.is_empty() { + status_label.set_visible(false); + } else { + status_label.set_label(&message); + status_label.set_visible(true); + } } glib::ControlFlow::Continue @@ -999,6 +1124,7 @@ enum InputEvent { MouseAbs { x: u32, y: u32 }, MouseRel { dx: i32, dy: i32 }, MouseWheel(MouseButton), + UiInfo { width: u32, height: u32 }, } #[cfg(test)]