From 2e86445c3d6787850a38fd677dc117e0edc1aa47 Mon Sep 17 00:00:00 2001 From: "Brian J. Tarricone" Date: Tue, 3 May 2022 17:05:06 -0700 Subject: [PATCH] Initial import. Most things seem working. This includes an abortive attempt to do a gtk4 dialog (which I don't think is possible, as gtk4 doesn't allow embedding toplevels anymore), and an iced dialog, which I just never started writing. --- .gitignore | 2 + Cargo.lock | 1938 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 + Makefile | 47 + async-xcb/Cargo.toml | 13 + async-xcb/src/lib.rs | 49 + auth/Cargo.toml | 8 + auth/src/main.rs | 16 + command/Cargo.toml | 9 + command/src/lib.rs | 184 ++++ command/src/main.rs | 72 ++ dbus-service/Cargo.toml | 16 + dbus-service/src/main.rs | 235 +++++ dialog-gtk3/Cargo.toml | 18 + dialog-gtk3/src/main.rs | 261 +++++ dialog-gtk4/Cargo.toml | 9 + dialog-gtk4/src/main.rs | 43 + dialog-iced/Cargo.toml | 8 + dialog-iced/src/main.rs | 0 locker/Cargo.toml | 27 + locker/src/main.rs | 992 ++++++++++++++++++ rust-toolchain | 1 + systemd/Cargo.toml | 18 + systemd/src/main.rs | 99 ++ util/Cargo.toml | 13 + util/src/lib.rs | 50 + xcb-xembed/Cargo.toml | 18 + xcb-xembed/src/embedder.rs | 341 +++++++ xcb-xembed/src/lib.rs | 96 ++ 29 files changed, 4597 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 async-xcb/Cargo.toml create mode 100644 async-xcb/src/lib.rs create mode 100644 auth/Cargo.toml create mode 100644 auth/src/main.rs create mode 100644 command/Cargo.toml create mode 100644 command/src/lib.rs create mode 100644 command/src/main.rs create mode 100644 dbus-service/Cargo.toml create mode 100644 dbus-service/src/main.rs create mode 100644 dialog-gtk3/Cargo.toml create mode 100644 dialog-gtk3/src/main.rs create mode 100644 dialog-gtk4/Cargo.toml create mode 100644 dialog-gtk4/src/main.rs create mode 100644 dialog-iced/Cargo.toml create mode 100644 dialog-iced/src/main.rs create mode 100644 locker/Cargo.toml create mode 100644 locker/src/main.rs create mode 100644 rust-toolchain create mode 100644 systemd/Cargo.toml create mode 100644 systemd/src/main.rs create mode 100644 util/Cargo.toml create mode 100644 util/src/lib.rs create mode 100644 xcb-xembed/Cargo.toml create mode 100644 xcb-xembed/src/embedder.rs create mode 100644 xcb-xembed/src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81e2005 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.tags diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..36de0bd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1938 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-broadcast" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bbd92a9bd0e9c1298118ecf8a2f825e86b12c3ec9e411573e34aaf3a0c03cdd" +dependencies = [ + "easy-parallel", + "event-listener", + "futures-core", + "parking_lot", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c290043c9a95b05d45e952fb6383c67bcb61471f60cfa21e890dba6654234f43" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-mutex", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-std" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-xcb" +version = "0.1.0" +dependencies = [ + "async-io", + "async-std", + "futures", + "futures-lite", + "nix", + "xcb", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "bscreensaver" +version = "0.1.0" +dependencies = [ + "anyhow", + "bscreensaver-command", + "bscreensaver-util", + "clap", + "humantime", + "log", + "nix", + "toml", + "xcb", + "xcb-xembed", + "xdg", +] + +[[package]] +name = "bscreensaver-auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "pam", +] + +[[package]] +name = "bscreensaver-command" +version = "0.1.0" +dependencies = [ + "bscreensaver-util", + "clap", + "xcb", +] + +[[package]] +name = "bscreensaver-dbus-service" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "async-xcb", + "bscreensaver-command", + "bscreensaver-util", + "futures", + "log", + "xcb", + "zbus", +] + +[[package]] +name = "bscreensaver-dialog-gtk3" +version = "0.1.0" +dependencies = [ + "anyhow", + "bscreensaver-util", + "chrono", + "gdk-sys", + "gdkx11", + "gethostname", + "glib", + "gtk", + "gtk-sys", + "log", + "pam", +] + +[[package]] +name = "bscreensaver-systemd" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "async-xcb", + "bscreensaver-command", + "bscreensaver-util", + "futures", + "log", + "logind-zbus", + "nix", + "xcb", + "zbus", +] + +[[package]] +name = "bscreensaver-util" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "lazy_static", + "libc", + "xcb", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "cairo-rs" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62be3562254e90c1c6050a72aa638f6315593e98c5cdaba9017cedbabf0a5dee" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-expr" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e068cb2806bbc15b439846dc16c5f89f8599f2c3e4d73d4449d38f9b2f0b6c5" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535434c063ced786eb04aaf529308092c5ab60889e8fe24275d15de07b01fa97" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "ctor" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "easy-parallel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" + +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "field-offset" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62de46d9503381e4ab0b7d7a99b1fda53bd312e19ddc4195ffbe1d76f336cf9" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gio" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f132be35e05d9662b9fa0fee3f349c6621f7782e0105917f4cc73c1bf47eceb" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124026a2fa8c33a3d17a3fe59c103f2d9fa5bd92c19e029e037736729abeab" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gloo-timers" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", + "value-bag", +] + +[[package]] +name = "logind-zbus" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03958f20018a20963daf0c16ada4f271ae2da3e0017fb40caa8b0e3dc5b0226" +dependencies = [ + "serde", + "zbus", + "zvariant", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "ordered-stream" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" + +[[package]] +name = "pam" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2bdc959c201c047004a1420a92aaa1dd1a6b64d5ef333aa3a4ac764fb93097" +dependencies = [ + "libc", + "pam-sys", + "users", +] + +[[package]] +name = "pam-sys" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4858311a097f01a0006ef7d0cd50bca81ec430c949d7bf95cbefd202282434" +dependencies = [ + "libc", +] + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_repr" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "system-deps" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "uds_windows" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486992108df0fe0160680af1941fe856c521be931d5a5ecccefe0de86dc47e4a" +dependencies = [ + "tempdir", + "winapi", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "users" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fed7d0912567d35f88010c23dbaf865e9da8b5227295e8dc0f2fdd109155ab7" +dependencies = [ + "libc", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "version-compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x11" +version = "2.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "xcb" +version = "1.1.1" +source = "git+https://github.com/rust-x-bindings/rust-xcb?rev=d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e#d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" +dependencies = [ + "bitflags", + "libc", + "quick-xml", +] + +[[package]] +name = "xcb-xembed" +version = "0.1.0" +dependencies = [ + "bitflags", + "log", + "xcb", +] + +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + +[[package]] +name = "zbus" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53819092b9db813b2c6168b097b4b13ad284d81c9f2b0165a0a1b190e505a1f3" +dependencies = [ + "async-broadcast", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "lazy_static", + "nix", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7174ebe6722c280d6d132d694bb5664ce50a788cb70eeb518e7fc1ca095a114" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e18ba99d71e03af262953f476071607da0c44e225236cf9b5b9f7f11f1d0b6b0" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9042892ebdca35261951a83d17bcbfd4d3d528cb3bde828498a9b306b50d05c0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9dfeff5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + "async-xcb", + "auth", + "command", + "locker", + "dbus-service", + "dialog-gtk3", +# "dialog-gtk4", +# "dialog-iced", + "util", + "systemd", + "xcb-xembed", +] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2395d4f --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +.PHONY: all build build-dev install clean uninstall run + +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +LIBEXECDIR ?= $(PREFIX)/libexec + +HELPER_DIR = $(LIBEXECDIR)/bscreensaver +HELPERS = \ + bscreensaver-dbus-service \ + bscreensaver-systemd \ + bscreensaver-dialog-gtk3 \ + $(NULL) + +INSTALL ?= install + +DEV_LOG_LEVEL = debug + +all: build + +build: + HELPER_DIR=$(HELPER_DIR) cargo build --release + +build-dev: + HELPER_DIR=target/debug cargo build + +install: build + $(INSTALL) -m 0755 -d $(BINDIR) $(HELPER_DIR) + $(INSTALL) -m 0755 target/release/bscreensaver $(BINDIR) + $(INSTALL) -m 0755 $(addprefix target/release/,$(HELPERS)) $(HELPER_DIR) + +clean: + cargo clean + +uninstall: + rm -f $(BINDIR)/bscreensaver $(addprefix $(HELPER_DIR)/,$(HELPERS)) || true + rmdir -p $(BINDIR) $(HELPER_DIR) || true + rmdir -p $(PREFIX) || true + +run: build-dev + BSCREENSAVER_LOCAL_DEV=1 \ + RUST_BACKTRACE=1 \ + BSCREENSAVER_LOG=$(DEV_LOG_LEVEL) \ + BSCREENSAVER_DBUS_SERVICE_LOG=$(DEV_LOG_LEVEL) \ + BSCREENSAVER_SYSTEMD_LOG=$(DEV_LOG_LEVEL) \ + BSCREENSAVER_DIALOG_GTK3_LOG=$(DEV_LOG_LEVEL) \ + HELPER_DIR=target/debug \ + cargo run --bin bscreensaver diff --git a/async-xcb/Cargo.toml b/async-xcb/Cargo.toml new file mode 100644 index 0000000..ff7281a --- /dev/null +++ b/async-xcb/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "async-xcb" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-io = "1.6" +async-std = { version = "1.11", features = ["attributes"] } +futures = "0.3" +futures-lite = "1.12" +nix = "0.23" +# git source needed until extension event error resolution fix is released +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" } diff --git a/async-xcb/src/lib.rs b/async-xcb/src/lib.rs new file mode 100644 index 0000000..ae0b5a3 --- /dev/null +++ b/async-xcb/src/lib.rs @@ -0,0 +1,49 @@ +use async_io::{Async, ReadableOwned}; +use futures::prelude::*; +use futures_lite::ready; +use nix::{fcntl::{fcntl, F_GETFL, F_SETFL, OFlag}, unistd::read}; +use std::{io, os::unix::io::AsRawFd, pin::Pin, sync::Arc, task::{Context, Poll}}; + +pub struct AsyncConnection { + conn: Arc>, + readable: Option>, +} + +impl AsyncConnection { + pub fn new(conn: xcb::Connection) -> io::Result { + let flags = fcntl(conn.as_raw_fd(), F_GETFL)?; + fcntl(conn.as_raw_fd(), F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK))?; + Ok(Self { + conn: Arc::new(Async::new(conn)?), + readable: None, + }) + } +} + +impl AsyncRead for AsyncConnection { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { + loop { + match read(self.conn.as_raw_fd(), buf) { + Err(nix::Error::EAGAIN) => (), + Err(err) => { + self.readable = None; + return Poll::Ready(Err(err.into())); + }, + Ok(count) => { + self.readable = None; + return Poll::Ready(Ok(count)); + } + } + + if self.readable.is_none() { + self.readable = Some(Arc::clone(&self.conn).readable_owned()); + } + + if let Some(f) = &mut self.readable { + let res = ready!(Pin::new(f).poll(cx)); + self.readable = None; + res?; + } + } + } +} diff --git a/auth/Cargo.toml b/auth/Cargo.toml new file mode 100644 index 0000000..efbf0b0 --- /dev/null +++ b/auth/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "bscreensaver-auth" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +pam = "0.7" diff --git a/auth/src/main.rs b/auth/src/main.rs new file mode 100644 index 0000000..01b77bd --- /dev/null +++ b/auth/src/main.rs @@ -0,0 +1,16 @@ +use std::io; + +fn main() -> anyhow::Result<()> { + let stdin = io::stdin(); + + let mut username = String::new(); + stdin.read_line(&mut username)?; + let mut password = String::new(); + stdin.read_line(&mut password)?; + + let mut authenticator = pam::Authenticator::with_password("xscreensaver")?; + authenticator.get_handler().set_credentials(username.trim(), password.trim()); + authenticator.authenticate()?; + + Ok(()) +} diff --git a/command/Cargo.toml b/command/Cargo.toml new file mode 100644 index 0000000..7fc0325 --- /dev/null +++ b/command/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bscreensaver-command" +version = "0.1.0" +edition = "2021" + +[dependencies] +bscreensaver-util = { path = "../util" } +clap = "3" +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" } diff --git a/command/src/lib.rs b/command/src/lib.rs new file mode 100644 index 0000000..fb13abd --- /dev/null +++ b/command/src/lib.rs @@ -0,0 +1,184 @@ +use std::{error::Error as StdError, fmt}; +use xcb::{x, Xid}; + +use bscreensaver_util::{create_atom, BSCREENSAVER_WM_CLASS}; + +const COMMAND_WINDOW_ID_ATOM_NAME: &[u8] = b"BSCREENSAVER_COMMAND_WINDOW_ID"; +const COMMAND_WINDOW_WM_NAME: &[u8] = b"bscreensaver command window"; + +const BSCREENSAVER_BLANK_ATOM_NAME: &[u8] = b"BSCREENSAVER_BLANK"; +const BSCREENSAVER_LOCK_ATOM_NAME: &[u8] = b"BSCREENSAVER_LOCK"; +const BSCREENSAVER_DEACTIVATE_ATOM_NAME: &[u8] = b"BSCREENSAVER_DEACTIVATE"; +const BSCREENSAVER_RESTART_ATOM_NAME: &[u8] = b"BSCREENSAVER_RESTART"; +const BSCREENSAVER_EXIT_ATOM_NAME: &[u8] = b"BSCREENSAVER_EXIT"; + +#[derive(Debug, Clone, Copy)] +pub enum BCommand { + Blank, + Lock, + Deactivate, + Restart, + Exit, +} + +impl BCommand { + pub fn atom_name(&self) -> &'static [u8] { + match self { + Self::Blank => BSCREENSAVER_BLANK_ATOM_NAME, + Self::Lock => BSCREENSAVER_LOCK_ATOM_NAME, + Self::Deactivate => BSCREENSAVER_DEACTIVATE_ATOM_NAME, + Self::Restart => BSCREENSAVER_RESTART_ATOM_NAME, + Self::Exit => BSCREENSAVER_EXIT_ATOM_NAME, + } + } +} + +#[derive(Debug)] +pub enum Error { + X(xcb::Error), + NotRunning, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::X(err) => write!(f, "{}", err), + Self::NotRunning => write!(f, "bscreensaver is not running"), + } + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::X(err) => Some(err), + Self::NotRunning => None, + } + } +} + +impl From for Error { + fn from(error: xcb::Error) -> Self { + Self::X(error) + } +} + +impl From for Error { + fn from(error: xcb::ProtocolError) -> Self { + Self::X(xcb::Error::Protocol(error)) + } +} + +impl From for Error { + fn from(error: xcb::ConnError) -> Self { + Self::X(xcb::Error::Connection(error)) + } +} + +pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Result { + let mut cookies = Vec::new(); + + let msg_win: x::Window = conn.generate_id(); + cookies.push(conn.send_request_checked(&x::CreateWindow { + depth: x::COPY_FROM_PARENT as u8, + wid: msg_win, + parent: screen.root(), + x: -50, + y: -50, + width: 1, + height: 1, + border_width: 0, + class: x::WindowClass::InputOnly, + visual: x::COPY_FROM_PARENT, + value_list: &[ + x::Cw::OverrideRedirect(true), + x::Cw::EventMask(x::EventMask::STRUCTURE_NOTIFY | x::EventMask::PROPERTY_CHANGE), + ], + })); + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: msg_win, + property: x::ATOM_WM_NAME, + r#type: x::ATOM_STRING, + data: COMMAND_WINDOW_WM_NAME, + })); + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: msg_win, + property: x::ATOM_WM_CLASS, + r#type: x::ATOM_STRING, + data: BSCREENSAVER_WM_CLASS, + })); + + let msg_win_atom = create_atom(&conn, COMMAND_WINDOW_ID_ATOM_NAME)?; + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: screen.root(), + property: msg_win_atom, + r#type: x::ATOM_WINDOW, + data: &[msg_win.resource_id()], + })); + + for cookie in cookies { + conn.check_request(cookie)?; + } + + Ok(msg_win) +} + +pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> { + let (conn, _) = xcb::Connection::connect(None)?; + let setup = conn.get_setup(); + + let msg_window_id_atom = create_atom(&conn, COMMAND_WINDOW_ID_ATOM_NAME)?; + let msg_window_id = 'outer: loop { + for screen in setup.roots() { + let cookie = conn.send_request(&x::GetProperty { + delete: false, + window: screen.root(), + property: msg_window_id_atom, + r#type: x::ATOM_WINDOW, + long_offset: 0, + long_length: std::mem::size_of::() as u32, + }); + let reply = conn.wait_for_reply(cookie)?; + let windows = reply.value::(); + if windows.is_empty() { + continue; + } + let window = windows[0]; + + let cookie = conn.send_request(&x::GetProperty { + delete: false, + window, + property: x::ATOM_WM_NAME, + r#type: x::ATOM_STRING, + long_offset: 0, + long_length: COMMAND_WINDOW_WM_NAME.len() as u32 + 1, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.value::() == COMMAND_WINDOW_WM_NAME { + break 'outer Ok(window); + } + } + break Err(Error::NotRunning); + }?; + let command_atom = create_atom(&conn, command.atom_name())?; + + let res = conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(msg_window_id), + event_mask: x::EventMask::STRUCTURE_NOTIFY, + event: &x::ClientMessageEvent::new( + msg_window_id, + command_atom, + x::ClientMessageData::Data32([0; 5]) + ), + }); + + match res { + Err(xcb::ProtocolError::X(x::Error::Window(x::WindowError { .. }), _)) => Err(Error::NotRunning), + Err(err) => Err(Error::X(xcb::Error::Protocol(err))), + Ok(_) => Ok(()), + } +} diff --git a/command/src/main.rs b/command/src/main.rs new file mode 100644 index 0000000..dd824ef --- /dev/null +++ b/command/src/main.rs @@ -0,0 +1,72 @@ +use clap::{Arg, Command}; +use std::{env, io, process::exit}; + +use bscreensaver_command::{BCommand, Error, bscreensaver_command}; + +fn main() -> io::Result<()> { + let mut command = Command::new("bscreensaver-command") + .author(env!("CARGO_PKG_AUTHORS")) + .version(env!("CARGO_PKG_VERSION")) + .about("Send commands to the running bscreensaver instance") + .arg( + Arg::new("blank") + .long("blank") + .short('b') + .help("Blanks the screen right now") + ) + .arg( + Arg::new("lock") + .long("lock") + .short('l') + .help("Lock the screen right now") + ) + .arg( + Arg::new("deactivate") + .long("deactivate") + .short('d') + .help("Deactivates the screen lock, presenting the unlock dialog if needed. This can be used to 'reset' things so the screensaver thinks there has been user input") + ) + .arg( + Arg::new("restart") + .long("restart") + .short('r') + .help("Restarts the bscreensaver daemon") + ) + .arg( + Arg::new("exit") + .long("exit") + .short('x') + .help("Causes the bscreensaver daemon to exit now, even if the screen is locked") + ); + let args = command.get_matches_mut(); + + let command = + if args.is_present("blank") { + BCommand::Blank + } else if args.is_present("lock") { + BCommand::Lock + } else if args.is_present("deactivate") { + BCommand::Deactivate + } else if args.is_present("restart") { + BCommand::Restart + } else if args.is_present("exit") { + BCommand::Exit + } else { + command.print_help()?; + exit(1); + }; + + match bscreensaver_command(command) { + Err(Error::NotRunning) => { + eprintln!("bscreensaver is not running"); + exit(1); + }, + Err(Error::X(err)) => { + eprintln!("Failed to communicate with X server: {}", err); + exit(1); + }, + Ok(_) => (), + } + + Ok(()) +} diff --git a/dbus-service/Cargo.toml b/dbus-service/Cargo.toml new file mode 100644 index 0000000..60680b1 --- /dev/null +++ b/dbus-service/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bscreensaver-dbus-service" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +async-std = { version = "1.11", features = ["attributes"] } +async-xcb = { path = "../async-xcb" } +bscreensaver-command = { path = "../command" } +bscreensaver-util = { path = "../util" } +futures = "0.3" +log = "0.4" +# git source needed until extension event error resolution fix is released +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" } +zbus = "2" diff --git a/dbus-service/src/main.rs b/dbus-service/src/main.rs new file mode 100644 index 0000000..e6fd3c3 --- /dev/null +++ b/dbus-service/src/main.rs @@ -0,0 +1,235 @@ +#![feature(option_result_contains)] +#![feature(is_some_with)] + +use async_std::{fs::File, prelude::*, sync::{Arc, Mutex}, task}; +use bscreensaver_util::init_logging; +use futures::{future::FutureExt, pin_mut, select}; +use log::{debug, error, info, trace, warn}; +use std::{io, process::exit, time::{Duration, Instant}}; +use zbus::{dbus_interface, fdo::{self, DBusProxy, RequestNameFlags}, names::{BusName, UniqueName, WellKnownName}, ConnectionBuilder, MessageHeader}; + +use bscreensaver_command::{bscreensaver_command, BCommand}; + +const OUR_DBUS_NAME: &str = "org.freedesktop.ScreenSaver"; +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(45); + +struct Inhibitor { + cookie: u32, + app_name: String, + peer: Option>, +} + +struct State { + inhibitors: Vec, +} + +struct ScreenSaver { + state: Arc>, +} + +#[dbus_interface(name = "org.freedesktop.ScreenSaver")] +impl ScreenSaver { + async fn inhibit( + &mut self, + #[zbus(header)] + hdr: MessageHeader<'_>, + app_name: &str, + reason: &str + ) -> fdo::Result { + debug!("Handling inhibit for app {}: {}", app_name, reason); + if app_name.trim().is_empty() { + return Err(fdo::Error::InvalidArgs("Application name is blank".to_string())); + } else if reason.trim().is_empty() { + return Err(fdo::Error::InvalidArgs("Reason is blank".to_string())); + } + + // Firefox tries to inhibit when only audio is playing, so ignore that + if reason.contains("audio") && !reason.contains("video") { + info!("Ignoring audio-only inhibit from app {}", app_name); + return Ok(0); + } + + let peer = hdr.sender()?; + let cookie = rand_u32().await + .map_err(|err| fdo::Error::IOError(err.to_string()))?; + self.state.lock().await.inhibitors.push(Inhibitor { + cookie, + app_name: app_name.to_string(), + peer: peer.map(|s| s.to_owned()), + }); + + Ok(cookie) + } + + async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> { + let mut state = self.state.lock().await; + + let before = state.inhibitors.len(); + state.inhibitors.retain(|inhibitor| { + if inhibitor.cookie == cookie { + info!("Uninhibit received from {} for cookie {}", inhibitor.app_name, cookie); + false + } else { + true + } + }); + if before == state.inhibitors.len() { + info!("No inhibitor found with cookie {}", cookie); + } + + Ok(()) + } +} + +#[async_std::main] +async fn main() { + init_logging("BSCREENSAVER_DBUS_SERVICE_LOG"); + + let state = Arc::new(Mutex::new(State { + inhibitors: Vec::new(), + })); + + let xcb_handle = task::spawn(xcb_task()).fuse(); + let dbus_handle = task::spawn(dbus_task(Arc::clone(&state))).fuse(); + let heartbeat_handle = task::spawn(heartbeat_task(Arc::clone(&state))).fuse(); + + pin_mut!(xcb_handle, dbus_handle, heartbeat_handle); + + let res = loop { + select! { + _ = xcb_handle => { + info!("Lost connection to X server; quitting"); + break Ok(()); + }, + res = dbus_handle => { + match res { + Err(err) => error!("Lost connection to the system bus: {}", err), + Ok(_) => error!("DBus task exited normally; this should not happen!"), + } + break Err(()); + }, + res = heartbeat_handle => { + match res { + Err(err) => error!("Heartbeat task terminated with error: {}", err), + Ok(_) => error!("Heartbeat task exited normally; this should not happen!"), + } + break Err(()); + } + }; + }; + + if let Err(_) = res { + exit(1); + } +} + +async fn xcb_task() -> anyhow::Result<()> { + let (xcb_conn, _) = task::block_on(async { xcb::Connection::connect(None) })?; + let mut xcb_conn = async_xcb::AsyncConnection::new(xcb_conn)?; + + // We need to drain the XCB connection periodically. Even though we have not + // asked for any events, we'll still get stuff like MappingNotify if the keyboard + // settings change. + loop { + let mut buf = [0u8; 512]; + xcb_conn.read(&mut buf).await?; + } +} + +async fn dbus_task(state: Arc>) -> anyhow::Result<()> { + let org_fdo_screensaver = ScreenSaver { state: Arc::clone(&state) }; + let screensaver = ScreenSaver { state: Arc::clone(&state) }; + + let dbus_conn = ConnectionBuilder::session()? + .serve_at("/org/freedesktop/ScreenSaver", org_fdo_screensaver)? + .serve_at("/ScreenSaver", screensaver)? + .build() + .await?; + + let our_unique_name = dbus_conn.unique_name().unwrap(); + + let dbus_proxy = DBusProxy::new(&dbus_conn).await?; + dbus_proxy.request_name( + WellKnownName::from_static_str(OUR_DBUS_NAME)?, + RequestNameFlags::AllowReplacement | RequestNameFlags::ReplaceExisting | RequestNameFlags::DoNotQueue + ).await?; + let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?; + + loop { + if let Some(name_owner_changed) = name_owner_changed_stream.next().await { + let args = name_owner_changed.args()?; + match args.name() { + BusName::WellKnown(name) if name == OUR_DBUS_NAME => { + if args.new_owner().is_none() || args.new_owner().is_some_and(|no| no != our_unique_name) { + info!("Lost bus name {}; quitting", OUR_DBUS_NAME); + exit(0); + } + }, + BusName::Unique(name) => { + if args.new_owner().is_none() { + state.lock().await.inhibitors.retain(|inhibitor| { + if inhibitor.peer.contains(name) { + info!("Canceling inhibit from {}, as the client has disappeared", inhibitor.app_name); + false + } else { + true + } + }); + } + }, + _ => (), + } + } + } +} + +async fn heartbeat_task(state_mtx: Arc>) -> anyhow::Result<()> { + let mut last_heartbeat: Option = None; + + loop { + let state = state_mtx.lock().await; + let next_heartbeat = + if state.inhibitors.is_empty() { + HEARTBEAT_INTERVAL + } else { + if let Some(lh) = last_heartbeat { + let since_last = Instant::now().duration_since(lh); + if since_last < HEARTBEAT_INTERVAL { + HEARTBEAT_INTERVAL - since_last + } else { + Duration::ZERO + } + } else { + Duration::ZERO + } + }; + drop(state); + + task::sleep(next_heartbeat).await; + debug!("Heartbeat timeout expired"); + + let state = state_mtx.lock().await; + if !state.inhibitors.is_empty() && (last_heartbeat.is_none() || last_heartbeat.as_ref().filter(|lh| lh.elapsed() < HEARTBEAT_INTERVAL).is_none()) { + trace!("About to deactivate; active inhibitors:"); + for inhibitor in &state.inhibitors { + trace!(" {}: {}", inhibitor.cookie, inhibitor.app_name); + } + drop(state); + task::block_on(async { + if let Err(err) = bscreensaver_command(BCommand::Deactivate) { + warn!("Failed to deactivate screen lock: {}", err); + } else { + debug!("Successfully issued deactivate heartbeat"); + last_heartbeat = Some(Instant::now()); + } + }); + } + } +} + +async fn rand_u32() -> io::Result { + let mut f = File::open("/dev/urandom").await?; + let mut buf = [0u8; 4]; + f.read_exact(&mut buf).await?; + Ok(((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | (buf[3] as u32)) +} diff --git a/dialog-gtk3/Cargo.toml b/dialog-gtk3/Cargo.toml new file mode 100644 index 0000000..cfbaf6b --- /dev/null +++ b/dialog-gtk3/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bscreensaver-dialog-gtk3" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +bscreensaver-util = { path = "../util" } +chrono = "0.4" +gethostname = "0.2" +glib = { version = "0.15", features = ["v2_68"] } +gtk = { version = "0.15", features = ["v3_24"] } +gtk-sys = "0.15" +gdk-sys = "0.15" +gdkx11 = "0.15" +log = "0.4" +pam = "0.7" +#x11 = "2.19" diff --git a/dialog-gtk3/src/main.rs b/dialog-gtk3/src/main.rs new file mode 100644 index 0000000..935fc49 --- /dev/null +++ b/dialog-gtk3/src/main.rs @@ -0,0 +1,261 @@ +use chrono::prelude::*; +use gdkx11::X11Window; +use gethostname::gethostname; +use glib::GString; +use gtk::{prelude::*, Button, Entry, Label, Plug, Window}; +use log::{debug, error}; +use std::{io::{self, Write}, process::exit, thread}; + +use bscreensaver_util::init_logging; + +fn main() -> anyhow::Result<()> { + init_logging("BSCREENSAVER_DIALOG_GTK3_LOG"); + + let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok(); + + unsafe { glib::log_writer_default_set_use_stderr(true) }; + gtk::init()?; + + let top_sg = gtk::SizeGroup::builder() + .mode(gtk::SizeGroupMode::Horizontal) + .build(); + let label_sg = gtk::SizeGroup::builder() + .mode(gtk::SizeGroupMode::Horizontal) + .build(); + let entry_sg = gtk::SizeGroup::builder() + .mode(gtk::SizeGroupMode::Horizontal) + .build(); + + let header = gtk::HeaderBar::builder() + .title("Unlock Screen") + .show_close_button(true) + .build(); + + let top_vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(0) + .build(); + + let dialog = + if standalone { + let win = Window::builder() + .type_(gtk::WindowType::Toplevel) + .title("Unlock Screen") + .modal(true) + .build(); + win.set_titlebar(Some(&header)); + win.upcast::() + } else { + let plug = Plug::builder() + .type_(gtk::WindowType::Toplevel) + .title("Unlock Screen") + .modal(true) + .build(); + plug.connect_embedded(|_| debug!("DIALOG EMBEDDED")); + plug.connect_embedded_notify(|_| debug!("DIALOG EMBEDDED (notify)")); + plug.connect_realize(|plug| { + let plug_window = match plug.window().unwrap().downcast::() { + Err(err) => { + error!("Failed to find XID of unlock dialog window: {}", err); + exit(2); + }, + Ok(w) => w, + }; + let xid = plug_window.xid() as u32; + let xid_buf: [u8; 4] = [ + ((xid >> 24) & 0xff) as u8, + ((xid >> 16) & 0xff) as u8, + ((xid >> 8) & 0xff) as u8, + (xid & 0xff) as u8, + ]; + let out = io::stdout(); + let mut out_locked = out.lock(); + if let Err(err) = out_locked.write_all(&xid_buf).and_then(|_| out_locked.flush()) { + error!("Failed to write XID to stdout: {}", err); + exit(2); + }; + }); + + // Walking the header's widget tree, finding the close button, and connecting + // to the 'clicked' signal strangely doesn't work either, so let's just + // disable it for now. + header.set_show_close_button(false); + top_vbox.pack_start(&header, true, false, 0); + + plug.upcast::() + }; + + // I don't know why, but this doesn't work when we're a GktPlug, despite + // an examination of the gtk source suggesting that the header should send + // a delete-event to the toplevel (which should be the plug) when the close + // button is clicked. For some reason, though, we never get the delete-event. + dialog.connect_delete_event(|_, _| exit(1)); + dialog.connect_realize(|_| debug!("DIALOG REALIZED")); + dialog.connect_map(|_| debug!("DIALOG MAPPED")); + dialog.connect_unmap(|_| debug!("DIALOG UNMAPPED")); + dialog.connect_unrealize(|_| debug!("DIALOG_UNREALIZED")); + dialog.add(&top_vbox); + + let top_hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .border_width(48) + .spacing(8) + .build(); + top_vbox.pack_start(&top_hbox, true, true, 0); + + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(4) + .build(); + top_sg.add_widget(&vbox); + top_hbox.pack_start(&vbox, true, true, 0); + + let attrs = gtk::pango::AttrList::new(); + attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_XX_LARGE)); + let mut bold_desc = gtk::pango::FontDescription::new(); + bold_desc.set_weight(gtk::pango::Weight::Bold); + attrs.insert(gtk::pango::AttrFontDesc::new(&bold_desc)); + let label = gtk::Label::builder() + .label(gethostname().to_str().unwrap_or("(unknown hostname)")) + .xalign(0.5) + .yalign(0.5) + .attributes(&attrs) + .build(); + vbox.pack_start(&label, false, false, 0); + + let attrs = gtk::pango::AttrList::new(); + attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_LARGE)); + let label = gtk::Label::builder() + .xalign(0.5) + .yalign(0.5) + .attributes(&attrs) + .build(); + set_time_label(&label); + vbox.pack_start(&label, false, false, 0); + glib::timeout_add_seconds_local(1, move || { + set_time_label(&label); + glib::source::Continue(true) + }); + + let sep = gtk::Separator::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + top_hbox.pack_start(&sep, true, false, 0); + + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(4) + .build(); + top_sg.add_widget(&vbox); + top_hbox.pack_start(&vbox, true, true, 0); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + vbox.pack_start(&hbox, true, true, 2); + + let label = Label::builder() + .label("Username:") + .xalign(0.0) + .build(); + label_sg.add_widget(&label); + hbox.pack_start(&label, false, true, 8); + + let username = bscreensaver_util::get_username()?; + let username_box = Entry::builder() + .text(&username) + .sensitive(false) + .build(); + entry_sg.add_widget(&username_box); + hbox.pack_start(&username_box, true, true, 8); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + vbox.pack_start(&hbox, true, true, 0); + + let label = Label::builder() + .label("Password:") + .xalign(0.0) + .build(); + label_sg.add_widget(&label); + hbox.pack_start(&label, false, true, 8); + + let password_box = Entry::builder() + .visibility(false) + .input_purpose(gtk::InputPurpose::Password) + .activates_default(true) + .width_chars(25) + .build(); + entry_sg.add_widget(&password_box); + hbox.pack_start(&password_box, true, true, 8); + password_box.connect_key_press_event(|_, ev| { + if ev.keyval().name() == Some(GString::from("Escape")) { + exit(1); + } + gtk::Inhibit(false) + }); + password_box.grab_focus(); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + vbox.pack_start(&hbox, true, true, 2); + + let button = Button::builder() + .label("Unlock") + .build(); + button.connect_clicked(move |button| { + button.set_sensitive(false); + password_box.set_sensitive(false); + + let username = username.clone(); + let password = password_box.text().to_string(); + + thread::spawn(move || { + if authenticate(&username, &password) { + exit(0); + } else { + exit(-1); + } + }); + }); + hbox.pack_end(&button, false, true, 8); + button.set_can_default(true); + button.set_has_default(true); + + dialog.show_all(); + + gtk::main(); + + Ok(()) +} + +fn set_time_label(label: >k::Label) { + let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + label.set_label(&now); +} + +fn authenticate(username: &String, password: &String) -> bool { + let mut authenticator = match pam::Authenticator::with_password("xscreensaver") { + Err(err) => { + error!("[PAM] {}", err); + return false; + }, + Ok(authenticator) => authenticator, + }; + authenticator.get_handler().set_credentials(username, password); + if let Err(err) = authenticator.authenticate() { + error!("[PAM] {}", err); + return false; + } + if let Err(err) = authenticator.open_session() { + error!("[PAM] {}", err); + false + } else { + true + } +} diff --git a/dialog-gtk4/Cargo.toml b/dialog-gtk4/Cargo.toml new file mode 100644 index 0000000..3670e90 --- /dev/null +++ b/dialog-gtk4/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bscreensaver-dialog-gtk4" +version = "0.1.0" +edition = "2021" + +[dependencies] +gtk = { version = "0.4", package = "gtk4", features = ["v4_6"]} +gdk-x11 = { version = "0.4", package = "gdk4-x11", features = ["v4_4", "xlib"]} +x11 = "2.19" diff --git a/dialog-gtk4/src/main.rs b/dialog-gtk4/src/main.rs new file mode 100644 index 0000000..3933f19 --- /dev/null +++ b/dialog-gtk4/src/main.rs @@ -0,0 +1,43 @@ +use gdk_x11::{X11Surface, X11Display}; +use gtk::{prelude::*, Application, ApplicationWindow, Label}; +use std::process::exit; + +fn main() { + let app = Application::builder() + .application_id("org.spurint.bscreensaver.dialog-gtk4") + .build(); + app.connect_activate(build_ui); + + app.run(); +} + +fn build_ui(app: &Application) { + let titlebar = Label::builder() + .label("Unlock Screen") + .halign(gtk::Align::Center) + .single_line_mode(true) + .build(); + titlebar.show(); + + let window = ApplicationWindow::builder() + .application(app) + .titlebar(&titlebar) + .modal(true) + .decorated(false) + .build(); + window.realize(); + + let surface = unsafe { window.surface().unsafe_cast::() }; + let xid = surface.xid(); + if xid == 0 { + eprintln!("Lock dialog has no XID"); + exit(1); + } + println!("{}", surface.xid()); + + let mut buf = String::new(); + let stdin = std::io::stdin(); + stdin.read_line(&mut buf).unwrap(); + + window.present(); +} diff --git a/dialog-iced/Cargo.toml b/dialog-iced/Cargo.toml new file mode 100644 index 0000000..89a1993 --- /dev/null +++ b/dialog-iced/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "bscreensaver-dialog-iced" +version = "0.1.0" +edition = "2021" + +[dependencies] +bscreensaver-util = { path = "../util" } +xcb = { version = "1.1", features = ["randr"] } diff --git a/dialog-iced/src/main.rs b/dialog-iced/src/main.rs new file mode 100644 index 0000000..e69de29 diff --git a/locker/Cargo.toml b/locker/Cargo.toml new file mode 100644 index 0000000..72b4fce --- /dev/null +++ b/locker/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bscreensaver" +version = "0.1.0" +authors = [ + "Brian Tarricone ", +] +edition = "2021" +description = "Secure, simple X11 screen locker" +license = "GPL-3.0" +repository = "https://github.com/kelnos/bscreensaver" +readme = "README.md" +keywords = ["gui", "screensaver", "screen-locker"] +categories = ["gui"] + +[dependencies] +anyhow = "1" +clap = "3" +bscreensaver-command = { path = "../command" } +bscreensaver-util = { path = "../util" } +humantime = "2" +log = "0.4" +nix = "0.23" +toml = "0.5" +# git source needed until extension event error resolution fix is released +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "xfixes", "xinput"] } +xcb-xembed = { path = "../xcb-xembed" } +xdg = "2" diff --git a/locker/src/main.rs b/locker/src/main.rs new file mode 100644 index 0000000..596bd61 --- /dev/null +++ b/locker/src/main.rs @@ -0,0 +1,992 @@ +#![feature(linux_pidfd)] +#![feature(option_result_contains)] + +use anyhow::anyhow; +use clap::{Arg, Command as ClapCommand}; +use log::{debug, error, info, trace, warn}; +use nix::{ + poll::{poll, PollFd, PollFlags}, + unistd::{execv, fork, setsid, ForkResult}, + sys::{ + signal::{sigprocmask, SigSet, SigmaskHow, Signal}, + signalfd::{SignalFd, SfdFlags}, + }, +}; +use std::{ + env, + ffi::CString, + fs::{read_link, File}, + io::{self, Read}, + os::{ + linux::process::{ChildExt, CommandExt, PidFd}, + unix::io::AsRawFd, + unix::process::ExitStatusExt, + }, + process::{exit, Child, Command, Stdio}, + time::{Duration, Instant}, +}; +use xcb::{randr, x, xinput, Xid}; +use xcb_xembed::embedder::Embedder; + +use bscreensaver_command::{BCommand, create_command_window}; +use bscreensaver_util::*; + +const BLANKED_ARG: &str = "blanked"; +const LOCKED_ARG: &str = "locked"; + +#[derive(Debug, Clone, Copy)] +enum DialogBackend { + Gtk3, +} + +impl DialogBackend { + pub fn binary_name(&self) -> &str { + match self { + Self::Gtk3 => "bscreensaver-dialog-gtk3", + } + } +} + +impl TryFrom<&str> for DialogBackend { + type Error = anyhow::Error; + fn try_from(value: &str) -> Result { + match value { + "gtk3" => Ok(Self::Gtk3), + other => Err(anyhow!("'{}' is not a valid dialog backend (valid: 'gtk3')", other)), + } + } +} + +struct Configuration { + lock_timeout: Duration, + blank_before_locking: Duration, + dialog_backend: DialogBackend, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + lock_timeout: Duration::from_secs(60 * 10), + blank_before_locking: Duration::ZERO, + dialog_backend: DialogBackend::Gtk3, + } + } +} + +#[derive(Clone, Copy)] +struct Monitor { + pub root: x::Window, + pub black_gc: x::Gcontext, + pub blanker_window: x::Window, + pub unlock_window: x::Window, + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +enum BlankerState { + Idle = 0, + Blanked = 1, + Locked = 2, +} + +struct UnlockDialog<'a> { + monitor: Monitor, + embedder: Embedder<'a>, + event_to_forward: Option, + child: Child, + child_pidfd: PidFd, +} + +struct State<'a> { + config: Configuration, + monitors: Vec, + dbus_service: Option<(Child, PidFd)>, + systemd_service: Option<(Child, PidFd)>, + last_user_activity: Instant, + blanker_state: BlankerState, + unlock_dialog: Option>, +} + +struct CommandAtoms { + blank: x::Atom, + lock: x::Atom, + deactivate: x::Atom, + restart: x::Atom, + exit: x::Atom, +} + +macro_rules! maybe_add_fd { + ($pfds:expr, $fd:expr) => { + if let Some(fd) = $fd { + $pfds.push(PollFd::new(fd, PollFlags::POLLIN)); + Some(fd) + } else { + None + } + }; +} + +fn main() -> anyhow::Result<()> { + init_logging("BSCREENSAVER_LOG"); + + let config = parse_config()?; + + let args = ClapCommand::new("Blanks and locks the screen after a period of time") + .author(env!("CARGO_PKG_AUTHORS")) + .version(env!("CARGO_PKG_VERSION")) + .arg( + Arg::new("blanked") + .long(BLANKED_ARG) + .help("Starts up in the blanked state") + ) + .arg( + Arg::new("locked") + .long(LOCKED_ARG) + .help("Starts up in the blanked and locked state") + ) + .get_matches(); + + let mut signal_fd = init_signals()?; + + let (conn, screen_num) = xcb::Connection::connect_with_extensions( + None, + &[xcb::Extension::RandR, xcb::Extension::XFixes, xcb::Extension::Input], + &[] + )?; + let setup = conn.get_setup(); + let screen = setup.roots().nth(screen_num as usize).unwrap(); + + init_xinput(&conn)?; + init_randr(&conn)?; + create_command_window(&conn, &screen)?; + + let command_atoms = CommandAtoms { + blank: create_atom(&conn, BCommand::Blank.atom_name())?, + lock: create_atom(&conn, BCommand::Lock.atom_name())?, + deactivate: create_atom(&conn, BCommand::Deactivate.atom_name())?, + restart: create_atom(&conn, BCommand::Restart.atom_name())?, + exit: create_atom(&conn, BCommand::Exit.atom_name())?, + }; + + let mut state = State { + config, + monitors: create_blanker_windows(&conn)?, + dbus_service: None, + systemd_service: None, + last_user_activity: Instant::now(), + blanker_state: BlankerState::Idle, + unlock_dialog: None, + }; + + start_dbus_service(&mut state)?; + start_systemd_service(&mut state)?; + + if args.is_present(LOCKED_ARG) { + match lock_screen(&conn, &mut state) { + Err(err) => error!("POSSIBLY FAILED TO LOCK SCREEN ON STARTUP: {}", err), + Ok(_) => debug!("Got --{} arg; screen locked on startup", LOCKED_ARG), + } + } else if args.is_present(BLANKED_ARG) { + match lock_screen(&conn, &mut state) { + Err(err) => warn!("Possibly failed to blank screen on startup: {}", err), + Ok(_) => debug!("Got --{} arg; screen locked on startup", BLANKED_ARG), + } + } + + let _ = conn.send_and_check_request(&x::SetScreenSaver { + timeout: 0, + interval: 0, + prefer_blanking: x::Blanking::NotPreferred, + allow_exposures: x::Exposures::NotAllowed, + }); + + loop { + if let Err(err) = handle_xcb_events(&conn, &mut state, &command_atoms) { + if conn.has_error().is_err() { + error!("Lost connection to X server; attempting to restart"); + restart_daemon(&mut state)?; + } + warn!("Error handling event: {}", err); + } + + let mut pfds = Vec::new(); + pfds.push(PollFd::new(signal_fd.as_raw_fd(), PollFlags::POLLIN)); + pfds.push(PollFd::new(conn.as_raw_fd(), PollFlags::POLLIN)); + let dbus_service_fd = maybe_add_fd!(&mut pfds, state.dbus_service.as_ref().map(|ds| ds.1.as_raw_fd())); + let systemd_service_fd = maybe_add_fd!(&mut pfds, state.systemd_service.as_ref().map(|ds| ds.1.as_raw_fd())); + let dialog_fd = maybe_add_fd!(&mut pfds, state.unlock_dialog.as_ref().map(|ud| ud.child_pidfd.as_raw_fd())); + + let since_last_activity = Instant::now().duration_since(state.last_user_activity); + let poll_timeout = match state.blanker_state { + BlankerState::Idle if since_last_activity > state.config.lock_timeout - state.config.blank_before_locking => Some(Duration::ZERO), + BlankerState::Idle => Some(state.config.lock_timeout - state.config.blank_before_locking - since_last_activity), + BlankerState::Blanked if since_last_activity > state.config.lock_timeout => Some(Duration::ZERO), + BlankerState::Blanked => Some(state.config.lock_timeout - since_last_activity), + BlankerState::Locked => None, + }; + let poll_timeout = poll_timeout.map(|pt| if pt.as_millis() > i32::MAX as u128 { + i32::MAX + } else { + pt.as_millis() as i32 + }).unwrap_or(-1); + + trace!("about to poll (timeout={})", poll_timeout); + let nready = poll(pfds.as_mut_slice(), poll_timeout)?; // FIXME: maybe shouldn't quit here on errors if screen is locked + trace!("polled; {} FD ready", nready); + if nready > 0 { + for pfd in pfds { + if pfd.revents().filter(|pf| pf.contains(PollFlags::POLLIN)).is_some() { + let result = match pfd.as_raw_fd() { + fd if fd == signal_fd.as_raw_fd() => handle_signals(&mut state, &mut signal_fd), + fd if fd == conn.as_raw_fd() => handle_xcb_events(&conn, &mut state, &command_atoms), + fd if dbus_service_fd.contains(&fd) => handle_subservice_quit(state.dbus_service.take(), "DBus", || start_dbus_service(&mut state)), + fd if systemd_service_fd.contains(&fd) => handle_subservice_quit(state.systemd_service.take(), "systemd", || start_systemd_service(&mut state)), + fd if dialog_fd.contains(&fd) => handle_unlock_dialog_quit(&conn, &mut state), + _ => Ok(()), + }; + + if let Err(err) = result { + if conn.has_error().is_err() { + error!("Lost connection to X server; atempting to restart"); + restart_daemon(&mut state)?; + } + warn!("Error handling event: {}", err); + } + } + } + } + + let since_last_activity = Instant::now().duration_since(state.last_user_activity); + + if state.blanker_state < BlankerState::Blanked && since_last_activity > state.config.lock_timeout - state.config.blank_before_locking { + if let Err(err) = blank_screen(&conn, &mut state) { + error!("POSSIBLY FAILED TO BLANK SCREEN: {}", err); + } + } + + if state.blanker_state < BlankerState::Locked && since_last_activity > state.config.lock_timeout { + if let Err(err) = lock_screen(&conn, &mut state) { + error!("POSSIBLY FAILED TO LOCK SCREEN: {}", err); + } + } + } +} + +fn parse_config() -> anyhow::Result { + use humantime::parse_duration; + use toml::Value; + + match xdg::BaseDirectories::new()?.find_config_file("bscreensaver.toml") { + None => Ok(Configuration::default()), + Some(config_path) => { + let mut f = File::open(config_path)?; + let mut config = String::new(); + f.read_to_string(&mut config)?; + drop(f); + + let config_toml = config.parse::()?; + let mut config = Configuration::default(); + + config.lock_timeout = match config_toml.get("lock-timeout") { + None => config.lock_timeout, + Some(val) => parse_duration(val.as_str().ok_or(anyhow!("'lock-timeout' must be a duration string like '10m' or '90s'"))?)?, + }; + config.blank_before_locking = match config_toml.get("blank-before-locking") { + None => config.blank_before_locking, + Some(val) => parse_duration(val.as_str().ok_or(anyhow!("'blank-before-locking' must be a duration string like '10m' or '90s'"))?)?, + }; + config.dialog_backend = match config_toml.get("dialog-backend") { + None => config.dialog_backend, + Some(val) => DialogBackend::try_from(val.as_str().ok_or(anyhow!("'dialog-backend' must be a string"))?)?, + }; + + if config.blank_before_locking >= config.lock_timeout { + Err(anyhow!("'blank-before-locking' cannot be greater than 'lock-timeout'")) + } else { + Ok(config) + } + }, + } +} + +fn init_signals() -> anyhow::Result { + let sigs = { + let mut s = SigSet::empty(); + s.add(Signal::SIGHUP); + s.add(Signal::SIGINT); + s.add(Signal::SIGQUIT); + s.add(Signal::SIGTERM); + s + }; + sigprocmask(SigmaskHow::SIG_BLOCK, Some(&sigs), None)?; + let flags = SfdFlags::SFD_NONBLOCK | SfdFlags::SFD_CLOEXEC; + let fd = SignalFd::with_flags(&sigs, flags)?; + Ok(fd) +} + +fn init_xinput(conn: &xcb::Connection) -> xcb::Result<()> { + let cookie = conn.send_request(&xcb::xinput::XiQueryVersion { + major_version: 2, + minor_version: 2, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.major_version() < 2 { + error!("Version 2 or greater of the Xinput extension is required (got {}.{})", reply.major_version(), reply.minor_version()); + exit(1); + } + + let mut cookies = Vec::new(); + for screen in conn.get_setup().roots() { + cookies.push(conn.send_request_checked(&xinput::XiSelectEvents { + window: screen.root(), + masks: &[ + xinput::EventMaskBuf::new( + xinput::Device::AllMaster, + &[ + xinput::XiEventMask::RAW_KEY_PRESS | + xinput::XiEventMask::RAW_KEY_RELEASE | + xinput::XiEventMask::RAW_BUTTON_PRESS | + xinput::XiEventMask::RAW_BUTTON_RELEASE | + xinput::XiEventMask::RAW_TOUCH_BEGIN | + xinput::XiEventMask::RAW_TOUCH_UPDATE | + xinput::XiEventMask::RAW_TOUCH_END | + xinput::XiEventMask::RAW_MOTION + ] + ) + ], + })); + } + for cookie in cookies { + conn.check_request(cookie)?; + } + + Ok(()) +} + +fn init_randr(conn: &xcb::Connection) -> xcb::Result<()> { + for screen in conn.get_setup().roots() { + conn.send_and_check_request(&randr::SelectInput { + window: screen.root(), + enable: randr::NotifyMask::SCREEN_CHANGE | randr::NotifyMask::CRTC_CHANGE | randr::NotifyMask::OUTPUT_CHANGE, + })?; + } + Ok(()) +} + +fn create_blanker_windows(conn: &xcb::Connection) -> xcb::Result> { + let mut cookies = Vec::new(); + let mut monitors = Vec::new(); + for screen in conn.get_setup().roots() { + let cookie = conn.send_request(&randr::GetScreenResources { + window: screen.root(), + }); + let reply = conn.wait_for_reply(cookie)?; + let config_timestamp = reply.config_timestamp(); + for output in reply.outputs() { + let cookie = conn.send_request(&randr::GetOutputInfo { + output: *output, + config_timestamp, + }); + let reply = conn.wait_for_reply(cookie)?; + if !reply.crtc().is_none() { + let cookie = conn.send_request(&randr::GetCrtcInfo { + crtc: reply.crtc(), + config_timestamp, + }); + let reply = conn.wait_for_reply(cookie)?; + + let blanker_window: x::Window = conn.generate_id(); + let unlock_window: x::Window = conn.generate_id(); + + debug!("creating blanker window 0x{:x}, {}x{}+{}+{}; unlock window 0x{:x}", blanker_window.resource_id(), reply.width(), reply.height(), reply.x(), reply.y(), unlock_window.resource_id()); + cookies.push(conn.send_request_checked(&x::CreateWindow { + depth: x::COPY_FROM_PARENT as u8, + wid: blanker_window, + parent: screen.root(), + x: reply.x(), + y: reply.y(), + width: reply.width(), + height: reply.height(), + border_width: 0, + class: x::WindowClass::InputOutput, + visual: x::COPY_FROM_PARENT, + value_list: &[ + x::Cw::BackPixel(screen.black_pixel()), + x::Cw::BorderPixel(screen.black_pixel()), + x::Cw::OverrideRedirect(true), + x::Cw::SaveUnder(true), + x::Cw::EventMask(x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT | x::EventMask::EXPOSURE), + ], + })); + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: blanker_window, + property: x::ATOM_WM_NAME, + r#type: x::ATOM_STRING, + data: b"bscreensaver blanker window", + })); + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: blanker_window, + property: x::ATOM_WM_CLASS, + r#type: x::ATOM_STRING, + data: BSCREENSAVER_WM_CLASS, + })); + + cookies.push(conn.send_request_checked(&x::CreateWindow { + depth: x::COPY_FROM_PARENT as u8, + wid: unlock_window, + parent: blanker_window, + x: 0, + y: 0, + width: 1, + height: 1, + border_width: 0, + class: x::WindowClass::InputOutput, + visual: x::COPY_FROM_PARENT, + value_list: &[ + x::Cw::BackPixel(screen.black_pixel()), + x::Cw::BorderPixel(screen.black_pixel()), + x::Cw::OverrideRedirect(true), + x::Cw::SaveUnder(true), + x::Cw::EventMask(x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT | x::EventMask::STRUCTURE_NOTIFY | x::EventMask::EXPOSURE), + ], + })); + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: unlock_window, + property: x::ATOM_WM_NAME, + r#type: x::ATOM_STRING, + data: b"bscreensaver unlock dialog socket window", + })); + cookies.push(conn.send_request_checked(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: unlock_window, + property: x::ATOM_WM_CLASS, + r#type: x::ATOM_STRING, + data: BSCREENSAVER_WM_CLASS, + })); + + let black_gc: x::Gcontext = conn.generate_id(); + cookies.push(conn.send_request_checked(&x::CreateGc { + cid: black_gc, + drawable: x::Drawable::Window(screen.root()), + value_list: &[ + x::Gc::Foreground(screen.black_pixel()), + x::Gc::Background(screen.black_pixel()), + ], + })); + + monitors.push(Monitor { + root: screen.root(), + black_gc, + blanker_window, + unlock_window, + x: reply.x(), + y: reply.y(), + width: reply.width(), + height: reply.height(), + }); + } + } + } + + for cookie in cookies { + conn.check_request(cookie)?; + } + + Ok(monitors) +} + +fn start_subservice(binary_name: &str) -> anyhow::Result<(Child, PidFd)> { + let mut child = Command::new(format!("{}/{}", env!("HELPER_DIR"), binary_name)) + .create_pidfd(true) + .spawn()?; + let pidfd = child.take_pidfd()?; + Ok((child, pidfd)) +} + +fn start_dbus_service(state: &mut State) -> anyhow::Result<()> { + state.dbus_service = Some(start_subservice("bscreensaver-dbus-service")?); + Ok(()) +} + +fn start_systemd_service(state: &mut State) -> anyhow::Result<()> { + state.systemd_service = Some(start_subservice("bscreensaver-systemd")?); + Ok(()) +} + +fn handle_subservice_quit(service: Option<(Child, PidFd)>, name: &str, start_subservice: F) -> anyhow::Result<()> +where + F: FnOnce() -> anyhow::Result<()> +{ + if let Some(mut service) = service { + if let Some(status) = service.0.try_wait().ok().flatten() { + if !status.success() { + warn!("{} service exited abnormally ({}{}); restarting", name, + status.code().map(|c| format!("code {}", c)).unwrap_or("".to_string()), + status.signal().map(|s| format!("signal {}", s)).unwrap_or("".to_string()) + ); + start_subservice()?; + } + } else { + info!("{} service didn't seem to actually quit", name); + } + } else { + info!("{} service wasn't running; starting it", name); + start_subservice()?; + } + Ok(()) +} + +fn handle_signals(state: &mut State, signal_fd: &mut SignalFd) -> anyhow::Result<()> { + match signal_fd.read_signal()? { + None => (), + Some(info) if info.ssi_signo == Signal::SIGHUP as u32 => restart_daemon(state)?, + Some(info) if info.ssi_signo == Signal::SIGINT as u32 => exit_daemon(state)?, + Some(info) if info.ssi_signo == Signal::SIGQUIT as u32 => exit_daemon(state)?, + Some(info) if info.ssi_signo == Signal::SIGTERM as u32 => exit_daemon(state)?, + Some(info) => trace!("Unexpected signal {}", info.ssi_signo), + } + Ok(()) +} + +fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, command_atoms: &CommandAtoms) -> anyhow::Result<()> { + loop { + if let Some(event) = conn.poll_for_event()? { + let embedder_handled = if let Some(mut unlock_dialog) = state.unlock_dialog.take() { + match unlock_dialog.embedder.event(&event) { + Err(err) => { + // XXX: should we assume unlock dialog is dead here? + warn!("Error sending event to unlock dialog: {}", err); + false + }, + Ok(handled) => { + state.unlock_dialog = Some(unlock_dialog); + handled + }, + } + } else { + false + }; + + if !embedder_handled { + match event { + xcb::Event::RandR(randr::Event::Notify(ev)) => { + debug!("Got xrandr notify event: {:#?}", ev); + for monitor in &state.monitors { + destroy_window(&conn, monitor.unlock_window)?; + destroy_window(&conn, monitor.blanker_window)?; + destroy_gc(&conn, monitor.black_gc)?; + } + state.monitors = create_blanker_windows(&conn)?; + match state.blanker_state { + BlankerState::Idle => (), + BlankerState::Blanked => { + state.blanker_state = BlankerState::Idle; + blank_screen(conn, state)?; + }, + BlankerState::Locked => { + state.blanker_state = BlankerState::Idle; + lock_screen(conn, state)?; + }, + } + }, + xcb::Event::Input(_) => { + // TODO: implement some sort of hysteresis/debouncing for mouse motion + state.last_user_activity = Instant::now(); + }, + xcb::Event::X(x::Event::ClientMessage(ev)) => match ev.r#type() { + b if b == command_atoms.blank => blank_screen(conn, state)?, + l if l == command_atoms.lock => lock_screen(conn, state)?, + d if d == command_atoms.deactivate => { + state.last_user_activity = Instant::now(); + match state.blanker_state { + BlankerState::Idle => (), + BlankerState::Blanked => unblank_screen(conn, state)?, + BlankerState::Locked => if state.unlock_dialog.is_none() { + state.unlock_dialog = Some(start_unlock_dialog(conn, state, None)?); + }, + } + }, + r if r == command_atoms.restart => restart_daemon(state)?, + e if e == command_atoms.exit => exit_daemon(state)?, + _ => (), + }, + xcb::Event::X(x::Event::MapNotify(ev)) if ev.window() == unlock_dialog_window(&state) => { + debug!("Unlock dialog mapped, requesting focus"); + if let Some(ref mut unlock_dialog) = &mut state.unlock_dialog { + let _ = unlock_dialog.embedder.activate_client(); + if let Err(err) = unlock_dialog.embedder.focus_client(xcb_xembed::XEmbedFocus::Current) { + warn!("Failed to focus unlock dialog: {}", err); + } + if let Some(event_to_forward) = unlock_dialog.event_to_forward.take() { + let _ = unlock_dialog.embedder.event(&event_to_forward); + } + } + }, + xcb::Event::X(x::Event::ConfigureNotify(ev)) if ev.window() == embedder_window(&state) => { + if let Some(unlock_dialog) = &state.unlock_dialog { + let monitor = &unlock_dialog.monitor; + let x = std::cmp::max(0, monitor.x as i32 + monitor.width as i32 / 2 - ev.width() as i32 / 2); + let y = std::cmp::max(0, monitor.y as i32 + monitor.height as i32 / 2 - ev.height() as i32 / 2); + if x != ev.x() as i32 || y != ev.y() as i32 { + conn.send_and_check_request(&x::ConfigureWindow { + window: unlock_dialog.embedder.embedder_window(), + value_list: &[ + x::ConfigWindow::X(x), + x::ConfigWindow::Y(y), + ], + })?; + } + } + }, + xcb::Event::X(x::Event::Expose(ev)) => { + if let Some(monitor) = state.monitors.iter().find(|m| ev.window() == m.blanker_window || ev.window() == m.unlock_window) { + debug!("got expose for {} at {}x{}+{}+{}", ev.window().resource_id(), ev.width(), ev.height(), ev.x(), ev.y()); + conn.send_and_check_request(&x::PolyFillRectangle { + drawable: x::Drawable::Window(ev.window()), + gc: monitor.black_gc, + rectangles: &[ + x::Rectangle { + x: ev.x() as i16, + y: ev.y() as i16, + width: ev.width(), + height: ev.height(), + }, + ], + })?; + } + }, + ev @ xcb::Event::X(x::Event::MotionNotify(_)) | ev @ xcb::Event::X(x::Event::KeyPress(_)) => match state.blanker_state { + BlankerState::Idle => (), + BlankerState::Blanked => unblank_screen(conn, state)?, + BlankerState::Locked => match &state.unlock_dialog { + None => state.unlock_dialog = match start_unlock_dialog(&conn, state, Some(ev)) { + Err(err) => { + error!("Unable to start unlock dialog: {}", err); + None + }, + Ok(unlock_dialog) => Some(unlock_dialog), + }, + Some(unlock_dialog) => { + let mut cookies = Vec::new(); + for win in [unlock_dialog.monitor.blanker_window, unlock_dialog.embedder.embedder_window(), unlock_dialog.embedder.client_window()] { + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: win, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + } + for cookie in cookies { + conn.check_request(cookie)?; + } + }, + } + }, + ev => trace!("Got other event: {:#?}", ev), + } + } + } else { + break; + } + } + + Ok(()) +} + +fn embedder_window(state: &State) -> x::Window { + state.unlock_dialog.as_ref().map(|ud| ud.embedder.embedder_window()).unwrap_or(x::WINDOW_NONE) +} + +fn unlock_dialog_window(state: &State) -> x::Window { + state.unlock_dialog.as_ref().map(|ud| ud.embedder.client_window()).unwrap_or(x::WINDOW_NONE) +} + +fn handle_unlock_dialog_quit(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { + if let Some(mut unlock_dialog) = state.unlock_dialog.take() { + match unlock_dialog.child.try_wait() { + Err(err) => { + warn!("Failed to check unlock dialog's status: {}", err); + state.unlock_dialog = Some(unlock_dialog); + }, + Ok(Some(status)) if status.success() => { + info!("Authentication succeeded"); + unlock_screen(conn, state)?; + } + Ok(Some(status)) if status.signal().is_some() => { + if let Some(signum) = status.signal() { + warn!("Unlock dialog crashed with signal {}", signum); + } + }, + Ok(Some(_)) => (), // auth failed, dialog has quit, do nothing + Ok(None) => state.unlock_dialog = Some(unlock_dialog), // dialog still running + } + } + Ok(()) +} + +fn start_unlock_dialog<'a>(conn: &'a xcb::Connection, state: &State<'a>, trigger_event: Option) -> anyhow::Result> { + let mut pointer_monitor = None; + for monitor in &state.monitors { + let cookie = conn.send_request(&x::QueryPointer { + window: monitor.root, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.same_screen() { + pointer_monitor = Some(monitor); + break; + } + } + let pointer_monitor = pointer_monitor.unwrap_or_else(|| { + warn!("Unable to determine which monitor pointer is on; using first one"); + state.monitors.iter().nth(0).unwrap() + }); + + let mut child = Command::new(format!("{}/{}", env!("HELPER_DIR"), state.config.dialog_backend.binary_name())) + .create_pidfd(true) + .stdout(Stdio::piped()) + .spawn()?; + + let mut child_out = child.stdout.take().unwrap(); + let child_pidfd = child.take_pidfd()?; + + let mut xid_buf: [u8; 4] = [0; 4]; + child_out.read_exact(&mut xid_buf)?; + let client_window = { + let wid: u32 = ((xid_buf[0] as u32) << 24) | ((xid_buf[1] as u32) << 16) | ((xid_buf[2] as u32) << 8) | (xid_buf[3] as u32); + unsafe { + use xcb::XidNew; + x::Window::new(wid) + } + }; + debug!("Dialog process created plug window 0x{:x}", client_window.resource_id()); + + let cookie = conn.send_request(&x::GetWindowAttributes { + window: client_window, + }); + let reply = conn.wait_for_reply(cookie)?; + if !reply.your_event_mask().contains(x::EventMask::STRUCTURE_NOTIFY) { + conn.send_and_check_request(&x::ChangeWindowAttributes { + window: client_window, + value_list: &[ + x::Cw::EventMask(reply.your_event_mask() | x::EventMask::STRUCTURE_NOTIFY), + ], + })?; + } + + let unlock_window = pointer_monitor.unlock_window; + let embedder = Embedder::start(conn, unlock_window, client_window)?; + + Ok(UnlockDialog { + monitor: *pointer_monitor, + embedder, + event_to_forward: trigger_event, + child, + child_pidfd, + }) +} + +fn blank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { + if state.blanker_state >= BlankerState::Blanked { + return Ok(()) + } + + info!("Blanking"); + + for monitor in &state.monitors { + let mut cookies = Vec::new(); + + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: monitor.blanker_window, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + cookies.push(conn.send_request_checked(&x::MapWindow { + window: monitor.blanker_window, + })); + + for cookie in cookies { + conn.check_request(cookie)?; + } + } + + state.blanker_state = BlankerState::Blanked; + + Ok(()) +} + +fn unblank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { + if state.blanker_state != BlankerState::Blanked { + return Ok(()); + } + + info!("Unblanking"); + + let mut cookies = Vec::new(); + for monitor in &state.monitors { + cookies.push(conn.send_request_checked(&x::UnmapWindow { + window: monitor.blanker_window, + })); + } + for cookie in cookies { + conn.check_request(cookie)?; + } + + state.blanker_state = BlankerState::Idle; + + Ok(()) +} + +fn lock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { + blank_screen(conn, state)?; + if state.blanker_state >= BlankerState::Locked { + return Ok(()); + } + + info!("Locking"); + + for monitor in &state.monitors { + let mut cookies = Vec::new(); + + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: monitor.unlock_window, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + cookies.push(conn.send_request_checked(&x::MapWindow { + window: monitor.unlock_window, + })); + + for cookie in cookies { + conn.check_request(cookie)?; + } + + let cookie = conn.send_request(&x::GrabKeyboard { + owner_events: true, + grab_window: monitor.unlock_window, + time: x::CURRENT_TIME, + pointer_mode: x::GrabMode::Async, + keyboard_mode: x::GrabMode::Async, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.status() != x::GrabStatus::Success { + // FIXME: try to grab later? + warn!("Failed to grab keyboard on window {:?}: {:?}", monitor.blanker_window, reply.status()); + } + + let cookie = conn.send_request(&x::GrabPointer { + owner_events: true, + grab_window: monitor.unlock_window, + event_mask: x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT, + pointer_mode: x::GrabMode::Async, + keyboard_mode: x::GrabMode::Async, + confine_to: monitor.blanker_window, + cursor: x::CURSOR_NONE, + time: x::CURRENT_TIME, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.status() != x::GrabStatus::Success { + // FIXME: try to grab later? + warn!("Failed to grab pointer on window {:?}: {:?}", monitor.blanker_window, reply.status()); + } + } + + state.blanker_state = BlankerState::Locked; + + Ok(()) +} + +fn unlock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { + if state.blanker_state != BlankerState::Locked { + return Ok(()); + } + + info!("Unlocking"); + + let mut cookies = Vec::new(); + for monitor in &state.monitors { + cookies.push(conn.send_request_checked(&x::UngrabKeyboard { + time: x::CURRENT_TIME, + })); + cookies.push(conn.send_request_checked(&x::UngrabPointer { + time: x::CURRENT_TIME, + })); + cookies.push(conn.send_request_checked(&x::UnmapWindow { + window: monitor.unlock_window, + })); + } + for cookie in cookies { + conn.check_request(cookie)?; + } + + state.blanker_state = BlankerState::Blanked; + unblank_screen(conn, state)?; + + Ok(()) +} + +fn restart_daemon(state: &mut State) -> anyhow::Result<()> { + info!("Restarting"); + + let exe = read_link("/proc/self/exe")?; + let exe = CString::new(exe.to_str().ok_or(io::Error::new(io::ErrorKind::InvalidData, "Path cannot be converted to str".to_string()))?)?; + + let argv = match state.blanker_state { + BlankerState::Idle => vec![], + BlankerState::Blanked => vec![CString::new("--".to_string() + BLANKED_ARG)?], + BlankerState::Locked => vec![CString::new("--".to_string() + LOCKED_ARG)?], + }; + + if let Err(err) = kill_child_processes(state) { + warn!("Failed to kill child processes: {}", err); + } + + match unsafe { fork() } { + Err(err) => { + error!("Failed to fork: {}", err); + Err(err)?; + }, + Ok(ForkResult::Parent { .. }) => exit(0), + Ok(ForkResult::Child) => { + if let Err(err) = setsid() { + warn!("Failed to start new session: {}", err); + } + execv(exe.as_c_str(), &argv)?; + }, + } + + Ok(()) +} + +fn exit_daemon(state: &mut State) -> anyhow::Result<()> { + info!("Quitting"); + if let Err(err) = kill_child_processes(state) { + warn!("Failed to kill child processes: {}", err); + } + exit(0); +} + +fn kill_child_processes(state: &mut State) -> anyhow::Result<()> { + if let Some(mut unlock_dialog) = state.unlock_dialog.take() { + unlock_dialog.embedder.end().ok(); + unlock_dialog.child.kill().ok(); + unlock_dialog.child.try_wait().ok(); + } + if let Some(mut dbus_service) = state.dbus_service.take() { + dbus_service.0.kill().ok(); + dbus_service.0.try_wait().ok(); + } + if let Some(mut systemd_service) = state.systemd_service.take() { + systemd_service.0.kill().ok(); + systemd_service.0.try_wait().ok(); + } + + Ok(()) +} diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/systemd/Cargo.toml b/systemd/Cargo.toml new file mode 100644 index 0000000..4b84257 --- /dev/null +++ b/systemd/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bscreensaver-systemd" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +async-std = { version = "1.11", features = ["attributes"] } +async-xcb = { path = "../async-xcb" } +bscreensaver-command = { path = "../command" } +bscreensaver-util = { path = "../util" } +futures = "0.3" +log = "0.4" +nix = "0.23" +# git source needed until extension event error resolution fix is released +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" } +zbus = "2.2" +logind-zbus = "3" diff --git a/systemd/src/main.rs b/systemd/src/main.rs new file mode 100644 index 0000000..ec3bc61 --- /dev/null +++ b/systemd/src/main.rs @@ -0,0 +1,99 @@ +use async_std::task; +use futures::{future::FutureExt, pin_mut, select, AsyncReadExt, StreamExt}; +use log::{debug, error, info, warn}; +use logind_zbus::manager::{InhibitType, ManagerProxy}; +use std::{os::unix::io::AsRawFd, process::exit}; +use zbus::Connection; + +use bscreensaver_command::{bscreensaver_command, BCommand}; +use bscreensaver_util::init_logging; + +#[async_std::main] +async fn main() { + init_logging("BSCREENSAVER_SYSTEMD_LOG"); + + let xcb_handle = task::spawn(xcb_task()).fuse(); + let dbus_handle = task::spawn(dbus_task()).fuse(); + + pin_mut!(xcb_handle, dbus_handle); + + let res = loop { + select! { + _ = xcb_handle => { + info!("Lost connection to X server; quitting"); + break Ok(()); + }, + res = dbus_handle => { + match res { + Err(err) => error!("Lost connection to the session bus: {}", err), + Ok(_) => error!("DBus task exited normally; this should not happen!"), + } + break Err(()); + }, + }; + }; + + if let Err(_) = res { + exit(1); + } +} + +async fn xcb_task() -> anyhow::Result<()> { + let (xcb_conn, _) = task::block_on(async { xcb::Connection::connect(None) })?; + let mut xcb_conn = async_xcb::AsyncConnection::new(xcb_conn)?; + + // We need to drain the XCB connection periodically. Even though we have not + // asked for any events, we'll still get stuff like MappingNotify if the keyboard + // settings change. + loop { + let mut buf = [0u8; 512]; + xcb_conn.read(&mut buf).await?; + } +} + +async fn dbus_task() -> anyhow::Result<()> { + + let system_bus = Connection::system().await?; + let manager_proxy = ManagerProxy::new(&system_bus).await?; + let mut prepare_for_sleep_stream = manager_proxy.receive_prepare_for_sleep().await?; + let mut inhibit_fd = Some(register_sleep_lock(&manager_proxy).await?); + + loop { + if let Some(prepare_for_sleep) = prepare_for_sleep_stream.next().await { + if *prepare_for_sleep.args()?.start() { + debug!("Preparing for sleep"); + if let Err(err) = do_bscreensaver_command(BCommand::Lock).await { + warn!("Failed to lock screen: {}", err); + } + if let Some(fd) = inhibit_fd.take() { + if let Err(err) = nix::unistd::close(fd.as_raw_fd()) { + warn!("Failed to close sleep inhibit lock: {}", err); + } + } else { + warn!("No sleep lock present"); + } + } else { + debug!("Resuming from sleep"); + if let Err(err) = do_bscreensaver_command(BCommand::Deactivate).await { + warn!("Failed to deactivate screen lock: {}", err); + } + inhibit_fd = Some(register_sleep_lock(&manager_proxy).await?); + } + } + } +} + +async fn do_bscreensaver_command(command: BCommand) -> anyhow::Result<()> { + task::block_on(async { + bscreensaver_command(command) + })?; + Ok(()) +} + +async fn register_sleep_lock<'a>(manager_proxy: &ManagerProxy<'a>) -> anyhow::Result { + debug!("Registering sleep lock"); + // ManagerProxy uses RawFd for the return value, which rust's type system thinks is an i32, + // which means the generated proxy uses the wrong dbus type signature. So instead, use a raw + // Proxy instance and do it all ourselves. + Ok((*manager_proxy).call("Inhibit", &(InhibitType::Sleep, "bscreensaver", "blank before sleep", "delay")).await?) +} diff --git a/util/Cargo.toml b/util/Cargo.toml new file mode 100644 index 0000000..4775d94 --- /dev/null +++ b/util/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bscreensaver-util" +version = "0.1.0" +edition = "2021" + +[lib] + +[dependencies] +clap = "3" +env_logger = "0.9" +lazy_static = "1" +libc = "0.2" +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] } diff --git a/util/src/lib.rs b/util/src/lib.rs new file mode 100644 index 0000000..8a5fbe2 --- /dev/null +++ b/util/src/lib.rs @@ -0,0 +1,50 @@ +use std::{ffi::CStr, io}; +use xcb::x; + +pub const BSCREENSAVER_WM_CLASS: &[u8] = b"bscreensaver\0Bscreensaver\0"; + +pub fn init_logging(env_name: &str) { + env_logger::builder() + .format_timestamp_millis() + .parse_env(env_name) + .init(); +} + +pub fn create_atom(conn: &xcb::Connection, name: &[u8]) -> xcb::Result { + let cookie = conn.send_request(&x::InternAtom { + only_if_exists: false, + name, + }); + Ok(conn.wait_for_reply(cookie)?.atom()) +} + +pub fn destroy_gc(conn: &xcb::Connection, gc: x::Gcontext) -> xcb::Result<()> { + conn.send_and_check_request(&x::FreeGc { + gc, + })?; + Ok(()) +} + +pub fn destroy_window(conn: &xcb::Connection, window: x::Window) -> xcb::Result<()> { + conn.send_and_check_request(&x::DestroyWindow { + window, + })?; + Ok(()) +} + +pub fn get_username() -> io::Result { + // SAFETY: libc must be sane + let uid = unsafe { libc::getuid() }; + // SAFETY: libc must be sane + let pwd = unsafe { libc::getpwuid(uid) }; + // SAFETY: null-check occurs on same line + if pwd.is_null() || unsafe { *pwd }.pw_name.is_null() { + Err(io::Error::new(io::ErrorKind::NotFound, "Username not found".to_string())) + } else { + // SAFETY: libc must be sane; null checks performed above + let cstr = unsafe { CStr::from_ptr((*pwd).pw_name) }; + cstr.to_str() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 data in username".to_string())) + .map(|s| s.to_string()) + } +} diff --git a/xcb-xembed/Cargo.toml b/xcb-xembed/Cargo.toml new file mode 100644 index 0000000..fc47ffc --- /dev/null +++ b/xcb-xembed/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "xcb-xembed" +version = "0.1.0" +authors = [ + "Brian Tarricone ", +] +edition = "2021" +description = "XCB-based implementation of the X11 XEMBED protocol" +license = "LGPL-3.0" +repository = "https://github.com/kelnos/xcb-xembed" +readme = "README.md" +keywords = ["gui", "x11", "xcb", "xembed"] +categories = ["gui"] + +[dependencies] +bitflags = "1" +log = "0.4" +xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] } diff --git a/xcb-xembed/src/embedder.rs b/xcb-xembed/src/embedder.rs new file mode 100644 index 0000000..7fc2d21 --- /dev/null +++ b/xcb-xembed/src/embedder.rs @@ -0,0 +1,341 @@ +use log::{debug, info, trace}; +use xcb::{x, xfixes, Xid}; + +use crate::*; + +pub struct Embedder<'a> { + conn: &'a xcb::Connection, + embedder: x::Window, + client: x::Window, + flags: XEmbedFlags, +} + +struct XEmbedInfo { + version: u32, + flags: XEmbedFlags, +} + +impl<'a> Embedder<'a> { + pub fn start(conn: &'a xcb::Connection, embedder: x::Window, client: x::Window) -> Result, Error> { + debug!("Reparenting {} to be a child of {}", client.resource_id(), embedder.resource_id()); + conn.send_and_check_request(&x::ReparentWindow { + window: client, + parent: embedder, + x: 0, + y: 0, + })?; + + let event_mask = x::EventMask::PROPERTY_CHANGE | x::EventMask::STRUCTURE_NOTIFY; + let cookie = conn.send_request(&x::GetWindowAttributes { + window: client, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.your_event_mask().intersection(event_mask) != event_mask { + conn.send_and_check_request(&x::ChangeWindowAttributes { + window: client, + value_list: &[ + x::Cw::EventMask(reply.your_event_mask() | event_mask), + ], + })?; + } + + let info = fetch_xembed_info(conn, client)?; + let supported_version = std::cmp::min(info.version, XEMBED_VERSION); + + if let Err(err) = conn.send_and_check_request(&xfixes::ChangeSaveSet { + mode: xfixes::SaveSetMode::Insert, + target: xfixes::SaveSetTarget::Root, + map: xfixes::SaveSetMapping::Unmap, + window: client, + }) { + info!("Failed to send XFIXES ChangeSaveSet request: {}", err); + } + + debug!("sending EMBEDDED_NOTIFY to client with embedder wid {} and version {}", embedder.resource_id(), supported_version); + send_xembed_message(conn, client, x::CURRENT_TIME, XEmbedMessage::EmbeddedNotify, None, Some(embedder.resource_id()), Some(supported_version))?; + + let cookie = conn.send_request(&x::GetInputFocus {}); + let reply = conn.wait_for_reply(cookie)?; + let focus_window = reply.focus(); + + let mut cur_window = embedder; + let activated_message = if focus_window == embedder { + XEmbedMessage::WindowActivate + } else { + loop { + let cookie = conn.send_request(&x::QueryTree { + window: cur_window, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.root() == reply.parent() { + break XEmbedMessage::WindowDeactivate; + } else if reply.parent() == focus_window { + break XEmbedMessage::WindowActivate; + } else { + cur_window = reply.parent(); + } + } + }; + send_xembed_message(conn, client, x::CURRENT_TIME, activated_message, None, None, None)?; + + let (focus_message, focus_detail) = if focus_window == embedder { + (XEmbedMessage::FocusIn, Some(XEmbedFocus::Current)) + } else { + (XEmbedMessage::FocusOut, None) + }; + send_xembed_message(conn, client, x::CURRENT_TIME, focus_message, focus_detail.map(|fd| fd as u32), None, None)?; + + // XXX: how do we know if there's something modal active? + send_xembed_message(conn, client, x::CURRENT_TIME, XEmbedMessage::ModalityOff, None, None, None)?; + + Ok(Embedder { + conn, + embedder, + client, + flags: info.flags, + }) + } + + pub fn end(self) -> Result<(), Error> { + if self.client == x::WINDOW_NONE { + return Err(Error::ClientDestroyed); + } + + debug!("Ending XEMBED"); + let cookie = self.conn.send_request(&x::QueryTree { + window: self.client, + }); + let reply = self.conn.wait_for_reply(cookie)?; + self.conn.send_and_check_request(&x::UnmapWindow { + window: self.client, + })?; + self.conn.send_and_check_request(&x::ReparentWindow { + window: self.client, + parent: reply.root(), + x: 0, + y: 0, + })?; + Ok(()) + } + + pub fn event(&mut self, event: &xcb::Event) -> Result { + if self.client == x::WINDOW_NONE { + return Err(Error::ClientDestroyed); + } + + match event { + xcb::Event::X(x::Event::PropertyNotify(ev)) if ev.window() == self.client && ev.atom() == intern_atom(self.conn, XEMBED_INFO_ATOM_NAME)? => { + let info = fetch_xembed_info(self.conn, self.client)?; + if (self.flags & XEmbedFlags::MAPPED) != (info.flags & XEmbedFlags::MAPPED) { + if info.flags.contains(XEmbedFlags::MAPPED) { + debug!("Mapping client window"); + self.conn.send_and_check_request(&x::MapWindow { + window: self.client, + })?; + } else { + debug!("Unmapping client window"); + self.conn.send_and_check_request(&x::UnmapWindow { + window: self.client, + })?; + } + self.flags = info.flags; + } + Ok(true) + }, + xcb::Event::X(x::Event::ConfigureNotify(ev)) if ev.window() == self.client => { + let mut cookies = Vec::new(); + if ev.x() != 0 || ev.y() != 0 { + cookies.push(self.conn.send_request_checked(&x::ConfigureWindow { + window: self.client, + value_list: &[ + x::ConfigWindow::X(0), + x::ConfigWindow::Y(0), + ], + })); + } + cookies.push(self.conn.send_request_checked(&x::ConfigureWindow { + window: self.embedder, + value_list: &[ + x::ConfigWindow::Width(ev.width() as u32), + x::ConfigWindow::Height(ev.height() as u32), + ], + })); + for cookie in cookies { + self.conn.check_request(cookie)?; + } + Ok(true) + }, + xcb::Event::X(x::Event::ClientMessage(ev)) if ev.window() == self.embedder && ev.r#type() == intern_atom(self.conn, XEMBED_MESSAGE_ATOM_NAME)? => { + match ev.data() { + x::ClientMessageData::Data32(data) if data[1] == XEmbedMessage::RequestFocus as u32 => { + debug!("Client requests focus"); + self.conn.send_and_check_request(&x::SetInputFocus { + revert_to: x::InputFocus::Parent, + focus: self.client, + time: x::CURRENT_TIME, + })?; + send_xembed_message(self.conn, self.client, x::CURRENT_TIME, XEmbedMessage::FocusIn, Some(XEmbedFocus::Current as u32), None, None)?; + Ok(true) + }, + // TODO: XEMBED_FOCUS_NEXT + // TODO: XEMBED_FOCUS_PREV + // TODO: XEMBED_REGISTER_ACCELERATOR + // TODO: XEMBED_UNREGISTER_ACCELERATOR + _ => Ok(false), + } + }, + xcb::Event::X(x::Event::KeyPress(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => { + trace!("Forwarding key press to client ({:?} + {})", ev.state(), ev.detail()); + self.conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(self.client), + event_mask: x::EventMask::NO_EVENT, + event: &x::KeyPressEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()), + })?; + Ok(true) + }, + /* + xcb::Event::X(x::Event::KeyRelease(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => { + trace!("Forwarding key release to client ({:?} + {})", ev.state(), ev.detail()); + self.conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(self.client), + event_mask: x::EventMask::NO_EVENT, + event: &x::KeyReleaseEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()), + })?; + Ok(true) + }, + */ + xcb::Event::X(x::Event::MotionNotify(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => { + trace!("Forwarding pointer motion to client ({}, {})", ev.event_x(), ev.event_y()); + self.conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(self.client), + event_mask: x::EventMask::NO_EVENT, + event: &x::MotionNotifyEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()), + })?; + Ok(true) + }, + xcb::Event::X(x::Event::ButtonPress(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => { + trace!("Forwarding button press to client ({:?} + {}: {}, {})", ev.state(), ev.detail(), ev.event_x(), ev.event_y()); + self.conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(self.client), + event_mask: x::EventMask::NO_EVENT, + event: &x::ButtonPressEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()), + })?; + Ok(true) + }, + xcb::Event::X(x::Event::ButtonRelease(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => { + trace!("Forwarding button release to client ({:?} + {}: {}, {})", ev.state(), ev.detail(), ev.event_x(), ev.event_y()); + self.conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(self.client), + event_mask: x::EventMask::NO_EVENT, + event: &x::ButtonReleaseEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()), + })?; + Ok(true) + }, + xcb::Event::X(x::Event::UnmapNotify(ev)) if ev.window() == self.client => { + debug!("Client was unmapped"); + self.flags -= XEmbedFlags::MAPPED; + Ok(true) + }, + xcb::Event::X(x::Event::DestroyNotify(ev)) if ev.window() == self.client => { + debug!("Client was destroyed"); + self.flags -= XEmbedFlags::MAPPED; + self.client = x::WINDOW_NONE; + Ok(true) + }, + event => { + trace!("Not handling event {:#?}", event); + Ok(false) + }, + } + } + + pub fn activate_client(&self) -> Result<(), Error> { + if self.client != x::WINDOW_NONE { + send_xembed_message(&self.conn, self.client, x::CURRENT_TIME, XEmbedMessage::WindowActivate, None, None, None)?; + Ok(()) + } else { + Err(Error::ClientDestroyed) + } + } + + pub fn focus_client(&self, mode: super::XEmbedFocus) -> Result<(), Error> { + if self.client != x::WINDOW_NONE { + self.conn.send_and_check_request(&x::SetInputFocus { + revert_to: x::InputFocus::Parent, + focus: self.client, + time: x::CURRENT_TIME, + })?; + send_xembed_message(&self.conn, self.client, x::CURRENT_TIME, XEmbedMessage::FocusIn, Some(mode as u32), None, None)?; + Ok(()) + } else { + Err(Error::ClientDestroyed) + } + } + + pub fn embedder_window(&self) -> x::Window { + self.embedder + } + + pub fn client_window(&self) -> x::Window { + self.client + } + + pub fn is_client_mapped(&self) -> bool { + self.flags.contains(XEmbedFlags::MAPPED) + } +} + +fn fetch_xembed_info(conn: &xcb::Connection, client: x::Window) -> Result { + let xembed_info_atom = intern_atom(conn, XEMBED_INFO_ATOM_NAME)?; + let cookie = conn.send_request(&x::GetProperty { + delete: false, + window: client, + property: xembed_info_atom, + r#type: xembed_info_atom, + long_offset: 0, + long_length: 8, + }); + let reply = conn.wait_for_reply(cookie)?; + if reply.value::().len() < 2 { + Err(Error::ProtocolError("Invalid format of _XEMBED_INFO property".to_string())) + } else { + debug!("_XEMBED_INFO -> ({}, 0x{:x})", reply.value::()[0], reply.value::()[1]); + Ok(XEmbedInfo { + version: reply.value()[0], + flags: XEmbedFlags::from_bits_truncate(reply.value()[1]), + }) + } +} + +fn intern_atom(conn: &xcb::Connection, name: &str) -> xcb::Result { + let cookie = conn.send_request(&x::InternAtom { + only_if_exists: false, + name: name.as_bytes(), + }); + conn.wait_for_reply(cookie).map(|reply| reply.atom()) +} + +fn send_xembed_message(conn: &xcb::Connection, window: x::Window, time: u32, message: XEmbedMessage, detail: Option, data1: Option, data2: Option) -> xcb::Result<()> { + conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(window), + event_mask: x::EventMask::NO_EVENT, + event: &x::ClientMessageEvent::new( + window, + intern_atom(conn, XEMBED_MESSAGE_ATOM_NAME)?, + x::ClientMessageData::Data32([ + time, + message as u32, + detail.unwrap_or(0), + data1.unwrap_or(0), + data2.unwrap_or(0), + ]), + ), + })?; + Ok(()) +} diff --git a/xcb-xembed/src/lib.rs b/xcb-xembed/src/lib.rs new file mode 100644 index 0000000..ee3646a --- /dev/null +++ b/xcb-xembed/src/lib.rs @@ -0,0 +1,96 @@ +use bitflags::bitflags; +use std::{ + error::Error as StdError, + fmt, +}; + +pub mod embedder; + +pub(crate) const XEMBED_VERSION: u32 = 0; +pub(crate) const XEMBED_INFO_ATOM_NAME: &str = "_XEMBED_INFO"; +pub(crate) const XEMBED_MESSAGE_ATOM_NAME: &str = "_XEMBED"; + +#[allow(dead_code)] +pub(crate) enum XEmbedMessage { + EmbeddedNotify = 0, + WindowActivate = 1, + WindowDeactivate = 2, + RequestFocus = 3, + FocusIn = 4, + FocusOut = 5, + FocusNext = 6, + FocusPrev = 7, + ModalityOn = 10, + ModalityOff = 11, + RegisterAccelerator = 12, + UnregisterAccelerator = 13, + ActivateAccelerator = 14, +} + +pub enum XEmbedFocus { + Current = 0, + First = 1, + Last = 2, +} + +bitflags! { + struct XEmbedFlags: u32 { + const MAPPED = (1 << 0); + } + + struct XEmbedModifier: u32 { + const SHIFT = (1 << 0); + const CONTROL = (1 << 1); + const ALT = (1 << 2); + const SUPER = (1 << 3); + const HYPER = (1 << 4); + } + + struct XEmbedAcceleratorFlags: u32 { + const OVERLOADED = (1 << 0); + } +} + +#[derive(Debug)] +pub enum Error { + ProtocolError(String), + Xcb(xcb::Error), + ClientDestroyed, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ProtocolError(reason) => write!(f, "XEMBED: Protocol error: {}", reason), + Self::Xcb(err) => write!(f, "XEMBED: {}", err), + Self::ClientDestroyed => write!(f, "XEMBED: client destroyed"), + } + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::Xcb(err) => Some(err), + _ => None, + } + } +} + +impl From for Error { + fn from(error: xcb::Error) -> Self { + Self::Xcb(error) + } +} + +impl From for Error { + fn from(error: xcb::ProtocolError) -> Self { + Self::Xcb(xcb::Error::Protocol(error)) + } +} + +impl From for Error { + fn from(error: xcb::ConnError) -> Self { + Self::Xcb(xcb::Error::Connection(error)) + } +}