feat: initial project with CPU-rendered ring light overlay
Wayland layer-shell overlay using tiny-skia for rendering a glowing rounded rectangle ring. Includes PID-based lock file toggle via SIGUSR1, keyboard capture (Escape to exit), and pointer tracking for cursor avoidance. Configurable ring geometry and glow parameters.
This commit is contained in:
7 files changed
+1484
No files matched your search
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+697
@@ -0,0 +1,697 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "ringa"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nix",
|
||||
"tempfile",
|
||||
"tiny-skia",
|
||||
"wayland-client",
|
||||
"wayland-protocols-wlr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[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.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"rustix",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[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 = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ringa"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Wayland ring light overlay"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
rust-version = "1.85"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
nix = { version = "0.29", features = ["signal", "process", "poll"] }
|
||||
tempfile = "3"
|
||||
tiny-skia = "0.12.0"
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
//! PID-based lock file for toggle-on/toggle-off behavior.
|
||||
//!
|
||||
//! The first invocation creates a lock file and runs the overlay.
|
||||
//! A second invocation sees the lock, sends SIGUSR1 to the running
|
||||
//! instance, and exits immediately.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
extern "C" fn handle_sigusr1(_: nix::libc::c_int) {
|
||||
SHOULD_EXIT.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub enum ToggleResult {
|
||||
RunNew,
|
||||
SignaledExisting,
|
||||
}
|
||||
|
||||
/// Check if another instance is running and signal it, or indicate we should run.
|
||||
pub fn try_toggle() -> Result<ToggleResult> {
|
||||
let path = lock_file_path();
|
||||
|
||||
let contents = match fs::read_to_string(&path) {
|
||||
Ok(c) => c,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(ToggleResult::RunNew),
|
||||
Err(e) => return Err(e).context("read lock file"),
|
||||
};
|
||||
|
||||
let pid: i32 = contents
|
||||
.trim()
|
||||
.parse()
|
||||
.context("parse PID from lock file")?;
|
||||
let pid = Pid::from_raw(pid);
|
||||
|
||||
// Check if the process is alive.
|
||||
match signal::kill(pid, None) {
|
||||
Ok(()) => {}
|
||||
Err(nix::errno::Errno::ESRCH) => {
|
||||
// Stale lock — process is gone.
|
||||
let _ = fs::remove_file(&path);
|
||||
return Ok(ToggleResult::RunNew);
|
||||
}
|
||||
Err(e) => return Err(e).context("check if PID is alive"),
|
||||
}
|
||||
|
||||
// Process is alive — send SIGUSR1 to tell it to exit.
|
||||
match signal::kill(pid, Signal::SIGUSR1) {
|
||||
Ok(()) => Ok(ToggleResult::SignaledExisting),
|
||||
Err(nix::errno::Errno::ESRCH) => {
|
||||
// Raced — process died between check and signal.
|
||||
let _ = fs::remove_file(&path);
|
||||
Ok(ToggleResult::RunNew)
|
||||
}
|
||||
Err(e) => Err(e).context("send SIGUSR1"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if SIGUSR1 has been received.
|
||||
pub fn should_exit() -> bool {
|
||||
SHOULD_EXIT.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Create the lock file and install the SIGUSR1 handler. Returns a guard
|
||||
/// that removes the lock file on drop.
|
||||
pub fn create_lock() -> Result<LockGuard> {
|
||||
let path = lock_file_path();
|
||||
|
||||
// Install SIGUSR1 handler before writing the lock file.
|
||||
let action = SigAction::new(
|
||||
SigHandler::Handler(handle_sigusr1),
|
||||
SaFlags::SA_RESTART,
|
||||
SigSet::empty(),
|
||||
);
|
||||
unsafe { signal::sigaction(Signal::SIGUSR1, &action) }.context("install SIGUSR1 handler")?;
|
||||
|
||||
let pid = std::process::id();
|
||||
fs::write(&path, pid.to_string()).context("write lock file")?;
|
||||
|
||||
Ok(LockGuard { path })
|
||||
}
|
||||
|
||||
pub struct LockGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for LockGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_file_path() -> PathBuf {
|
||||
if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
PathBuf::from(dir).join("ringa.lock")
|
||||
} else {
|
||||
PathBuf::from("/tmp/ringa.lock")
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
mod lock;
|
||||
mod render;
|
||||
mod wayland;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tiny_skia::Pixmap;
|
||||
|
||||
use lock::ToggleResult;
|
||||
use wayland::{OverlayEvent, WaylandBackend};
|
||||
|
||||
const DISPLAY_DURATION: Duration = Duration::from_secs(5);
|
||||
|
||||
fn main() -> Result<()> {
|
||||
match lock::try_toggle()? {
|
||||
ToggleResult::SignaledExisting => return Ok(()),
|
||||
ToggleResult::RunNew => {}
|
||||
}
|
||||
|
||||
let _lock = lock::create_lock()?;
|
||||
|
||||
let mut wl = WaylandBackend::new()?;
|
||||
let (w, h) = wl.screen_size();
|
||||
let mut pixmap = Pixmap::new(w, h).context("create pixmap")?;
|
||||
let mut argb_buf = vec![0u8; (w as usize) * (h as usize) * 4];
|
||||
|
||||
let mut cursor_x = w as f64 / 2.0;
|
||||
let mut cursor_y = h as f64 / 2.0;
|
||||
|
||||
draw(&mut wl, &mut pixmap, &mut argb_buf, cursor_x, cursor_y)?;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
let elapsed = start.elapsed();
|
||||
if elapsed >= DISPLAY_DURATION || lock::should_exit() {
|
||||
break;
|
||||
}
|
||||
let remaining = DISPLAY_DURATION - elapsed;
|
||||
let event = match wl.next_event(remaining)? {
|
||||
Some(ev) => ev,
|
||||
None => continue, // poll timeout — loop back to check elapsed time
|
||||
};
|
||||
match event {
|
||||
OverlayEvent::Close => break,
|
||||
OverlayEvent::CursorMoved(x, y) => {
|
||||
cursor_x = x;
|
||||
cursor_y = y;
|
||||
draw(&mut wl, &mut pixmap, &mut argb_buf, cursor_x, cursor_y)?;
|
||||
}
|
||||
OverlayEvent::Configure(new_w, new_h) => {
|
||||
if new_w != w || new_h != h {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(
|
||||
wl: &mut WaylandBackend,
|
||||
pixmap: &mut Pixmap,
|
||||
argb_buf: &mut [u8],
|
||||
cursor_x: f64,
|
||||
cursor_y: f64,
|
||||
) -> Result<()> {
|
||||
render::render_ring_light(pixmap, cursor_x, cursor_y);
|
||||
render::pixmap_to_argb8888(pixmap, argb_buf);
|
||||
let (w, h) = wl.screen_size();
|
||||
wl.present(argb_buf, w, h)
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
//! Ring light glow rendering.
|
||||
//!
|
||||
//! The ring is a thick stroke centered on a single rounded rectangle SDF.
|
||||
//! This guarantees uniform width everywhere, including at corners.
|
||||
//! Glow bleeds outward and inward from the stroke edges.
|
||||
|
||||
use tiny_skia::Pixmap;
|
||||
|
||||
// The rounded rect centerline is inset this far from the screen edge.
|
||||
// The visible ring extends RING_HALF_WIDTH on either side of this line.
|
||||
const CENTERLINE_INSET: f32 = 80.0;
|
||||
// Half the stroke width. Total ring thickness = 2 * RING_HALF_WIDTH.
|
||||
const RING_HALF_WIDTH: f32 = 30.0;
|
||||
// Corner radius of the centerline rounded rect.
|
||||
const CORNER_RADIUS: f32 = 30.0;
|
||||
|
||||
// Soft glow spread outward and inward from the ring edges.
|
||||
const GLOW_SPREAD: f32 = 50.0;
|
||||
const RING_ALPHA: f32 = 1.00;
|
||||
|
||||
// Cursor avoidance.
|
||||
const CURSOR_AVOID_RADIUS: f32 = 120.0;
|
||||
const CURSOR_AVOID_FALLOFF: f32 = 60.0;
|
||||
|
||||
/// Render the ring light glow into `pixmap`, avoiding the cursor at `(cx, cy)`.
|
||||
pub fn render_ring_light(pixmap: &mut Pixmap, cx: f64, cy: f64) {
|
||||
let w = pixmap.width() as f32;
|
||||
let h = pixmap.height() as f32;
|
||||
let cx = cx as f32;
|
||||
let cy = cy as f32;
|
||||
let wi = pixmap.width() as usize;
|
||||
let hi = pixmap.height() as usize;
|
||||
|
||||
let data = pixmap.data_mut();
|
||||
data.fill(0);
|
||||
|
||||
let screen_cx = w / 2.0;
|
||||
let screen_cy = h / 2.0;
|
||||
|
||||
// Single rounded rect centerline.
|
||||
let hw = w / 2.0 - CENTERLINE_INSET;
|
||||
let hh = h / 2.0 - CENTERLINE_INSET;
|
||||
|
||||
// The affected band: from (CENTERLINE_INSET - RING_HALF_WIDTH - GLOW_SPREAD)
|
||||
// to (CENTERLINE_INSET + RING_HALF_WIDTH + GLOW_SPREAD) from each screen edge.
|
||||
let band_start = (CENTERLINE_INSET - RING_HALF_WIDTH - GLOW_SPREAD)
|
||||
.max(0.0)
|
||||
.floor() as usize;
|
||||
let band_end = (CENTERLINE_INSET + RING_HALF_WIDTH + GLOW_SPREAD).ceil() as usize + 1;
|
||||
|
||||
// Top band
|
||||
for py in band_start..band_end.min(hi) {
|
||||
for px in 0..wi {
|
||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
||||
write_pixel(data, py * wi + px, a);
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom band
|
||||
let bottom_start = hi.saturating_sub(band_end);
|
||||
let bottom_end = hi.saturating_sub(band_start);
|
||||
for py in bottom_start..bottom_end.min(hi) {
|
||||
if py < band_end {
|
||||
continue;
|
||||
}
|
||||
for px in 0..wi {
|
||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
||||
write_pixel(data, py * wi + px, a);
|
||||
}
|
||||
}
|
||||
|
||||
// Left band (between top and bottom)
|
||||
let vert_start = band_end.min(hi);
|
||||
let vert_end = hi.saturating_sub(band_end);
|
||||
for py in vert_start..vert_end {
|
||||
for px in band_start..band_end.min(wi) {
|
||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
||||
write_pixel(data, py * wi + px, a);
|
||||
}
|
||||
}
|
||||
|
||||
// Right band (between top and bottom)
|
||||
let right_start = wi.saturating_sub(band_end);
|
||||
let right_end = wi.saturating_sub(band_start);
|
||||
for py in vert_start..vert_end {
|
||||
for px in right_start..right_end.min(wi) {
|
||||
if px < band_end {
|
||||
continue;
|
||||
}
|
||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
||||
write_pixel(data, py * wi + px, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a tiny-skia `Pixmap` (premultiplied RGBA) to Wayland's ARGB8888
|
||||
/// format (premultiplied BGRA in memory on little-endian).
|
||||
pub fn pixmap_to_argb8888(pixmap: &Pixmap, out: &mut [u8]) {
|
||||
let src = pixmap.data();
|
||||
assert_eq!(out.len(), src.len());
|
||||
for (dst, src) in out.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
|
||||
dst[0] = src[2]; // B
|
||||
dst[1] = src[1]; // G
|
||||
dst[2] = src[0]; // R
|
||||
dst[3] = src[3]; // A
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Private helpers --------------------------------------------------------
|
||||
|
||||
/// Compute glow alpha for a single pixel using a single rounded rect SDF.
|
||||
///
|
||||
/// The ring is all pixels where |sdf| <= RING_HALF_WIDTH (a thick stroke).
|
||||
/// Glow fades from the ring edges outward/inward over GLOW_SPREAD pixels.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn pixel_alpha(
|
||||
px: f32,
|
||||
py: f32,
|
||||
scx: f32,
|
||||
scy: f32,
|
||||
hw: f32,
|
||||
hh: f32,
|
||||
cursor_x: f32,
|
||||
cursor_y: f32,
|
||||
) -> u8 {
|
||||
// Signed distance to the rounded rect centerline.
|
||||
let sd = rounded_rect_sdf(px, py, scx, scy, hw, hh, CORNER_RADIUS);
|
||||
let abs_sd = sd.abs();
|
||||
|
||||
let glow = if abs_sd <= RING_HALF_WIDTH {
|
||||
// Inside the ring stroke — full brightness.
|
||||
RING_ALPHA
|
||||
} else {
|
||||
// Distance from the nearest ring edge.
|
||||
let dist = abs_sd - RING_HALF_WIDTH;
|
||||
if dist >= GLOW_SPREAD {
|
||||
return 0;
|
||||
}
|
||||
let t = dist / GLOW_SPREAD;
|
||||
smoothstep(1.0 - t) * RING_ALPHA
|
||||
};
|
||||
|
||||
// Cursor avoidance.
|
||||
let dx = px - cursor_x;
|
||||
let dy = py - cursor_y;
|
||||
let cursor_dist = (dx * dx + dy * dy).sqrt();
|
||||
let cursor_factor =
|
||||
smoothstep(((cursor_dist - CURSOR_AVOID_RADIUS) / CURSOR_AVOID_FALLOFF).clamp(0.0, 1.0));
|
||||
|
||||
let alpha = glow * cursor_factor;
|
||||
(alpha * 255.0) as u8
|
||||
}
|
||||
|
||||
/// Signed distance field for a rounded rectangle.
|
||||
///
|
||||
/// Returns negative inside, positive outside, zero on the boundary.
|
||||
fn rounded_rect_sdf(px: f32, py: f32, cx: f32, cy: f32, hw: f32, hh: f32, r: f32) -> f32 {
|
||||
let dx = (px - cx).abs() - (hw - r);
|
||||
let dy = (py - cy).abs() - (hh - r);
|
||||
let outside = dx.max(0.0);
|
||||
let outside_y = dy.max(0.0);
|
||||
let outside_len = (outside * outside + outside_y * outside_y).sqrt();
|
||||
let inside = dx.max(dy).min(0.0);
|
||||
outside_len + inside - r
|
||||
}
|
||||
|
||||
/// Write a premultiplied white pixel with the given alpha.
|
||||
fn write_pixel(data: &mut [u8], idx: usize, alpha: u8) {
|
||||
if alpha == 0 {
|
||||
return;
|
||||
}
|
||||
let offset = idx * 4;
|
||||
data[offset] = alpha;
|
||||
data[offset + 1] = alpha;
|
||||
data[offset + 2] = alpha;
|
||||
data[offset + 3] = alpha;
|
||||
}
|
||||
|
||||
/// Hermite smoothstep: 3t^2 - 2t^3
|
||||
fn smoothstep(t: f32) -> f32 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
//! Wayland backend: layer-shell overlay with keyboard and pointer tracking.
|
||||
//!
|
||||
//! Creates a full-screen layer-shell surface for the ring light overlay.
|
||||
//! Tracks pointer position for cursor avoidance and captures keyboard
|
||||
//! for Escape to exit.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
use std::os::fd::AsFd;
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_client::{
|
||||
delegate_noop,
|
||||
protocol::{
|
||||
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat,
|
||||
wl_shm, wl_shm_pool, wl_surface,
|
||||
},
|
||||
Connection, Dispatch, EventQueue, QueueHandle, WEnum,
|
||||
};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
// Linux evdev key code for Escape.
|
||||
const KEY_ESCAPE: u32 = 1;
|
||||
|
||||
/// Events emitted by the Wayland backend to the main loop.
|
||||
pub enum OverlayEvent {
|
||||
/// Escape pressed or surface closed by compositor.
|
||||
Close,
|
||||
/// Pointer moved to (x, y) in surface-local coordinates.
|
||||
CursorMoved(f64, f64),
|
||||
/// Surface (re-)configured with new dimensions.
|
||||
Configure(u32, u32),
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WaylandBackend — public API.
|
||||
// ===========================================================================
|
||||
|
||||
pub struct WaylandBackend {
|
||||
state: WaylandState,
|
||||
event_queue: EventQueue<WaylandState>,
|
||||
queue_handle: QueueHandle<WaylandState>,
|
||||
}
|
||||
|
||||
impl WaylandBackend {
|
||||
pub fn new() -> Result<Self> {
|
||||
let conn = Connection::connect_to_env().context("connect to Wayland display")?;
|
||||
let mut event_queue = conn.new_event_queue();
|
||||
let queue_handle = event_queue.handle();
|
||||
|
||||
conn.display().get_registry(&queue_handle, ());
|
||||
|
||||
let mut state = WaylandState {
|
||||
conn,
|
||||
compositor: None,
|
||||
shm: None,
|
||||
surface: None,
|
||||
layer_shell: None,
|
||||
layer_surface: None,
|
||||
seat: None,
|
||||
screen_w: 0,
|
||||
screen_h: 0,
|
||||
configured: false,
|
||||
pending_events: VecDeque::new(),
|
||||
};
|
||||
|
||||
event_queue
|
||||
.roundtrip(&mut state)
|
||||
.context("initial roundtrip")?;
|
||||
|
||||
if state.compositor.is_none() {
|
||||
anyhow::bail!("compositor does not advertise wl_compositor");
|
||||
}
|
||||
if state.shm.is_none() {
|
||||
anyhow::bail!("compositor does not advertise wl_shm");
|
||||
}
|
||||
if state.layer_shell.is_none() {
|
||||
anyhow::bail!(
|
||||
"compositor does not support wlr-layer-shell-unstable-v1. \
|
||||
Supported compositors: Sway, Hyprland, river, etc."
|
||||
);
|
||||
}
|
||||
|
||||
state.init_layer_surface(&queue_handle)?;
|
||||
event_queue
|
||||
.roundtrip(&mut state)
|
||||
.context("roundtrip after layer surface")?;
|
||||
|
||||
while !state.configured {
|
||||
event_queue
|
||||
.blocking_dispatch(&mut state)
|
||||
.context("waiting for configure")?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
event_queue,
|
||||
queue_handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn screen_size(&self) -> (u32, u32) {
|
||||
(self.state.screen_w, self.state.screen_h)
|
||||
}
|
||||
|
||||
/// Wait up to `timeout` for the next event. Returns `None` on timeout
|
||||
/// or if the surface has been torn down.
|
||||
pub fn next_event(&mut self, timeout: Duration) -> Result<Option<OverlayEvent>> {
|
||||
// First drain any already-buffered events.
|
||||
if let Some(ev) = self.state.pending_events.pop_front() {
|
||||
return Ok(Some(ev));
|
||||
}
|
||||
if self.state.surface.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Flush outgoing requests, then poll the fd with a timeout.
|
||||
self.event_queue
|
||||
.flush()
|
||||
.context("flush before poll")?;
|
||||
|
||||
let read_guard = self.event_queue.prepare_read().unwrap();
|
||||
let fd = read_guard.connection_fd();
|
||||
let mut pollfd = [nix::poll::PollFd::new(fd, nix::poll::PollFlags::POLLIN)];
|
||||
let millis = timeout.as_millis().min(u16::MAX as u128) as u16;
|
||||
let n = nix::poll::poll(&mut pollfd, millis).context("poll")?;
|
||||
|
||||
if n > 0 {
|
||||
// Data available — read and dispatch.
|
||||
let _ = read_guard.read();
|
||||
} else {
|
||||
// Timeout — drop the read guard (cancels the read).
|
||||
drop(read_guard);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.event_queue
|
||||
.dispatch_pending(&mut self.state)
|
||||
.context("dispatch_pending")?;
|
||||
|
||||
Ok(self.state.pending_events.pop_front())
|
||||
}
|
||||
|
||||
/// Copy an ARGB8888 pixel buffer to the overlay surface via wl_shm.
|
||||
pub fn present(&mut self, pixels: &[u8], width: u32, height: u32) -> Result<()> {
|
||||
let shm = self.state.shm.as_ref().context("wl_shm not available")?;
|
||||
let surface = self
|
||||
.state
|
||||
.surface
|
||||
.as_ref()
|
||||
.context("wl_surface not available")?;
|
||||
|
||||
let stride = width * 4;
|
||||
let mut file = tempfile::tempfile().context("create shm tempfile")?;
|
||||
file.write_all(pixels).context("write pixel buffer")?;
|
||||
|
||||
let pool = shm.create_pool(file.as_fd(), pixels.len() as i32, &self.queue_handle, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
width as i32,
|
||||
height as i32,
|
||||
stride as i32,
|
||||
wl_shm::Format::Argb8888,
|
||||
&self.queue_handle,
|
||||
(),
|
||||
);
|
||||
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.damage_buffer(0, 0, width as i32, height as i32);
|
||||
surface.commit();
|
||||
pool.destroy();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WaylandBackend {
|
||||
fn drop(&mut self) {
|
||||
if let Some(ls) = self.state.layer_surface.take() {
|
||||
ls.destroy();
|
||||
}
|
||||
if let Some(s) = self.state.surface.take() {
|
||||
s.destroy();
|
||||
}
|
||||
let _ = self.state.conn.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Internal state.
|
||||
// ===========================================================================
|
||||
|
||||
struct WaylandState {
|
||||
conn: Connection,
|
||||
|
||||
compositor: Option<wl_compositor::WlCompositor>,
|
||||
shm: Option<wl_shm::WlShm>,
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
|
||||
surface: Option<wl_surface::WlSurface>,
|
||||
layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
|
||||
|
||||
screen_w: u32,
|
||||
screen_h: u32,
|
||||
configured: bool,
|
||||
|
||||
pending_events: VecDeque<OverlayEvent>,
|
||||
}
|
||||
|
||||
impl WaylandState {
|
||||
fn init_layer_surface(&mut self, qh: &QueueHandle<Self>) -> Result<()> {
|
||||
let compositor = self
|
||||
.compositor
|
||||
.as_ref()
|
||||
.context("wl_compositor not available")?;
|
||||
let layer_shell = self
|
||||
.layer_shell
|
||||
.as_ref()
|
||||
.context("zwlr_layer_shell_v1 not available")?;
|
||||
|
||||
let surface = compositor.create_surface(qh, ());
|
||||
let layer_surface = layer_shell.get_layer_surface(
|
||||
&surface,
|
||||
None,
|
||||
zwlr_layer_shell_v1::Layer::Overlay,
|
||||
"ringa".to_string(),
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
|
||||
layer_surface.set_size(0, 0);
|
||||
layer_surface.set_anchor(
|
||||
zwlr_layer_surface_v1::Anchor::Top
|
||||
| zwlr_layer_surface_v1::Anchor::Bottom
|
||||
| zwlr_layer_surface_v1::Anchor::Left
|
||||
| zwlr_layer_surface_v1::Anchor::Right,
|
||||
);
|
||||
layer_surface.set_exclusive_zone(-1);
|
||||
layer_surface
|
||||
.set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive);
|
||||
|
||||
surface.commit();
|
||||
|
||||
self.surface = Some(surface);
|
||||
self.layer_surface = Some(layer_surface);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Wayland dispatch implementations.
|
||||
// ===========================================================================
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match interface.as_str() {
|
||||
"wl_compositor" => {
|
||||
state.compositor = Some(registry.bind(name, version.min(4), qh, ()));
|
||||
}
|
||||
"wl_shm" => {
|
||||
state.shm = Some(registry.bind(name, 1, qh, ()));
|
||||
}
|
||||
"wl_seat" => {
|
||||
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
||||
}
|
||||
"zwlr_layer_shell_v1" => {
|
||||
state.layer_shell = Some(registry.bind(name, version.min(4), qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_seat::WlSeat, ()> for WaylandState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: wl_seat::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_seat::Event::Capabilities {
|
||||
capabilities: WEnum::Value(caps),
|
||||
} = event
|
||||
{
|
||||
if caps.contains(wl_seat::Capability::Keyboard) {
|
||||
seat.get_keyboard(qh, ());
|
||||
}
|
||||
if caps.contains(wl_seat::Capability::Pointer) {
|
||||
seat.get_pointer(qh, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_keyboard::Event::Key {
|
||||
key,
|
||||
state: WEnum::Value(wl_keyboard::KeyState::Pressed),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if key == KEY_ESCAPE {
|
||||
state.pending_events.push_back(OverlayEvent::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_pointer::WlPointer, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &wl_pointer::WlPointer,
|
||||
event: wl_pointer::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_pointer::Event::Enter {
|
||||
surface_x,
|
||||
surface_y,
|
||||
..
|
||||
} => {
|
||||
state
|
||||
.pending_events
|
||||
.push_back(OverlayEvent::CursorMoved(surface_x, surface_y));
|
||||
}
|
||||
wl_pointer::Event::Motion {
|
||||
surface_x,
|
||||
surface_y,
|
||||
..
|
||||
} => {
|
||||
state
|
||||
.pending_events
|
||||
.push_back(OverlayEvent::CursorMoved(surface_x, surface_y));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
event: zwlr_layer_surface_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zwlr_layer_surface_v1::Event::Configure {
|
||||
serial,
|
||||
width,
|
||||
height,
|
||||
} => {
|
||||
layer_surface.ack_configure(serial);
|
||||
const MAX_SCREEN_DIM: u32 = 16384;
|
||||
if width > 0 && height > 0 && width <= MAX_SCREEN_DIM && height <= MAX_SCREEN_DIM {
|
||||
state.screen_w = width;
|
||||
state.screen_h = height;
|
||||
state.configured = true;
|
||||
state
|
||||
.pending_events
|
||||
.push_back(OverlayEvent::Configure(width, height));
|
||||
}
|
||||
}
|
||||
zwlr_layer_surface_v1::Event::Closed => {
|
||||
state.pending_events.push_back(OverlayEvent::Close);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(WaylandState: ignore wl_compositor::WlCompositor);
|
||||
delegate_noop!(WaylandState: ignore wl_region::WlRegion);
|
||||
delegate_noop!(WaylandState: ignore wl_surface::WlSurface);
|
||||
delegate_noop!(WaylandState: ignore wl_shm::WlShm);
|
||||
delegate_noop!(WaylandState: ignore wl_shm_pool::WlShmPool);
|
||||
delegate_noop!(WaylandState: ignore wl_buffer::WlBuffer);
|
||||
delegate_noop!(WaylandState: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
|
||||
Reference in new issue
Block a user