max / alloy_tui
21 files changed,
+437 insertions,
-1181 deletions
| @@ -22,8 +22,8 @@ | |||
| 22 | 22 | ||
| 23 | 23 | ## Code style (when code lands) | |
| 24 | 24 | ||
| 25 | - | - The egui design-system crate is `alloy_ui`. Token constants and themed widgets live there; downstream apps re-export rather than redefine. | |
| 26 | - | - Tokens are the source of truth for color, type, spacing, and radius. See [`docs/TOKENS.md`](docs/TOKENS.md). No hardcoded color or pixel values in widget code — pull from tokens. | |
| 25 | + | - The ratatui design-system crate is `alloy_tui`. Token constants and themed widgets live there; downstream apps re-export rather than redefine. | |
| 26 | + | - Tokens are the source of truth for color and font-family hints. See [`docs/TOKENS.md`](docs/TOKENS.md). No hardcoded colors in widget code — pull from tokens. | |
| 27 | 27 | - Prose surfaces stay in the working monospace. Mono-only is part of the brand. See [`docs/TOKENS.md`](docs/TOKENS.md). | |
| 28 | 28 | - Color is reserved for information. Chrome is tinted-greyscale. See [`docs/DESIGN-LANGUAGE.md`](docs/DESIGN-LANGUAGE.md). | |
| 29 | 29 |
| @@ -3,127 +3,70 @@ | |||
| 3 | 3 | version = 4 | |
| 4 | 4 | ||
| 5 | 5 | [[package]] | |
| 6 | - | name = "accesskit" | |
| 7 | - | version = "0.24.1" | |
| 6 | + | name = "aho-corasick" | |
| 7 | + | version = "1.1.4" | |
| 8 | 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | - | checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" | |
| 9 | + | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" | |
| 10 | 10 | dependencies = [ | |
| 11 | - | "uuid", | |
| 11 | + | "memchr", | |
| 12 | 12 | ] | |
| 13 | 13 | ||
| 14 | 14 | [[package]] | |
| 15 | - | name = "adler2" | |
| 16 | - | version = "2.0.1" | |
| 15 | + | name = "allocator-api2" | |
| 16 | + | version = "0.2.21" | |
| 17 | 17 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 18 | - | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" | |
| 18 | + | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 19 | 19 | ||
| 20 | 20 | [[package]] | |
| 21 | - | name = "ahash" | |
| 22 | - | version = "0.8.12" | |
| 23 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 24 | - | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" | |
| 25 | - | dependencies = [ | |
| 26 | - | "cfg-if", | |
| 27 | - | "once_cell", | |
| 28 | - | "version_check", | |
| 29 | - | "zerocopy", | |
| 30 | - | ] | |
| 31 | - | ||
| 32 | - | [[package]] | |
| 33 | - | name = "alloy_lockscreen" | |
| 21 | + | name = "alloy_tui" | |
| 34 | 22 | version = "0.0.0" | |
| 35 | 23 | dependencies = [ | |
| 36 | - | "alloy_ui", | |
| 37 | - | "egui", | |
| 38 | - | "egui_glow", | |
| 39 | - | "pam-sys", | |
| 40 | - | "smithay-client-toolkit", | |
| 41 | - | "tokio", | |
| 24 | + | "ratatui", | |
| 25 | + | "theme-common", | |
| 42 | 26 | ] | |
| 43 | 27 | ||
| 44 | 28 | [[package]] | |
| 45 | - | name = "alloy_ui" | |
| 46 | - | version = "0.0.0" | |
| 47 | - | dependencies = [ | |
| 48 | - | "eframe", | |
| 49 | - | "egui", | |
| 50 | - | "palette", | |
| 51 | - | "serde", | |
| 52 | - | "toml", | |
| 53 | - | ] | |
| 29 | + | name = "anyhow" | |
| 30 | + | version = "1.0.103" | |
| 31 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 32 | + | checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" | |
| 54 | 33 | ||
| 55 | 34 | [[package]] | |
| 56 | - | name = "android-activity" | |
| 35 | + | name = "atomic" | |
| 57 | 36 | version = "0.6.1" | |
| 58 | 37 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 59 | - | checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" | |
| 38 | + | checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" | |
| 60 | 39 | dependencies = [ | |
| 61 | - | "android-properties", | |
| 62 | - | "bitflags 2.13.0", | |
| 63 | - | "cc", | |
| 64 | - | "jni", | |
| 65 | - | "libc", | |
| 66 | - | "log", | |
| 67 | - | "ndk", | |
| 68 | - | "ndk-context", | |
| 69 | - | "ndk-sys", | |
| 70 | - | "num_enum", | |
| 71 | - | "thiserror 2.0.18", | |
| 40 | + | "bytemuck", | |
| 72 | 41 | ] | |
| 73 | 42 | ||
| 74 | - | [[package]] | |
| 75 | - | name = "android-properties" | |
| 76 | - | version = "0.2.2" | |
| 77 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 78 | - | checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" | |
| 79 | - | ||
| 80 | - | [[package]] | |
| 81 | - | name = "approx" | |
| 82 | - | version = "0.5.1" | |
| 83 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 84 | - | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" | |
| 85 | - | dependencies = [ | |
| 86 | - | "num-traits", | |
| 87 | - | ] | |
| 88 | - | ||
| 89 | - | [[package]] | |
| 90 | - | name = "arboard" | |
| 91 | - | version = "3.6.1" | |
| 92 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 93 | - | checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" | |
| 94 | - | dependencies = [ | |
| 95 | - | "clipboard-win", | |
| 96 | - | "image", | |
| 97 | - | "log", | |
| 98 | - | "objc2 0.6.4", | |
| 99 | - | "objc2-app-kit 0.3.2", | |
| 100 | - | "objc2-core-foundation", | |
| 101 | - | "objc2-core-graphics", | |
| 102 | - | "objc2-foundation 0.3.2", | |
| 103 | - | "parking_lot", | |
| 104 | - | "percent-encoding", | |
| 105 | - | "windows-sys 0.60.2", | |
| 106 | - | "x11rb", | |
| 107 | - | ] | |
| 108 | - | ||
| 109 | - | [[package]] | |
| 110 | - | name = "arrayvec" | |
| 111 | - | version = "0.7.7" | |
| 112 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 113 | - | checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" | |
| 114 | - | ||
| 115 | - | [[package]] | |
| 116 | - | name = "atomic-waker" | |
| 117 | - | version = "1.1.2" | |
| 118 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 119 | - | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | |
| 120 | - | ||
| 121 | 43 | [[package]] | |
| 122 | 44 | name = "autocfg" | |
| 123 | 45 | version = "1.5.1" | |
| 124 | 46 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 125 | 47 | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | |
| 126 | 48 | ||
| 49 | + | [[package]] | |
| 50 | + | name = "base64" | |
| 51 | + | version = "0.22.1" | |
| 52 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 53 | + | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | |
| 54 | + | ||
| 55 | + | [[package]] | |
| 56 | + | name = "bit-set" | |
| 57 | + | version = "0.5.3" | |
| 58 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 59 | + | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" | |
| 60 | + | dependencies = [ | |
| 61 | + | "bit-vec", | |
| 62 | + | ] | |
| 63 | + | ||
| 64 | + | [[package]] | |
| 65 | + | name = "bit-vec" | |
| 66 | + | version = "0.6.3" | |
| 67 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 68 | + | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" | |
| 69 | + | ||
| 127 | 70 | [[package]] | |
| 128 | 71 | name = "bitflags" | |
| 129 | 72 | version = "1.3.2" | |
| @@ -137,21 +80,12 @@ | |||
| 137 | 80 | checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" | |
| 138 | 81 | ||
| 139 | 82 | [[package]] | |
| 140 | - | name = "block2" | |
| 141 | - | version = "0.5.1" | |
| 83 | + | name = "block-buffer" | |
| 84 | + | version = "0.10.4" | |
| 142 | 85 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 143 | - | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" | |
| 86 | + | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | |
| 144 | 87 | dependencies = [ | |
| 145 | - | "objc2 0.5.2", | |
| 146 | - | ] | |
| 147 | - | ||
| 148 | - | [[package]] | |
| 149 | - | name = "block2" | |
| 150 | - | version = "0.6.2" | |
| 151 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 152 | - | checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" | |
| 153 | - | dependencies = [ | |
| 154 | - | "objc2 0.6.4", | |
| 88 | + | "generic-array", | |
| 155 | 89 | ] | |
| 156 | 90 | ||
| 157 | 91 | [[package]] | |
| @@ -160,93 +94,19 @@ | |||
| 160 | 94 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 161 | 95 | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" | |
| 162 | 96 | ||
| 163 | - | [[package]] | |
| 164 | - | name = "by_address" | |
| 165 | - | version = "1.2.1" | |
| 166 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 167 | - | checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" | |
| 168 | - | ||
| 169 | 97 | [[package]] | |
| 170 | 98 | name = "bytemuck" | |
| 171 | 99 | version = "1.25.0" | |
| 172 | 100 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 173 | 101 | checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" | |
| 102 | + | ||
| 103 | + | [[package]] | |
| 104 | + | name = "castaway" | |
| 105 | + | version = "0.2.4" | |
| 106 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 107 | + | checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" | |
| 174 | 108 | dependencies = [ | |
| 175 | - | "bytemuck_derive", | |
| 176 | - | ] | |
| 177 | - | ||
| 178 | - | [[package]] | |
| 179 | - | name = "bytemuck_derive" | |
| 180 | - | version = "1.10.2" | |
| 181 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 182 | - | checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" | |
| 183 | - | dependencies = [ | |
| 184 | - | "proc-macro2", | |
| 185 | - | "quote", | |
| 186 | - | "syn", | |
| 187 | - | ] | |
| 188 | - | ||
| 189 | - | [[package]] | |
| 190 | - | name = "byteorder-lite" | |
| 191 | - | version = "0.1.0" | |
| 192 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 193 | - | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" | |
| 194 | - | ||
| 195 | - | [[package]] | |
| 196 | - | name = "bytes" | |
| 197 | - | version = "1.12.0" | |
| 198 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 199 | - | checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" | |
| 200 | - | ||
| 201 | - | [[package]] | |
| 202 | - | name = "calloop" | |
| 203 | - | version = "0.13.0" | |
| 204 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 205 | - | checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" | |
| 206 | - | dependencies = [ | |
| 207 | - | "bitflags 2.13.0", | |
| 208 | - | "log", | |
| 209 | - | "polling", | |
| 210 | - | "rustix 0.38.44", | |
| 211 | - | "slab", | |
| 212 | - | "thiserror 1.0.69", | |
| 213 | - | ] | |
| 214 | - | ||
| 215 | - | [[package]] | |
| 216 | - | name = "calloop" | |
| 217 | - | version = "0.14.4" | |
| 218 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 219 | - | checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" | |
| 220 | - | dependencies = [ | |
| 221 | - | "bitflags 2.13.0", | |
| 222 | - | "polling", | |
| 223 | - | "rustix 1.1.4", | |
| 224 | - | "slab", | |
| 225 | - | "tracing", | |
| 226 | - | ] | |
| 227 | - | ||
| 228 | - | [[package]] | |
| 229 | - | name = "calloop-wayland-source" | |
| 230 | - | version = "0.4.1" | |
| 231 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 232 | - | checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" | |
| 233 | - | dependencies = [ | |
| 234 | - | "calloop 0.14.4", | |
| 235 | - | "rustix 1.1.4", | |
| 236 | - | "wayland-backend", | |
| 237 | - | "wayland-client", | |
| 238 | - | ] | |
| 239 | - | ||
| 240 | - | [[package]] | |
| 241 | - | name = "cc" | |
| 242 | - | version = "1.2.65" | |
| 243 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 244 | - | checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" | |
| 245 | - | dependencies = [ | |
| 246 | - | "find-msvc-tools", | |
| 247 | - | "jobserver", | |
| 248 | - | "libc", | |
| 249 | - | "shlex", | |
| 109 | + | "rustversion", | |
| 250 | 110 | ] | |
| 251 | 111 | ||
| 252 | 112 | [[package]] | |
| @@ -262,162 +122,164 @@ | |||
| 262 | 122 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" | |
| 263 | 123 | ||
| 264 | 124 | [[package]] | |
| 265 | - | name = "cgl" | |
| 266 | - | version = "0.3.2" | |
| 125 | + | name = "compact_str" | |
| 126 | + | version = "0.9.1" | |
| 267 | 127 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 268 | - | checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" | |
| 269 | - | dependencies = [ | |
| 270 | - | "libc", | |
| 271 | - | ] | |
| 272 | - | ||
| 273 | - | [[package]] | |
| 274 | - | name = "clipboard-win" | |
| 275 | - | version = "5.4.1" | |
| 276 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 277 | - | checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" | |
| 278 | - | dependencies = [ | |
| 279 | - | "error-code", | |
| 280 | - | ] | |
| 281 | - | ||
| 282 | - | [[package]] | |
| 283 | - | name = "color" | |
| 284 | - | version = "0.3.3" | |
| 285 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 286 | - | checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" | |
| 287 | - | dependencies = [ | |
| 288 | - | "bytemuck", | |
| 289 | - | ] | |
| 290 | - | ||
| 291 | - | [[package]] | |
| 292 | - | name = "combine" | |
| 293 | - | version = "4.6.7" | |
| 294 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 295 | - | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" | |
| 296 | - | dependencies = [ | |
| 297 | - | "bytes", | |
| 298 | - | "memchr", | |
| 299 | - | ] | |
| 300 | - | ||
| 301 | - | [[package]] | |
| 302 | - | name = "concurrent-queue" | |
| 303 | - | version = "2.5.0" | |
| 304 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 305 | - | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" | |
| 306 | - | dependencies = [ | |
| 307 | - | "crossbeam-utils", | |
| 308 | - | ] | |
| 309 | - | ||
| 310 | - | [[package]] | |
| 311 | - | name = "core-foundation" | |
| 312 | - | version = "0.9.4" | |
| 313 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 314 | - | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" | |
| 315 | - | dependencies = [ | |
| 316 | - | "core-foundation-sys", | |
| 317 | - | "libc", | |
| 318 | - | ] | |
| 319 | - | ||
| 320 | - | [[package]] | |
| 321 | - | name = "core-foundation" | |
| 322 | - | version = "0.10.1" | |
| 323 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 324 | - | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" | |
| 325 | - | dependencies = [ | |
| 326 | - | "core-foundation-sys", | |
| 327 | - | "libc", | |
| 328 | - | ] | |
| 329 | - | ||
| 330 | - | [[package]] | |
| 331 | - | name = "core-foundation-sys" | |
| 332 | - | version = "0.8.7" | |
| 333 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 334 | - | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | |
| 335 | - | ||
| 336 | - | [[package]] | |
| 337 | - | name = "core-graphics" | |
| 338 | - | version = "0.23.2" | |
| 339 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 340 | - | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" | |
| 341 | - | dependencies = [ | |
| 342 | - | "bitflags 1.3.2", | |
| 343 | - | "core-foundation 0.9.4", | |
| 344 | - | "core-graphics-types", | |
| 345 | - | "foreign-types", | |
| 346 | - | "libc", | |
| 347 | - | ] | |
| 348 | - | ||
| 349 | - | [[package]] | |
| 350 | - | name = "core-graphics-types" | |
| 351 | - | version = "0.1.3" | |
| 352 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 353 | - | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" | |
| 354 | - | dependencies = [ | |
| 355 | - | "bitflags 1.3.2", | |
| 356 | - | "core-foundation 0.9.4", | |
| 357 | - | "libc", | |
| 358 | - | ] | |
| 359 | - | ||
| 360 | - | [[package]] | |
| 361 | - | name = "crc32fast" | |
| 362 | - | version = "1.5.0" | |
| 363 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 364 | - | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" | |
| 128 | + | checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" | |
| 365 | 129 | dependencies = [ | |
| 130 | + | "castaway", | |
| 366 | 131 | "cfg-if", | |
| 132 | + | "itoa", | |
| 133 | + | "rustversion", | |
| 134 | + | "ryu", | |
| 135 | + | "static_assertions", | |
| 367 | 136 | ] | |
| 368 | 137 | ||
| 369 | 138 | [[package]] | |
| 370 | - | name = "crossbeam-utils" | |
| 371 | - | version = "0.8.21" | |
| 139 | + | name = "convert_case" | |
| 140 | + | version = "0.10.0" | |
| 372 | 141 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 373 | - | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" | |
| 142 | + | checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" | |
| 143 | + | dependencies = [ | |
| 144 | + | "unicode-segmentation", | |
| 145 | + | ] | |
| 374 | 146 | ||
| 375 | 147 | [[package]] | |
| 376 | - | name = "crunchy" | |
| 377 | - | version = "0.2.4" | |
| 148 | + | name = "cpufeatures" | |
| 149 | + | version = "0.2.17" | |
| 378 | 150 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 379 | - | checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" | |
| 151 | + | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | |
| 152 | + | dependencies = [ | |
| 153 | + | "libc", | |
| 154 | + | ] | |
| 380 | 155 | ||
| 381 | 156 | [[package]] | |
| 382 | - | name = "cursor-icon" | |
| 383 | - | version = "1.2.0" | |
| 157 | + | name = "crossterm" | |
| 158 | + | version = "0.29.0" | |
| 384 | 159 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 385 | - | checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" | |
| 386 | - | ||
| 387 | - | [[package]] | |
| 388 | - | name = "dispatch" | |
| 389 | - | version = "0.2.0" | |
| 390 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 391 | - | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" | |
| 392 | - | ||
| 393 | - | [[package]] | |
| 394 | - | name = "dispatch2" | |
| 395 | - | version = "0.3.1" | |
| 396 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 397 | - | checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" | |
| 160 | + | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" | |
| 398 | 161 | dependencies = [ | |
| 399 | 162 | "bitflags 2.13.0", | |
| 400 | - | "objc2 0.6.4", | |
| 163 | + | "crossterm_winapi", | |
| 164 | + | "derive_more", | |
| 165 | + | "document-features", | |
| 166 | + | "mio", | |
| 167 | + | "parking_lot", | |
| 168 | + | "rustix", | |
| 169 | + | "signal-hook", | |
| 170 | + | "signal-hook-mio", | |
| 171 | + | "winapi", | |
| 401 | 172 | ] | |
| 402 | 173 | ||
| 403 | 174 | [[package]] | |
| 404 | - | name = "displaydoc" | |
| 405 | - | version = "0.2.6" | |
| 175 | + | name = "crossterm_winapi" | |
| 176 | + | version = "0.9.1" | |
| 406 | 177 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 407 | - | checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" | |
| 178 | + | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" | |
| 408 | 179 | dependencies = [ | |
| 180 | + | "winapi", | |
| 181 | + | ] | |
| 182 | + | ||
| 183 | + | [[package]] | |
| 184 | + | name = "crypto-common" | |
| 185 | + | version = "0.1.7" | |
| 186 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 187 | + | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 188 | + | dependencies = [ | |
| 189 | + | "generic-array", | |
| 190 | + | "typenum", | |
| 191 | + | ] | |
| 192 | + | ||
| 193 | + | [[package]] | |
| 194 | + | name = "csscolorparser" | |
| 195 | + | version = "0.6.2" | |
| 196 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 197 | + | checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" | |
| 198 | + | dependencies = [ | |
| 199 | + | "lab", | |
| 200 | + | "phf", | |
| 201 | + | ] | |
| 202 | + | ||
| 203 | + | [[package]] | |
| 204 | + | name = "darling" | |
| 205 | + | version = "0.20.11" | |
| 206 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 207 | + | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" |
Lines truncated
| @@ -10,16 +10,8 @@ | |||
| 10 | 10 | authors = ["Max Johnson <me@maxj.phd>"] | |
| 11 | 11 | ||
| 12 | 12 | [workspace.dependencies] | |
| 13 | - | egui = { version = "0.34", default-features = false, features = ["default_fonts"] } | |
| 14 | - | eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] } | |
| 15 | - | egui_glow = { version = "0.34", default-features = false } | |
| 16 | - | toml = "0.8" | |
| 17 | - | palette = { version = "0.7", default-features = false, features = ["std"] } | |
| 18 | - | ||
| 19 | - | # Lockscreen-specific | |
| 20 | - | tokio = { version = "1", features = ["rt", "macros", "sync", "time"] } | |
| 21 | - | smithay-client-toolkit = "0.20" | |
| 22 | - | pam-sys = "0.5" | |
| 13 | + | ratatui = "0.30" | |
| 14 | + | theme-common = { path = "../MNW/shared/theme-common" } | |
| 23 | 15 | ||
| 24 | 16 | [profile.release] | |
| 25 | 17 | lto = "thin" |
| @@ -19,7 +19,7 @@ | |||
| 19 | 19 | - [`docs/MANIFESTO.md`](docs/MANIFESTO.md) — what Alloy is, who it's for, the thesis. | |
| 20 | 20 | - [`docs/STACK.md`](docs/STACK.md) — every pick (base, compositor, bar, lock, launcher, notifications, screenshot, file manager) with reasoning and what was rejected. | |
| 21 | 21 | - [`docs/MARQUEE-APPS.md`](docs/MARQUEE-APPS.md) — the egui design-system app pipeline. | |
| 22 | - | - [`docs/COMPONENT-LIBRARY.md`](docs/COMPONENT-LIBRARY.md) — `alloy_ui` crate idioms (functions, builders, HOFs, `impl Widget`) and the design-token pipeline. | |
| 22 | + | - [`docs/COMPONENT-LIBRARY.md`](docs/COMPONENT-LIBRARY.md) — `alloy_tui` crate idioms and the design-token pipeline. (Pre-2026-07-17 pivot: authored around egui/`alloy_ui`; the tokens pipeline and authoring idioms carry over.) | |
| 23 | 23 | - [`docs/RESEARCH-IMMEDIATE-MODE-DESIGN.md`](docs/RESEARCH-IMMEDIATE-MODE-DESIGN.md) — late-term research sketch: what does Figma look like for immediate-mode UI? How do designers and engineers collaborate without designers writing Rust? | |
| 24 | 24 | - [`docs/DESIGN-LANGUAGE.md`](docs/DESIGN-LANGUAGE.md) — tinted-greyscale chrome, color reserved for information. | |
| 25 | 25 | - [`docs/CONSOLE.md`](docs/CONSOLE.md) — Alloy Console: the ratatui unified control surface (`alloy net`, `alloy audio`, `alloy config <path>` schema-driven editor). Replaces the "graphical settings app" question. |
| @@ -57,6 +57,9 @@ | |||
| 57 | 57 | pub fn focused(mut self, yes: bool) -> Self { /* ... */ } | |
| 58 | 58 | pub fn kind(mut self, kind: SurfaceKind) -> Self { /* ... */ } | |
| 59 | 59 | // Returns a themed ratatui Block; the caller renders it. | |
| 60 | + | // Per DESIGN-LANGUAGE.md, focus swaps *only* the border color. Surface, | |
| 61 | + | // title, and everything else stay constant across focused/unfocused — | |
| 62 | + | // border-only is the whole rule. | |
| 60 | 63 | pub fn block(self) -> Block<'a> { | |
| 61 | 64 | let border_style = if self.focused { tokens::BORDER_STRONG } else { tokens::BORDER }; | |
| 62 | 65 | Block::default() |
| @@ -47,13 +47,13 @@ | |||
| 47 | 47 | ||
| 48 | 48 | Monochrome chrome, no shadows, and color-reserved-for-information remove the usual vocabularies for signaling interactivity. In the Apple HIG tradition of *discoverable depth*, the replacement vocabulary: | |
| 49 | 49 | ||
| 50 | - | **Surface elevation is the depth language.** Use at least three ramp positions that read as elevation: `surface` (base, the terminal background), `surface-raised` (the focused pane, active list, controls), `surface-overlay` (popovers, menus, dialogs). The step between levels is tonal. A control announces itself by sitting on a raised, bordered surface; a data panel sits flush and borderless on `surface`. | |
| 50 | + | **Surface elevation is the depth language.** Use at least three ramp positions that read as elevation: `surface` (base, the terminal background), `surface-raised` (bordered controls), `surface-overlay` (popovers, menus, dialogs). The step between levels is tonal. A control announces itself by sitting on a raised, bordered surface; a data panel sits flush and borderless on `surface`. Elevation is a static affordance for control chrome, not a focus cue — the focused pane does *not* swap its surface. | |
| 51 | 51 | ||
| 52 | - | **State variants are mandatory, not optional.** A terminal has no pointer, so the states reduce to what keyboard focus expresses: `default`, `focused`, `selected`, `disabled`. Focused switches the pane's border to `border-strong`. Selected adds a `border-strong` leading-edge stripe on the row. Disabled drops text to `text-muted`. No Alloy widget ships with only a default state. (Hover/pressed remain documented for any adopted GUI surface but do not apply to authored TUIs.) | |
| 52 | + | **State variants are mandatory, not optional.** A terminal has no pointer, so the states reduce to what keyboard focus expresses: `default`, `focused`, `selected`, `disabled`. Focused switches the pane's border to `border-strong` — and nothing else changes: no surface swap, no title reweighting, no glyph. Selected does the same at row scope: a `border-strong` leading-edge stripe (a border-like cue) and no surface change. Disabled drops text to `text-muted`. No Alloy widget ships with only a default state. (Hover/pressed remain documented for any adopted GUI surface but do not apply to authored TUIs.) | |
| 53 | 53 | ||
| 54 | 54 | **Borders are the boundary cue.** A single box-drawing border at `border` announces a control's edge; switching that border to `border-strong` announces focus. This is functional chrome, not decoration. | |
| 55 | 55 | ||
| 56 | - | **Focus is obligatory and unambiguous.** Keyboard focus is shown by the focused pane's border going to `border-strong`, with enough contrast to be unmistakable at arm's length. Focus uses the ramp, not the information palette; it is chrome, not data. Making focus subtle as a "clean" choice is a regression. | |
| 56 | + | **Focus is obligatory, unambiguous, and border-only.** Keyboard focus is shown by the focused pane's border going to `border-strong`, with enough contrast to be unmistakable at arm's length — and that is the *entire* cue. No surface swap, no accent, no glyph, no title styling change. Focus uses the ramp, not the information palette; it is chrome, not data. Making focus subtle as a "clean" choice is a regression; stacking extra signals on top of the border is also a regression, in the other direction. | |
| 57 | 57 | ||
| 58 | 58 | **Square-and-flush is for data; bordered is for controls.** Data panels and tabular regions are borderless and flush on `surface`. Interactive controls (fields, list rows that respond to Enter, buttons) carry a box border. The presence or absence of a border is itself the affordance cue. | |
| 59 | 59 | ||
| @@ -63,7 +63,7 @@ | |||
| 63 | 63 | ||
| 64 | 64 | **Action hierarchy: polarity-driven.** A primary action inverts polarity (its cell run is `text-primary` background with `surface-raised` text), visually heavy without recruiting an accent. Secondary actions sit on `surface` with normal text. Disabled actions hold `text-muted`. A pane never shows more than one primary action. | |
| 65 | 65 | ||
| 66 | - | **List and pane selection: leading-edge stripe.** The current selection in a list is marked with a `border-strong` stripe (a single reverse or block cell) along the leading edge of the row, with the row's surface raised one step. It rhymes with the focus border (also `border-strong`) but reads as an edge, so selection and focus stay distinguishable when they coincide. Selection is chrome; never an accent. | |
| 66 | + | **List and pane selection: leading-edge stripe.** The current selection in a list is marked with a `border-strong` stripe (a single reverse or block cell) along the leading edge of the row. The row's surface does not change, and no glyph or reweighting is added — the stripe is the only cue, matching the pane-border rule at row scope. It rhymes with the focus border (also `border-strong`) but reads as an edge, so selection and focus stay distinguishable when they coincide. Selection is chrome; never an accent. | |
| 67 | 67 | ||
| 68 | 68 | **Inline messages: accent only at the edge and glyph.** A color-as-information message (error, warn, healthy, info) carries a single-cell accent bar at the leading edge and an accent glyph; the message body stays `text-primary`. The accent never fills the message's surface (that would turn chrome into data). | |
| 69 | 69 |
| @@ -1,27 +1,0 @@ | |||
| 1 | - | [package] | |
| 2 | - | name = "alloy_lockscreen" | |
| 3 | - | version = "0.0.0" | |
| 4 | - | description = "Alloy lockscreen: ext-session-lock-v1 + PAM, rendered in egui." | |
| 5 | - | edition.workspace = true | |
| 6 | - | rust-version.workspace = true | |
| 7 | - | license.workspace = true | |
| 8 | - | repository.workspace = true | |
| 9 | - | authors.workspace = true | |
| 10 | - | ||
| 11 | - | [[bin]] | |
| 12 | - | name = "alloy-lock" | |
| 13 | - | path = "src/main.rs" | |
| 14 | - | ||
| 15 | - | [dependencies] | |
| 16 | - | alloy_ui = { path = "../alloy_ui" } | |
| 17 | - | tokio = { workspace = true } | |
| 18 | - | ||
| 19 | - | # Linux-only: the lockscreen binds Wayland (ext-session-lock-v1) and | |
| 20 | - | # authenticates against PAM. The crate stays a workspace member on | |
| 21 | - | # non-Linux so alloy_ui dev work isn't blocked, but the binary's real | |
| 22 | - | # functionality is gated behind these. | |
| 23 | - | [target.'cfg(target_os = "linux")'.dependencies] | |
| 24 | - | smithay-client-toolkit = { workspace = true } | |
| 25 | - | pam-sys = { workspace = true } | |
| 26 | - | egui = { workspace = true } | |
| 27 | - | egui_glow = { workspace = true } |
| @@ -1,90 +1,0 @@ | |||
| 1 | - | # alloy_lockscreen | |
| 2 | - | ||
| 3 | - | The Alloy lockscreen. First marquee app; first concrete consumer of `alloy_ui`. Binary name: `alloy-lock`. | |
| 4 | - | ||
| 5 | - | ## Status | |
| 6 | - | ||
| 7 | - | Stub. The crate compiles, depends on `alloy_ui` + the lockscreen stack (SCTK, pam-sys, tokio, egui_glow), and prints a placeholder. All four open questions about scope/architecture are settled — see below. Wayland and PAM wiring land next. | |
| 8 | - | ||
| 9 | - | ## Build | |
| 10 | - | ||
| 11 | - | Linux-only at runtime. The crate is a workspace member on every platform so `alloy_ui` dev work isn't blocked on macOS, but its Wayland and PAM deps (`smithay-client-toolkit`, `pam-sys`, `egui_glow`) are gated behind `cfg(target_os = "linux")`. Building the lockscreen for real needs: | |
| 12 | - | ||
| 13 | - | - `libpam` runtime + headers (`pam-devel` on Fedora). | |
| 14 | - | - Wayland client libraries (`wayland-devel` on Fedora) and an EGL provider (Mesa). | |
| 15 | - | - GL: a working OpenGL ES 2 / GLES3-capable Mesa driver. | |
| 16 | - | ||
| 17 | - | On non-Linux platforms `cargo build` produces a binary that prints the platform-unsupported message and exits non-zero — useful as a sanity check that the workspace still resolves. | |
| 18 | - | ||
| 19 | - | ## v1 scope | |
| 20 | - | ||
| 21 | - | Deliberately small. Everything not on this list is out of scope for v1. | |
| 22 | - | ||
| 23 | - | **In scope** | |
| 24 | - | - Lock the session via `ext-session-lock-v1`. Render a fullscreen surface per output. | |
| 25 | - | - Display the current time, the current date, and a single password field. | |
| 26 | - | - Authenticate via PAM (`pam` system service stack). | |
| 27 | - | - Honor system locale for time/date formatting. | |
| 28 | - | - Multi-monitor: render an identical lockscreen on every connected output. Capture the keyboard on whichever output the cursor is on at lock time; mirror keystrokes' visual feedback (dot count, error state) on the others. | |
| 29 | - | - Theme via `alloy_ui::apply_alloy_visuals` so the lockscreen tracks the system mode. | |
| 30 | - | ||
| 31 | - | **Out of scope for v1** | |
| 32 | - | - Media controls (play/pause, skip, volume). | |
| 33 | - | - Notification preview on the lock surface. | |
| 34 | - | - Wallpaper / image background. The v1 surface is the chrome ramp; the wallpaper picker app handles image curation separately. | |
| 35 | - | - Fingerprint, smartcard, or any non-PAM-password auth path. | |
| 36 | - | - Custom keymap handling beyond what the compositor passes through. | |
| 37 | - | - Animation. The lockscreen is a stable surface; the only motion is the cursor blink and the password-dot reveal as keys land. See [DESIGN-LANGUAGE.md](../../docs/DESIGN-LANGUAGE.md) on the non-reactive principle. | |
| 38 | - | - Session-switching, user-picker. Single-user assumption holds for v1. | |
| 39 | - | ||
| 40 | - | ## Surface layout | |
| 41 | - | ||
| 42 | - | Authored mockup pending. Working sketch: | |
| 43 | - | ||
| 44 | - | - Centered column on each output. | |
| 45 | - | - Top: the time, **Departure Mono** at `size-readout-xl`, `tracking-display-caps`. This is the lockscreen's reason to exist visually — the display font's canonical surface. | |
| 46 | - | - Beneath the time: the date, Departure Mono at `size-readout-sm`, uppercase. | |
| 47 | - | - Below the date, separated by `space-xl`: the password field, **Iosevka** at `size-body`, no visible label, focus ring per `alloy_ui::focus_ring`. Reveals dots as keys are typed, never the password text. | |
| 48 | - | - Below the password field: a single-line status area in Iosevka at `size-caption`. Empty on idle; carries the PAM error message after a failed attempt. | |
| 49 | - | ||
| 50 | - | No other chrome. No clock seconds. No battery indicator on the lockscreen — the lockscreen is a wake surface, not a status display. | |
| 51 | - | ||
| 52 | - | ## Design-system call-outs | |
| 53 | - | ||
| 54 | - | - This is the first place Departure Mono renders in a real Alloy surface. Verify the font registration in `alloy_ui::theme` before relying on it here, and document the fallback chain if Departure's unicode coverage misses anything in the user's locale-formatted date. | |
| 55 | - | - The focus ring is `border-strong`, never accent. Failed-auth state may *temporarily* color the status text in `accent-error`, per the accent-on-text rule in [TOKENS.md](../../docs/TOKENS.md#accent-on-surface-text-rule). The password field's border does **not** flip to accent. | |
| 56 | - | - The lockscreen runs at `surface` tier. The password field sits on `surface-raised`. No popovers, no overlays — there is nowhere for an overlay to go on a lockscreen. | |
| 57 | - | ||
| 58 | - | ## Resilience contract | |
| 59 | - | ||
| 60 | - | The lockscreen has a sharper failure profile than any other Alloy surface: when it breaks, the user is locked out of their session with no fallback UI to debug from. Two non-negotiable rules: | |
| 61 | - | ||
| 62 | - | - **Services launch async.** PAM context init, clock ticker, Wayland event pump, and output enumeration all start concurrently at process boot. The render loop draws the first frame as soon as the Wayland surface is up — it does not wait for PAM. PAM submit calls run on a worker task; the UI thread never blocks on auth. Default runtime: a single current-thread async executor (likely tokio with `rt` only — no IO/net features needed). Each service feeds its state into a snapshot struct the render pass reads each frame. | |
| 63 | - | - **Architectural reference: Ironbar.** Ironbar (the curated v0 bar) already does async service launching — each module spins up its own async task that feeds state into the bar's render pass without ever blocking it. Read how Ironbar structures its module bootstrap, channel topology, and per-task error containment before designing the lockscreen's equivalent. Source: https://github.com/JakeStanger/ironbar. | |
| 64 | - | - **The login option never crashes.** Every error on the auth path (PAM context init failure, PAM auth error, worker-task panic, locale lookup failure) is caught and rendered as status text in the surface. The password field stays present and focused regardless of upstream failure. No `.unwrap()`, no `.expect()`, no panic on any code path reachable from a keystroke. If PAM cannot be initialized at startup, the surface shows the failure mode explicitly and offers a retry — it does not exit the process. This rule is stricter than open question #4 (whole-process crash recovery): the auth path is the user's only way out, so it cannot be allowed to fail loudly. | |
| 65 | - | ||
| 66 | - | ## Architecture | |
| 67 | - | ||
| 68 | - | The lockscreen does **not** use `eframe`. eframe drives Wayland through winit, which opens its own `wl_display` connection — that connection can't share state with whatever owns the session lock, and the session lock has to be owned by the same client that owns the surfaces it draws into. So alloy_lockscreen owns its Wayland connection directly via `smithay-client-toolkit` and drives egui via `egui_glow` on raw GL contexts (one per output). | |
| 69 | - | ||
| 70 | - | Practically: a small async render loop pulls Wayland events from SCTK's `calloop` source, feeds keyboard/pointer events into an `egui::Context`, runs the egui pass, and submits the painted output through `egui_glow` to the GL context bound to each `ExtSessionLockSurfaceV1`. PAM, the clock ticker, and Wayland I/O each run as their own async task feeding a snapshot the render pass reads each frame — per the resilience contract below. The alloy_ui crate is consumed exactly as in the showcase example — `apply_alloy_visuals` + primitives — only the host loop differs. | |
| 71 | - | ||
| 72 | - | ## Stack picks | |
| 73 | - | ||
| 74 | - | | Layer | Pick | Why | | |
| 75 | - | |---|---|---| | |
| 76 | - | | Wayland client | **`smithay-client-toolkit`** | First-class `session_lock` module encodes ext-session-lock-v1's lifecycle (must wait for `locked`, must `unlock` before drop) as types. Upstream `examples/session_lock.rs` is a working starting point. Multi-maintainer Smithay org vs wayrs's single maintainer. Dep-tree premium (~20 crates over wayrs) doesn't matter for a compile-once binary. | | |
| 77 | - | | egui rendering | **`egui` + `egui_glow`** (not `eframe`) | We need to own the Wayland connection, which means we can't use winit (which eframe wraps). egui_glow lets us submit egui paint to GL contexts we manage. | | |
| 78 | - | | GL context | **`glutin`** (or `glow` directly if SCTK exposes EGL helpers) | Decision deferred until first protocol code lands; pick after seeing what SCTK gives us for EGL surface creation. | | |
| 79 | - | | PAM | **`pam-sys`** + a thin in-crate wrapper (`src/pam/`) | The only Rust-PAM consumer with real security-critical precedent is greetd, which uses `pam-sys` directly even though `pam-client` exists — pam-client's higher-level abstractions aren't worth the awkwardness around `pam_set_item`. pam-client is also dormant since 2022; for the only auth path the user has back into the session, owning ~200 lines of stable FFI wrapper is cheaper than depending on an unmaintained upstream. The wrapper exposes a `Result`-returning `Context::new(service, conversation)` mirroring pam-client's shape (which we can copy — MPL-2.0 is GPLv3-compat — without taking the dep), with a `ConversationHandler` trait that owns an `mpsc::Receiver` so the egui input handler feeds the auth task. Service name: `alloy-lock`, registered at `/etc/pam.d/alloy-lock`. | | |
| 80 | - | ||
| 81 | - | ## Open questions blocking implementation | |
| 82 | - | ||
| 83 | - | 1. ~~**Wayland protocol crate.**~~ Resolved: smithay-client-toolkit. See above. | |
| 84 | - | 2. ~~**PAM crate.**~~ Resolved: pam-sys + thin in-crate wrapper. See Stack picks above. | |
| 85 | - | 3. ~~**Process model.**~~ Resolved: **single process, multiple lock surfaces.** ext-session-lock-v1 is a per-client protocol — the `ExtSessionLockV1` object can only be owned by one Wayland client, and its child `ExtSessionLockSurfaceV1` instances must be created by that owner. One-process-per-output therefore isn't really an option for a lock holder; it'd require either an N-renderer-process IPC dance under one holder, or independent processes racing for the lock (only one can win). Single process owns one Wayland connection, one async runtime, one lock, and N surfaces — one EGL/GL context bound per surface, all surfaces rendering from the same snapshot struct so the dot count, failure state, and clock appear simultaneously on every output without coordination. The "isolation if PAM blocks" motivation that originally argued for per-output processes has been retired by the Resilience contract: PAM runs on a worker task, the UI thread never blocks, so process isolation buys nothing the task model doesn't already give us. Precedent: swaylock, hyprlock, waylock are all single-process. | |
| 86 | - | 4. ~~**Recovery path.**~~ Resolved for v1: **rely on the compositor's existing fallback behavior; revisit before stabilizing.** v1 ships without a watchdog or crash-to-greeter handoff. The Resilience contract already makes the auth path crash-proof from the user's keystrokes; the remaining risk is a structural crash (GL context loss, Wayland disconnect, OOM) that the auth-path guards don't catch. For v1 we accept this and rely on whatever Niri does when the lock client dies — document Niri's actual fallback behavior during first-light testing. Watchdog and crash-to-greeter handoff are explicit revisit items, not v1 deliverables. See the follow-up in [`docs/todo.md`](../../docs/todo.md). | |
| 87 | - | ||
| 88 | - | ## Non-goals as a marquee app | |
| 89 | - | ||
| 90 | - | The lockscreen is not where we prove the design system handles dense forms, scrolling lists, or popovers. Its scope is intentionally narrow: a clock, a date, a password field. The package GUI takes those broader proofs once `alloy_ui` is shaken down here. |
| @@ -1,30 +1,0 @@ | |||
| 1 | - | //! Alloy lockscreen — entry point. | |
| 2 | - | //! | |
| 3 | - | //! Real implementation is pending. This stub brings the async runtime up so | |
| 4 | - | //! the dep tree is shaken down and the `#[tokio::main]` framing exists for | |
| 5 | - | //! the protocol and PAM code that will land next. | |
| 6 | - | //! | |
| 7 | - | //! See `README.md` for scope, design decisions, and the build/runtime | |
| 8 | - | //! requirements (Linux + Wayland + PAM). | |
| 9 | - | ||
| 10 | - | #[tokio::main(flavor = "current_thread")] | |
| 11 | - | async fn main() { | |
| 12 | - | println!("alloy-lock v{}", env!("CARGO_PKG_VERSION")); | |
| 13 | - | ||
| 14 | - | #[cfg(target_os = "linux")] | |
| 15 | - | { | |
| 16 | - | println!("target: linux"); | |
| 17 | - | println!("lock state: not acquired (stub — Wayland and PAM wiring pending)"); | |
| 18 | - | std::process::exit(1); | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | #[cfg(not(target_os = "linux"))] | |
| 22 | - | { | |
| 23 | - | println!( | |
| 24 | - | "target: {} (unsupported — lockscreen is Linux-only)", | |
| 25 | - | std::env::consts::OS | |
| 26 | - | ); | |
| 27 | - | println!("lock state: n/a"); | |
| 28 | - | std::process::exit(1); | |
| 29 | - | } | |
| 30 | - | } |
| @@ -1,0 +1,13 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "alloy_tui" | |
| 3 | + | version = "0.0.0" | |
| 4 | + | description = "Alloy design system: theme-common intents rendered as ratatui Color/Style, plus themed widgets for the alloy console and siblings." | |
| 5 | + | edition.workspace = true | |
| 6 | + | rust-version.workspace = true | |
| 7 | + | license.workspace = true | |
| 8 | + | repository.workspace = true | |
| 9 | + | authors.workspace = true | |
| 10 | + | ||
| 11 | + | [dependencies] | |
| 12 | + | ratatui.workspace = true | |
| 13 | + | theme-common.workspace = true |
| @@ -1,0 +1,16 @@ | |||
| 1 | + | //! Alloy design system for ratatui. | |
| 2 | + | //! | |
| 3 | + | //! Consumes theme-common `.toml` themes (the same schema every make-family app | |
| 4 | + | //! reads) and renders them as ratatui `Color` / `Style`. Two Alloy-specific | |
| 5 | + | //! tokens (`border-subtle`, `border-strong`) are derived locally so theme | |
| 6 | + | //! files stay minimal — see `docs/TOKENS.md` for the storage format and | |
| 7 | + | //! derivation math, `docs/CONSOLE.md` for the widget roster this crate | |
| 8 | + | //! targets, and `docs/DESIGN-LANGUAGE.md` for the color-is-information rule | |
| 9 | + | //! enforced here: chrome is tinted-greyscale, accents live on text via | |
| 10 | + | //! `Severity`. | |
| 11 | + | ||
| 12 | + | pub mod theme; | |
| 13 | + | pub mod widgets; | |
| 14 | + | ||
| 15 | + | pub use theme::{Mode, Theme, ThemeError}; | |
| 16 | + | pub use widgets::*; |
| @@ -1,0 +1,196 @@ | |||
| 1 | + | //! Theme palette: theme-common intents resolved into ratatui `Color`s, plus | |
| 2 | + | //! the two Alloy-derived border tokens. | |
| 3 | + | //! | |
| 4 | + | //! Per docs/TOKENS.md, Alloy consumes theme-common `.toml` files (the same | |
| 5 | + | //! schema every make-family app already reads) and derives two extra tokens | |
| 6 | + | //! locally so theme files stay minimal and cross-app compatible: | |
| 7 | + | //! | |
| 8 | + | //! - `border-subtle = mix(line.border, surface.page, 60%)` decorative divider | |
| 9 | + | //! - `border-strong = mix(line.border, content.primary, 65%)` focus / selection | |
| 10 | + | //! | |
| 11 | + | //! Mix is in linear sRGB, matching TOKENS.md's worked audit math. | |
| 12 | + | //! | |
| 13 | + | //! ratatui is immediate-mode with per-widget styling — there is no global | |
| 14 | + | //! visuals object. Widgets in this crate take a `&Theme` at construction time | |
| 15 | + | //! and pull colors from it. Apps build one `Theme` per theme load (via | |
| 16 | + | //! `theme_common::load_theme` + `Theme::from_theme`) and thread it through. | |
| 17 | + | ||
| 18 | + | use ratatui::style::Color; | |
| 19 | + | use theme_common::{Rgb, ThemeColors}; | |
| 20 | + | ||
| 21 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 22 | + | pub enum Mode { | |
| 23 | + | Light, | |
| 24 | + | Dark, | |
| 25 | + | HighContrast, | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | #[derive(Debug, Clone, Copy)] | |
| 29 | + | pub struct Theme { | |
| 30 | + | pub mode: Mode, | |
| 31 | + | ||
| 32 | + | pub surface_page: Color, | |
| 33 | + | pub surface_raised: Color, | |
| 34 | + | pub surface_sunken: Color, | |
| 35 | + | pub surface_overlay: Color, | |
| 36 | + | ||
| 37 | + | pub content_primary: Color, | |
| 38 | + | pub content_secondary: Color, | |
| 39 | + | pub content_muted: Color, | |
| 40 | + | ||
| 41 | + | pub action_primary: Color, | |
| 42 | + | ||
| 43 | + | pub status_danger: Color, | |
| 44 | + | pub status_success: Color, | |
| 45 | + | pub status_warning: Color, | |
| 46 | + | pub status_info: Color, | |
| 47 | + | ||
| 48 | + | pub line_border: Color, | |
| 49 | + | pub border_subtle: Color, | |
| 50 | + | pub border_strong: Color, | |
| 51 | + | ||
| 52 | + | pub category: [Color; 6], | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | #[derive(Debug, Clone)] | |
| 56 | + | pub enum ThemeError { | |
| 57 | + | MissingKey(&'static str), | |
| 58 | + | InvalidHex { key: &'static str, value: String }, | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | impl std::fmt::Display for ThemeError { | |
| 62 | + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 63 | + | match self { | |
| 64 | + | ThemeError::MissingKey(k) => write!(f, "theme missing required key `{k}`"), | |
| 65 | + | ThemeError::InvalidHex { key, value } => { | |
| 66 | + | write!(f, "theme key `{key}` has invalid hex value `{value}`") | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | impl std::error::Error for ThemeError {} | |
| 73 | + | ||
| 74 | + | impl Theme { | |
| 75 | + | /// Resolve a loaded theme-common `ThemeColors` into an Alloy `Theme`. | |
| 76 | + | /// Requires every intent Alloy renders — a malformed or partial theme is | |
| 77 | + | /// rejected explicitly rather than silently rendering with defaults. | |
| 78 | + | pub fn from_theme(theme: &ThemeColors) -> Result<Self, ThemeError> { | |
| 79 | + | let get = |key: &'static str| -> Result<Rgb, ThemeError> { | |
| 80 | + | let hex = theme.colors.get(key).ok_or(ThemeError::MissingKey(key))?; | |
| 81 | + | Rgb::from_hex(hex).ok_or_else(|| ThemeError::InvalidHex { | |
| 82 | + | key, | |
| 83 | + | value: hex.clone(), | |
| 84 | + | }) | |
| 85 | + | }; | |
| 86 | + | ||
| 87 | + | let surface_page = get("surface.page")?; | |
| 88 | + | let content_primary = get("content.primary")?; | |
| 89 | + | let line_border = get("line.border")?; | |
| 90 | + | ||
| 91 | + | let border_subtle = mix_linear_srgb(line_border, surface_page, 0.60); | |
| 92 | + | let border_strong = mix_linear_srgb(line_border, content_primary, 0.65); | |
| 93 | + | ||
| 94 | + | let mode = match theme.meta.variant.as_str() { | |
| 95 | + | "dark" => Mode::Dark, | |
| 96 | + | "high-contrast" => Mode::HighContrast, | |
| 97 | + | _ => Mode::Light, | |
| 98 | + | }; | |
| 99 | + | ||
| 100 | + | Ok(Self { | |
| 101 | + | mode, | |
| 102 | + | ||
| 103 | + | surface_page: rgb(surface_page), | |
| 104 | + | surface_raised: rgb(get("surface.raised")?), | |
| 105 | + | surface_sunken: rgb(get("surface.sunken")?), | |
| 106 | + | surface_overlay: rgb(get("surface.overlay")?), | |
| 107 | + | ||
| 108 | + | content_primary: rgb(content_primary), | |
| 109 | + | content_secondary: rgb(get("content.secondary")?), | |
| 110 | + | content_muted: rgb(get("content.muted")?), | |
| 111 | + | ||
| 112 | + | action_primary: rgb(get("action.primary")?), | |
| 113 | + | ||
| 114 | + | status_danger: rgb(get("status.danger")?), | |
| 115 | + | status_success: rgb(get("status.success")?), | |
| 116 | + | status_warning: rgb(get("status.warning")?), | |
| 117 | + | status_info: rgb(get("status.info")?), | |
| 118 | + | ||
| 119 | + | line_border: rgb(line_border), | |
| 120 | + | border_subtle: rgb(border_subtle), | |
| 121 | + | border_strong: rgb(border_strong), | |
| 122 | + | ||
| 123 | + | category: [ | |
| 124 | + | rgb(get("category.one")?), | |
| 125 | + | rgb(get("category.two")?), | |
| 126 | + | rgb(get("category.three")?), | |
| 127 | + | rgb(get("category.four")?), | |
| 128 | + | rgb(get("category.five")?), | |
| 129 | + | rgb(get("category.six")?), | |
| 130 | + | ], | |
| 131 | + | }) | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | fn rgb(c: Rgb) -> Color { | |
| 136 | + | Color::Rgb(c.r, c.g, c.b) | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // Linear-sRGB interpolation. Matches TOKENS.md's audit math exactly: values are | |
| 140 | + | // gamma-decoded to linear light, mixed, then gamma-encoded back. Perceptually | |
| 141 | + | // less uniform than OKLab but keeps the derived hex reproducible against the | |
| 142 | + | // contrast tables in TOKENS.md. | |
| 143 | + | fn mix_linear_srgb(a: Rgb, b: Rgb, t: f32) -> Rgb { | |
| 144 | + | let al = srgb_to_linear(a); | |
| 145 | + | let bl = srgb_to_linear(b); | |
| 146 | + | let m = ( | |
| 147 | + | al.0 + (bl.0 - al.0) * t, | |
| 148 | + | al.1 + (bl.1 - al.1) * t, | |
| 149 | + | al.2 + (bl.2 - al.2) * t, | |
| 150 | + | ); | |
| 151 | + | linear_to_srgb(m) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | fn srgb_to_linear(c: Rgb) -> (f32, f32, f32) { | |
| 155 | + | (channel_to_linear(c.r), channel_to_linear(c.g), channel_to_linear(c.b)) | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | fn linear_to_srgb(c: (f32, f32, f32)) -> Rgb { | |
| 159 | + | Rgb { | |
| 160 | + | r: channel_to_srgb(c.0), | |
| 161 | + | g: channel_to_srgb(c.1), | |
| 162 | + | b: channel_to_srgb(c.2), | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | fn channel_to_linear(c: u8) -> f32 { | |
| 167 | + | let c = c as f32 / 255.0; | |
| 168 | + | if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) } | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | fn channel_to_srgb(c: f32) -> u8 { | |
| 172 | + | let v = if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }; | |
| 173 | + | (v * 255.0).round().clamp(0.0, 255.0) as u8 | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | #[cfg(test)] | |
| 177 | + | mod tests { | |
| 178 | + | use super::*; | |
| 179 | + | ||
| 180 | + | // TOKENS.md line 61 anchors the derivation math against Akari Dawn: | |
| 181 | + | // line.border = #cabeae, content.primary = #1a1816, mix 65% toward primary | |
| 182 | + | // must produce #7f786d (the value the contrast-audit table is calibrated on). | |
| 183 | + | // If this test fails, the audit table in TOKENS.md is stale, not the code. | |
| 184 | + | #[test] | |
| 185 | + | fn akari_dawn_border_strong_matches_tokens_md() { | |
| 186 | + | let border = Rgb::from_hex("#cabeae").unwrap(); | |
| 187 | + | let primary = Rgb::from_hex("#1a1816").unwrap(); | |
| 188 | + | let got = mix_linear_srgb(border, primary, 0.65); | |
| 189 | + | assert_eq!( | |
| 190 | + | (got.r, got.g, got.b), | |
| 191 | + | (0x7f, 0x78, 0x6d), | |
| 192 | + | "border-strong derivation drifted; got #{:02x}{:02x}{:02x}, expected #7f786d", | |
| 193 | + | got.r, got.g, got.b | |
| 194 | + | ); | |
| 195 | + | } | |
| 196 | + | } |
| @@ -1,0 +1,88 @@ | |||
| 1 | + | //! Themed ratatui widget wrappers. | |
| 2 | + | //! | |
| 3 | + | //! v1 target per docs/CONSOLE.md: `AlloyBlock`, `AlloyList`, `AlloyForm`, | |
| 4 | + | //! `AlloyTable`, `AlloyStatusBar`, `AlloyLog`, plus form-field widgets driven | |
| 5 | + | //! by the config schema. This module seeds the crate with `AlloyBlock` and the | |
| 6 | + | //! `Severity` accent — the two pieces every other widget composes with — and | |
| 7 | + | //! grows from there. | |
| 8 | + | ||
| 9 | + | use ratatui::style::{Color, Style}; | |
| 10 | + | use ratatui::widgets::{Block, Borders}; | |
| 11 | + | ||
| 12 | + | use crate::theme::Theme; | |
| 13 | + | ||
| 14 | + | /// Themed `Block`: default borders + palette chrome. Wraps `ratatui::widgets::Block` | |
| 15 | + | /// so downstream code composes with it directly (`AlloyBlock::new(theme).build()` | |
| 16 | + | /// returns the inner `Block`, which callers then `.title(..)` and pass to a | |
| 17 | + | /// widget). | |
| 18 | + | /// | |
| 19 | + | /// Per DESIGN-LANGUAGE.md: chrome is tinted-greyscale; borders never carry an | |
| 20 | + | /// accent — accents live on text via `Severity`. Focused vs. unfocused chrome | |
| 21 | + | /// swaps `border-subtle` (decorative) for `border-strong` (focus/selection), | |
| 22 | + | /// matching TOKENS.md's derived-border tiers. | |
| 23 | + | pub struct AlloyBlock<'a> { | |
| 24 | + | theme: &'a Theme, | |
| 25 | + | focused: bool, | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | impl<'a> AlloyBlock<'a> { | |
| 29 | + | pub fn new(theme: &'a Theme) -> Self { | |
| 30 | + | Self { theme, focused: false } | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | pub fn focused(mut self, focused: bool) -> Self { | |
| 34 | + | self.focused = focused; | |
| 35 | + | self | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | pub fn build(self) -> Block<'a> { | |
| 39 | + | let border_color = if self.focused { | |
| 40 | + | self.theme.border_strong | |
| 41 | + | } else { | |
| 42 | + | self.theme.border_subtle | |
| 43 | + | }; | |
| 44 | + | Block::default() | |
| 45 | + | .borders(Borders::ALL) | |
| 46 | + | .border_style(Style::default().fg(border_color)) | |
| 47 | + | .style( | |
| 48 | + | Style::default() | |
| 49 | + | .bg(self.theme.surface_page) | |
| 50 | + | .fg(self.theme.content_primary), | |
| 51 | + | ) | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /// Severity accent — categorical status tag whose color comes from the theme's | |
| 56 | + | /// `status.*` intents. Per DESIGN-LANGUAGE.md, this is one of the few places | |
| 57 | + | /// color is on-purpose: never on chrome, only on glyphs and text. | |
| 58 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 59 | + | pub enum Severity { | |
| 60 | + | Info, | |
| 61 | + | Healthy, | |
| 62 | + | Warn, | |
| 63 | + | Error, | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | impl Severity { | |
| 67 | + | pub const fn label(self) -> &'static str { | |
| 68 | + | match self { | |
| 69 | + | Severity::Info => "INFO", | |
| 70 | + | Severity::Healthy => "OK", | |
| 71 | + | Severity::Warn => "WARN", | |
| 72 | + | Severity::Error => "ERROR", | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | pub fn color(self, theme: &Theme) -> Color { | |
| 77 | + | match self { | |
| 78 | + | Severity::Info => theme.status_info, | |
| 79 | + | Severity::Healthy => theme.status_success, | |
| 80 | + | Severity::Warn => theme.status_warning, | |
| 81 | + | Severity::Error => theme.status_danger, | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | pub fn style(self, theme: &Theme) -> Style { | |
| 86 | + | Style::default().fg(self.color(theme)) | |
| 87 | + | } | |
| 88 | + | } |