Initial Wayland greeter source

This commit is contained in:
wfalkowski committed 2026-10-15 22:01:06 +00:00
commit c302251fef
5 files changed
+1324

No files matched your search

+1
View File
@@ -0,0 +1 @@
/target/
Generated
+708
View File
@@ -0,0 +1,708 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "cairo-rs"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4"
dependencies = [
"bitflags",
"cairo-sys-rs",
"glib",
"libc",
]
[[package]]
name = "cairo-sys-rs"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]]
name = "cfg-expr"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba9e9ec16c447027685b1f897b720e18e9a8afd00bd7332c483537e38086c9f"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "field-offset"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
"memoffset",
"rustc_version",
]
[[package]]
name = "futures-channel"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-executor"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-macro"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "gdk-pixbuf"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c"
dependencies = [
"gdk-pixbuf-sys",
"gio",
"glib",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7"
dependencies = [
"gio-sys",
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]]
name = "gdk4"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf"
dependencies = [
"cairo-rs",
"gdk-pixbuf",
"gdk4-sys",
"gio",
"glib",
"libc",
"pango",
]
[[package]]
name = "gdk4-sys"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"libc",
"pango-sys",
"pkg-config",
"system-deps",
]
[[package]]
name = "gio"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"gio-sys",
"glib",
"libc",
"pin-project-lite",
"smallvec",
]
[[package]]
name = "gio-sys"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"windows-sys",
]
[[package]]
name = "glib"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"glib-macros",
"glib-sys",
"gobject-sys",
"libc",
"memchr",
"smallvec",
]
[[package]]
name = "glib-macros"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"
dependencies = [
"heck",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "glib-sys"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"
dependencies = [
"libc",
"system-deps",
]
[[package]]
name = "gobject-sys"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]]
name = "graphene-rs"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9"
dependencies = [
"glib",
"graphene-sys",
"libc",
]
[[package]]
name = "graphene-sys"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93"
dependencies = [
"glib-sys",
"libc",
"pkg-config",
"system-deps",
]
[[package]]
name = "gsk4"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153"
dependencies = [
"cairo-rs",
"gdk4",
"glib",
"graphene-rs",
"gsk4-sys",
"libc",
"pango",
]
[[package]]
name = "gsk4-sys"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7"
dependencies = [
"cairo-sys-rs",
"gdk4-sys",
"glib-sys",
"gobject-sys",
"graphene-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "gtk4"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4"
dependencies = [
"cairo-rs",
"field-offset",
"futures-channel",
"gdk-pixbuf",
"gdk4",
"gio",
"glib",
"graphene-rs",
"gsk4",
"gtk4-macros",
"gtk4-sys",
"libc",
"pango",
]
[[package]]
name = "gtk4-macros"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "gtk4-sys"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk4-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"graphene-sys",
"gsk4-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "pango"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69"
dependencies = [
"gio",
"glib",
"libc",
"pango-sys",
]
[[package]]
name = "pango-sys"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
dependencies = [
"serde_core",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "system-deps"
version = "7.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"version-compare",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "toml"
version = "1.1.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.17+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]]
name = "wayland-greeter"
version = "0.1.0"
dependencies = [
"gtk4",
"serde",
"serde_json",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
dependencies = [
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "wayland-greeter"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "A compact GTK4 greetd greeter for Wayland sessions"
license = "MIT"
[dependencies]
gtk = { package = "gtk4", version = "0.10" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+41
View File
@@ -0,0 +1,41 @@
# Wayland Greeter
A compact Rust + GTK4 graphical greeter for greetd. It uses greetd for PAM authentication and launches installed Wayland session entries, including Hyprland.
## Current status
Early project build. The greeter UI and greetd IPC client are implemented; it has not yet been activated as the system login path. The target is a Gentoo amd64 musl + LLVM host with GTK4 installed.
## Build
Build on the target Gentoo host so the binary links against the host's musl and GTK libraries:
```sh
cargo build --release
```
Native build requirements: Rust/Cargo, pkg-config, GTK4 development files, and a working Wayland GTK backend. This produces `target/release/wayland-greeter`; install it through a local ebuild or a reviewed manual installation step after confirming the output's dynamic dependencies.
## Run under greetd
The application needs `GREETD_SOCK`, which greetd provides. A graphical greeter also needs a compositor. A typical setup is greetd → Cage → this app, then greetd starts Hyprland after authentication. Example only (adapt the executable path and service/init setup to the host):
```toml
[terminal]
vt = 1
[default_session]
command = "cage -s -- /usr/local/bin/wayland-greeter"
user = "greeter"
```
Cage and a greeter account must be installed/configured separately. Gentoo's compositor listing places Cage in the `guru` overlay (`gui-wm/cage::guru`), rather than the main Gentoo repository; inspect that overlay and the host's `emerge -pv` plan before considering it. Keep a working TTY login route before changing the greetd default session. Do not activate this configuration until the binary, Cage startup, PAM flow, session descriptors, VT switching, and rollback have been checked locally.
The session picker is hidden by default; press **F2** to show or hide it. The login surface starts with only the username and login button; PAM prompts reveal the response field when needed. The picker reads `*.desktop` entries from `/usr/share/wayland-sessions` and `/usr/local/share/wayland-sessions`. If neither has entries, it offers a `Hyprland` fallback command. The selected entry's `Exec` is parsed into argv and passed to greetd directly, without shell expansion.
## Design notes
- PAM prompts come from greetd IPC and are not assumed to be a single username/password exchange.
- Secrets are never written to disk or logged.
- The card is centered by GTK's center container in both dimensions. Its UI colors come only from `~/.config/foot/foot.ini`; it uses Maple Mono and GTK4 system libraries.
- See the project skill `hyprland-greetd-greeter` for protocol, host, and maintenance guidance.
+561
View File
@@ -0,0 +1,561 @@
use gtk::glib;
use gtk::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Request {
CreateSession { username: String },
PostAuthMessageResponse { response: Option<String> },
StartSession { cmd: Vec<String>, env: Vec<String> },
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Response {
Success,
Error {
error_type: String,
description: String,
},
AuthMessage {
auth_message_type: String,
auth_message: String,
},
}
struct Ipc(UnixStream);
impl Ipc {
fn connect() -> Result<Self, String> {
let path = std::env::var_os("GREETD_SOCK")
.ok_or("GREETD_SOCK is not set; start this program from greetd")?;
UnixStream::connect(path)
.map(Self)
.map_err(|e| format!("Could not connect to greetd: {e}"))
}
fn request(&mut self, req: &Request) -> Result<Response, String> {
let payload = serde_json::to_vec(req).map_err(|e| e.to_string())?;
let size = u32::try_from(payload.len()).map_err(|_| "greetd request is too large")?;
self.0
.write_all(&size.to_ne_bytes())
.and_then(|_| self.0.write_all(&payload))
.map_err(|e| format!("Could not send greetd request: {e}"))?;
let mut len = [0_u8; 4];
self.0
.read_exact(&mut len)
.map_err(|e| format!("Could not read greetd response: {e}"))?;
let size = u32::from_ne_bytes(len) as usize;
if size > 1024 * 1024 {
return Err("greetd response exceeded the 1 MiB safety limit".into());
}
let mut payload = vec![0; size];
self.0
.read_exact(&mut payload)
.map_err(|e| format!("Could not read greetd response: {e}"))?;
serde_json::from_slice(&payload).map_err(|e| format!("Invalid greetd response: {e}"))
}
}
enum UiEvent {
Prompt {
message: String,
secret: bool,
input: bool,
},
Status(String),
Started,
Failed(String),
}
enum WorkerAction {
Answer(Option<String>),
}
#[derive(Clone)]
struct Session {
name: String,
command: Vec<String>,
}
fn main() {
let app = gtk::Application::builder()
.application_id("dev.wfalkowski.WaylandGreeter")
.flags(gtk::gio::ApplicationFlags::NON_UNIQUE)
.build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &gtk::Application) {
install_style();
let sessions = discover_sessions();
let window = gtk::ApplicationWindow::builder()
.application(app)
.title("Sign in")
.default_width(1000)
.default_height(700)
.build();
window.fullscreen();
let backdrop = gtk::CenterBox::new();
backdrop.set_orientation(gtk::Orientation::Vertical);
backdrop.set_hexpand(true);
backdrop.set_vexpand(true);
backdrop.add_css_class("backdrop");
let card = gtk::Box::new(gtk::Orientation::Vertical, 10);
card.set_halign(gtk::Align::Center);
card.set_valign(gtk::Align::Center);
card.set_width_request(340);
card.set_margin_top(16);
card.set_margin_bottom(16);
card.set_margin_start(16);
card.set_margin_end(16);
card.add_css_class("card");
let username = gtk::Entry::builder()
.placeholder_text("Username")
.hexpand(true)
.build();
username.add_css_class("field");
username.set_activates_default(true);
card.append(&username);
let response_entry = gtk::Entry::builder()
.placeholder_text("Password")
.visibility(false)
.hexpand(true)
.build();
response_entry.add_css_class("field");
response_entry.set_activates_default(true);
response_entry.set_sensitive(false);
response_entry.set_visible(false);
card.append(&response_entry);
let session_picker = gtk::ComboBoxText::new();
for (i, session) in sessions.iter().enumerate() {
session_picker.append(Some(&i.to_string()), &session.name);
}
if !sessions.is_empty() {
session_picker.set_active(Some(0));
}
session_picker.add_css_class("field");
let session_options = gtk::Box::new(gtk::Orientation::Vertical, 6);
session_options.add_css_class("session-options");
session_options.append(&field_label("Session"));
session_options.append(&session_picker);
session_options.set_visible(false);
card.append(&session_options);
let status = gtk::Label::new(None);
status.set_xalign(0.0);
status.set_wrap(true);
status.add_css_class("status");
status.set_visible(false);
card.append(&status);
let login = gtk::Button::with_label("Log in");
login.add_css_class("login-button");
window.set_default_widget(Some(&login));
card.append(&login);
backdrop.set_center_widget(Some(&card));
window.set_child(Some(&backdrop));
let (event_tx, event_rx) = mpsc::channel::<UiEvent>();
let action_sender = std::rc::Rc::new(std::cell::RefCell::new(None::<Sender<WorkerAction>>));
let login_busy = std::rc::Rc::new(std::cell::Cell::new(false));
{
let tx = event_tx.clone();
let login_busy = login_busy.clone();
let login = login.clone();
let username = username.clone();
let response_entry = response_entry.clone();
let status = status.clone();
let session_picker = session_picker.clone();
let sessions = sessions.clone();
let action_sender = action_sender.clone();
let login_for_callback = login.clone();
login.connect_clicked(move |_| {
let Some(selected) = session_picker
.active_id()
.and_then(|id| id.parse::<usize>().ok())
.and_then(|i| sessions.get(i).cloned())
else {
status.set_text("No Wayland session was found in /usr/share/wayland-sessions.");
status.set_visible(true);
return;
};
if login_busy.get() {
let answer = response_entry.text().to_string();
response_entry.set_text("");
if let Some(sender) = action_sender.borrow().as_ref() {
let _ = sender.send(WorkerAction::Answer(Some(answer)));
}
return;
}
let user = username.text().trim().to_string();
if user.is_empty() {
status.set_text("Username required");
status.set_visible(true);
username.grab_focus();
return;
}
if selected.command.is_empty() {
status.set_text("Selected session has no launch command");
status.set_visible(true);
return;
}
login_busy.set(true);
login_for_callback.set_sensitive(false);
let (action_tx, action_rx) = mpsc::channel::<WorkerAction>();
*action_sender.borrow_mut() = Some(action_tx);
login_for_callback.set_label("Continue");
status.set_text("");
status.set_visible(false);
response_entry.set_text("");
response_entry.set_visible(false);
response_entry.set_sensitive(false);
let tx = tx.clone();
thread::spawn(move || authenticate(user, selected, tx, action_rx));
});
}
{
let login = login.clone();
response_entry.connect_activate(move |_| login.emit_clicked());
}
{
let login = login.clone();
username.connect_activate(move |_| {
login.emit_clicked();
});
}
let sessions_visible = std::rc::Rc::new(std::cell::Cell::new(false));
let session_toggle = gtk::EventControllerKey::new();
session_toggle.set_propagation_phase(gtk::PropagationPhase::Capture);
{
let sessions_visible = sessions_visible.clone();
let session_options = session_options.clone();
let session_picker = session_picker.clone();
let response_entry = response_entry.clone();
session_toggle.connect_key_pressed(move |_, key, _, _| {
if key == gtk::gdk::Key::F2 {
let visible = !sessions_visible.get();
sessions_visible.set(visible);
session_options.set_visible(visible);
if visible {
session_picker.grab_focus();
} else {
response_entry.grab_focus();
}
glib::Propagation::Stop
} else {
glib::Propagation::Proceed
}
});
}
window.add_controller(session_toggle);
let event_rx = std::rc::Rc::new(std::cell::RefCell::new(event_rx));
let login_busy_for_events = login_busy.clone();
let login_for_events = login.clone();
let username_for_events = username.clone();
let response_for_events = response_entry.clone();
let status_for_events = status.clone();
let app_for_events = app.clone();
let action_sender_for_events = action_sender.clone();
glib::timeout_add_local(Duration::from_millis(40), move || {
loop {
let event = event_rx.borrow().try_recv();
match event {
Ok(UiEvent::Prompt {
message,
secret,
input,
}) => {
login_for_events.set_sensitive(input);
response_for_events.set_visibility(!secret);
response_for_events.set_sensitive(input);
response_for_events.set_visible(input);
if input {
login_for_events.set_label("Continue");
response_for_events.set_placeholder_text(Some(&message));
status_for_events.set_text("");
status_for_events.set_visible(false);
response_for_events.grab_focus();
} else {
status_for_events.set_text(&message);
status_for_events.set_visible(true);
}
}
Ok(UiEvent::Status(message)) => {
login_for_events.set_sensitive(false);
status_for_events.set_text(&message);
status_for_events.set_visible(true);
}
Ok(UiEvent::Started) => {
app_for_events.quit();
return glib::ControlFlow::Break;
}
Ok(UiEvent::Failed(message)) => {
login_busy_for_events.set(false);
*action_sender_for_events.borrow_mut() = None;
login_for_events.set_label("Log in");
login_for_events.set_sensitive(true);
response_for_events.set_text("");
response_for_events.set_sensitive(true);
response_for_events.set_visibility(false);
response_for_events.set_visible(false);
response_for_events.set_placeholder_text(Some("Password"));
status_for_events.set_text(&message);
status_for_events.set_visible(true);
username_for_events.grab_focus();
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => break,
}
}
glib::ControlFlow::Continue
});
window.present();
username.grab_focus();
}
fn authenticate(
username: String,
session: Session,
events: Sender<UiEvent>,
actions: Receiver<WorkerAction>,
) {
let result = (|| -> Result<(), String> {
let mut ipc = Ipc::connect()?;
let mut response = ipc.request(&Request::CreateSession { username })?;
loop {
match response {
Response::Success => break,
Response::AuthMessage {
auth_message_type,
auth_message,
} => {
let kind = auth_message_type.as_str();
let input = matches!(kind, "visible" | "secret");
events
.send(UiEvent::Prompt {
message: auth_message,
secret: kind == "secret",
input,
})
.map_err(|_| "Greeter UI closed during authentication")?;
let answer = if input {
match actions
.recv()
.map_err(|_| "Greeter UI closed during authentication")?
{
WorkerAction::Answer(value) => value,
}
} else {
None
};
response =
ipc.request(&Request::PostAuthMessageResponse { response: answer })?;
}
Response::Error {
error_type,
description,
} => {
return Err(if error_type == "auth_error" {
format!("Authentication failed: {description}")
} else {
description
});
}
}
}
events
.send(UiEvent::Status("Starting your session…".into()))
.ok();
let env = vec![
"XDG_SESSION_TYPE=wayland".into(),
format!("XDG_CURRENT_DESKTOP={}", session.name),
format!("XDG_SESSION_DESKTOP={}", session.name),
];
match ipc.request(&Request::StartSession {
cmd: session.command,
env,
})? {
Response::Success => {
let _ = events.send(UiEvent::Started);
Ok(())
}
Response::Error { description, .. } => {
Err(format!("Could not start session: {description}"))
}
Response::AuthMessage { .. } => {
Err("Unexpected authentication prompt while starting session".into())
}
}
})();
if let Err(error) = result {
let _ = events.send(UiEvent::Failed(error));
}
}
fn field_label(text: &str) -> gtk::Label {
let label = gtk::Label::new(Some(text));
label.set_xalign(0.0);
label.add_css_class("field-label");
label
}
fn discover_sessions() -> Vec<Session> {
let mut sessions = Vec::new();
for dir in [
// Match the user's previous tuigreet configuration. Put local greetd
// entries first so a custom launcher can override a system desktop
// entry with the same session name.
"/etc/greetd/sessions",
"/usr/share/wayland-sessions",
"/usr/local/share/wayland-sessions",
] {
let Ok(entries) = fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_none_or(|ext| ext != "desktop") {
continue;
}
if let Some(session) = parse_session(&path) {
sessions.push(session);
}
}
}
sessions.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
sessions.dedup_by(|a, b| a.name == b.name);
if sessions.is_empty() {
sessions.push(Session {
name: "Hyprland".into(),
command: vec!["Hyprland".into()],
});
}
sessions
}
fn parse_session(path: &Path) -> Option<Session> {
let contents = fs::read_to_string(path).ok()?;
let mut in_desktop_entry = false;
let mut name = None;
let mut exec = None;
for line in contents.lines().map(str::trim) {
if line.starts_with('[') {
in_desktop_entry = line == "[Desktop Entry]";
continue;
}
if !in_desktop_entry || line.starts_with('#') {
continue;
}
if let Some(value) = line.strip_prefix("Name=") {
name = Some(value.trim().to_string());
}
if let Some(value) = line.strip_prefix("Exec=") {
exec = Some(value.trim());
}
if line == "Hidden=true" || line == "NoDisplay=true" {
return None;
}
}
let mut command = split_exec(exec?);
command.retain(|part| !(part.starts_with('%') && part.len() == 2));
if command.is_empty() {
return None;
}
Some(Session {
name: name.filter(|s| !s.is_empty()).unwrap_or_else(|| {
path.file_stem()
.unwrap_or_default()
.to_string_lossy()
.into_owned()
}),
command,
})
}
// Desktop Entry Exec is a command line, not a shell program. This tokenizer
// handles quoting and escaping while intentionally avoiding shell expansion.
fn split_exec(input: &str) -> Vec<String> {
let mut out = Vec::new();
let mut word = String::new();
let mut quote = None;
let mut escaped = false;
for ch in input.chars() {
if escaped {
word.push(ch);
escaped = false;
continue;
}
if ch == '\\' && quote != Some('\'') {
escaped = true;
continue;
}
if let Some(q) = quote {
if ch == q {
quote = None;
} else {
word.push(ch);
}
} else if ch == '\'' || ch == '"' {
quote = Some(ch);
} else if ch.is_whitespace() {
if !word.is_empty() {
out.push(std::mem::take(&mut word));
}
} else {
word.push(ch);
}
}
if escaped {
word.push('\\');
}
if !word.is_empty() {
out.push(word);
}
out
}
fn install_style() {
let css = r#"
.backdrop { background: #151515; color: #d0d0d0; font-family: "Maple Mono Normal"; font-size: 9pt; }
.card { background: #151515; border: 1px solid #505050; border-radius: 1px; padding: 18px; }
.field-label { color: #505050; font-size: 9pt; }
entry, combobox button { color: #d0d0d0; background: #151515; border: 1px solid #505050; border-radius: 1px; min-height: 36px; padding: 0 10px; caret-color: #f5f5f5; }
entry:focus, combobox button:focus { border-color: #6c99bb; }
entry placeholder { color: #505050; }
button.login-button { color: #d0d0d0; background: #151515; border: 1px solid #505050; border-radius: 1px; min-height: 36px; margin-top: 2px; }
button.login-button:hover { color: #f5f5f5; background: #547c99; }
button.login-button:focus { border-color: #6c99bb; }
button.login-button:disabled { color: #505050; background: #151515; border-color: #505050; }
.status { color: #d3a04d; font-size: 9pt; }
"#;
let provider = gtk::CssProvider::new();
provider.load_from_data(css);
if let Some(display) = gtk::gdk::Display::default() {
gtk::style_context_add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}