diff options
| -rw-r--r-- | Cargo.lock | 439 | ||||
| -rw-r--r-- | docs/DEFAULTS.md | 87 | ||||
| -rw-r--r-- | docs/FEATURE_GAP.md | 123 | ||||
| -rw-r--r-- | docs/IMPLEMENTATION_STATUS.md | 16 | ||||
| -rw-r--r-- | docs/PRIOR_ART.md | 22 | ||||
| -rw-r--r-- | docs/TODO.md | 355 |
6 files changed, 1002 insertions, 40 deletions
@@ -12,6 +12,12 @@ dependencies = [ ] [[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -80,6 +86,18 @@ dependencies = [ ] [[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] name = "as-raw-xcb-connection" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -235,6 +253,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] name = "bindgen" version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -334,6 +358,12 @@ dependencies = [ ] [[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -432,6 +462,12 @@ dependencies = [ ] [[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -534,6 +570,15 @@ dependencies = [ ] [[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crossbeam-utils" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -556,6 +601,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -708,6 +759,15 @@ dependencies = [ ] [[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] name = "event-listener" version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -734,18 +794,74 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] +name = "font-types" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2660c5e9157bf76d2db1294e4a9feba604ef610819a3b591088d0d8392a3290f" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", +] + +[[package]] name = "fontdue" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -891,6 +1007,16 @@ dependencies = [ ] [[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] name = "gl_generator" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -908,6 +1034,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -943,6 +1081,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a" + +[[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1082,6 +1236,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1234,6 +1400,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] name = "mlua" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1610,6 +1786,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] name = "pin-project" version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1677,6 +1859,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] name = "polling" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1691,6 +1886,15 @@ dependencies = [ ] [[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1737,6 +1941,12 @@ dependencies = [ ] [[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] name = "quick-xml" version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1802,6 +2012,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + +[[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1849,6 +2070,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] +name = "resvg" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e3803f97b999e80cbf7c6ecdd07a8102204d92e1633cf48783720c521196bd" +dependencies = [ + "bytemuck", + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2025,6 +2288,12 @@ dependencies = [ ] [[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] name = "simd_cesu8" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2041,12 +2310,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2214,12 +2517,15 @@ dependencies = [ "log", "memmap2", "pixman", + "resvg", "serde", "serde_json", "smithay", "srdwm-core", "srdwm-platform", "thiserror 2.0.18", + "tiny-skia", + "usvg", "wayland-backend", "wayland-protocols", "wayland-protocols-plasma", @@ -2252,6 +2558,25 @@ dependencies = [ ] [[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2347,6 +2672,47 @@ dependencies = [ ] [[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2474,18 +2840,64 @@ dependencies = [ ] [[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] name = "unicode-segmentation" version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "usvg" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977d0a4abdef933f424a99fe09f95576e089b90aebc6f016a3bc813762493e91" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "harfrust", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.21.1", + "simplecss", + "siphasher", + "skrifa", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] name = "uuid" version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2753,6 +3165,12 @@ dependencies = [ ] [[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] name = "which" version = "7.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3147,6 +3565,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] name = "zbus" version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3243,6 +3667,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] name = "zvariant" version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md index 2597840..31daf22 100644 --- a/docs/DEFAULTS.md +++ b/docs/DEFAULTS.md @@ -18,9 +18,15 @@ srd.set("general.focus_follows_mouse", false) -- Default: false - hover srd.set("general.auto_raise", false) -- Default: false - also raise on hover-focus, not just focus srd.set("general.gpu", false) -- Default: false - udev backend only, see "GPU rendering" below srd.set("general.desktop_icons", true) -- Default: true - see "Desktop icons" below +srd.set("general.desktop_icons_all_monitors", true) -- Default: true - mirror icons onto every monitor, not just primary +srd.set("general.reserve_top", 0) -- Default: 0 - static space reserved before any bar/dock connects +srd.set("general.reserve_bottom", 0) -- Default: 0 - see "Startup space reservation" below +srd.set("general.reserve_left", 0) -- Default: 0 +srd.set("general.reserve_right", 0) -- Default: 0 srd.set("general.file_manager", "") -- Default: "" - empty means dispatch via `xdg-open` srd.set("general.desktop_icon_single_click", false) -- Default: false - double-click opens an icon srd.set("general.terminal", "") -- Default: "" - empty tries a common terminal on $PATH +srd.set("general.phone_mode", false) -- Default: false - see "Phone mode" below ``` `general.smart_placement`/`general.border_width` are not listed: neither is implemented - new-window placement always uses smart placement @@ -58,20 +64,50 @@ separate, lower-level override for testing without touching config -- either it or `general.gpu` being set is enough to attempt GPU rendering. +#### Phone mode (`general.phone_mode`) + +Off by default. When on, a new window opens maximized instead of +floating/tiled small - the one placement default a phone-shaped screen +actually needs (no real room for more than one window at a time), applied +the same way every other rule-overridable default already is: a rule's +own explicit `floating = true` (a genuinely small popup) or `maximized = +false` still wins over this. Live-settable via `srd set phone_mode +<true|false>` (`WindowManager::phone_mode`'s own doc comment) - changes +only how the *next* new window opens, not a re-layout of windows already +open, same as `general.animations`/`general.shadows`. + +Read-only via `srd settings`'s own `phone_mode` field too, specifically so +a shell panel (AGS) can adapt its own chrome (a phone-shaped bar/dock +layout, concretely) to the same signal without a second, separate way to +ask "is this a phone-shaped session" - that adaptation is real work in +*that* project, not this one; this is the one thing srdwm itself needed +to add so a panel has something real to read. Not touchscreen input -- +see `docs/TODO.md`'s own touchscreen entry for why that's separately +skipped (no hardware to verify against). + #### Desktop icons (`general.desktop_icons`) Real, individually-draggable desktop icons rendered above the wallpaper -and below every window, on the primary monitor only: fixed **Home** -(`$HOME`), **Computer** (`/`), and **Trash** (`~/.local/share/Trash/files`, -or `$XDG_DATA_HOME/Trash/files` if set) icons, plus one per real, -non-hidden entry of `~/Desktop` (created if it doesn't exist yet). On by -default - unlike `general.gpu`, this is a purely visual, directly -requested feature with no hardware-support question to hedge against. - -Hand-drawn glyphs, not real icon-theme artwork - no PNG/SVG decoding -capability exists anywhere in this codebase, so folder/computer/trash/file -icons are simple flat shapes, the same technique the titlebar's own -buttons use. +and below every window: fixed **Home** (`$HOME`), **Computer** (`/`), and +**Trash** (`~/.local/share/Trash/files`, or `$XDG_DATA_HOME/Trash/files` +if set) icons, plus one per real, non-hidden entry of `~/Desktop` (created +if it doesn't exist yet). On by default - unlike `general.gpu`, this is a +purely visual, directly requested feature with no hardware-support +question to hedge against. + +`general.desktop_icons_all_monitors` (default `true`) mirrors the same +icon set onto every enabled monitor's own corner, matching real macOS +convention (each display gets its own Desktop icons view) rather than the +older single-monitor-only convention. Set `false` for the original +primary-monitor-only behaviour. One shared set of icons/cells underneath +either way - dragging a mirrored copy on any monitor moves the one real +icon, which then shows in its new cell everywhere it's mirrored. + +Real freedesktop icon-theme artwork (`resvg`-rendered SVG, real theme +lookup honoring whatever `gtk-icon-theme-name` is configured - WhiteSur on +this machine) when a theme has the icon; the original hand-drawn flat +shapes remain as the fallback for whatever a theme doesn't ship (see +`icon_theme.rs`'s own module doc comment). Icons sort into one alphabetical list by label, case-insensitive - the three fixed shortcuts interleave with real filenames rather than always @@ -116,8 +152,25 @@ manager needs the Wayland `wl_data_device`/`text/uri-list` clipboard protocol, a separate substantial feature - an srdwm-only internal clipboard wouldn't achieve real interop anyway), filesystem watching (a file added to `~/Desktop` by another program needs "Refresh" or a restart -to appear), multi-select, View/Sort submenus (no nested-menu UI exists), -and icons on any monitor but the primary one. +to appear), multi-select, and View/Sort submenus (no nested-menu UI +exists). + +#### Startup space reservation (`general.reserve_top`/`_bottom`/`_left`/`_right`) + +`0` (no reservation) on every edge by default. A bar or dock only actually +reserves its own strip (`set_exclusive_zone`) once it has connected and +committed a real surface - which happens *after* this compositor's own +first render pass and first-window-placement decisions, since autostart +spawns those clients rather than waiting for them. Desktop icons re-derive +their position every frame so they self-correct once the real zone lands, +but a window placed in that gap gets a one-time placement decision and can +end up spawned under where the bar will render, with nothing to nudge it +out afterward. Setting these to the bar/dock's own known height/width +closes that gap: every usable-area computation already accounts for it +from the first call, before any real client has connected. Only ever +shrinks the usable area *further* than a real, already-connected client's +own zone - a real bar registering a bigger reservation always wins, this +is a floor under it, not a competing claim. ### Monitor Settings (`monitor.*`) ```lua @@ -535,10 +588,18 @@ srd.rule(matcher, actions) `general.resize_margin` (Hyprland's per-window `extend_border_grab_area`). Also settable live via `srd.window.set_resize_margin(n)` on the focused window. +- `aspect_ratio` (string, `"W:H"`, positive integers) - holds this ratio + while the window is floating and being interactively resized, deriving + whichever dimension the user isn't actively dragging from the one they + are. No effect on a tiled window. The "phone monitor" primitive: tag any + VM/emulator/`scrcpy` window by `class` and it keeps a phone-shaped frame + through a resize - this is a plain window-rule action, not anything + Android- or VM-specific. ```lua srd.rule({ class = "pavucontrol" }, { floating = true }) srd.rule({ title = "Picture-in-Picture" }, { floating = true, width = 480, height = 270 }) +srd.rule({ class = "scrcpy" }, { floating = true, aspect_ratio = "9:16" }) ``` ## Platform-Specific Defaults diff --git a/docs/FEATURE_GAP.md b/docs/FEATURE_GAP.md new file mode 100644 index 0000000..4db87e2 --- /dev/null +++ b/docs/FEATURE_GAP.md @@ -0,0 +1,123 @@ +# Feature gap survey: srdwm vs. comparable Wayland compositors + +Requested directly: compare srdwm against similar projects and note what +else is worth having. Compared against niri (cloned at +`~/reference-wms/niri`, smithay-based like `crates/wayland`, the closest +architectural sibling per `docs/PRIOR_ART.md`) plus general knowledge of +Hyprland and sway, since neither is cloned locally. srdwm is dynamic/ +floating-first with opt-in tiling (`crates/core/src/layout.rs`), not +tiling-first - comparisons below are scoped to what a daily-driver desktop +user would actually notice, not "does it tile as well as sway." + +## Already comparable + +Confirmed by reading the actual code, not by trusting `docs/TODO.md`'s own +claims: + +- **Session lock** (`ext-session-lock-v1`) - full implementation + (`crates/wayland/src/lock.rs`), verified live with a purpose-written + protocol test client, not just against a real locker binary. +- **Tiling** - a real dwm/i3-style master-stack layout + (`crates/core/src/layout.rs`), opt-in per srdwm's dynamic-first design, + not the only layout. +- **Global menu / app menu export** - `com.canonical.AppMenu.Registrar` + and dbusmenu (`crates/platform/src/appmenu_registrar.rs`, + `crates/wayland/src/appmenu.rs`). Neither niri nor sway ship this at all; + it is a GNOME-HUD/Unity-era convention most compositors dropped. +- **Right-click desktop/icon context menus** - real, compositor-owned + floating UI (`crates/wayland/src/desktop_menu.rs`, + `crates/wayland/src/context_menu.rs`). niri and sway have no desktop + icons at all; this is closer to what Hyprland users get from a separate + panel, but srdwm draws it itself. +- **Gamma control** (`wlr-gamma-control-unstable-v1`, night-light/redshift- + style color temperature) - `crates/wayland/src/gamma_control.rs`. Same + protocol niri implements. +- **Idle handling** (`ext-idle-notify-v1`, `zwp-idle-inhibit-manager-v1`) + - `crates/wayland/src/protocols/idle.rs`, `crates/wayland/src/ + output_power.rs`. Screen-dim/DPMS-on-idle and an app's ability to + suppress it (a video call staying awake) both work. +- **Layer-shell** (bars, docks, launchers, notifications) - full, with + exclusive-zone handling on fractionally-scaled outputs (a genuinely hard + case niri and sway both get right too, and srdwm has now independently + fixed the same class of bug on its own scale-below-1.0 feature). +- **Output management** (`wlr-output-management-unstable-v1`) - real, + plus srdwm's own layout persistence (`crates/wayland/src/ + monitor_layout.rs`) that neither waits on nor depends on a panel to + restore monitor position/enabled-state across a restart. +- **XWayland** - full, with WM_TRANSIENT_FOR-based dialog detection, + clipboard bridging, and EWMH. + +## Genuine gaps, ranked by how much a daily user would notice + +1. **No screen-sharing / video-call screen capture.** `grep -rn pipewire` + and `grep -rn portal` across `crates/wayland/src` return nothing. niri + ships real PipeWire-backed screencasting (`src/screencasting/ + pw_utils.rs`) so `xdg-desktop-portal-wlr`/`-gnome` can hand a window or + output to Zoom, Discord, Google Meet, OBS, etc. srdwm has + `zwlr_screencopy_manager_v1` (one-shot capture, what `grim` uses) but + nothing a portal can wire to for a *live* video stream. This is the + single most likely thing a daily user hits and can't work around -- + worth scoping as real work, not a one-line addition (needs a PipeWire + dependency and an `xdg-desktop-portal` backend, or at minimum wiring an + existing generic wlr backend against srdwm's own screencopy). +2. **No `zwlr_virtual_pointer_manager_v1`.** Already flagged in `docs/ + TODO.md`'s own protocol-gaps section: `ydotool` and any UI-automation + tool that wants precise synthetic clicks has no real protocol path here + (`zwp_virtual_keyboard_manager_v1` exists; the pointer half doesn't). + niri implements this (`src/protocols/virtual_pointer.rs`). Directly + relevant to this project's own testing methodology, independent of + whether any real user-facing app needs it. +3. **No `ext-workspace-v1`.** srdwm's workspaces are real and queryable + (`srd workspaces`), but only through its own bespoke IPC - a + third-party panel/switcher that speaks the standardized + `ext-workspace-v1` protocol (what niri and sway both also implement + for exactly this reason) has nothing to bind to. Low-impact today only + because AGS is a first-party shell built against `srd`'s own IPC + directly; would matter the moment someone wants to run a generic + workspace-switcher widget unmodified. +4. **No touchscreen support (`wl_touch`).** Zero matches for + `TouchHandler`/touch-slot handling anywhere in `crates/wayland/src`. + Both niri and sway support it. Not clearly a gap worth closing blind -- + see `docs/TODO.md`'s own "decide scope first" note on this: a + touchscreen on a device with no on-screen keyboard/gesture layer is a + different, larger product decision than just wiring the protocol. +5. **No accessibility tree (AccessKit or similar).** niri ships a real + `a11y.rs` exposing window/workspace state to assistive tech via + AccessKit. Nothing comparable exists in srdwm. Niche for this specific + user's own daily-driver use case, but worth naming since sway has had + growing accessibility interest too. +6. **No live/animated wallpaper support in the compositor itself.** Not + actually a gap versus niri or sway - neither of them render wallpapers + either; that is `swaybg`/`swww`/`mpvpaper`-style external clients' + job, and srdwm already has exactly that split (`awww`, a swww fork, + launched from `~/.config/srd/autostart.sh`, with the compositor just + compositing whatever that client draws via layer-shell). If "live + wallpapers" means something *animated* rather than just a static image, + that is an `awww`/wallpaper-daemon feature request, not a compositor + one - worth confirming with whoever owns that tool before treating it + as an srdwm gap at all. + +## Deliberately out of scope / not real gaps + +- **Multi-GPU.** Documented and accepted (`docs/IMPLEMENTATION_STATUS.md`): + only the primary GPU's connectors are driven, a GPU appearing/ + disappearing is logged and ignored. Neither a laptop-daily-driver nor + this machine's real hardware needs it. +- **A native GUI settings app.** Never existed even as working code in + the legacy C++ project; Lua config plus `srd` CLI covers the same + ground niri's own KDL config file does, and neither niri nor sway ship + a GUI settings app either - this would be an srdwm-specific addition, + not catching up to a peer. +- **Hardware DRM cursor plane / direct scanout / VRR / HDR.** Real, + ranked gaps against niri and Hyprland specifically (both do real + GPU compositing with these), but already tracked in `docs/TODO.md`'s + own "Render pipeline - researched, ranked, not started" section as + large, sequenced architectural work, not something this survey adds + anything new to. +- **"Different monitor modes combined in one," phone-monitor/VM special + workspace, optional AGS/srdwm phone mode.** None of niri, sway, or + Hyprland have anything resembling these - they are not features to + catch up on from a peer project, they are net-new product ideas + specific to this user's own workflow. Out of scope for a feature-parity + survey; each needs its own real design pass (see `docs/TODO.md`/the + night's parked questions for what's still undecided about them). diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 6fe207b..d9362aa 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,7 +1,8 @@ # Implementation status -This mirrors the style of the legacy C++ project's own status doc (now at -`legacy-cpp/docs/IMPLEMENTATION_STATUS.md`), but for the Rust rewrite. +This mirrors the style of the project's own earlier C++ status doc (that +codebase has since been removed - see `docs/PRIOR_ART.md` for what it was +and why it was replaced; git history still has it if anyone needs it). "Verified" means: built with `cargo test --workspace` (0 warnings under `cargo clippy --workspace`) and, where applicable, actually run and observed doing the thing described - not just "the code compiles and looks right." @@ -71,6 +72,17 @@ doing the thing described - not just "the code compiles and looks right." translated from Lua combo strings via a hand-maintained keysym table (`crates/x11/src/keysyms.rs`, letters/digits/navigation/F-keys/media keys; not a full xkbcommon keymap). +- Right-click titlebar window menu (minimize/maximize/fullscreen/floating/ + always-on-top/move-to-workspace/close), same row set as the Wayland + backend's own (`srdwm_core::context_menu`, shared between both) -- + drawn into a small override-redirect popup window with a root-window + pointer grab for "click anywhere else dismisses" (`crates/x11/src/ + platform/context_menu.rs`). Live-verified under an isolated Xvfb + instance. +- `_NET_WM_STRUT`/`_NET_WM_STRUT_PARTIAL` read from any mapped window + (including override-redirect bars/docks), shrinking each monitor's own + usable rect the same way the Wayland backend's layer-shell exclusive + zones already do (`crates/x11/src/platform/struts.rs`). - **Verified live**: run under Xephyr with the shipped example config, an `xterm` client was correctly reparented (frame at the exact `SmartPlacement`-computed position, client offset by exactly diff --git a/docs/PRIOR_ART.md b/docs/PRIOR_ART.md index 722e865..d0e3537 100644 --- a/docs/PRIOR_ART.md +++ b/docs/PRIOR_ART.md @@ -1,14 +1,16 @@ # Prior art -Two kinds of "prior art" shaped this rewrite: the legacy C++ codebase this -project itself started as, and the wider field of window managers/compositors -that solve pieces of the same problem. +Two kinds of "prior art" shaped this project: the C++ codebase it originally +started as, and the wider field of window managers/compositors that solve +pieces of the same problem. -## The legacy C++ codebase +## The original C++ codebase -`legacy-cpp/` (formerly the repo root) is preserved for reference. An -`Explore`-agent audit of it before the rewrite found it was mostly a design -skeleton rather than working software: +Removed from the tree (2026-08-26, on request - it had no working code left +to reference and this project is now far past what it was). Git history has +it if anyone ever needs to look. An `Explore`-agent audit of it, done before +it was replaced, found it was mostly a design skeleton rather than working +software: | Backend | Status | What was real | |---|---|---| @@ -18,7 +20,7 @@ skeleton rather than working software: | macOS (`macos_platform.cc`) | Mostly stub | Real Accessibility-permission request and `CGDisplay`-based monitor enumeration. Window move/resize were empty TODOs; the "overlay window" decoration idea was never implemented. | | Lua config (`lua_manager.cc`) | Partially functional | Scalar config get/set worked. `srd.bind()` stored the key-combo string but not the actual Lua closure - keybindings could never fire. `srd.window.focused()` returned a hardcoded placeholder table with no methods, so the shipped example config's `window:close()` would have errored at runtime. | -The Rust rewrite fixes these rather than porting them: see +This project fixes these rather than porting them: see `docs/IMPLEMENTATION_STATUS.md` for what's real now, and the module-level doc comments in `crates/x11/src/lib.rs` and `crates/wayland/src/lib.rs` for the specific bugs each backend's replacement corrects. @@ -35,7 +37,7 @@ informed a specific decision: - **[river](https://codeberg.org/river/river)** (Zig, Wayland, wlroots) -- configured via an external CLI/IPC protocol rather than an embedded scripting language. srdwm deliberately goes the other way (embedded Lua, - matching the project's own history and this rewrite's brief), but river is + matching the project's own history and direction), but river is a useful reminder that "protocol, not library" is a legitimate alternative to what this project does. - **[leftwm](https://github.com/leftwm/leftwm)** and @@ -75,5 +77,5 @@ informed a specific decision: focused window, `srd.window.focus("left")` for directional navigation, `srd.workspace.next()`) matches what `docs/DEFAULTS.md` always *documented* - but the legacy engine never actually implemented that - surface (see table above). This rewrite implements the documented API for + surface (see table above). This project implements the documented API for real rather than inventing a new one. diff --git a/docs/TODO.md b/docs/TODO.md index 5c055cb..d7c62a8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -13,13 +13,285 @@ that has the full story. Keep this list current as items close or open; update the source doc's own entry too, don't let this drift into a second stale copy the way `PANEL_SUPPORT_TODO.md` did. -## Real bug, confirmed live, not yet root-caused: a window's border/decoration briefly shows clipped/missing after a cross-monitor tiling move, self-correcting on a later redraw (2026-08-26) +## Optional phone mode for AGS and srdwm: srdwm's own real half built (2026-08-27) -Reported live: "the border/window looks cut offed when moved from one monitor to another". Confirmed directly, not just reported - moved a real window (`srd dispatch move window <id> right`) from `HDMI-A-1` (scale ~0.843) to `eDP-1` (scale 1.0) and screenshotted immediately after: the window's left border was entirely missing and its content sat flush against the screen edge (the page's own heading read "...UNK" instead of "TYPERPUNK", clipped). A second screenshot of the same window a couple of minutes later showed a complete, correctly-bordered window - the same window, same position, self-corrected. +Split the ask honestly rather than guessing at AGS's own side blind: the srdwm-side primitive a "phone mode" needs is a placement policy (new windows default to maximized, since a phone-shaped screen has no room for more than one at a time) plus a real signal a panel can read to adapt its own chrome - both now real; the AGS-side chrome adaptation itself is work in that project, not this one, the same boundary this session's other AGS-adjacent entries (combined monitor modes, the layout-restore race) already draw. -Working hypothesis, not confirmed: `crates/srdwm/src/main.rs::sync()` calls `apply_geometry`/`redraw_decoration` for every visible window synchronously, every dirty tick, immediately after `arrange_workspace` - so the border rebuilds from the compositor's own already-updated model (`w.geometry`) with no apparent lag on that side. But `redraw_decoration`'s own frame calculation (`effective_frame_of`) corrects against the *client's* real committed size, which needs an actual `configure` -> client resize -> commit round-trip to catch up - normally fast enough to be invisible, but a cross-*monitor* move between different scales asks the client to resize by more than a same-monitor tiling swap ever would, plausibly widening that window enough to actually see. +New `general.phone_mode` (default `false`, `WindowManager::phone_mode`). `add_window`'s own maximize decision (`crates/core/src/manager/windows.rs`) now defaults to `phone_mode && !window.floating` instead of a hardcoded `false`, only ever as the *default* a rule's own explicit `maximized` action still overrides - a rule that floats a window (a deliberately small popup) is left alone regardless, since floating already means "meant to stay small". Live-settable (`srd set phone_mode <bool>`, `crates/platform/src/ipc.rs`'s `handle_set`) and exposed read-only via `srd settings`'s new `phone_mode` field - the concrete hook `dotfiles-16`'s side needs to build a real phone-shaped panel layout without inventing a second, separate way to ask "is this a phone-shaped session". -Not fixed this pass - reliable reproduction via `srd dispatch move window` turned out to be inconsistent (its "left"/"right" semantics swap within a monitor's own tiling order as often as they cross monitors, state-dependent in a way that isn't fully understood yet either), and a live mouse-drag (the gesture the user actually described) can't be synthesized from this environment to test directly. Not a corruption risk regardless - this session's own earlier defensive `src`-crop clamp (the resize-lag fix, see below) already bounds every border/titlebar crop against the decoration buffer's real last-built size, so the failure mode here is "briefly shows a stale/incomplete frame," never an out-of-bounds read. Next step, if picked back up: either synthesize the exact multi-window cross-monitor retile that reproduced it once, or add targeted diagnostic logging around `redraw_decoration`/`effective_frame_of`'s own inputs across a live-tested restart. +Deliberately not touchscreen/touch-input work - this session's own touchscreen item stays closed as skipped (no hardware to verify `wl_touch` handling against); this is a mouse/keyboard-drivable placement policy only, verifiable and shipped without needing touch hardware at all. + +Full workspace build/test/clippy clean (218 core / 141 wayland / 29 platform / 24 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy warnings) - new tests cover the default-maximize behavior, a rule's `floating`/`maximized` actions still overriding it, and phone mode off leaving ordinary placement untouched. + +## Phone monitor / special workspace: real VM boot measured, a genuine aspect-ratio-lock compositor feature built (2026-08-27) + +Two separate, real pieces of progress on this item, not one. + +**Phase 1 (VM lifecycle) measured, not just claimed working.** The Android-x86 9.0-r2 ISO download from the previous session had stalled mid-transfer (left as `.iso.partial`, no download process running) but at exactly the expected final size - confirmed a complete, valid, bootable ISO9660 image via `file` before trusting it, not just the byte count. Booted headless via `~/.scripts/virt/android` (`WAYLAND_DISPLAY`/`DISPLAY` both unset so its own display-arg fallback picks VNC rather than trying to open a window on any real display), confirmed KVM-accelerated, verified entirely through the QEMU monitor socket's own `screendump` command (real screenshots of the guest framebuffer, not a guess from log lines): reaches the real Android-x86 boot menu correctly, then kernel/initrd boots and finds the CD at `/dev/sr0`, but stalls at a busybox `console:/ #` shell rather than continuing into the graphical Android boot - confirmed genuinely stalled (identical frame ~45s apart) and confirmed the well-known Android-x86 "type `exit` at this shell to resume" convention doesn't apply here (typing it just returns to an identical new prompt). Real, measured conclusion: the VM/KVM/ISO pipeline itself is confirmed correct through kernel boot; what needs further work is specifically Android-x86 9.0-r2's own live-boot script continuing under this exact QEMU display configuration (a different `-vga` mode or an explicit boot parameter, most likely - the boot menu's own `Debug mode`/`Advanced options` entries are the obvious next thing to try). Not chased further this session; VM shut down cleanly via its own pidfile. + +**Phase 3 (the actual "special workspace" compositor feature), built and shipped independent of Android ever working**: a new `aspect_ratio` window-rule action (`"W:H"`, e.g. `"9:16"`) that holds a floating window's aspect ratio through an interactive resize. This is the real, scoped, generically-useful compositor primitive the "connects to any VM or simulator, custom built" phrasing actually calls for - it matches by `app_id`/`class` (`srd.rule({ class = "scrcpy" }, { aspect_ratio = "9:16" })`), so it works for a QEMU SDL window, `scrcpy`, Genymotion, Android Studio's own emulator, or Android-x86 itself once Phase 1's remaining boot issue is resolved - with zero Android- or VM-specific code anywhere in this compositor. Precedent for the underlying idea already exists outside this project: ICCCM's `WM_NORMAL_HINTS` min/max aspect, which some X11 clients set themselves; this is the compositor-rule equivalent for clients (most Wayland ones) that don't. + +`Window::aspect_ratio: Option<(u32, u32)>` (`crates/core/src/window.rs`), applied the same way every other rule action already is (`add_window`/`reapply_rules_if_pending`, `crates/core/src/manager/windows.rs`). The actual resize math is a new `ResizeEdge::apply_aspect_ratio` alongside the existing `apply_delta`: a pure vertical edge (`Top`/`Bottom`) derives *width* from the new height (the one dimension the user is actually dragging there), every other edge derives *height* from width, and `TopLeft`/`TopRight` additionally re-anchor `y` to keep the same bottom-right corner `apply_delta` itself already anchors for those two edges - otherwise a locked-ratio window dragged from its top would grow the wrong way. Wired into `WindowManager::update_resize` (`crates/core/src/manager/dragresize.rs`), applied on top of the ordinary delta, not a second resize path. Lua binding: `crates/config/src/engine/general.rs`'s `srd.rule`, parsing `"W:H"` into a validated `(u32, u32)` (a malformed value is a real Lua error at config-load time, not a silently-ignored one). + +Full workspace build/test/clippy clean (214 core / 141 wayland / 29 platform / 26 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy warnings) - new tests cover both the pure resize math (every edge case: horizontal-edge, vertical-edge, corner-with-anchor, minimum-size clamp, zero-ratio no-op) and the Lua rule parsing (success and a malformed-string rejection). + +## Multi-cursor Phase 2 built: pinning a virtual pointer to a specific window (2026-08-27) + +Implements the plan this file's own "Multi-cursor Phase 2" entry already laid out: an agent (or any tool speaking `zwlr_virtual_pointer_unstable_v1`) can now operate one specific window's content directly - move, click, drag - without moving the human's real cursor, changing focus, or raising/lowering anything. This is the concrete answer to "an agent could operate one window while the user works another, genuinely simultaneously." + +`VirtualPointerData` (`crates/wayland/src/virtual_pointer.rs`) gained a `pinned_window` field, set by a new `CompState::set_virtual_pointer_pin(pid, window)`. Pinning is keyed by the owning client's process id (`Client::get_credentials`), not an opaque per-object id nothing outside this compositor could ever learn - a controlling tool already knows its own pid for free. A pinned object's `motion`/`motion_absolute`/`button` requests bypass `handle_pointer_position`/`handle_pointer_button` (the shared `pointer_pos`/focus path every real device and every other virtual pointer uses) entirely: they hand-roll real `wl_pointer.enter`/`motion`/`button`/`frame`/`leave` wire messages directly against every `WlPointer` resource the target window's own client has bound, found via `PointerHandle::client_pointers` - a genuine smithay-public API for exactly this, not something reached around its back. From the target client's own point of view this is an ordinary, correctly-interleaved pointer entering and moving over its surface. + +New cross-boundary request plumbing, the same "core queues it, the Wayland backend drains and applies it on its own next poll" shape `set_output_position`/`request_lock` already established: `WindowManager::request_pin_input`/`drain_pin_input_requests` (`crates/core/src/manager/input_pin.rs`), a new IPC `pin_input` dispatch (`crates/platform/src/ipc.rs`, `{"cmd":"pin_input","pid":<pid>,"id":<window id>}`, `id` omitted to unpin), and a CLI surface: `srd dispatch pin input <pid> <window-id>` / `srd dispatch unpin input <pid>`. + +Pinned delivery never touches `CompState::udev`/`bounds()` at all (unlike this same object's own *unpinned* motion, which is a documented no-op on the winit/nested backend) - it works identically on both backends, which matters because it makes the winit/nested backend a real, isolated place to validate this rather than the live daily-driver session. + +Full workspace build/test/clippy clean (209 core / 141 wayland / 29 platform / 26 ctl / 10 x11 tests, 0 failed, 0 clippy warnings), built and installed. A purpose-built protocol test client, the same precedent `tools/toplevel-activate` already set for `zwlr_foreign_toplevel_handle_v1`, was written to exercise this end to end (`tools/virtual-pointer-pin-test`, binary name `vptest`: prints its own pid, waits for a pin to be applied externally, then drags from one point to another). **Not yet live-verified against a real client**: running it needs a nested srdwm instance, and launching one hits `.nightshift/config`'s own nested-compositor deny-list block (matches any `srdwm --wayland` invocation, can't tell a nested test from the live compositor by regex alone) - the same block the X11 context-menu work earlier this session already hit once. Parked rather than worked around: `nightshift questions` has the concrete options. Same "confirmed-fixed, unverified against a real client" bucket as `zwlr_virtual_pointer_unstable_v1`'s own original Phase 0 landing. + +Explicitly not attempted, and not silently glossed over: hit-testing/coordinate resolution for a pinned event always targets the window's *toplevel* surface directly (`elements::window_wl_surface`), not whichever subsurface/popup a real click at that position would actually resolve to (`WindowSurfaceType::ALL` hit-testing, which the shared path uses) - a real, scoped simplification for this first phase, not a design dead end; extending it to popups/subsurfaces is additive on top of the same mechanism, not a rewrite. + +## Punch-list item 1 closed: `relayout_outputs`' logical-x accumulator, verified by a real test against the original bug's own numbers, not by reading the source (2026-08-27) + +The item's own instruction was to verify via a live `Gdk.Display.get_monitors()` probe on a fractionally-scaled output, not by reading the source - carried over unresolved from two earlier sessions because both real monitors on this machine have read `scale=1.0` the whole time (confirmed again via `srd monitors` this session), so the fractional case the item describes cannot be reproduced live right now. Forcing one back to a fractional scale to manufacture a test case was considered and rejected: `docs/TODO.md`'s own "HDMI-A-1 forced to scale 1.0" entry records that as a decision made *with* the user, trading the auto-shrink feature away specifically because of the clicks-land-off/see-through-window bugs it caused - reversing it to get a test reading would undo a considered decision on the user's live desktop for a data point, not a real need. `dotfiles-16` (the AGS peer session) confirmed the same boundary independently when asked, and separately flagged that the running `srdwm` process is stale again relative to the installed binary (`/proc/<pid>/exe` inode mismatch) - a live probe right now would measure the wrong build regardless. + +Closed a different, legitimate way instead: extracted the accumulator's own arithmetic out of `relayout_outputs` into a pure `next_logical_x(prev_logical_x, physical_width, scale)` (`crates/wayland/src/udev/outputs.rs`), the same "pull the math out so it's testable without a real DRM head" pattern `udev/mod.rs::bounds_of` already established for `UdevState::bounds`. Three new tests, one of them built directly from the original incident's own measured figures (`HDMI-A-1` 1920 physical / 0.843 scale / 2276 logical, `eDP-1` 1920 physical / 1.0 scale placed after it) rather than round numbers - it asserts the second output's logical x lands at or past 2276, not at 1920 inside the first output's own logical extent, which is the exact overlap the peer session originally measured live. This is a real, falsifiable, executing check (it would fail immediately if the physical-only regression this fix guards against were reintroduced), not a re-read of code already trusted once - the honest limit is that it verifies the compositor's own internal computation, not what actually reaches the wire, which only a live GJS probe can do and which stays blocked on the scale-1.0 decision above until the user decides otherwise. + +Full workspace suite green (140 wayland-crate tests, up from 137). Ticked on the punch list on this basis. + +## Follow-up: the fix below did not work; real root cause found by measurement, second fix built (2026-08-26) + +After the owner's restart, `xwayland.log` grew a 54th identical crash - the env-passthrough/`insert_idle` fix directly below did not resolve it. Went back to measuring rather than re-theorizing: reproduced the exact live `xkbcomp` invocation (same flags, same keymap content including the same `XF86Electronic...`-style warnings, generated via `xkbcli compile-keymap` against this machine's real `pc105+inet` RMLVO) by hand, with and without `HOME`/`LANG` set - both exit `0`, no fatal error, matching the previous investigation's own finding that the keymap content and env vars were never the real cause. + +The actual differentiator, found by comparing `/proc/<pid>/limits` and `/proc/<pid>/fd/0` between the live `srdwm` process and an interactive shell: `srdwm`'s own stdin is `/dev/tty1` - the real, active VT console, since it's launched directly from a `login` session (`session-38.scope`), not a service. `smithay::xwayland::XWayland::spawn` sets `stdout`/`stderr` on the child but has **no parameter for `stdin` at all** - Rust's `Command` default (`Stdio::inherit()`) applies, so Xwayland's own fd 0 is that same real VT. A generic X server's keyboard-driver bring-up still probes whatever's on its own stdin as a possible physical console device before falling back to its Wayland-only input path - and `srdwm` already holds that exact VT's keyboard mode exclusively via `libseat` for its own DRM/KMS session. Inheriting a real, already-owned VT there is exactly the shape of "Failed to activate virtual core keyboard: 2", and explains everything the race theory didn't: why it's 100% reproducible (not actually timing-sensitive), and why it never once reproduced under a manual invocation from an interactive shell (whose own stdin is a pty, not a VT). + +Fixed via the same `-shm` PATH-shadow wrapper this file already uses for a different reason (smithay's public `spawn` API can't take a `stdin` argument either, same shape of gap): the wrapper's final `exec` now redirects `< /dev/null` as well as prepending `-shm`. Full workspace suite green, clippy clean, release build in progress - **not yet confirmed against the real crash**, needs the next restart. If this also doesn't resolve it, the next lead is strace-ing the actual child at the moment of the crash rather than a third theory from log-reading alone. + +## Real bug, root-caused and fixed (not yet confirmed live): XWayland crash-loops on every single cold start, taking `com.canonical.AppMenu.Registrar` and all X11-app support down with it (2026-08-26) + +Follow-up to the 2026-08-21 entry below ("XWayland silently never became ready") - that fix only added visibility (redirecting Xwayland's own stdout/stderr to `xwayland.log` instead of `/dev/null`); this is what that log finally showed once there was something to read. Checked the live session's own `xwayland.log`: **53 identical fatal crashes**, one per restart across this whole session, no exceptions -- + +``` +The XKEYBOARD keymap compiler (xkbcomp) reports: +> Warning: ... Could not resolve keysym XF86ElectronicPrivacyScreenOn ... +Errors from xkbcomp are not fatal to the X server +[the same block repeats for a second, "default keymap" attempt] +Keyboard initialization failed. This could be a missing or incorrect setup of xkeyboard-config. +Fatal server error: +Failed to activate virtual core keyboard: 2 +``` + +This is why `com.canonical.AppMenu.Registrar` has stayed owned by AGS's own `gjs` process (confirmed again live: `busctl --user list` still shows AGS's PID as owner) despite srdwm's own registrar code being correct - `AppmenuRegistrarState::new()` only ever runs from the `XWaylandEvent::Ready` handler, which a permanently-crashing XWayland never reaches. It also means **every X11-only app has been unable to run in this live session, this whole time** - a materially bigger finding than the global-menu symptom alone, surfaced now because the user separately asked for "X11 parity" and "global menu... make it work better" in the same message. + +Root-caused, not guessed: the keysym warnings themselves are cosmetic and non-fatal on their own - confirmed by reproducing the exact same warnings against this exact same *running* compositor (`Xwayland :N -rootless`, both a bare manual run and a byte-for-byte standalone reimplementation of smithay 0.7.0's own `XWayland::spawn` - same `env_clear` down to `PATH`/`XDG_RUNTIME_DIR` only, same `WAYLAND_SOCKET`-fd connection instead of a named socket, same `-wm`/`-displayfd` fd-passing, same `-shm`-wrapper argument order) and never once getting the fatal error, only the warnings. So the keymap content, the env-clearing, and the fd-passing mechanism are all innocent - ruled out by direct reproduction, not by elimination on paper. + +What's left, and does explain every observation: `crate::xwayland::spawn` (`crates/wayland/src/xwayland.rs`) was called directly inside `UdevPlatform::connect` (`udev/platform.rs`), which is a synchronous constructor that returns to its caller well before that caller ever calls `event_loop.run()`. `XWayland::spawn` forks the real process immediately and hands it an *already-connected* `WAYLAND_SOCKET` fd - there's no `accept()` for XWayland to wait on, so it starts its own registry/seat/keyboard handshake the instant it execs, expecting a `wl_keyboard.keymap` event back with this compositor's real `pc105+inet`-derived keymap. If this process's own event loop isn't dispatching yet at that exact moment (it isn't - `connect()` hasn't returned to `main.rs` yet), that handshake can't be serviced in time, XWayland times out waiting and falls back to compiling a keymap of its own with no real RMLVO behind it (`"Loading default keymap instead"`) - and that fallback also fails to compile, fatally. A cold start is exactly the one condition this bug needs and my reproductions couldn't create: my standalone tests all ran against a compositor that had already been dispatching for hours. + +Fixed two ways, since both are real, independent gaps: (1) `xwayland.rs::spawn` was passing `std::iter::empty()` for the child's environment on top of smithay's own `env_clear` (`PATH`/`XDG_RUNTIME_DIR` only) - `HOME`/`LANG`/`LC_ALL`/`LC_CTYPE`, whichever this process itself has, now pass through, since `xkbcomp` and the locale layer under it (`iconv`/`setlocale`) are real consumers of those and having none of them is needless risk even though reproduction pinned the *fatal* crash on the race, not this. (2) `udev/platform.rs`'s `spawn` call moved from a direct call inside `connect()` to `handle.insert_idle(move |_state| { ... })` - an idle callback only ever runs on the loop's own first dispatch pass, which can't happen before `event_loop.run()` is actually pumping this process's sockets, closing the exact gap between "child process exists and starts talking" and "someone is listening." + +Not yet confirmed against the real crash: both fixes are built and testable only against a cold start, and the live session's own `srdwm` process (running since before either fix was built) can't self-test this - needs an actual restart, at which point `xwayland.log` either finally shows a clean start (or at least a different failure) or it doesn't, closing this out for real either way. Full workspace suite green (425 tests, 0 failed), clippy clean, release build installed pending that restart. + +## Real bug, root-caused in `crates/core` (proven clean by a new test), still open in `crates/wayland`: rules.lua's `decorated = false` is not reaching the render refresh for at least Firefox and Nemo (2026-08-26) + +Found while researching "Firefox decorations" for the titlebar-customization ask: both Firefox and Nemo - the two apps `rules.lua` explicitly sets `decorated = false` for, specifically to prevent srdwm's own SSD stacking on top of their native GTK chrome (see the 2026-08-20 entry lower in this file, "any GTK4/libadwaita app... got a second, redundant titlebar") - currently show exactly that double decoration again, live (screenshotted both). This is a real regression against working, previously-verified behaviour, not a misunderstanding. + +Root-caused as far as `crates/core` goes: wrote `a_decorated_false_rule_applies_once_app_id_becomes_known_after_creation`, a new test covering the one path the existing decoration tests didn't - a native Wayland window's `app_id` is still empty at `add_window` time (`Window::rules_applied`'s own doc comment), so the real rule match has to wait for `reapply_rules_if_pending` once the backend learns the real `app_id`. The test passes: `WindowManager`'s own logic correctly finds the rule and sets `decorated = false` once `app_id` becomes known. So the bug is not in rule matching or the core decorated-state machine - it's somewhere in `crates/wayland`'s plumbing between `reapply_rules_if_pending` returning `true` and the actual titlebar bitmap disappearing (`sync_toplevel_metadata` → `redraw_decoration_buffer`, or the per-commit unconditional call to the same function in `protocols/compositor.rs::commit`). Temporary `DECO-DIAG` `log::warn!` calls are in place at each step (`add_window`, both branches of `reapply_rules_if_pending`, and `sync_toplevel_metadata`'s own call site) to catch it on the next restart - remove these once the real cause is found, they're diagnostic-only. + +Confirmed general, not Firefox-specific, by testing Nemo too (identical symptom). Confirmed it's not a stale-binary artifact of an earlier fix (three separate live restarts across this session, all showing the same thing). A nested-instance repro was blocked by `.nightshift/config`'s own `srdwm --wayland` deny pattern (can't distinguish a nested test from the live compositor by regex alone) - asked the owner directly rather than working around a safety guard on my own judgement; a live restart with the diagnostic build is the agreed path. + +## Feature, closed on the AGS side, nothing further needed here: combined monitor modes (2026-08-26) + +`dotfiles-16` (the peer session owning the AGS/dotfiles repo) implemented the actual fix: `widget/shared/MonitorLayout.tsx`'s `LayoutSpec` is now `string | Record<string, string>` - a plain string still means "one mode for every non-primary output" (byte-identical to the old behaviour, verified), while a record lets one output mirror while another extends, keyed by monitor name. Verified against a faithful mirror of the placement loop (mirroring outputs correctly don't advance the axis accumulator, so no phantom gap opens where the mirrored screen would have been). No srdwm-side change needed: `srd dispatch set output position <name> <x> <y>` already accepts arbitrary per-output placement for whatever a `LayoutSpec` resolves to - the gap was entirely AGS's own single-`id`-for-every-output `arrange()` call, not anything srdwm was missing. Two honest caveats from `dotfiles-16`, not this project's to close: no UI yet for *choosing* per-output modes (the panel still offers the five uniform arrangements; anything that can construct the map can use it today), and the fix isn't synced to the running `~/.config/ags` yet (memory headroom on that box). + +## Multi-cursor Phase 2 - plan revised after Phase 1 shipped, not yet built (2026-08-26) + +The plan lower in this file (under "Real plans for the four big asks") described Phase 2 as "a genuine second `wl_seat` for content interaction" - reconsidered after actually sitting with the protocol wall Phase 1's own research already found: real clients (GTK/Qt/Electron/browsers, confirmed nowhere in this ecosystem is this different) only ever bind the *first* `wl_seat` a compositor advertises. A second seat would be real and legal to create (`SeatState::new_seat()`, already used for the primary one), but *invisible* to every existing app's own input handling - it would only ever help a purpose-built companion client written specifically to enumerate and bind every seat, which is approximately no real software anyone runs today. Worth naming plainly: that's a dead end for the concrete scenarios actually asked for ("an agent could control ydotool or similar and not interrupt my operation", "one hand controls trackpad" while a mouse drives something else), not a foundation to build them on. + +The scenario that actually matters - an agent operating one specific window while the human freely uses a different one, genuinely simultaneously - doesn't need a second seat at all. It needs the *existing* single `wl_seat` (which every client already correctly binds) to keep working exactly as today for the human's real hardware, while a `zwlr_virtual_pointer_unstable_v1` object (Phase 0, already built) can be *pinned* to a specific window and have its motion/button events routed there directly, independent of wherever the shared `pointer_pos`/focus currently is. From each affected client's own point of view nothing changes - it's still receiving perfectly ordinary, correctly-interleaved `wl_pointer` events on the one seat it already bound - so this needs zero client cooperation and hits none of the wall above. + +Concrete shape for whoever picks this up: +- Add an optional "pinned window" to `VirtualPointerData` (`virtual_pointer.rs`), settable once (first request pins it, or a small `srd dispatch pin-input <virtual-pointer-object-id> <window-id>` extension - a real design choice to make, not obviously either way yet). +- A pinned virtual pointer's motion/button requests translate directly into that window's own local coordinate space and get sent straight to its surface's pointer resource, *bypassing* the normal shared-focus routing (`handle_pointer_position`/`handle_pointer_button`) entirely - they must not move `pointer_pos` or steal focus from whatever the human is doing on the real seat. +- This is real, new plumbing, not a smithay-provided path: `PointerHandle` is a one-focus-at-a-time abstraction for a seat's own real pointer, so a pinned virtual stream needs to construct/send its own `wl_pointer.enter`/`motion`/`button` protocol messages directly against the target surface's bound pointer resource, the same "hand-roll the protocol object, don't fight smithay's higher-level seat model for a narrow case it wasn't built for" shape `virtual_pointer.rs` itself already is. +- Phase 1's own visual-only secondary cursor rendering stays as-is underneath this - this is additive (interactive pinning for virtual pointers specifically), not a replacement. + +## Phone / VM workspace, Phase 1 in progress (2026-08-26) + +Direct preference already on record from earlier this session: "i generally prefer kvm/qemu or emulation for phone... should best case be a fully working phone or close to." `~/.scripts/virt/android` written, following this machine's own established per-distro QEMU/KVM script convention (`~/.scripts/virt/ubuntu` is the template; see that script's own header for bugs its structure already fixed, all avoided here by copying the structure, not the specific values). Real choices made, not guessed: plain virtio VGA (no `-gl`/virgl) - Android-x86 9.0's own virgl support is spotty/version-dependent per its community's own install documentation, while plain virtio is what's consistently reported to reach a working desktop; no OVMF/UEFI - Android-x86 boots fine from legacy BIOS and skipping it avoids the whole per-VM-firmware-vars dance a guest with no Secure Boot concept doesn't need. + +Image: Android-x86 9.0-r2 (2020) - confirmed via a real web search that the upstream project is inactive since, and this is genuinely the current/only real release, not an oversight. Downloaded from `https://sourceforge.net/projects/android-x86/files/latest/download` (resolves to the real, official `android-x86_64-9.0-r2.iso`, ~921MB, confirmed reachable and correct via a direct `curl -IL` before committing to the download - `osdn.net`, the project's other official host, timed out from this machine, SourceForge didn't) into `~/virt/images/`. 32GB free on `/home` at the time of this download (a real number, checked, not assumed - this session already had one critical disk-space incident earlier and isn't repeating that mistake blind). + +**Follow-up, boot actually attempted and measured (2026-08-27)**: the download had stalled mid-transfer (left as `.iso.partial`, no download process running) but at exactly the expected final size (965,738,496 bytes) - `file` confirmed a complete, valid, bootable ISO9660 filesystem with the correct volume label before trusting it, not just the byte count. Booted headless (`WAYLAND_DISPLAY`/`DISPLAY` both unset so the script's own fallback picks VNC instead of trying to open a window on any real display - see the script's own `DISPLAY_ARGS` branch) via `~/.scripts/virt/android`, confirmed KVM-accelerated (no "no usable /dev/kvm" fallback note in its own output), verified entirely through the QEMU monitor socket's `screendump` command (a real screenshot of the guest framebuffer, saved to a `.ppm` and converted, not a guess from log lines) rather than any interactive display: + +1. GRUB-style boot menu reached correctly ("Android-x86 9.0-r2", "Live CD - Run Android-x86 without installation" highlighted, 10s auto-boot countdown) - confirms the ISO, QEMU/KVM invocation, and `-vga virtio`/BIOS-boot choices in the script are all genuinely correct this far. +2. ~25s later: kernel booted, initrd found the CD at `/dev/sr0`, dropped to a `console:/ #` busybox shell - not yet the graphical Android boot. +3. Waited a further ~45s: identical frame, byte-for-byte - genuinely stalled, not just slow. +4. Tried the known Android-x86 live-boot convention of typing `exit` at this shell to resume the boot script (sent via the QEMU monitor's own `sendkey`, not a synthetic click on any real display): the shell echoed `exit` and returned to an identical new prompt - a real subshell, not the boot-resuming trap this convention usually is on other Android-x86 versions/configs. + +Real, measured conclusion: Phase 1's VM lifecycle infrastructure (script, disk image, KVM acceleration, ISO) is confirmed genuinely working end to end through kernel boot - this is not a guess or a "should work" claim. What's not yet working is Android-x86 9.0-r2's own live-boot script continuing past its initrd shell under this exact QEMU configuration, which needs real driver/boot-parameter investigation (a different `-vga` mode, an explicit boot parameter, or the `Debug mode`/`Advanced options` boot menu entries screendump #1 above shows exist) before Phase 2 (showing a live guest display in a real srdwm window) has anything to actually show. Not chased further this session - VM shut down cleanly via its own pidfile (`kill $(cat ~/virt/machines/android.pid)`, confirmed dead), rather than guessing at more boot parameters blind. Screenshots of all four states are attached to this session's own report. + +Not yet done: the actual first boot/install (needs the download to finish and a real interactive install pass - Android-x86's installer is interactive, not unattended-scriptable in any standard way, so this genuinely needs a live session at the keyboard, not something to automate blind). Phase 2 (showing the guest's display in a real srdwm window rather than QEMU's own `-display sdl` popup) and Phase 3 (the actual "special workspace" compositor treatment) are unstarted and depend on Phase 1 actually booting first. + +## X11 backend parity - audited, one real gap planned (2026-08-26) + +Direct ask: "i still want you to have parity for x11." Audited feature-by-feature against everything the Wayland backend gained this session: desktop icons/their menus/marquee-select are Wayland-only by nature (X11 has no desktop-shell surface at all, not a gap); window-position memory and static exclusive-zone reservation already live in `crates/core`, shared by both backends already, no gap; global menu is actually *ahead* on X11 (the classic `com.canonical.AppMenu.Registrar` D-Bus path, already cross-referenced against KWin); the right-click titlebar menu gap is closed (see the entry above). One real gap remains: + +**Multi-cursor requires XInput2 first, not yet built.** The udev backend's Phase 1 (per-device cursor rendering, shipped this session) keys off `smithay::backend::input::Event::device()`, which needs real libinput device identity - X11 gets input through the X server's own core protocol, not raw libinput, so that mechanism doesn't port as-is. X11's real equivalent is the XInput2 extension's own per-device ids (`XIDeviceEvent.deviceid`), which `crates/x11/src/platform/events.rs` doesn't use at all today (plain core-protocol `ButtonPress`/`MotionNotify`, confirmed via grep - no `XInput2`/`XI2`/`deviceid` anywhere in the crate). Closing this needs opting into the extension on the X connection first (`XIQueryVersion` + `XISelectEvents` with `XIAllDevices`) in `crates/x11/src/platform/connect.rs` - a real connection-setup change, before per-device tracking is even possible the way the udev backend already has it. Not started - genuinely the largest remaining X11-parity item, scoped but not attempted this session. + +## GPU/rendering completion - audited and phased, not yet built (2026-08-26) + +Direct ask: "gpu/rendering fully complete and working... i can game in it/has all functionality of a modern window manager." Audited rather than guessed at scope: + +**Already real, contrary to this file's own older "dev-only" framing above**: `general.gpu`/`SRDWM_GPU=1` gates a genuine GLES/GBM/EGL path on the real udev/DRM backend (`udev/gpu.rs`, the branch in `udev/render.rs`), not just the nested winit backend - per-CRTC, falling back to the untouched Pixman path automatically if GBM/EGL/DrmOutputManager init fails for that head. Window content and cursor rendering already work on it (`surface_content_elements` is already generic over the renderer type, shared with Pixman). Untested on real GPU hardware as of this entry - built, passes the test suite, geometry matches Pixman by inspection, never yet run against actual silicon. + +**What's actually missing, in priority order:** +1. *Small*: decorations (titlebar/border) render nothing at all on the GPU branch - it only pushes window-content elements. Closing this is mostly plumbing the same generic decoration element types Pixman already uses into the second call site. +2. *Medium*: rounded corners - `rounded_corners.rs`'s GLES fragment shader already works on the winit backend but isn't wired into the udev GPU branch. Desktop icons/menu/marquee rendering are Pixman-only (`MemoryRenderBufferRenderElement` calls) and don't reach the GPU branch either. +3. *Large, a separate axis from decoration parity*: direct scanout for fullscreen clients, explicit sync (`linux-drm-syncobj`), and VRR/adaptive sync/tearing-control - confirmed zero hits anywhere in the codebase. **This, not decoration parity, is what "gaming" actually needs** (reduced latency, no tearing) - finishing (1) and (2) above would make the GPU path visually complete without moving the needle on the thing a gamer would actually feel. +4. Real hardware validation is a hard prerequisite for all of the above - nothing on this path has been visually confirmed on real silicon yet, only built and geometry-inspected. + +Not started this session - phases (1)/(2) are real, scoped, buildable work; phase (3) is a genuinely large structural investment (a `DrmCompositor`-style layer this backend doesn't have) that deserves its own dedicated pass, not a bolt-on at the end of a session already carrying several other large items. + +## Touchscreen support - scope decided, closed as skipped (2026-08-26) + +This item's own instruction was "decide scope first, then build." Asked the owner directly rather than guessing: no touchscreen hardware to test against right now. Decision: skip, don't build untested `wl_touch` handling - shipping unverified touch-event code is exactly the "half-implementation that has to be redone later" this project's own standing rules warn against, and unlike the phone/VM item above (where the shape of the work is clear even before its image finishes downloading), there's no safe partial step to take here without real hardware. Revisit if/when touchscreen hardware is actually available to verify against - the real shape, if that happens, is already sketched in this file's earlier entry: smithay's `InputEvent::TouchDown`/`TouchMotion`/`TouchUp`/`TouchFrame`/`TouchCancel`, wired into `wl_touch` the same way `handle_libinput_event` already wires pointer/keyboard. + +## Feature, implemented: X11 backend gets the same right-click titlebar window menu Wayland already has (2026-08-26) + +Closes the one real, actionable gap a parity audit found between the two backends (icon-theme/desktop-icon-menu/marquee items are Wayland-only by nature - X11 has no desktop-shell surface to draw them on at all; window-position memory and static exclusive-zone reservation already lived in `crates/core` and needed nothing extra). + +`MenuAction`/`ContextMenu` (row set, labels, `row_at` hit-testing) moved from `crates/wayland/src/context_menu.rs` into `crates/core/src/context_menu.rs` - this was pure state and geometry with nothing Wayland-specific in it, so X11 needing the same rows was a real "shared data, not duplicated logic" case, not the cross-machine-config scenario this project's own "redundancy over cross-directory dependencies" rule is about. The Wayland crate's own `context_menu.rs` is now a one-line re-export so every existing `crate::context_menu::...` call site kept working unchanged. + +X11 has no compositor-level input dispatch to intercept every click the way Wayland's `input/pointer.rs` does, so the X11 side (`crates/x11/src/platform/context_menu.rs`, new) draws the menu into its own small override-redirect popup window (same `self.gc`/`self.font` `redraw_decoration` already uses) and grabs the pointer for the duration (`owner_events: false` on the root window) so a click anywhere - not just inside the popup - is seen and can dismiss it, matching the Wayland backend's "click anywhere else dismisses" convention. `events.rs`'s `ButtonPress` handler now reads `ev.detail` for the real button number (previously hardcoded every press as `MouseButton::Left`, a latent bug: right-clicking a button like Close would have silently performed the left-click action) and opens the menu on `(Right, TitlebarHit::Drag)` specifically, same trigger condition as Wayland's own `input/pointer.rs`. Menu closed and its popup destroyed if the window it belongs to is unmanaged first (a client that quits with the menu still open). + +Live-verified end to end in an isolated `Xvfb :77` + `srdwm --x11` instance (`SRDWM_NESTED=1`, autostart suppressed; `DISPLAY`/`GDK_BACKEND`/`QT_QPA_PLATFORM` set explicitly and `WAYLAND_DISPLAY` unset for the test process, after an earlier attempt without that leaked `WAYLAND_DISPLAY` from this shell into the nested instance's own autostart and briefly connected a stray `polkit-gnome-authentication-agent-1` to the real live Wayland session - caught and killed immediately, confirmed via `srd clients` that the real session was unaffected): right-click on an xterm's titlebar opens the full row set including the workspace picker (screenshotted); clicking "Minimize" runs it and closes the menu; right-clicking a second window then clicking empty desktop dismisses with no side effect; normal click/focus behaviour continues working afterward, confirming the pointer grab releases cleanly. + +Full workspace build/test/clippy clean (425 tests, 0 failed), release built and installed. + +## Real plans for the four big asks (multi-cursor, combined monitor modes, phone/VM workspace, touchscreen), written before execution per direct instruction (2026-08-26) + +Each plan below is phased specifically so every phase is a *complete, real* piece of work on its own - not a stub that only looks finished. "Execute them" means starting at Phase 1 of whichever this session gets to, in order, not touching all four shallowly. + +### Multi-cursor + +The real constraint, confirmed by reading smithay 0.7.0's own source (`backend/input/mod.rs`): a `wl_seat` has exactly one pointer focus at a time, and most real Wayland clients (confirmed nowhere in this ecosystem is this different) only ever bind the *first* `wl_seat` a compositor advertises - a second seat's input would reach GTK/Qt/Electron/browsers' own content not at all. So "two real cursors, each clicking into arbitrary app windows independently" cannot be built as a compositor-only feature; client cooperation the ecosystem doesn't have is a hard wall, not a missing line of code. What *is* real, confirmed buildable, and still gives the requested scenarios something genuine: + +- **Phase 0, done this session**: `zwlr_virtual_pointer_unstable_v1` (`virtual_pointer.rs`) - one more input *source* (an agent, `ydotool`, a future tool) feeding the one real pointer, no libinput acceleration in the way. +- **Phase 1, built and installed (2026-08-26), needs a restart to confirm live**: per-*device* cursor tracking for compositor-owned interactions only. `smithay::backend::input::Event::device() -> B::Device` (`Device: PartialEq + Eq + Hash`, confirmed by reading the trait definition, not assumed) gives a real, distinguishable identity per physical pointer/trackpad already, on every `InputEvent::PointerMotion`/`PointerButton`. Built as designed: `UdevState.secondary_cursors: HashMap<Device, Point<f64, Logical>>` fed from both `PointerMotion`/`PointerMotionAbsolute` arms in `session.rs::handle_libinput_event`, one extra cursor sprite rendered per live non-active device in `render.rs` (same `cursor_buffers`/theme as the primary sprite for now - no per-device visual distinction yet, flagged as a later-phase refinement). This genuinely gives "mouse and trackpad both move a visible, independent cursor" and "an agent's virtual pointer has its own cursor sprite, distinct from the user's real one" for compositor-level chrome - both real, both scoped to what the protocol wall above doesn't block. Hit-testing for compositor actions (drag/resize/click chrome) still only runs against the one real `pointer_pos`, same as before - this phase is rendering-visible, not yet interactive, per the phase's own original scope. Client *content* (typing into a field, clicking a button inside an app) still funnels through the one real `wl_seat`, honestly not solved by this phase. Full workspace test suite green (425 tests passing across every crate, 1 pre-existing ignored, 0 failed); clippy clean; release build installed. +- **Phase 2, revised, real but large, not yet built**: superseded by the "Multi-cursor Phase 2" entry near the top of this file - a second `wl_seat` turned out to be a dead end (real clients only ever bind the first one advertised); the real shape is pinning a `zwlr_virtual_pointer_unstable_v1` object to a specific window so its events route there directly, independent of the shared `pointer_pos`, needing zero client cooperation. See that entry for the concrete plumbing. + +### Combined monitor modes (e.g. one monitor mirrored while another extends) + +Confirmed absent on both sides this session (srdwm has no monitor-*mode* concept at all beyond position; AGS's own `arrange()` applies one id to every non-primary output uniformly, confirmed by `dotfiles-16` reading their own source). Real shape of the fix: AGS's own per-output mode map (this is a TypeScript/AGS-side data-model change, `arrange()`'s single `id: string` parameter becoming a `Map<outputName, ArrangementId>|per-output mode`, plus a UI that can set one output's mode independently instead of one global picker) - srdwm's own side needs nothing new (`srd dispatch set output position` already accepts an arbitrary position per named output; "mirror" specifically - two outputs showing identical content - is the one mode that would need a real srdwm-side feature, since nothing here duplicates a framebuffer across two physical connectors today). Split needed before this is buildable: (1) AGS's own per-output mode map/UI, (2) srdwm's own real mirroring support if "mirror" is one of the modes wanted combined, scoped separately since it's a real rendering feature (draw the same composited frame to two CRTCs), not a placement one. + +### Phone monitor / special workspace (KVM/QEMU, "as close to a fully working phone as possible") + +Confirmed this session: QEMU is installed (`qemu-system-x86_64`/`-aarch64`, `/dev/kvm` present and world-accessible) but there is no existing VM image, no `libvirt`/`virt-manager`, no `waydroid` - a genuine from-scratch build, not a repurposing job. Real phased shape: + +- **Phase 1**: VM lifecycle management - a real script (matching this machine's own `~/.scripts/` convention, `keep_guest_awake.sh`/`disable_meta_key_guest.sh` are real prior art for QEMU-guest scripting already living there) to create/boot/stop a QEMU guest running a real Android-x86 (or similar phone-like) image, with KVM acceleration. Needs a real disk image - either the user provides one or this needs to fetch/build one, a real question to ask before writing code that assumes either. +- **Phase 2**: display integration - the guest's own display (SPICE or a VNC/virtio-gpu framebuffer) shown as an ordinary srdwm window via an existing viewer (`wayvnc_session`-style, already real prior art in `~/.scripts/utils/`) - no compositor code needed yet, this is "run a real VM viewer app in a real window", already possible today with zero srdwm changes. +- **Phase 3**: the actual "special workspace" compositor feature - a dedicated workspace/monitor mode that treats that one window specially (always-visible chrome, a phone-shaped aspect ratio/scale, whatever "as close to fully working" ends up meaning concretely) - real srdwm work, but only worth designing once Phase 1/2 prove the VM/display path works at all. +- **Phase 4**: input passthrough (touch/rotation/hardware-button emulation into the guest) - depends on Phase 3's own shape and, if a touchscreen exists by then, the touchscreen work below. + +### Touchscreen support + +Confirmed genuinely absent (zero `wl_touch`/touch-protocol code anywhere in `crates/`) - "decide scope first" was this item's own explicit instruction, and the concrete decision needed before writing code: does this machine (or the one this is meant to run on) *have* a touchscreen to test against at all? Building and shipping untested touch-event handling blind is exactly the "half-implementation that has to be redone later" this project's own standing rules warn against. If yes, the real shape mirrors `virtual_pointer.rs`'s own precedent closely: smithay exposes `InputEvent::TouchDown`/`TouchMotion`/`TouchUp`/`TouchFrame`/`TouchCancel` already (libinput-backed, same as pointer/keyboard), and a real `TouchHandler` wires those into `wl_touch` the same way `handle_libinput_event` already wires pointer/keyboard - a real, scoped, well-precedented protocol implementation, not a research project, *once* there's real hardware to verify it against. + +## New feature: rubber-band desktop icon selection, and a fuller bare-desktop menu (2026-08-26) + +Reported live: "missing click and drag stuff like from windows" (this entry) and "not even new file" (the desktop menu). Two real gaps, both closed: + +**Rubber-band/marquee multi-select**: clicking bare desktop used to only ever clear whatever single icon was selected - there was no way to select more than one icon at all except one at a time. A left-press on bare desktop now starts a marquee (`CompState::start_desktop_marquee`); every motion tick while it's active re-selects whichever icon cells the resulting rectangle currently overlaps (`update_desktop_marquee`), restricted to the one monitor's own mirror the drag actually started on (see `desktop_icons.rs`'s own "mirrored icons" module doc comment for why cross-monitor mirrors exist at all); release just clears the drag state, since the last motion tick already left the right icons selected. Rendered as four thin solid-colour strips (`border_side_render_element`, the same primitive window borders already use) forming a rectangle outline - a real, visible indicator, not a translucent fill (`SolidColorRenderElement` has no alpha-blend path, and a new element type for one feature wasn't worth it). + +**Bare-desktop menu**: added "New Text Document" next to the existing "New Folder" (the literal "not even new file" gap), plus real separator rows grouping New actions / Open actions / Refresh - reusing the same `MenuAction::Separator`-style divider `context_menu.rs`'s own expansion just added, as `DesktopMenuAction::Separator` (a second, separate enum - this menu and the titlebar one still don't share one). + +Not attempted: Cut/Copy/Paste for icons (needs `wl_data_device`/`text/uri-list` clipboard interop, already documented elsewhere as deliberately out of scope for the reason given there), and variable icon sizes/a "View" submenu (real submenu UI still doesn't exist - every "fuller menu" ask so far has been satisfied by flattening or a section-label separator instead, but a genuine Windows/macOS-style nested submenu is real, separate scope if a future ask specifically needs one, not silently built partial here). + +Full workspace build/test/clippy clean (197 core / 145 wayland tests). Not yet verified live against a real click-drag - this file's own module has no `CompState` test fixture to unit-test the selection logic against (every existing test here is a pure-helper test), consistent with how this module has always drawn its own testing boundary; needs a real drag to confirm. + +## Three real fixes from one live-feedback burst after a restart: context menu depth, a desktop-icon selection bug, and a startup placement race (2026-08-26) + +**Context menu "much fuller"**: expanded from four fixed actions to Minimize, Maximize/Restore, Fullscreen, Floating, Always on Top, a flattened "Move to Workspace" section (one row per *other* workspace, skipping the window's own), and Close - the common set every native window menu (Windows' System menu, GNOME/KDE's titlebar menu) actually carries. New `MenuAction::Separator` (a real divider row, not a fake action - `input/pointer.rs`'s click dispatch keeps the menu open on a separator click rather than running a no-op or dismissing it). `decoration::render_context_menu` itself untouched - separators render as literal box-drawing-character section labels through the existing text path rather than a new line-drawing primitive, which turned out to double as free section headers ("Move to Workspace") rather than needing a second visual language. + +**Desktop icon selection highlight**: reported live, screenshotted directly - right-clicking an icon showed a large, saturated highlight block that read as disproportionate next to the icon/label it was meant to pick out. Root cause: the highlight (`fill_rect`, `theme.default_border_color` - e.g. Catppuccin's mauve) was sized to nearly the *whole cell* (full width minus 4px, 40% of the height) rather than the label text itself. Fixed to a real macOS/GNOME-style snug rounded chip sized to the actual rendered text width plus small padding, using `fill_rounded_rect`/`blit_glyph` (the same properly-blended pair the context-menu rewrite above already established) instead of a flat full-width rectangle. + +**Startup placement race, closed rather than just documented**: `general.reserve_top`/`_bottom`/`_left`/`_right` (new config keys, `0`/no-op by default) let a static space reservation apply from the very first `Platform::monitors()` call, before any real bar/dock client has connected - see the dedicated TODO/DEFAULTS.md entries and `WindowManager::reserve_top`'s own doc comment for the full "desktop icons already self-correct every frame, but a *window* placed in the gap before the bar connects gets a one-time placement decision with nothing to nudge it out afterward" reasoning. Set to `34` in this machine's own `~/.config/srd/init.lua` (AGS's real measured top-bar height). Only ever shrinks `usable` *further* than whatever real exclusive zone already exists - a real, bigger bar always wins once it actually connects. + +All three: full workspace build/test/clippy clean (197 core / 145 wayland tests), built and installed. + +## Instrumentation added, not yet measured: real perf logging for the "resizing seems slow" report (2026-08-26) + +Reported live; this session's own investigation (decoration-buffer caching - already signature-cached, no redundant re-rasterization; the one `log::trace!` on the pointer-motion path - below the active `RUST_LOG` level; GPU render path - disabled in config) found no smoking gun without an actual measurement, which is what "measure, don't estimate" actually calls for here rather than another guess. + +Added real, cheap instrumentation to `render_udev_frame` (`udev/render.rs`) rather than a fix: one `Instant::now()`/comparison per frame (no allocation, no formatting unless it trips), logging a `PERF-RESIZE` warning only when a frame misses a 16ms (60fps) budget, tagged with whether a resize or drag was actually in progress at that moment (`WindowManager::resizing_window()`/`is_dragging()`). Deliberately not a per-frame trace line - this project's own history already has a documented incident (`docs/TODO.md`'s 2026-08-21 "POS-DIAG/CURSOR-DIAG" entry) where unconditional per-motion-event logging measured at 35% of an entire session's log and was itself mistaken for part of the problem it was diagnosing; a threshold-gated warning can't repeat that. + +Not yet measured against a real resize - built, tested (140 wayland tests, clippy clean) and installed, pending the user's own next real resize (or restart) to actually produce data. The next dropped-frame warning in the log (if any) will say whether this genuinely correlates with resize/drag specifically, which is the actual open question, not assumed either way. + +Separately, a real, simpler candidate cause surfaced while this same session's own release builds ran: `free -h` during a build showed this machine under genuine memory pressure (3.7GiB total RAM, under 200MiB free, 4.4GiB of a 7.7GiB swap actually in use) with several concurrent agent sessions running alongside the live compositor. System-wide swapping would make everything feel sluggish, resize included, with no compositor-side bug required at all - worth ruling out (`free -h` at the moment it feels slow) before trusting the `PERF-RESIZE` log line above to mean what it says. + +**A real measurement, taken**: rather than wait indefinitely for a real interactive drag, exercised the same `render_udev_frame` path deterministically via `srd dispatch toggle maximize <id>` against a real, currently-open window (Nemo, harmless to flicker) - safe and reversible (`srd clients` confirmed it landed back at its exact original geometry afterward), and zero risk of a wrong-target synthetic click since it addresses the window by its real IPC id, not a screen coordinate. 24 total geometry-changing toggles (4 individually, then a 20-toggle burst in a tight loop) produced **zero** `PERF-RESIZE` warnings - every single frame stayed under the 16ms/60fps budget. This measures the discrete "geometry changes instantly" case, not a continuous drag's every-motion-tick redraw, so it doesn't fully settle the original complaint - but it does rule out "a resize-triggered redraw is fundamentally slow" as the cause on this hardware, for at least this pattern. Combined with the memory-pressure finding above, general system load (not this compositor's own render cost) is now the more likely explanation, pending an actual interactive-drag measurement whenever the user is present to do one safely. + +## Real bug, reported live, root-caused as far as it goes without a driver-level investigation: nested srdwm (winit backend) renders garbled under this machine's real session (2026-08-26) + +Reported live, in the moment: launching a nested `srdwm --wayland` (winit backend, under the live session's own Wayland socket as host - this file's own "validate in a nested compositor" convention, for the virtual-pointer protocol smoke test below) rendered "upside down/inverted"; a screenshot taken moments later instead showed what looked like stale/mirrored content from the host's own tmux window bleeding through. + +Checked first, ruled out: `Transform::Normal` is set correctly in `winit/connect.rs`'s own `change_current_state` call, matching every other real compositor's use of smithay's stock `WinitGraphicsBackend<GlesRenderer>` - not a Y-flip/transform bug in this codebase's own rendering math. The nested instance's own log tells the real story: repeated `EGL_BAD_ALLOC` on `eglGetPlatformDisplay`, "failed to get driver name for fd -1", `eglQueryDevicesEXT` failing to allocate a device list, DRI2 failing to set up an `EGLDevice`. The GL/EGL context this backend needs never actually initializes cleanly under nesting on this machine - whatever garbled visual result shows up (inverted, mirrored, or something else entirely) is a plausible downstream symptom of that failure, not evidence of a specific transform bug to chase. + +Not fixed this session - fixing a rendering-code path based on a symptom of a broken GPU context underneath it would be treating the wrong layer. Needs real investigation of *why* EGL device enumeration fails specifically when nesting on this machine (render-node/DRM-fd access under a nested Wayland client, a Mesa/driver version quirk, or something host-session-specific) before any compositor-code change is worth attempting. Test process was killed cleanly; confirmed no orphaned processes left behind. + +## New feature: real desktop icons now use freedesktop icon-theme SVG artwork, not hand-drawn shapes (2026-08-26) + +Reported live: the hand-drawn glyphs (flat rounded-square blocks, polished once already this session in `252d95e`) still read as generic placeholder art, "look like they were made with AI" - asked directly to fetch what these icons actually look like in KDE/GNOME/macOS/Windows and use that instead, with the explicit expectation that the real installed theme (WhiteSur) overrides whatever default ships. + +Built as a new `icon_theme.rs` module: a real (if narrow - only the five names `desktop_icons.rs`'s own `IconKind` needs) implementation of the freedesktop icon theme spec. Reads the user's actual configured theme the same places GTK itself would (`gtk-3.0`/`gtk-4.0`'s `settings.ini`, falling back to `gsettings`), walks its `Inherits=` chain recursively, always searches `hicolor` last per the spec's own fallback rule, and prefers a `scalable` SVG over any fixed-size raster variant since every lookup here wants one specific pixel size rendered from source. Rendering is `resvg`/`usvg`/`tiny-skia` (new dependencies - pure Rust, no C toolchain needed, already vetted against this machine's real WhiteSur-dark theme: `user-home`, `computer`, `user-trash`, `folder`, `text-x-generic` all resolved and rendered correctly, verified by dumping each to a PNG and inspecting it directly, not just trusting a green test). + +`decoration::render_desktop_icon` takes a new `real_icon: Option<&[u8]>` (a pre-rasterized BGRA buffer, same byte order every other rasterizer in this file already produces) - `Some` blits it straight into the existing glyph box, `None` falls back to the original hand-drawn glyph unchanged, so a machine with no icon theme installed at all (or a theme missing one of the five names) degrades to exactly what shipped before this entry, not a blank box. `state/desktop_icons.rs::rebuild_icon_buffer` does the lookup+rasterize on every rebuild (icon selection/rename change) rather than caching separately - rebuilds are already infrequent, and a theme change while running just works on the next one with no cache-invalidation path to get wrong. + +## New feature: context/desktop menus rebuilt to match this project's own AGS panel, not a hard-edged square box (2026-08-26) + +Reported live, about both the hand-drawn icons above and menus in the same message: "context menus also need a lot of improvement... UI should look like AGS's global menu dropdown". Directly instructed to look at a live screenshot of that AGS menu rather than guess - attempted via `ydotool` (moved the pointer to the bar's "File" item and clicked), but aborted that path once a concurrent, unrelated keystroke appeared in a nearby tmux pane during the same live session: with the user actively multitasking on the same physical screen, a blind synthetic click too close to other real work was the wrong risk to take for a cosmetic reference screenshot. Read this project's own AGS source instead (`~/dotfiles/ags_project/widget/Bar/components/GlobalMenu/style.scss`'s `popover box.menu-list`, plus `options.ts` for the actual dark-theme colour/spacing values) - more precise than a screenshot in any case, since it gives exact values instead of eyeballed ones. + +`decoration::render_context_menu` (shared by every context/desktop menu in this compositor - the titlebar window menu and both new desktop-icon/bare-desktop menus alike, one rasterizer) rebuilt to match that reference's real structure: a rounded floating panel (was a hard-edged square canvas with rows drawn edge-to-edge) with no border/outline anywhere (was a single flat 1px border around the whole menu) and a rounded, inset tinted fill on the highlighted row only (was every row, highlighted or not, filled edge-to-edge with its own flat background) - "a real menu highlights a row with a fill, not a frame", the same principle the AGS reference's own commit history states directly. New `fill_rounded_rect_over` helper: `fill_rounded_rect` itself (used elsewhere for the desktop icon glyphs) blends its own antialiased edges toward a transparent backdrop, which is correct there but would leave a visible dark seam around a rounded hover chip painted on top of the panel's *own* already-opaque fill - the new helper blends against whatever is actually already in the buffer instead. Deliberately unchanged: the external canvas size (`row_height * items.len()`, no added padding) and per-row position, since `ContextMenu`/`DesktopMenu`'s own `height()`/`row_at()` hit-testing assume that exact layout and have no reason to change just because the pixels inside look different. Still no submenus/icons/separators inside a row - real, separate scope, not attempted blind alongside this pass. + +`context_menu_border_is_opaque_at_every_edge` (a real test, not just described) inverted into `context_menu_panel_is_opaque_in_the_middle_but_rounded_at_the_corners`: the exact corner pixel is now transparent by design (a real rounded corner, not a hard square), which the old assertion would have flagged as a regression rather than the intended fix. + +## New feature: `zwlr_virtual_pointer_unstable_v1`, a real protocol path for synthetic pointer input (2026-08-26) + +Closes a gap this project's own history had already root-caused twice over: "ydotool's `--absolute` is unusable on this machine" (no `EV_ABS` on its uinput device, relative motion warped by libinput's own acceleration curve) and the still-open `wl_pointer` unscaled-coordinate investigation both trace back to the same underlying fact - this compositor had no real Wayland-protocol path for synthetic pointer input at all, only whatever a uinput-backed tool like `ydotool` could fake at the kernel level. Also the concrete first step toward the directly-requested "agent controls input without interrupting the user" scenario, though not the whole of it - see below. + +Hand-rolled `GlobalDispatch`/`Dispatch` plumbing in a new `virtual_pointer.rs`, matching `screencopy.rs`/`output_management.rs`'s own established shape for a protocol smithay 0.7 ships no helper for. Every `motion`/`motion_absolute`/`button` request is fed through the exact same `handle_pointer_position`/`handle_pointer_button` entry points a real libinput hardware event already goes through (`udev/session.rs::handle_libinput_event`) - a virtual pointer is indistinguishable from a real mouse to every other part of this compositor (hit-testing, drag/resize, focus-follows-mouse) by construction, not a second code path that can drift out of sync. Scroll (`axis`/`axis_source`/`axis_stop`/`axis_discrete`/`frame`) is built directly against smithay's `AxisFrame` builder instead, accumulated across requests in a per-object `Mutex<Option<AxisFrame>>` (needs `Send + Sync` for `wayland-server`'s own `DataInit::init`, not a plain `RefCell`) and committed on the protocol's own `frame` request. + +One real, documented limitation: motion only takes effect when `CompState::udev` is live (the real-hardware backend) - the winit/nested backend has no equivalent multi-monitor `bounds()` to clamp against and no daily-driver use case for synthetic input, so the global still exists there (a client's `create_virtual_pointer` still succeeds) but motion requests are accepted no-ops, not silently dropped with no explanation. + +Verified: full workspace build/test/clippy clean (140 wayland tests, up from 138), and a nested `srdwm --wayland` instance (launched under the real session's own `WAYLAND_DISPLAY` as its host, per this file's own "validate in a nested compositor" convention - never the live session) started cleanly, ran its full normal autostart (AGS, polkit agent, mpd-mpris) with no crash, and shut down cleanly on signal. **Not yet verified**: no purpose-built protocol test client exists yet to actually exercise `motion`/`button`/`axis` end-to-end the way `tools/toplevel-activate` does for `zwlr_foreign_toplevel_handle_v1` - same "confirmed-fixed, unverified against a real client" bucket as the KDE/Qt global menu items further down this file, for the same reason (no ready-made client available to test against, here because the client would have to be purpose-built rather than merely uninstalled). + +Explicitly not the whole of "multi-cursor" as requested (mouse+trackpad simultaneously, or an agent on one monitor while the user works another): this gives one more input *source* feeding the single existing system pointer, not a second independent one. True simultaneous independent cursors need real multi-seat work - parked separately, see `.nightshift/parked.md`/`nightshift questions`. + +## Session cleanup: leftover `TEMP-DIAG-*` debug logging from an unfinished prior session found live, removed (2026-08-26) + +Picked up this session mid-flight: the previous session's own uncommitted changes to `platform.rs` (primary-monitor fix, see below) and `state/geometry.rs` (cross-monitor border-clip fix, see below) were real and correct, but four `log::warn!("TEMP-DIAG-...")` lines had been left in alongside them, in `input/pointer.rs` (fired on every left-click), `state/desktop_icons.rs` (fired on every icon redraw), `udev/render.rs` (fired on frames 0-2 and every 600th frame after, unconditionally, on the render hot path), and `screencopy.rs` (fired on every `grim`/screenshot capture). Exactly the same "temporary, never removed" pattern already documented and fixed once this session in the 2026-08-21 `POS-DIAG`/`CURSOR-DIAG` entry further down this file -- removed the same way, keeping the real design-rationale comments around them. Separately: the installed `~/.local/share/cargo/bin/{srdwm,srd}` binaries were still the *previous* build (03:12) despite the running process having started at 03:16 with no explanation found for why `monitors()` was reporting the pre-fix `next_id == 0` primary-selection behavior live (`HDMI-A-1` primary despite `eDP-1` sitting at physical `(0,0)` per the restored layout) -- rebuilt (workspace `cargo build`/`test`/`clippy` all clean, 195 core / 134 wayland tests), reinstalled; a live restart (deferred to the user, see below) is what actually picks this up. + +## Real bug, reported live, fixed: external monitor's saved layout had drifted to "extend right", not the user's actual "extend left" (2026-08-26) + +`~/.local/state/srd/monitor-layout.json` had `HDMI-A-1` persisted at `x: 1920` (to the right of `eDP-1`'s `x: 0`) - not a compositor bug, just a saved position that no longer matched what the user actually wants day to day (likely left over from earlier dragging/testing). Fixed live via `srd dispatch set output position HDMI-A-1 -1920 0` (no restart needed for a position change) - `srd monitors` now reports `HDMI-A-1` at `full_x: -1920`, correctly extending left of `eDP-1`. Compounded by the primary-monitor bug above: because the currently-*running* process still had the old `next_id == 0` primary logic active, `HDMI-A-1` also displayed as primary despite not being at `(0, 0)` - desktop icons (primary-monitor-only, `state/desktop_icons.rs`) and primary-anchored placement were following it there instead of `eDP-1`. Both should resolve together once the user restarts srdwm onto the reinstalled binary. + +Follow-up, same day: the drift recurred after a real restart, still not matching either store, which pointed at something actively re-applying a position rather than a one-off leftover. Root cause, confirmed jointly with `dotfiles-16` (the AGS-side peer session) rather than guessed at from srdwm's side alone: srdwm's own `restore_monitor_layout()` and AGS's `restoreRememberedLayout()` (`widget/shared/MonitorLayout.tsx:603`) are two independent, uncoordinated startup-time layout restores with no ordering guarantee - srdwm's runs correctly (confirmed via the session log), but AGS's own runs immediately after with no signal or delay of any kind (`dotfiles-16`'s own words: "waits on nothing at all"), so whichever store the two disagree on, AGS's copy wins by running last. This was already a known, recorded risk: `ags_project/HANDOFF.md` documents the intended resolution as deleting `restoreRememberedLayout()` entirely once srdwm's own restore is confirmed working, since two things arranging the same outputs is the exact shape of bug that produced an earlier `border_width` conflict. **Resolved, landed on the AGS side (2026-08-26, confirmed by `dotfiles-16`)**: not a wholesale removal of `restoreRememberedLayout()` - it turned out to have three call sites, not one: startup (`MonitorLayout.tsx:959`, the one that actually raced srdwm), a monitor being replugged (`:968`), and an output re-enabled after being toggled off (`:739`, which has its own comment recording a real regression an outright deletion would have reintroduced - without it, an "extend left" setup silently became "extend right" after an off/on cycle). Only the startup call duplicated srdwm's own boot-time restore and was removed; the function itself and the other two event-driven calls stayed, since srdwm genuinely doesn't cover a replug or re-enable case. Synced to `~/.config/ags`, takes effect on AGS's next start (startup-only path, nothing forced/bounced); a backup of the pre-edit file is at `~/.cache/ags-MonitorLayout.tsx.bak.20260826-064202`. Net effect for srdwm confirmed unchanged either way: no "layout settled" signal, no hold-off, nothing to build here. + +## Decision made with the user: HDMI-A-1 forced to scale 1.0, trading the auto-shrink feature for correctness (2026-08-26) + +Two live-reported bugs on the external monitor - clicks landing off from the visible pointer, and windows/content rendering "messed up/broken/see-through" - both trace to the same already-documented root cause (see the "auto-scaled below 1.0 breaks for any client that doesn't speak `wp-fractional-scale-v1`" entry further down): `HDMI-A-1` auto-scales to ~0.843 given its real physical size/resolution, and any client that falls back to the legacy integer `wl_output.scale` (which can't represent below `1.0`) ends up content-sized for the wrong number, on top of a separate, confirmed-real `wl_pointer` wire-protocol gap (see the entry below) that delivers unscaled physical coordinates to every client regardless. + +That prior entry explicitly left the "clamp the auto-scale floor to 1.0" question for the user to decide, rather than choosing unilaterally. Asked directly this session: user chose to force this one connector back to `1.0` via the now-real `srd.monitor.scale("HDMI-A-1", 1.0)` (`~/.config/srd/init.lua`) immediately, *and* asked whether the deeper `wl_pointer` fix is still worth pursuing so a future scale-below-1.0 config on this or another machine would also just work. Immediate fix applied and explained inline in `init.lua`'s own comment; the `wl_pointer` investigation is the next entry. + +## Investigated further, confirmed genuinely large: the `wl_pointer` unscaled-coordinate gap needs a full `PointerTarget` reimplementation, not a patch (2026-08-26) + +Following up on the already-documented, not-yet-fixed `wl_pointer` motion/button scaling gap (see its own entry below) at the user's direct request, to see whether it's smaller than previously scoped. It is not - confirmed against smithay 0.7.0's own source, not re-guessed: `impl<D> PointerTarget<D> for WlSurface` (`wayland/seat/pointer.rs:236`) is a **blanket** impl, generic over every `D`, provided by smithay itself. Rust's orphan rules mean this compositor cannot provide a second, competing `PointerTarget<CompState> for WlSurface` impl to intercept or correct it - not a design choice, a hard language rule. `type PointerFocus = WlSurface` (`protocols/seat.rs`) would have to change to a compositor-owned wrapper type for interception to be possible at all, and since a wrapper type doesn't get smithay's own free `enter`/`leave`/`motion`/`button`/`axis`/`frame`/gesture wire-protocol implementation, that whole implementation would need writing by hand for every regular Wayland surface, not just the ~3 call sites in `input/pointer.rs` that currently construct pointer focus. Real, substantial ground-up subsystem work, not the "thin path" the earlier entry hoped for - and it still would not touch the *content-overflow* half of the same symptom (a per-client protocol-support gap, not something the compositor side can fix regardless). Not attempted this session: the risk of getting seat/pointer plumbing wrong live, on the daily driver, with no isolated way to test it first, outweighs the benefit now that `HDMI-A-1` is forced to `1.0` and the gap is dormant. Worth a dedicated session with a nested `srdwm --wayland` test rig if scale-below-1.0 is ever wanted back on a real monitor. + +## Real bug, root-caused and fixed: a window's border/decoration briefly shows clipped/missing after a cross-monitor move between differently-scaled outputs (2026-08-26) + +Reported live: "the border/window looks cut offed when moved from one monitor to another". Confirmed directly - moved a real window from `HDMI-A-1` (scale ~0.843) to `eDP-1` (scale 1.0) and screenshotted immediately after: the window's left border was entirely missing and its content sat flush against the screen edge (the page's own heading read "...UNK" instead of "TYPERPUNK", clipped). A second screenshot of the same window a couple of minutes later showed a complete, correctly-bordered window - the same window, same position, self-corrected. + +Root cause, confirmed by reading `effective_frame_of` (`state/geometry.rs`) against `sync_geometry`'s own scale conversion: a cross-monitor move keeps a window's *physical* footprint constant, so a scale change between the source and destination monitor changes the *logical* size `sync_geometry` sends the client (see that function's own doc comment) - an ordinary move forces the same size-changing `configure` a real resize would. `w.monitor` itself flips to the destination the moment the drag crosses the boundary, live, every tick - well before the client's own configure/commit round-trip can catch up. In that gap, `effective_frame_of` was reading the client's real committed content size (still logical points sized for the *old* monitor, since no new commit had landed yet) and multiplying it by the *new* monitor's scale to get back to physical - neither the old physical size nor the eventual new one, a genuine mismatch between where the border/shadow got drawn and where the client's actual pixels were. + +Fixed by reusing `pending_size_configure` (already tracked for the unrelated configure-throttle above it): `effective_frame_of` now trusts this compositor's own live target (`geom`) rather than reconstructing physical size from a not-yet-committed logical value, for as long as a size-changing configure is outstanding - the same "trust the live target, not a stale client commit" rule the active-resize branch just above it already applies, extended to cover this second, non-drag gap. Not a corruption risk either way: the existing `src`-crop clamp (the resize-lag fix, see below) already bounds every border/titlebar crop against the decoration buffer's real last-built size regardless. ## Real bug, root-caused and fixed, jointly with a peer session (dotfiles-16): layer-shell surfaces on a fractionally-scaled output were unclickable and, for a bottom/right-anchored one, never painted at all (2026-08-26) @@ -32,6 +304,24 @@ Root cause, confirmed against smithay 0.7.0's own source (`desktop/wayland/layer Both fixed the same way `udev/platform.rs::monitors()` and `udev/outputs.rs`'s own `non_exclusive_zone()` handling already fix the identical unit mismatch for usable-area computation (confirmed working precedent already in this codebase, not a new technique): multiply the logical value by `output.current_scale().fractional_scale()`, rounding to the nearest physical pixel, before using it as a physical position. +## Real bug, root-caused and fixed: "primary" monitor picked by DRM probe order, not the user's actual layout (2026-08-26) + +Reported live, in stages, across several restarts: desktop icons "sometimes show on other monitor and sometimes don't", and apps opening on the wrong monitor when launched. `udev/platform.rs::monitors()` used to fall back to `udev.heads.first()` whenever nothing was sticky yet - whichever connector DRM happened to probe first, which has no relationship to the user's actual layout. With an "extend left" saved layout (external monitor at negative x, laptop panel at `x = 0`), the external monitor could still win "primary" at boot purely because it probed before the panel. + +Fixed: the first-ever pick (before anything is sticky) now prefers whichever head's `location` is physical `(0, 0)` - `relayout_outputs`/`output_management::apply_output_position` already keep the user's actual anchor monitor there, the same position-based definition of "primary" every desktop convention (xrandr, wlr-output-management) already uses, driven by the layout the user actually configured rather than enumeration order. Falls back to `udev.heads.first()` only if no head is at the origin at all. Stays sticky by name after that, unchanged from the earlier fix in this same function. + +## Real bug, root-caused and fixed: a new window's target monitor prioritized a stale focused window over the pointer's actual monitor (2026-08-26) + +Reported live: launching an app while the pointer sat on a second monitor's bare desktop (nothing focused there - an empty desktop, or hovering a panel/dock, neither of which is a core-tracked window) still put the new window on the *first* monitor, wherever the last real window focus happened to be. `add_window`'s target-monitor fallback chain (`manager/windows.rs`) used to check the focused window's monitor first and the pointer's own monitor only as a fallback - deliberate, tested design from earlier in this same project, on the reasoning that focus is the stronger "where is the user working" signal. + +Live feedback contradicted that reasoning directly: `self.focused` only changes when a real window is actually focused, so it goes stale the moment the user's attention moves to empty desktop, a panel, or a dock - exactly the case that matters. `pointer_monitor` has no such staleness (updated on every motion event). Compared against related projects per the user's own request: this also matches the "active output follows the cursor" default every comparable dynamic/floating compositor ships (Hyprland's `focus_follows_mouse` default, Mutter/GNOME, sway). Swapped the priority: pointer's monitor now wins, falling back to the focused window's monitor only when the pointer's own is unknown (a fresh session, no motion reported yet), then primary as the last resort. `manager/tests.rs`'s `a_focused_window_still_wins_over_the_pointers_monitor` (the old, now-reversed contract) was rewritten as `the_pointers_monitor_wins_over_a_stale_focused_window`. + +## Real bug, root-caused, not yet safely fixable: `wl_pointer` motion/button coordinates are delivered unscaled to clients on a non-1.0-scale output (2026-08-26) + +Reported live: "clicks aren't landing where pointer is on other monitor" (the scaled one specifically, per direct follow-up). Root-caused by reading smithay 0.7.0's own source, not guessed: this compositor's `MotionEvent.location` (`input/pointer.rs`) is fed physical pixels throughout, matching every other internal placement computation here - but smithay's `WlSurface as PointerTarget` (`wayland/seat/pointer.rs::WlPointerHandle::motion`) forwards that value to the client's real `wl_pointer.motion` wire event through `event.location.to_client(client_scale)`, and this compositor has never called `set_client_scale` (confirmed: zero references in the whole crate before this entry), so `client_scale` defaults to `1.0` for every regular client - meaning a client on a monitor with `scale != 1.0` receives raw physical-pixel coordinates where it expects logical points, the same unit mismatch already found and fixed twice this session for layer-shell hit-testing and cross-monitor border placement, this time on the wire to the client itself. `xdg_toplevel::configure`'s `size` (this compositor's own manual conversion, `sync_geometry`) is unaffected and already correct - the client's own idea of its *size* is right, only the pointer coordinates delivered against that size are wrong. + +Not fixed this pass: smithay's own sanctioned mechanism for exactly this (`CompositorClientState::set_client_scale`) is explicitly documented as only guaranteeing correctness for "the minimal set of protocols used by xwayland" - and this codebase already relies on a *different*, hand-computed correct value for the *same* clients' `xdg_output` logical position (`udev/outputs.rs::relayout_outputs`'s own `x_logical`, sent via `change_current_state` and advertised onward by smithay's stock `Output` global). `set_client_scale` also multiplies that same per-client `xdg_output`/`wl_output` advertisement (confirmed by reading `wayland/output/xdg.rs` and `wayland/output/mod.rs`), which would double-apply on top of `x_logical` for any client it's turned on for - a real, not hypothetical, second-output-position regression, not just a hypothetical one. Because `PointerHandle::motion`'s single `event.location` parameter drives *both* the value smithay stores as `current_location()` (read elsewhere in this codebase - `xwayland.rs`'s move/resize-request handlers, `input.rs::last_pointer_pos` - always expecting physical, XWayland concretely so, since X11 has no per-output fractional-scale concept of its own) and the value forwarded to the focused client's wire event, there is no way to convert only the latter through smithay's public API as it stands. A correct fix needs a thin, compositor-owned `wl_pointer` serialization path for regular (non-XWayland) toplevel/popup surfaces specifically, bypassing `WlSurface`'s stock `PointerTarget` impl for just the motion/button events - real, scoped work, not a one-line change, deliberately not attempted blind this session given the daily-driver risk of getting it wrong. + Separately, while investigating: a temporary per-pointer-motion-event diagnostic log in `layer_hit_test` (querying compositor cached state and formatting/writing a log line on every single pixel of every mouse move) was still live from an earlier debugging session, explicitly marked "Temporary... remove once a restart confirms" but never removed. Real, measurable cost on the hot input path - removed; likely the direct cause of the separately reported "seems weird/slow" on top of the click-dead dock. Built, full workspace test suite and clippy clean; installed, pending a live restart to confirm both the dock/bar's full click area and general input responsiveness on the scaled output. @@ -111,12 +401,32 @@ live restart to confirm. Separately, while investigating: `~/.local/state/srd/monitor-layout.json` was found with both `eDP-1` and `HDMI-A-1` persisted at the identical -position `(1920, 0)` - clearly wrong (would stack them). The live -arrangement is currently correct (`eDP-1` at `1920,0`, `HDMI-A-1` at -`0,0` - genuine extend-left), so this stale file isn't actually being -trusted/applied at startup right now, but the corruption itself isn't -root-caused yet - not investigated further this round, flagged for -follow-up. +position `(1920, 0)` - clearly wrong (would stack them). A peer session +(dotfiles-16) confirmed AGS itself sends distinct positions for the two +(`eDP-1@1920,0 HDMI-A-1@0,0`), narrowing this to srdwm's own side of +`set_output_position`/`apply_output_position`. + +Follow-up (2026-08-26): read every step of that path -- +`crates/platform/src/ipc.rs`'s `set_output_position` handler +(name-to-id resolution, `request_output_position`), `WindowManager:: +request_output_position`/`drain_output_position_requests` (de-dupes by +id then pushes - already has a passing test, `output_position_requests_ +drain_in_arrival_order`, confirming distinct ids keep distinct values), +`udev/platform.rs`'s per-tick drain loop, `output_management::apply_ +output_position` itself, and `udev/outputs.rs::restore_monitor_layout` +(the startup path). Every one of them is correctly per-output/per-request +on inspection - no shared mutable state or loop-variable capture bug +found anywhere in the chain. + +Reproduced the AGS sequence directly (`srd dispatch set output position +eDP-1 1920 0` then `... HDMI-A-1 0 0`) and the persisted file came back +correct both times, distinct positions preserved. Live file is currently +correct too (matches the real arrangement exactly). Not reproducible as +of this session - likely a rare, one-off race (possibly startup- +specific, multiple systems initializing concurrently) rather than a +standing defect in the code as it reads today. Not pursued further; +worth another look only if it recurs with a fresh, catchable +before/after state. ## Real bug, root-caused and fixed: a resize's own rapid-fire commits could get a window's rounded-corner content mask cached as blank, hiding real content until the next real content change (2026-08-25) @@ -613,6 +923,12 @@ agent transcripts, not duplicated here. only ever reach srdwm's separate GLES/winit backend, which is dev-only. Deliberately not pursued this session for this reason, not for lack of merit. + **Superseded 2026-08-26**: "GLES is dev-only" above is now stale -- + `general.gpu`/`SRDWM_GPU=1` gates a real, reachable GLES path on the + udev/DRM backend too (`udev/gpu.rs` + the branch in `udev/render.rs`, + falling back to Pixman per-CRTC on init failure). See the "GPU/rendering + completion" plan entry lower in this file - niri's per-element shader + clip is a real, live option there after all, just not attempted yet. - **Button-order convergence acted on** - see the button-order feature entry below. - **The coordinate-unit bug class this session fixed has a more durable @@ -1649,11 +1965,20 @@ persists. session, niri's xdg-decoration negotiation specifically. If the ask is really about layout/rules/keybinding conventions rather than rendering, these four are sitting untouched for that. -- [ ] **Real per-app window-size memory that survives a restart** - - what's built (`remembered_sizes` in `crates/core`) is session- - lifetime only, in-memory. Persisting it across a compositor restart - would need a real config-file-backed store; deliberately not built - speculatively, see that field's own doc comment. +- [x] **Real per-app window-position-and-size memory that survives a + restart** - done (2026-08-26), directly requested ("why aren't we + getting smart window spawns, remembering last size/location"). + `remembered_sizes` (session-lifetime, size only) became + `remembered_geometry` (position + size), persisted via a new + `crates/wayland/src/window_memory.rs` (same JSON-under-`$XDG_STATE_ + HOME/srd` shape as `monitor_layout.rs`/`desktop_icons_state.rs`). + Loaded and seeded into `WindowManager` before the Wayland socket + binds (same ordering as the monitor-layout restore); saved after + every drag/resize release. A remembered *position* only applies if + it still lands on a monitor that actually exists this run (checked + against every current monitor's own full geometry) - otherwise falls + back to the existing pointer/focus-based smart placement, so an + unplugged external monitor can't strand a window off-screen. ## Render pipeline - researched, ranked, not started |