workspace skeleton and IPC protocol
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
9d71c5e4a3d45bd8a8a2fa4bcd64e7f5ab195902- Parents
-
ab309fd - Tree
c2b6aa7
9d71c5e
9d71c5e4a3d45bd8a8a2fa4bcd64e7f5ab195902ab309fd
c2b6aa7| Status | File | + | - |
|---|---|---|---|
| A |
.fackr/workspace.json
|
53 | 0 |
| M |
.gitignore
|
1 | 3 |
| A |
Cargo.lock
|
659 | 0 |
| A |
Cargo.toml
|
41 | 0 |
| A |
docs/architecture.md
|
219 | 0 |
| A |
docs/checklist.md
|
157 | 0 |
| A |
docs/gardm.md
|
14 | 0 |
| A |
docs/sprints/sprint-0-setup.md
|
246 | 0 |
| A |
docs/sprints/sprint-1-pam.md
|
356 | 0 |
| A |
docs/sprints/sprint-2-x11.md
|
487 | 0 |
| A |
docs/sprints/sprint-3-greeter-ui.md
|
586 | 0 |
| A |
docs/sprints/sprint-4-garbg-integration.md
|
453 | 0 |
| A |
etc/config.toml
|
34 | 0 |
| A |
etc/gardm.service
|
25 | 0 |
| A |
gardm-greeter/Cargo.toml
|
18 | 0 |
| A |
gardm-greeter/src/main.rs
|
35 | 0 |
| A |
gardm-ipc/Cargo.toml
|
12 | 0 |
| A |
gardm-ipc/src/lib.rs
|
199 | 0 |
| A |
gardmd/Cargo.toml
|
23 | 0 |
| A |
gardmd/src/config.rs
|
155 | 0 |
| A |
gardmd/src/ipc.rs
|
99 | 0 |
| A |
gardmd/src/lib.rs
|
9 | 0 |
| A |
gardmd/src/main.rs
|
174 | 0 |
.fackr/workspace.jsonadded@@ -0,0 +1,53 @@ | |||
| 1 | +{ | ||
| 2 | + "active_tab": 1, | ||
| 3 | + "tabs": [ | ||
| 4 | + { | ||
| 5 | + "files": [ | ||
| 6 | + { | ||
| 7 | + "path": ".gitignore", | ||
| 8 | + "is_orphan": false | ||
| 9 | + } | ||
| 10 | + ], | ||
| 11 | + "active_pane": 0, | ||
| 12 | + "panes": [ | ||
| 13 | + { | ||
| 14 | + "buffer_idx": 0, | ||
| 15 | + "cursor_line": 3, | ||
| 16 | + "cursor_col": 0, | ||
| 17 | + "viewport_line": 0, | ||
| 18 | + "viewport_col": 0, | ||
| 19 | + "bounds": { | ||
| 20 | + "x_start": 0.0, | ||
| 21 | + "y_start": 0.0, | ||
| 22 | + "x_end": 1.0, | ||
| 23 | + "y_end": 1.0 | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + ] | ||
| 27 | + }, | ||
| 28 | + { | ||
| 29 | + "files": [ | ||
| 30 | + { | ||
| 31 | + "path": "docs/gardm.md", | ||
| 32 | + "is_orphan": false | ||
| 33 | + } | ||
| 34 | + ], | ||
| 35 | + "active_pane": 0, | ||
| 36 | + "panes": [ | ||
| 37 | + { | ||
| 38 | + "buffer_idx": 0, | ||
| 39 | + "cursor_line": 13, | ||
| 40 | + "cursor_col": 109, | ||
| 41 | + "viewport_line": 0, | ||
| 42 | + "viewport_col": 9, | ||
| 43 | + "bounds": { | ||
| 44 | + "x_start": 0.0, | ||
| 45 | + "y_start": 0.0, | ||
| 46 | + "x_end": 1.0, | ||
| 47 | + "y_end": 1.0 | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + ] | ||
| 51 | + } | ||
| 52 | + ] | ||
| 53 | +} | ||
.gitignoremodified@@ -1,3 +1,1 @@ | |||
| 1 | -docs/ | 1 | +/target |
| 2 | -.fackr/ | ||
| 3 | -CLAUDE.md | ||
Cargo.lockadded@@ -0,0 +1,659 @@ | |||
| 1 | +# This file is automatically @generated by Cargo. | ||
| 2 | +# It is not intended for manual editing. | ||
| 3 | +version = 4 | ||
| 4 | + | ||
| 5 | +[[package]] | ||
| 6 | +name = "aho-corasick" | ||
| 7 | +version = "1.1.4" | ||
| 8 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9 | +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" | ||
| 10 | +dependencies = [ | ||
| 11 | + "memchr", | ||
| 12 | +] | ||
| 13 | + | ||
| 14 | +[[package]] | ||
| 15 | +name = "anyhow" | ||
| 16 | +version = "1.0.100" | ||
| 17 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 18 | +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" | ||
| 19 | + | ||
| 20 | +[[package]] | ||
| 21 | +name = "bitflags" | ||
| 22 | +version = "2.10.0" | ||
| 23 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 24 | +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" | ||
| 25 | + | ||
| 26 | +[[package]] | ||
| 27 | +name = "bytes" | ||
| 28 | +version = "1.11.0" | ||
| 29 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 30 | +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" | ||
| 31 | + | ||
| 32 | +[[package]] | ||
| 33 | +name = "cfg-if" | ||
| 34 | +version = "1.0.4" | ||
| 35 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 36 | +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | ||
| 37 | + | ||
| 38 | +[[package]] | ||
| 39 | +name = "equivalent" | ||
| 40 | +version = "1.0.2" | ||
| 41 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 42 | +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | ||
| 43 | + | ||
| 44 | +[[package]] | ||
| 45 | +name = "errno" | ||
| 46 | +version = "0.3.14" | ||
| 47 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 48 | +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | ||
| 49 | +dependencies = [ | ||
| 50 | + "libc", | ||
| 51 | + "windows-sys 0.61.2", | ||
| 52 | +] | ||
| 53 | + | ||
| 54 | +[[package]] | ||
| 55 | +name = "gardm-greeter" | ||
| 56 | +version = "0.1.0" | ||
| 57 | +dependencies = [ | ||
| 58 | + "anyhow", | ||
| 59 | + "gardm-ipc", | ||
| 60 | + "thiserror", | ||
| 61 | + "tokio", | ||
| 62 | + "tracing", | ||
| 63 | + "tracing-subscriber", | ||
| 64 | +] | ||
| 65 | + | ||
| 66 | +[[package]] | ||
| 67 | +name = "gardm-ipc" | ||
| 68 | +version = "0.1.0" | ||
| 69 | +dependencies = [ | ||
| 70 | + "serde", | ||
| 71 | + "serde_json", | ||
| 72 | + "thiserror", | ||
| 73 | + "tokio", | ||
| 74 | +] | ||
| 75 | + | ||
| 76 | +[[package]] | ||
| 77 | +name = "gardmd" | ||
| 78 | +version = "0.1.0" | ||
| 79 | +dependencies = [ | ||
| 80 | + "anyhow", | ||
| 81 | + "gardm-ipc", | ||
| 82 | + "nix", | ||
| 83 | + "sd-notify", | ||
| 84 | + "serde", | ||
| 85 | + "serde_json", | ||
| 86 | + "thiserror", | ||
| 87 | + "tokio", | ||
| 88 | + "toml", | ||
| 89 | + "tracing", | ||
| 90 | + "tracing-subscriber", | ||
| 91 | +] | ||
| 92 | + | ||
| 93 | +[[package]] | ||
| 94 | +name = "hashbrown" | ||
| 95 | +version = "0.16.1" | ||
| 96 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 97 | +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | ||
| 98 | + | ||
| 99 | +[[package]] | ||
| 100 | +name = "indexmap" | ||
| 101 | +version = "2.13.0" | ||
| 102 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 103 | +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" | ||
| 104 | +dependencies = [ | ||
| 105 | + "equivalent", | ||
| 106 | + "hashbrown", | ||
| 107 | +] | ||
| 108 | + | ||
| 109 | +[[package]] | ||
| 110 | +name = "itoa" | ||
| 111 | +version = "1.0.17" | ||
| 112 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 113 | +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" | ||
| 114 | + | ||
| 115 | +[[package]] | ||
| 116 | +name = "lazy_static" | ||
| 117 | +version = "1.5.0" | ||
| 118 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 119 | +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" | ||
| 120 | + | ||
| 121 | +[[package]] | ||
| 122 | +name = "libc" | ||
| 123 | +version = "0.2.180" | ||
| 124 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 125 | +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" | ||
| 126 | + | ||
| 127 | +[[package]] | ||
| 128 | +name = "lock_api" | ||
| 129 | +version = "0.4.14" | ||
| 130 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 131 | +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | ||
| 132 | +dependencies = [ | ||
| 133 | + "scopeguard", | ||
| 134 | +] | ||
| 135 | + | ||
| 136 | +[[package]] | ||
| 137 | +name = "log" | ||
| 138 | +version = "0.4.29" | ||
| 139 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 140 | +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" | ||
| 141 | + | ||
| 142 | +[[package]] | ||
| 143 | +name = "matchers" | ||
| 144 | +version = "0.2.0" | ||
| 145 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 146 | +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" | ||
| 147 | +dependencies = [ | ||
| 148 | + "regex-automata", | ||
| 149 | +] | ||
| 150 | + | ||
| 151 | +[[package]] | ||
| 152 | +name = "memchr" | ||
| 153 | +version = "2.7.6" | ||
| 154 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 155 | +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" | ||
| 156 | + | ||
| 157 | +[[package]] | ||
| 158 | +name = "mio" | ||
| 159 | +version = "1.1.1" | ||
| 160 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 161 | +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" | ||
| 162 | +dependencies = [ | ||
| 163 | + "libc", | ||
| 164 | + "wasi", | ||
| 165 | + "windows-sys 0.61.2", | ||
| 166 | +] | ||
| 167 | + | ||
| 168 | +[[package]] | ||
| 169 | +name = "nix" | ||
| 170 | +version = "0.27.1" | ||
| 171 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 172 | +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" | ||
| 173 | +dependencies = [ | ||
| 174 | + "bitflags", | ||
| 175 | + "cfg-if", | ||
| 176 | + "libc", | ||
| 177 | +] | ||
| 178 | + | ||
| 179 | +[[package]] | ||
| 180 | +name = "nu-ansi-term" | ||
| 181 | +version = "0.50.3" | ||
| 182 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 183 | +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | ||
| 184 | +dependencies = [ | ||
| 185 | + "windows-sys 0.61.2", | ||
| 186 | +] | ||
| 187 | + | ||
| 188 | +[[package]] | ||
| 189 | +name = "once_cell" | ||
| 190 | +version = "1.21.3" | ||
| 191 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 192 | +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" | ||
| 193 | + | ||
| 194 | +[[package]] | ||
| 195 | +name = "parking_lot" | ||
| 196 | +version = "0.12.5" | ||
| 197 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 198 | +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" | ||
| 199 | +dependencies = [ | ||
| 200 | + "lock_api", | ||
| 201 | + "parking_lot_core", | ||
| 202 | +] | ||
| 203 | + | ||
| 204 | +[[package]] | ||
| 205 | +name = "parking_lot_core" | ||
| 206 | +version = "0.9.12" | ||
| 207 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 208 | +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | ||
| 209 | +dependencies = [ | ||
| 210 | + "cfg-if", | ||
| 211 | + "libc", | ||
| 212 | + "redox_syscall", | ||
| 213 | + "smallvec", | ||
| 214 | + "windows-link", | ||
| 215 | +] | ||
| 216 | + | ||
| 217 | +[[package]] | ||
| 218 | +name = "pin-project-lite" | ||
| 219 | +version = "0.2.16" | ||
| 220 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 221 | +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" | ||
| 222 | + | ||
| 223 | +[[package]] | ||
| 224 | +name = "proc-macro2" | ||
| 225 | +version = "1.0.105" | ||
| 226 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 227 | +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" | ||
| 228 | +dependencies = [ | ||
| 229 | + "unicode-ident", | ||
| 230 | +] | ||
| 231 | + | ||
| 232 | +[[package]] | ||
| 233 | +name = "quote" | ||
| 234 | +version = "1.0.43" | ||
| 235 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 236 | +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" | ||
| 237 | +dependencies = [ | ||
| 238 | + "proc-macro2", | ||
| 239 | +] | ||
| 240 | + | ||
| 241 | +[[package]] | ||
| 242 | +name = "redox_syscall" | ||
| 243 | +version = "0.5.18" | ||
| 244 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 245 | +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | ||
| 246 | +dependencies = [ | ||
| 247 | + "bitflags", | ||
| 248 | +] | ||
| 249 | + | ||
| 250 | +[[package]] | ||
| 251 | +name = "regex-automata" | ||
| 252 | +version = "0.4.13" | ||
| 253 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 254 | +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" | ||
| 255 | +dependencies = [ | ||
| 256 | + "aho-corasick", | ||
| 257 | + "memchr", | ||
| 258 | + "regex-syntax", | ||
| 259 | +] | ||
| 260 | + | ||
| 261 | +[[package]] | ||
| 262 | +name = "regex-syntax" | ||
| 263 | +version = "0.8.8" | ||
| 264 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 265 | +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" | ||
| 266 | + | ||
| 267 | +[[package]] | ||
| 268 | +name = "scopeguard" | ||
| 269 | +version = "1.2.0" | ||
| 270 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 271 | +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | ||
| 272 | + | ||
| 273 | +[[package]] | ||
| 274 | +name = "sd-notify" | ||
| 275 | +version = "0.4.5" | ||
| 276 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 277 | +checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4" | ||
| 278 | +dependencies = [ | ||
| 279 | + "libc", | ||
| 280 | +] | ||
| 281 | + | ||
| 282 | +[[package]] | ||
| 283 | +name = "serde" | ||
| 284 | +version = "1.0.228" | ||
| 285 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 286 | +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | ||
| 287 | +dependencies = [ | ||
| 288 | + "serde_core", | ||
| 289 | + "serde_derive", | ||
| 290 | +] | ||
| 291 | + | ||
| 292 | +[[package]] | ||
| 293 | +name = "serde_core" | ||
| 294 | +version = "1.0.228" | ||
| 295 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 296 | +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | ||
| 297 | +dependencies = [ | ||
| 298 | + "serde_derive", | ||
| 299 | +] | ||
| 300 | + | ||
| 301 | +[[package]] | ||
| 302 | +name = "serde_derive" | ||
| 303 | +version = "1.0.228" | ||
| 304 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 305 | +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | ||
| 306 | +dependencies = [ | ||
| 307 | + "proc-macro2", | ||
| 308 | + "quote", | ||
| 309 | + "syn", | ||
| 310 | +] | ||
| 311 | + | ||
| 312 | +[[package]] | ||
| 313 | +name = "serde_json" | ||
| 314 | +version = "1.0.149" | ||
| 315 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 316 | +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" | ||
| 317 | +dependencies = [ | ||
| 318 | + "itoa", | ||
| 319 | + "memchr", | ||
| 320 | + "serde", | ||
| 321 | + "serde_core", | ||
| 322 | + "zmij", | ||
| 323 | +] | ||
| 324 | + | ||
| 325 | +[[package]] | ||
| 326 | +name = "serde_spanned" | ||
| 327 | +version = "0.6.9" | ||
| 328 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 329 | +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" | ||
| 330 | +dependencies = [ | ||
| 331 | + "serde", | ||
| 332 | +] | ||
| 333 | + | ||
| 334 | +[[package]] | ||
| 335 | +name = "sharded-slab" | ||
| 336 | +version = "0.1.7" | ||
| 337 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 338 | +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" | ||
| 339 | +dependencies = [ | ||
| 340 | + "lazy_static", | ||
| 341 | +] | ||
| 342 | + | ||
| 343 | +[[package]] | ||
| 344 | +name = "signal-hook-registry" | ||
| 345 | +version = "1.4.8" | ||
| 346 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 347 | +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | ||
| 348 | +dependencies = [ | ||
| 349 | + "errno", | ||
| 350 | + "libc", | ||
| 351 | +] | ||
| 352 | + | ||
| 353 | +[[package]] | ||
| 354 | +name = "smallvec" | ||
| 355 | +version = "1.15.1" | ||
| 356 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 357 | +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" | ||
| 358 | + | ||
| 359 | +[[package]] | ||
| 360 | +name = "socket2" | ||
| 361 | +version = "0.6.1" | ||
| 362 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 363 | +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" | ||
| 364 | +dependencies = [ | ||
| 365 | + "libc", | ||
| 366 | + "windows-sys 0.60.2", | ||
| 367 | +] | ||
| 368 | + | ||
| 369 | +[[package]] | ||
| 370 | +name = "syn" | ||
| 371 | +version = "2.0.114" | ||
| 372 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 373 | +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" | ||
| 374 | +dependencies = [ | ||
| 375 | + "proc-macro2", | ||
| 376 | + "quote", | ||
| 377 | + "unicode-ident", | ||
| 378 | +] | ||
| 379 | + | ||
| 380 | +[[package]] | ||
| 381 | +name = "thiserror" | ||
| 382 | +version = "1.0.69" | ||
| 383 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 384 | +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | ||
| 385 | +dependencies = [ | ||
| 386 | + "thiserror-impl", | ||
| 387 | +] | ||
| 388 | + | ||
| 389 | +[[package]] | ||
| 390 | +name = "thiserror-impl" | ||
| 391 | +version = "1.0.69" | ||
| 392 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 393 | +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | ||
| 394 | +dependencies = [ | ||
| 395 | + "proc-macro2", | ||
| 396 | + "quote", | ||
| 397 | + "syn", | ||
| 398 | +] | ||
| 399 | + | ||
| 400 | +[[package]] | ||
| 401 | +name = "thread_local" | ||
| 402 | +version = "1.1.9" | ||
| 403 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 404 | +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" | ||
| 405 | +dependencies = [ | ||
| 406 | + "cfg-if", | ||
| 407 | +] | ||
| 408 | + | ||
| 409 | +[[package]] | ||
| 410 | +name = "tokio" | ||
| 411 | +version = "1.49.0" | ||
| 412 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 413 | +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" | ||
| 414 | +dependencies = [ | ||
| 415 | + "bytes", | ||
| 416 | + "libc", | ||
| 417 | + "mio", | ||
| 418 | + "parking_lot", | ||
| 419 | + "pin-project-lite", | ||
| 420 | + "signal-hook-registry", | ||
| 421 | + "socket2", | ||
| 422 | + "tokio-macros", | ||
| 423 | + "windows-sys 0.61.2", | ||
| 424 | +] | ||
| 425 | + | ||
| 426 | +[[package]] | ||
| 427 | +name = "tokio-macros" | ||
| 428 | +version = "2.6.0" | ||
| 429 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 430 | +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" | ||
| 431 | +dependencies = [ | ||
| 432 | + "proc-macro2", | ||
| 433 | + "quote", | ||
| 434 | + "syn", | ||
| 435 | +] | ||
| 436 | + | ||
| 437 | +[[package]] | ||
| 438 | +name = "toml" | ||
| 439 | +version = "0.8.23" | ||
| 440 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 441 | +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" | ||
| 442 | +dependencies = [ | ||
| 443 | + "serde", | ||
| 444 | + "serde_spanned", | ||
| 445 | + "toml_datetime", | ||
| 446 | + "toml_edit", | ||
| 447 | +] | ||
| 448 | + | ||
| 449 | +[[package]] | ||
| 450 | +name = "toml_datetime" | ||
| 451 | +version = "0.6.11" | ||
| 452 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 453 | +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" | ||
| 454 | +dependencies = [ | ||
| 455 | + "serde", | ||
| 456 | +] | ||
| 457 | + | ||
| 458 | +[[package]] | ||
| 459 | +name = "toml_edit" | ||
| 460 | +version = "0.22.27" | ||
| 461 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 462 | +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" | ||
| 463 | +dependencies = [ | ||
| 464 | + "indexmap", | ||
| 465 | + "serde", | ||
| 466 | + "serde_spanned", | ||
| 467 | + "toml_datetime", | ||
| 468 | + "toml_write", | ||
| 469 | + "winnow", | ||
| 470 | +] | ||
| 471 | + | ||
| 472 | +[[package]] | ||
| 473 | +name = "toml_write" | ||
| 474 | +version = "0.1.2" | ||
| 475 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 476 | +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" | ||
| 477 | + | ||
| 478 | +[[package]] | ||
| 479 | +name = "tracing" | ||
| 480 | +version = "0.1.44" | ||
| 481 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 482 | +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | ||
| 483 | +dependencies = [ | ||
| 484 | + "pin-project-lite", | ||
| 485 | + "tracing-attributes", | ||
| 486 | + "tracing-core", | ||
| 487 | +] | ||
| 488 | + | ||
| 489 | +[[package]] | ||
| 490 | +name = "tracing-attributes" | ||
| 491 | +version = "0.1.31" | ||
| 492 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 493 | +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | ||
| 494 | +dependencies = [ | ||
| 495 | + "proc-macro2", | ||
| 496 | + "quote", | ||
| 497 | + "syn", | ||
| 498 | +] | ||
| 499 | + | ||
| 500 | +[[package]] | ||
| 501 | +name = "tracing-core" | ||
| 502 | +version = "0.1.36" | ||
| 503 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 504 | +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | ||
| 505 | +dependencies = [ | ||
| 506 | + "once_cell", | ||
| 507 | + "valuable", | ||
| 508 | +] | ||
| 509 | + | ||
| 510 | +[[package]] | ||
| 511 | +name = "tracing-log" | ||
| 512 | +version = "0.2.0" | ||
| 513 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 514 | +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" | ||
| 515 | +dependencies = [ | ||
| 516 | + "log", | ||
| 517 | + "once_cell", | ||
| 518 | + "tracing-core", | ||
| 519 | +] | ||
| 520 | + | ||
| 521 | +[[package]] | ||
| 522 | +name = "tracing-subscriber" | ||
| 523 | +version = "0.3.22" | ||
| 524 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 525 | +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" | ||
| 526 | +dependencies = [ | ||
| 527 | + "matchers", | ||
| 528 | + "nu-ansi-term", | ||
| 529 | + "once_cell", | ||
| 530 | + "regex-automata", | ||
| 531 | + "sharded-slab", | ||
| 532 | + "smallvec", | ||
| 533 | + "thread_local", | ||
| 534 | + "tracing", | ||
| 535 | + "tracing-core", | ||
| 536 | + "tracing-log", | ||
| 537 | +] | ||
| 538 | + | ||
| 539 | +[[package]] | ||
| 540 | +name = "unicode-ident" | ||
| 541 | +version = "1.0.22" | ||
| 542 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 543 | +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" | ||
| 544 | + | ||
| 545 | +[[package]] | ||
| 546 | +name = "valuable" | ||
| 547 | +version = "0.1.1" | ||
| 548 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 549 | +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" | ||
| 550 | + | ||
| 551 | +[[package]] | ||
| 552 | +name = "wasi" | ||
| 553 | +version = "0.11.1+wasi-snapshot-preview1" | ||
| 554 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 555 | +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | ||
| 556 | + | ||
| 557 | +[[package]] | ||
| 558 | +name = "windows-link" | ||
| 559 | +version = "0.2.1" | ||
| 560 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 561 | +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | ||
| 562 | + | ||
| 563 | +[[package]] | ||
| 564 | +name = "windows-sys" | ||
| 565 | +version = "0.60.2" | ||
| 566 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 567 | +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" | ||
| 568 | +dependencies = [ | ||
| 569 | + "windows-targets", | ||
| 570 | +] | ||
| 571 | + | ||
| 572 | +[[package]] | ||
| 573 | +name = "windows-sys" | ||
| 574 | +version = "0.61.2" | ||
| 575 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 576 | +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | ||
| 577 | +dependencies = [ | ||
| 578 | + "windows-link", | ||
| 579 | +] | ||
| 580 | + | ||
| 581 | +[[package]] | ||
| 582 | +name = "windows-targets" | ||
| 583 | +version = "0.53.5" | ||
| 584 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 585 | +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" | ||
| 586 | +dependencies = [ | ||
| 587 | + "windows-link", | ||
| 588 | + "windows_aarch64_gnullvm", | ||
| 589 | + "windows_aarch64_msvc", | ||
| 590 | + "windows_i686_gnu", | ||
| 591 | + "windows_i686_gnullvm", | ||
| 592 | + "windows_i686_msvc", | ||
| 593 | + "windows_x86_64_gnu", | ||
| 594 | + "windows_x86_64_gnullvm", | ||
| 595 | + "windows_x86_64_msvc", | ||
| 596 | +] | ||
| 597 | + | ||
| 598 | +[[package]] | ||
| 599 | +name = "windows_aarch64_gnullvm" | ||
| 600 | +version = "0.53.1" | ||
| 601 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 602 | +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" | ||
| 603 | + | ||
| 604 | +[[package]] | ||
| 605 | +name = "windows_aarch64_msvc" | ||
| 606 | +version = "0.53.1" | ||
| 607 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 608 | +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" | ||
| 609 | + | ||
| 610 | +[[package]] | ||
| 611 | +name = "windows_i686_gnu" | ||
| 612 | +version = "0.53.1" | ||
| 613 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 614 | +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" | ||
| 615 | + | ||
| 616 | +[[package]] | ||
| 617 | +name = "windows_i686_gnullvm" | ||
| 618 | +version = "0.53.1" | ||
| 619 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 620 | +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" | ||
| 621 | + | ||
| 622 | +[[package]] | ||
| 623 | +name = "windows_i686_msvc" | ||
| 624 | +version = "0.53.1" | ||
| 625 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 626 | +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" | ||
| 627 | + | ||
| 628 | +[[package]] | ||
| 629 | +name = "windows_x86_64_gnu" | ||
| 630 | +version = "0.53.1" | ||
| 631 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 632 | +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" | ||
| 633 | + | ||
| 634 | +[[package]] | ||
| 635 | +name = "windows_x86_64_gnullvm" | ||
| 636 | +version = "0.53.1" | ||
| 637 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 638 | +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" | ||
| 639 | + | ||
| 640 | +[[package]] | ||
| 641 | +name = "windows_x86_64_msvc" | ||
| 642 | +version = "0.53.1" | ||
| 643 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 644 | +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" | ||
| 645 | + | ||
| 646 | +[[package]] | ||
| 647 | +name = "winnow" | ||
| 648 | +version = "0.7.14" | ||
| 649 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 650 | +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" | ||
| 651 | +dependencies = [ | ||
| 652 | + "memchr", | ||
| 653 | +] | ||
| 654 | + | ||
| 655 | +[[package]] | ||
| 656 | +name = "zmij" | ||
| 657 | +version = "1.0.14" | ||
| 658 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 659 | +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" | ||
Cargo.tomladded@@ -0,0 +1,41 @@ | |||
| 1 | +[workspace] | ||
| 2 | +members = ["gardmd", "gardm-greeter", "gardm-ipc"] | ||
| 3 | +resolver = "2" | ||
| 4 | + | ||
| 5 | +[workspace.package] | ||
| 6 | +version = "0.1.0" | ||
| 7 | +edition = "2021" | ||
| 8 | +license = "MIT" | ||
| 9 | +repository = "https://github.com/mfwolffe/gardesk" | ||
| 10 | + | ||
| 11 | +[workspace.dependencies] | ||
| 12 | +# Serialization | ||
| 13 | +serde = { version = "1.0", features = ["derive"] } | ||
| 14 | +serde_json = "1.0" | ||
| 15 | +toml = "0.8" | ||
| 16 | + | ||
| 17 | +# Async runtime | ||
| 18 | +tokio = { version = "1", features = ["full", "signal"] } | ||
| 19 | + | ||
| 20 | +# Logging | ||
| 21 | +tracing = "0.1" | ||
| 22 | +tracing-subscriber = { version = "0.3", features = ["env-filter"] } | ||
| 23 | + | ||
| 24 | +# Error handling | ||
| 25 | +anyhow = "1.0" | ||
| 26 | +thiserror = "1.0" | ||
| 27 | + | ||
| 28 | +# Unix/Linux | ||
| 29 | +nix = { version = "0.27", features = ["user", "process", "signal"] } | ||
| 30 | + | ||
| 31 | +# X11 | ||
| 32 | +x11rb = { version = "0.13", features = ["allow-unsafe-code"] } | ||
| 33 | + | ||
| 34 | +# Graphics | ||
| 35 | +cairo-rs = { version = "0.18", features = ["png"] } | ||
| 36 | +pango = "0.18" | ||
| 37 | +pangocairo = "0.18" | ||
| 38 | +image = "0.24" | ||
| 39 | + | ||
| 40 | +# IPC | ||
| 41 | +gardm-ipc = { path = "gardm-ipc" } | ||
docs/architecture.mdadded@@ -0,0 +1,219 @@ | |||
| 1 | +# gardm Architecture | ||
| 2 | + | ||
| 3 | +## Overview | ||
| 4 | + | ||
| 5 | +gardm is the display manager for the gar desktop suite. It follows a **daemon + greeter** architecture similar to [greetd](https://github.com/kennylevinsen/greetd), providing clean separation between authentication/session management and the user interface. | ||
| 6 | + | ||
| 7 | +## Components | ||
| 8 | + | ||
| 9 | +``` | ||
| 10 | +┌─────────────────────────────────────────────────────────────┐ | ||
| 11 | +│ gardmd │ | ||
| 12 | +│ (Display Manager Daemon) │ | ||
| 13 | +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ | ||
| 14 | +│ │ PAM │ │ X11/Xorg │ │ Session Manager │ │ | ||
| 15 | +│ │ Integration │ │ Control │ │ (systemd-logind) │ │ | ||
| 16 | +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ | ||
| 17 | +│ │ │ | ||
| 18 | +│ Unix Socket IPC │ | ||
| 19 | +│ │ │ | ||
| 20 | +└──────────────────────────┼──────────────────────────────────┘ | ||
| 21 | + │ | ||
| 22 | +┌──────────────────────────┼──────────────────────────────────┐ | ||
| 23 | +│ ▼ │ | ||
| 24 | +│ gardm-greeter │ | ||
| 25 | +│ (Graphical UI) │ | ||
| 26 | +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ | ||
| 27 | +│ │ Cairo/ │ │ garbg │ │ User Input │ │ | ||
| 28 | +│ │ Pango │ │ Integration │ │ (login form) │ │ | ||
| 29 | +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ | ||
| 30 | +└─────────────────────────────────────────────────────────────┘ | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +### gardmd (Daemon) | ||
| 34 | + | ||
| 35 | +The privileged daemon that runs as root. Responsibilities: | ||
| 36 | + | ||
| 37 | +1. **X11 Server Management**: Start/stop Xorg server on appropriate VT | ||
| 38 | +2. **PAM Authentication**: Verify user credentials via `/etc/pam.d/gardm` | ||
| 39 | +3. **Session Launching**: Start selected session (gar, other WMs/DEs) | ||
| 40 | +4. **systemd-logind Integration**: Register sessions, handle multi-seat | ||
| 41 | +5. **IPC Server**: Listen on Unix socket for greeter commands | ||
| 42 | + | ||
| 43 | +### gardm-greeter (Frontend) | ||
| 44 | + | ||
| 45 | +The unprivileged graphical interface. Runs as dedicated `gardm` user. Responsibilities: | ||
| 46 | + | ||
| 47 | +1. **User Interface**: Render login form, session selector, power buttons | ||
| 48 | +2. **garbg Integration**: Display same wallpaper that gar session will use | ||
| 49 | +3. **Visual Effects**: Blur background, animations, theming | ||
| 50 | +4. **IPC Client**: Send auth requests to daemon, receive responses | ||
| 51 | + | ||
| 52 | +## IPC Protocol | ||
| 53 | + | ||
| 54 | +JSON-based protocol over Unix socket at `/run/gardm.sock`: | ||
| 55 | + | ||
| 56 | +### Requests (Greeter → Daemon) | ||
| 57 | + | ||
| 58 | +```json | ||
| 59 | +// Create authentication session | ||
| 60 | +{ "type": "create_session", "username": "user" } | ||
| 61 | + | ||
| 62 | +// Attempt authentication | ||
| 63 | +{ "type": "authenticate", "response": "password123" } | ||
| 64 | + | ||
| 65 | +// Start session after successful auth | ||
| 66 | +{ "type": "start_session", "cmd": ["gar-session.sh"], "env": [] } | ||
| 67 | + | ||
| 68 | +// Cancel current auth attempt | ||
| 69 | +{ "type": "cancel_session" } | ||
| 70 | + | ||
| 71 | +// System actions | ||
| 72 | +{ "type": "shutdown" } | ||
| 73 | +{ "type": "reboot" } | ||
| 74 | +{ "type": "suspend" } | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +### Responses (Daemon → Greeter) | ||
| 78 | + | ||
| 79 | +```json | ||
| 80 | +// Success | ||
| 81 | +{ "type": "success" } | ||
| 82 | + | ||
| 83 | +// Auth prompt (PAM asking for input) | ||
| 84 | +{ "type": "auth_prompt", "prompt": "Password:", "echo": false } | ||
| 85 | + | ||
| 86 | +// Auth info message | ||
| 87 | +{ "type": "auth_info", "message": "..." } | ||
| 88 | + | ||
| 89 | +// Auth error | ||
| 90 | +{ "type": "auth_error", "message": "Authentication failed" } | ||
| 91 | + | ||
| 92 | +// General error | ||
| 93 | +{ "type": "error", "message": "..." } | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +## Session Discovery | ||
| 97 | + | ||
| 98 | +Sessions are discovered from standard locations: | ||
| 99 | + | ||
| 100 | +- `/usr/share/xsessions/*.desktop` - X11 sessions | ||
| 101 | +- `/usr/share/wayland-sessions/*.desktop` - Wayland sessions (future) | ||
| 102 | + | ||
| 103 | +gar session file example (`/usr/share/xsessions/gar.desktop`): | ||
| 104 | +```ini | ||
| 105 | +[Desktop Entry] | ||
| 106 | +Name=gar | ||
| 107 | +Comment=Tiling window manager with smart splits | ||
| 108 | +Exec=/usr/local/bin/gar-session.sh | ||
| 109 | +Type=XSession | ||
| 110 | +DesktopNames=gar | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +## User Discovery | ||
| 114 | + | ||
| 115 | +Users are enumerated from: | ||
| 116 | + | ||
| 117 | +- `/etc/passwd` (filter by UID range, shell, home directory existence) | ||
| 118 | +- AccountsService D-Bus interface (if available) | ||
| 119 | + | ||
| 120 | +Hidden users (UID < 1000, nologin shell, system accounts) are filtered out. | ||
| 121 | + | ||
| 122 | +## garbg Integration | ||
| 123 | + | ||
| 124 | +The greeter reads garbg's config to display the same wallpaper: | ||
| 125 | + | ||
| 126 | +1. Read `~/.config/garbg/config.toml` for default wallpaper source | ||
| 127 | +2. Or read playlist state from `$XDG_RUNTIME_DIR/garbg-state.json` | ||
| 128 | +3. Apply blur effect for greeter aesthetic | ||
| 129 | +4. On successful login, gar session starts garbg which shows same image (seamless transition) | ||
| 130 | + | ||
| 131 | +## Security Model | ||
| 132 | + | ||
| 133 | +- **gardmd**: Runs as root, handles PAM, minimal attack surface | ||
| 134 | +- **gardm-greeter**: Runs as unprivileged `gardm` user | ||
| 135 | +- **IPC**: Socket permissions restrict access to gardm user | ||
| 136 | +- **X11**: Greeter runs on isolated X server started by daemon | ||
| 137 | + | ||
| 138 | +## Configuration | ||
| 139 | + | ||
| 140 | +`/etc/gardm/config.toml`: | ||
| 141 | + | ||
| 142 | +```toml | ||
| 143 | +[general] | ||
| 144 | +# Default session if user hasn't selected one | ||
| 145 | +default_session = "gar" | ||
| 146 | + | ||
| 147 | +# Greeter command | ||
| 148 | +greeter = "/usr/bin/gardm-greeter" | ||
| 149 | + | ||
| 150 | +# VT to use (0 = auto-select) | ||
| 151 | +vt = 0 | ||
| 152 | + | ||
| 153 | +[greeter] | ||
| 154 | +# Theme settings | ||
| 155 | +blur_radius = 20 | ||
| 156 | +blur_brightness = 0.7 | ||
| 157 | + | ||
| 158 | +# Show/hide elements | ||
| 159 | +show_power_buttons = true | ||
| 160 | +show_session_selector = true | ||
| 161 | + | ||
| 162 | +# garbg integration | ||
| 163 | +use_garbg_wallpaper = true | ||
| 164 | +fallback_wallpaper = "/usr/share/gardm/backgrounds/default.jpg" | ||
| 165 | + | ||
| 166 | +[security] | ||
| 167 | +# Allow empty passwords | ||
| 168 | +allow_empty_password = false | ||
| 169 | + | ||
| 170 | +# Lock after N failed attempts (0 = disabled) | ||
| 171 | +lockout_attempts = 5 | ||
| 172 | +lockout_duration = 300 | ||
| 173 | +``` | ||
| 174 | + | ||
| 175 | +## PAM Configuration | ||
| 176 | + | ||
| 177 | +`/etc/pam.d/gardm`: | ||
| 178 | + | ||
| 179 | +``` | ||
| 180 | +#%PAM-1.0 | ||
| 181 | +auth required pam_securetty.so | ||
| 182 | +auth requisite pam_nologin.so | ||
| 183 | +auth include system-local-login | ||
| 184 | +account include system-local-login | ||
| 185 | +session include system-local-login | ||
| 186 | +password include system-local-login | ||
| 187 | +``` | ||
| 188 | + | ||
| 189 | +## systemd Integration | ||
| 190 | + | ||
| 191 | +`/usr/lib/systemd/system/gardm.service`: | ||
| 192 | + | ||
| 193 | +```ini | ||
| 194 | +[Unit] | ||
| 195 | +Description=gar Display Manager | ||
| 196 | +After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service | ||
| 197 | +Conflicts=getty@tty1.service | ||
| 198 | + | ||
| 199 | +[Service] | ||
| 200 | +ExecStart=/usr/bin/gardmd | ||
| 201 | +Restart=always | ||
| 202 | + | ||
| 203 | +[Install] | ||
| 204 | +Alias=display-manager.service | ||
| 205 | +``` | ||
| 206 | + | ||
| 207 | +## Key Dependencies | ||
| 208 | + | ||
| 209 | +| Component | Crates | | ||
| 210 | +|-----------|--------| | ||
| 211 | +| gardmd | pam, nix, sd-notify, serde, tokio | | ||
| 212 | +| gardm-greeter | x11rb, cairo-rs, pango, image, serde | | ||
| 213 | + | ||
| 214 | +## References | ||
| 215 | + | ||
| 216 | +- [freedesktop.org: Writing Display Managers](https://systemd.io/WRITING_DISPLAY_MANAGERS/) | ||
| 217 | +- [greetd](https://github.com/kennylevinsen/greetd) - Architecture inspiration | ||
| 218 | +- [SDDM](https://github.com/sddm/sddm) - Feature reference | ||
| 219 | +- [LightDM](https://github.com/canonical/lightdm) - Protocol reference | ||
docs/checklist.mdadded@@ -0,0 +1,157 @@ | |||
| 1 | +# gardm Feature Checklist | ||
| 2 | + | ||
| 3 | +## Core Functionality | ||
| 4 | + | ||
| 5 | +### Authentication | ||
| 6 | +- [ ] PAM integration for password authentication | ||
| 7 | +- [ ] Multi-factor auth support (PAM handles this transparently) | ||
| 8 | +- [ ] Account lockout after failed attempts | ||
| 9 | +- [ ] "Remember last user" functionality | ||
| 10 | +- [ ] Guest session support (optional) | ||
| 11 | + | ||
| 12 | +### Session Management | ||
| 13 | +- [ ] Discover X11 sessions from `/usr/share/xsessions/` | ||
| 14 | +- [ ] Parse .desktop files for session metadata | ||
| 15 | +- [ ] Remember user's last session choice | ||
| 16 | +- [ ] Support custom session commands | ||
| 17 | +- [ ] Proper session environment setup (DISPLAY, XDG_*, etc.) | ||
| 18 | +- [ ] systemd-logind session registration | ||
| 19 | + | ||
| 20 | +### X11 Server Management | ||
| 21 | +- [ ] Start Xorg on appropriate VT | ||
| 22 | +- [ ] Handle X server crashes gracefully | ||
| 23 | +- [ ] Support multi-seat (seat0 initially) | ||
| 24 | +- [ ] Pass correct display/VT to sessions | ||
| 25 | +- [ ] Clean X server shutdown on logout | ||
| 26 | + | ||
| 27 | +### Power Management | ||
| 28 | +- [ ] Shutdown button (requires polkit or root) | ||
| 29 | +- [ ] Reboot button | ||
| 30 | +- [ ] Suspend button | ||
| 31 | +- [ ] Hibernate button (if available) | ||
| 32 | +- [ ] Scheduled actions (shutdown in 5 min, etc.) - optional | ||
| 33 | + | ||
| 34 | +## User Interface | ||
| 35 | + | ||
| 36 | +### Login Form | ||
| 37 | +- [ ] Username input (with dropdown of available users) | ||
| 38 | +- [ ] Password input (masked) | ||
| 39 | +- [ ] Session selector dropdown | ||
| 40 | +- [ ] Login button | ||
| 41 | +- [ ] Clear error messages on auth failure | ||
| 42 | +- [ ] Loading/spinner during authentication | ||
| 43 | + | ||
| 44 | +### Visual Design | ||
| 45 | +- [ ] Centered login form | ||
| 46 | +- [ ] Blurred background image | ||
| 47 | +- [ ] Configurable blur radius/brightness | ||
| 48 | +- [ ] Smooth fade-in on greeter start | ||
| 49 | +- [ ] Smooth transition to session (fade-out) | ||
| 50 | +- [ ] Clock display | ||
| 51 | +- [ ] Current date display | ||
| 52 | + | ||
| 53 | +### User List | ||
| 54 | +- [ ] Show available users with avatars | ||
| 55 | +- [ ] Filter system accounts (UID < 1000) | ||
| 56 | +- [ ] Filter accounts with nologin shell | ||
| 57 | +- [ ] Support user icons from AccountsService | ||
| 58 | +- [ ] Keyboard navigation through user list | ||
| 59 | + | ||
| 60 | +### Theming | ||
| 61 | +- [ ] Configurable colors/fonts via config file | ||
| 62 | +- [ ] Support custom background images | ||
| 63 | +- [ ] garbg wallpaper integration | ||
| 64 | +- [ ] Logo/branding support | ||
| 65 | +- [ ] Dark/light theme variants | ||
| 66 | + | ||
| 67 | +### Accessibility | ||
| 68 | +- [ ] Keyboard-only navigation (Tab, Enter, Escape) | ||
| 69 | +- [ ] High contrast mode (optional) | ||
| 70 | +- [ ] Screen reader support (optional, complex) | ||
| 71 | +- [ ] On-screen keyboard (optional, complex) | ||
| 72 | + | ||
| 73 | +## gar Integration | ||
| 74 | + | ||
| 75 | +### garbg Wallpaper Sync | ||
| 76 | +- [ ] Read garbg config for default wallpaper | ||
| 77 | +- [ ] Read garbg playlist state for current wallpaper | ||
| 78 | +- [ ] Apply consistent blur to match gar lock screen | ||
| 79 | +- [ ] Seamless visual transition to gar session | ||
| 80 | + | ||
| 81 | +### Shared Configuration | ||
| 82 | +- [ ] Read gar color scheme for consistent theming | ||
| 83 | +- [ ] Use same fonts as garbar | ||
| 84 | +- [ ] Respect gar's corner radius settings | ||
| 85 | + | ||
| 86 | +## System Integration | ||
| 87 | + | ||
| 88 | +### systemd | ||
| 89 | +- [ ] Proper service file | ||
| 90 | +- [ ] Socket activation (optional) | ||
| 91 | +- [ ] sd_notify for readiness | ||
| 92 | +- [ ] Proper shutdown handling (SIGTERM) | ||
| 93 | +- [ ] Alias as display-manager.service | ||
| 94 | + | ||
| 95 | +### D-Bus | ||
| 96 | +- [ ] systemd-logind integration for session management | ||
| 97 | +- [ ] AccountsService for user info (optional) | ||
| 98 | +- [ ] polkit for power actions (optional) | ||
| 99 | + | ||
| 100 | +### Files and Paths | ||
| 101 | +- [ ] Config file: `/etc/gardm/config.toml` | ||
| 102 | +- [ ] PAM config: `/etc/pam.d/gardm` | ||
| 103 | +- [ ] Socket: `/run/gardm.sock` | ||
| 104 | +- [ ] PID file: `/run/gardm.pid` | ||
| 105 | +- [ ] Log file: via journald | ||
| 106 | +- [ ] State dir: `/var/lib/gardm/` | ||
| 107 | + | ||
| 108 | +## Security | ||
| 109 | + | ||
| 110 | +### Hardening | ||
| 111 | +- [ ] Greeter runs as unprivileged user | ||
| 112 | +- [ ] Minimal daemon attack surface | ||
| 113 | +- [ ] Socket permissions (0600, gardm:gardm) | ||
| 114 | +- [ ] No credentials in logs | ||
| 115 | +- [ ] Secure memory handling for passwords | ||
| 116 | + | ||
| 117 | +### Input Validation | ||
| 118 | +- [ ] Sanitize username input | ||
| 119 | +- [ ] Prevent path traversal in session selection | ||
| 120 | +- [ ] Rate limiting on auth attempts | ||
| 121 | +- [ ] Timeout for idle greeter | ||
| 122 | + | ||
| 123 | +## Error Handling | ||
| 124 | + | ||
| 125 | +- [ ] X server fail to start | ||
| 126 | +- [ ] PAM errors (account locked, expired, etc.) | ||
| 127 | +- [ ] Session fail to start | ||
| 128 | +- [ ] IPC communication errors | ||
| 129 | +- [ ] Missing configuration graceful fallback | ||
| 130 | +- [ ] Greeter crash recovery (restart greeter) | ||
| 131 | + | ||
| 132 | +## Testing | ||
| 133 | + | ||
| 134 | +- [ ] Unit tests for IPC protocol | ||
| 135 | +- [ ] Unit tests for session discovery | ||
| 136 | +- [ ] Integration tests with mock PAM | ||
| 137 | +- [ ] Manual testing checklist | ||
| 138 | +- [ ] Xephyr-based testing for greeter UI | ||
| 139 | + | ||
| 140 | +## Documentation | ||
| 141 | + | ||
| 142 | +- [ ] README with quick start | ||
| 143 | +- [ ] Configuration reference | ||
| 144 | +- [ ] Theming guide | ||
| 145 | +- [ ] Troubleshooting guide | ||
| 146 | +- [ ] Man pages (gardmd.8, gardm-greeter.1) | ||
| 147 | + | ||
| 148 | +## Nice-to-Have (Future) | ||
| 149 | + | ||
| 150 | +- [ ] Wayland greeter support | ||
| 151 | +- [ ] Network login (LDAP, etc.) | ||
| 152 | +- [ ] Fingerprint authentication | ||
| 153 | +- [ ] Auto-login configuration | ||
| 154 | +- [ ] Remote desktop support | ||
| 155 | +- [ ] Multi-monitor greeter | ||
| 156 | +- [ ] Animated backgrounds | ||
| 157 | +- [ ] Weather/calendar widgets | ||
docs/gardm.mdadded@@ -0,0 +1,14 @@ | |||
| 1 | +# GARDM | ||
| 2 | + | ||
| 3 | +the next step in the gar desktop experience is the gar desktop manager. it should be oriented towards integration with other gar products. | ||
| 4 | +it should set the same background as will be set by garbg on launching gar so that the only thing that changes during the transition is the animation to load in | ||
| 5 | + | ||
| 6 | +it should have users centered, and all the standard desktop manger buttons. | ||
| 7 | + | ||
| 8 | +it should have a sleek interface like my current sddm approach. background should be blurred. username/pass should be centered. | ||
| 9 | + | ||
| 10 | +first stage is planning. we need to reference existing foss greeters/desktop managers to see what ours needs to do, otherwise we're lost. | ||
| 11 | +We'll make a checklist of targets to hit and generate sprint files in the .gitignored directory docs/sprints/. each sprint file should have a list of targets and pitfalls to avoid | ||
| 12 | + | ||
| 13 | +gardm should do everything that a modern greeter does, and look pretty while doing it. it should be gar-first, that is oriented towards the gar desktop environment and window manager, but it should be compatible with all linux machines that support x11. | ||
| 14 | +we're going with x11 because of nvidia issues on wayland, but we can still make a pretty greeter in xorgland. | ||
docs/sprints/sprint-0-setup.mdadded@@ -0,0 +1,246 @@ | |||
| 1 | +# Sprint 0: Project Setup | ||
| 2 | + | ||
| 3 | +**Goal:** Establish project structure, build system, and minimal daemon that can start and stop cleanly. | ||
| 4 | + | ||
| 5 | +## Objectives | ||
| 6 | + | ||
| 7 | +- Initialize Rust workspace with two crates (gardmd, gardm-greeter) | ||
| 8 | +- Set up shared library for IPC protocol | ||
| 9 | +- Create systemd service file | ||
| 10 | +- Establish logging infrastructure | ||
| 11 | +- Basic daemon lifecycle (start, signal handling, shutdown) | ||
| 12 | + | ||
| 13 | +## Tasks | ||
| 14 | + | ||
| 15 | +### 0.1 Initialize Cargo Workspace | ||
| 16 | + | ||
| 17 | +``` | ||
| 18 | +gardm/ | ||
| 19 | +├── Cargo.toml # Workspace root | ||
| 20 | +├── gardmd/ | ||
| 21 | +│ ├── Cargo.toml | ||
| 22 | +│ └── src/ | ||
| 23 | +│ ├── main.rs # Daemon entry point | ||
| 24 | +│ └── lib.rs | ||
| 25 | +├── gardm-greeter/ | ||
| 26 | +│ ├── Cargo.toml | ||
| 27 | +│ └── src/ | ||
| 28 | +│ ├── main.rs # Greeter entry point | ||
| 29 | +│ └── lib.rs | ||
| 30 | +└── gardm-ipc/ | ||
| 31 | + ├── Cargo.toml | ||
| 32 | + └── src/ | ||
| 33 | + └── lib.rs # Shared IPC types | ||
| 34 | +``` | ||
| 35 | + | ||
| 36 | +**Root Cargo.toml:** | ||
| 37 | +```toml | ||
| 38 | +[workspace] | ||
| 39 | +members = ["gardmd", "gardm-greeter", "gardm-ipc"] | ||
| 40 | +resolver = "2" | ||
| 41 | + | ||
| 42 | +[workspace.dependencies] | ||
| 43 | +serde = { version = "1.0", features = ["derive"] } | ||
| 44 | +serde_json = "1.0" | ||
| 45 | +tokio = { version = "1", features = ["full"] } | ||
| 46 | +tracing = "0.1" | ||
| 47 | +tracing-subscriber = "0.3" | ||
| 48 | +anyhow = "1.0" | ||
| 49 | +thiserror = "1.0" | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +### 0.2 Define IPC Protocol Types | ||
| 53 | + | ||
| 54 | +In `gardm-ipc/src/lib.rs`: | ||
| 55 | + | ||
| 56 | +```rust | ||
| 57 | +use serde::{Deserialize, Serialize}; | ||
| 58 | + | ||
| 59 | +#[derive(Debug, Serialize, Deserialize)] | ||
| 60 | +#[serde(tag = "type", rename_all = "snake_case")] | ||
| 61 | +pub enum Request { | ||
| 62 | + CreateSession { username: String }, | ||
| 63 | + Authenticate { response: String }, | ||
| 64 | + StartSession { cmd: Vec<String>, env: Vec<String> }, | ||
| 65 | + CancelSession, | ||
| 66 | + Shutdown, | ||
| 67 | + Reboot, | ||
| 68 | + Suspend, | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +#[derive(Debug, Serialize, Deserialize)] | ||
| 72 | +#[serde(tag = "type", rename_all = "snake_case")] | ||
| 73 | +pub enum Response { | ||
| 74 | + Success, | ||
| 75 | + AuthPrompt { prompt: String, echo: bool }, | ||
| 76 | + AuthInfo { message: String }, | ||
| 77 | + AuthError { message: String }, | ||
| 78 | + Error { message: String }, | ||
| 79 | +} | ||
| 80 | +``` | ||
| 81 | + | ||
| 82 | +### 0.3 Basic Daemon Skeleton | ||
| 83 | + | ||
| 84 | +In `gardmd/src/main.rs`: | ||
| 85 | + | ||
| 86 | +```rust | ||
| 87 | +use tokio::signal::unix::{signal, SignalKind}; | ||
| 88 | +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; | ||
| 89 | + | ||
| 90 | +#[tokio::main] | ||
| 91 | +async fn main() -> anyhow::Result<()> { | ||
| 92 | + // Initialize logging to journald | ||
| 93 | + tracing_subscriber::registry() | ||
| 94 | + .with(fmt::layer()) | ||
| 95 | + .with(EnvFilter::from_default_env()) | ||
| 96 | + .init(); | ||
| 97 | + | ||
| 98 | + tracing::info!("gardmd starting"); | ||
| 99 | + | ||
| 100 | + // Signal handling | ||
| 101 | + let mut sigterm = signal(SignalKind::terminate())?; | ||
| 102 | + let mut sigint = signal(SignalKind::interrupt())?; | ||
| 103 | + | ||
| 104 | + tokio::select! { | ||
| 105 | + _ = sigterm.recv() => { | ||
| 106 | + tracing::info!("Received SIGTERM, shutting down"); | ||
| 107 | + } | ||
| 108 | + _ = sigint.recv() => { | ||
| 109 | + tracing::info!("Received SIGINT, shutting down"); | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + tracing::info!("gardmd stopped"); | ||
| 114 | + Ok(()) | ||
| 115 | +} | ||
| 116 | +``` | ||
| 117 | + | ||
| 118 | +### 0.4 systemd Service File | ||
| 119 | + | ||
| 120 | +Create `gardm.service`: | ||
| 121 | + | ||
| 122 | +```ini | ||
| 123 | +[Unit] | ||
| 124 | +Description=gar Display Manager | ||
| 125 | +Documentation=man:gardmd(8) | ||
| 126 | +After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service | ||
| 127 | +Conflicts=getty@tty1.service | ||
| 128 | + | ||
| 129 | +[Service] | ||
| 130 | +Type=notify | ||
| 131 | +ExecStart=/usr/bin/gardmd | ||
| 132 | +Restart=always | ||
| 133 | +RestartSec=1 | ||
| 134 | + | ||
| 135 | +# Security hardening | ||
| 136 | +ProtectSystem=strict | ||
| 137 | +ProtectHome=read-only | ||
| 138 | +PrivateTmp=true | ||
| 139 | +NoNewPrivileges=false # Required for PAM | ||
| 140 | +ReadWritePaths=/run | ||
| 141 | + | ||
| 142 | +[Install] | ||
| 143 | +Alias=display-manager.service | ||
| 144 | +``` | ||
| 145 | + | ||
| 146 | +### 0.5 Configuration Scaffolding | ||
| 147 | + | ||
| 148 | +Create `gardmd/src/config.rs`: | ||
| 149 | + | ||
| 150 | +```rust | ||
| 151 | +use serde::Deserialize; | ||
| 152 | +use std::path::PathBuf; | ||
| 153 | + | ||
| 154 | +#[derive(Debug, Deserialize)] | ||
| 155 | +pub struct Config { | ||
| 156 | + #[serde(default)] | ||
| 157 | + pub general: GeneralConfig, | ||
| 158 | + #[serde(default)] | ||
| 159 | + pub greeter: GreeterConfig, | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +#[derive(Debug, Deserialize, Default)] | ||
| 163 | +pub struct GeneralConfig { | ||
| 164 | + #[serde(default = "default_session")] | ||
| 165 | + pub default_session: String, | ||
| 166 | + #[serde(default = "default_greeter")] | ||
| 167 | + pub greeter: PathBuf, | ||
| 168 | + #[serde(default)] | ||
| 169 | + pub vt: u32, | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +#[derive(Debug, Deserialize, Default)] | ||
| 173 | +pub struct GreeterConfig { | ||
| 174 | + #[serde(default = "default_blur_radius")] | ||
| 175 | + pub blur_radius: u32, | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +fn default_session() -> String { "gar".to_string() } | ||
| 179 | +fn default_greeter() -> PathBuf { "/usr/bin/gardm-greeter".into() } | ||
| 180 | +fn default_blur_radius() -> u32 { 20 } | ||
| 181 | + | ||
| 182 | +impl Config { | ||
| 183 | + pub fn load() -> anyhow::Result<Self> { | ||
| 184 | + let path = PathBuf::from("/etc/gardm/config.toml"); | ||
| 185 | + if path.exists() { | ||
| 186 | + let content = std::fs::read_to_string(&path)?; | ||
| 187 | + Ok(toml::from_str(&content)?) | ||
| 188 | + } else { | ||
| 189 | + Ok(Config::default()) | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | +} | ||
| 193 | +``` | ||
| 194 | + | ||
| 195 | +## Acceptance Criteria | ||
| 196 | + | ||
| 197 | +1. `cargo build --release` succeeds for all workspace members | ||
| 198 | +2. `gardmd` starts, logs to stdout, and exits cleanly on SIGTERM | ||
| 199 | +3. IPC types serialize/deserialize correctly (unit test) | ||
| 200 | +4. Config loads from file or uses defaults | ||
| 201 | +5. Service file passes `systemd-analyze verify` | ||
| 202 | + | ||
| 203 | +## Pitfalls to Avoid | ||
| 204 | + | ||
| 205 | +1. **Don't start X11 yet** - that's Sprint 2. Keep this sprint minimal. | ||
| 206 | +2. **Don't implement PAM yet** - that's Sprint 1. Focus on structure. | ||
| 207 | +3. **Avoid premature optimization** - get it working first. | ||
| 208 | +4. **Don't forget `sd_notify`** - systemd needs to know when daemon is ready. | ||
| 209 | +5. **Test without root first** - use `RUST_LOG=debug cargo run` for development. | ||
| 210 | + | ||
| 211 | +## Testing | ||
| 212 | + | ||
| 213 | +```bash | ||
| 214 | +# Build | ||
| 215 | +cargo build --release | ||
| 216 | + | ||
| 217 | +# Run daemon in foreground (Ctrl+C to stop) | ||
| 218 | +RUST_LOG=debug ./target/release/gardmd | ||
| 219 | + | ||
| 220 | +# Verify signal handling | ||
| 221 | +kill -TERM $(pidof gardmd) | ||
| 222 | + | ||
| 223 | +# Test config loading | ||
| 224 | +mkdir -p /tmp/gardm-test | ||
| 225 | +echo '[general]' > /tmp/gardm-test/config.toml | ||
| 226 | +echo 'default_session = "test"' >> /tmp/gardm-test/config.toml | ||
| 227 | +``` | ||
| 228 | + | ||
| 229 | +## Dependencies for This Sprint | ||
| 230 | + | ||
| 231 | +```toml | ||
| 232 | +# gardmd/Cargo.toml | ||
| 233 | +[dependencies] | ||
| 234 | +gardm-ipc = { path = "../gardm-ipc" } | ||
| 235 | +tokio = { workspace = true } | ||
| 236 | +tracing = { workspace = true } | ||
| 237 | +tracing-subscriber = { workspace = true } | ||
| 238 | +anyhow = { workspace = true } | ||
| 239 | +serde = { workspace = true } | ||
| 240 | +toml = "0.8" | ||
| 241 | +sd-notify = "0.4" | ||
| 242 | +``` | ||
| 243 | + | ||
| 244 | +## Next Sprint | ||
| 245 | + | ||
| 246 | +Sprint 1 will add PAM authentication to the daemon. | ||
docs/sprints/sprint-1-pam.mdadded@@ -0,0 +1,356 @@ | |||
| 1 | +# Sprint 1: PAM Authentication | ||
| 2 | + | ||
| 3 | +**Goal:** Implement PAM-based user authentication in the daemon with proper conversation handling. | ||
| 4 | + | ||
| 5 | +## Objectives | ||
| 6 | + | ||
| 7 | +- Integrate with PAM for user authentication | ||
| 8 | +- Handle PAM conversation (prompts, messages) | ||
| 9 | +- Create IPC server for greeter communication | ||
| 10 | +- Implement session state machine | ||
| 11 | +- Test authentication flow end-to-end | ||
| 12 | + | ||
| 13 | +## Background: How PAM Works | ||
| 14 | + | ||
| 15 | +PAM (Pluggable Authentication Modules) uses a "conversation" model: | ||
| 16 | + | ||
| 17 | +1. Application calls `pam_authenticate()` | ||
| 18 | +2. PAM modules may request information via conversation callback | ||
| 19 | +3. Callback returns user input (password, OTP, etc.) | ||
| 20 | +4. PAM returns success/failure | ||
| 21 | + | ||
| 22 | +For a display manager, we need to bridge this conversation to the greeter UI: | ||
| 23 | + | ||
| 24 | +``` | ||
| 25 | +Greeter ←→ IPC ←→ Daemon ←→ PAM ←→ System | ||
| 26 | +``` | ||
| 27 | + | ||
| 28 | +## Tasks | ||
| 29 | + | ||
| 30 | +### 1.1 Add PAM Dependency | ||
| 31 | + | ||
| 32 | +The `pam` crate provides safe Rust bindings: | ||
| 33 | + | ||
| 34 | +```toml | ||
| 35 | +# gardmd/Cargo.toml | ||
| 36 | +[dependencies] | ||
| 37 | +pam = "0.8" | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +### 1.2 Create PAM Configuration | ||
| 41 | + | ||
| 42 | +`/etc/pam.d/gardm`: | ||
| 43 | + | ||
| 44 | +``` | ||
| 45 | +#%PAM-1.0 | ||
| 46 | +auth required pam_securetty.so | ||
| 47 | +auth requisite pam_nologin.so | ||
| 48 | +auth include system-local-login | ||
| 49 | +account include system-local-login | ||
| 50 | +session include system-local-login | ||
| 51 | +password include system-local-login | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +### 1.3 Implement Authentication State Machine | ||
| 55 | + | ||
| 56 | +```rust | ||
| 57 | +// gardmd/src/auth.rs | ||
| 58 | + | ||
| 59 | +use pam::Authenticator; | ||
| 60 | +use std::ffi::CString; | ||
| 61 | + | ||
| 62 | +pub enum AuthState { | ||
| 63 | + /// No active authentication | ||
| 64 | + Idle, | ||
| 65 | + /// Waiting for username | ||
| 66 | + AwaitingUsername, | ||
| 67 | + /// PAM conversation in progress | ||
| 68 | + Authenticating { | ||
| 69 | + username: String, | ||
| 70 | + authenticator: Authenticator<'static, PasswordConv>, | ||
| 71 | + }, | ||
| 72 | + /// Authentication succeeded, ready to start session | ||
| 73 | + Authenticated { | ||
| 74 | + username: String, | ||
| 75 | + }, | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +pub struct AuthSession { | ||
| 79 | + state: AuthState, | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +impl AuthSession { | ||
| 83 | + pub fn new() -> Self { | ||
| 84 | + Self { state: AuthState::Idle } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + pub fn create_session(&mut self, username: &str) -> Result<AuthResponse> { | ||
| 88 | + let service = CString::new("gardm")?; | ||
| 89 | + let username_c = CString::new(username)?; | ||
| 90 | + | ||
| 91 | + // Create PAM authenticator | ||
| 92 | + let mut auth = Authenticator::with_password(&service)?; | ||
| 93 | + auth.get_handler().set_credentials(username_c, /* password later */); | ||
| 94 | + | ||
| 95 | + self.state = AuthState::Authenticating { | ||
| 96 | + username: username.to_string(), | ||
| 97 | + authenticator: auth, | ||
| 98 | + }; | ||
| 99 | + | ||
| 100 | + // PAM will ask for password | ||
| 101 | + Ok(AuthResponse::Prompt { | ||
| 102 | + prompt: "Password:".to_string(), | ||
| 103 | + echo: false, | ||
| 104 | + }) | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + pub fn authenticate(&mut self, response: &str) -> Result<AuthResponse> { | ||
| 108 | + match &mut self.state { | ||
| 109 | + AuthState::Authenticating { username, authenticator } => { | ||
| 110 | + // Provide password to PAM | ||
| 111 | + authenticator.get_handler().set_credentials( | ||
| 112 | + CString::new(username.as_str())?, | ||
| 113 | + CString::new(response)?, | ||
| 114 | + ); | ||
| 115 | + | ||
| 116 | + // Attempt authentication | ||
| 117 | + match authenticator.authenticate() { | ||
| 118 | + Ok(()) => { | ||
| 119 | + let username = username.clone(); | ||
| 120 | + self.state = AuthState::Authenticated { username }; | ||
| 121 | + Ok(AuthResponse::Success) | ||
| 122 | + } | ||
| 123 | + Err(e) => { | ||
| 124 | + self.state = AuthState::Idle; | ||
| 125 | + Ok(AuthResponse::Error { | ||
| 126 | + message: format!("Authentication failed: {}", e), | ||
| 127 | + }) | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + _ => Ok(AuthResponse::Error { | ||
| 132 | + message: "No authentication in progress".to_string(), | ||
| 133 | + }), | ||
| 134 | + } | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + pub fn cancel(&mut self) { | ||
| 138 | + self.state = AuthState::Idle; | ||
| 139 | + } | ||
| 140 | +} | ||
| 141 | +``` | ||
| 142 | + | ||
| 143 | +### 1.4 Implement IPC Server | ||
| 144 | + | ||
| 145 | +```rust | ||
| 146 | +// gardmd/src/ipc.rs | ||
| 147 | + | ||
| 148 | +use gardm_ipc::{Request, Response}; | ||
| 149 | +use tokio::net::{UnixListener, UnixStream}; | ||
| 150 | +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; | ||
| 151 | + | ||
| 152 | +pub struct IpcServer { | ||
| 153 | + listener: UnixListener, | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +impl IpcServer { | ||
| 157 | + pub async fn new(path: &str) -> anyhow::Result<Self> { | ||
| 158 | + // Remove stale socket | ||
| 159 | + let _ = std::fs::remove_file(path); | ||
| 160 | + | ||
| 161 | + let listener = UnixListener::bind(path)?; | ||
| 162 | + | ||
| 163 | + // Set permissions (only gardm user can connect) | ||
| 164 | + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; | ||
| 165 | + | ||
| 166 | + Ok(Self { listener }) | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + pub async fn accept(&self) -> anyhow::Result<IpcClient> { | ||
| 170 | + let (stream, _) = self.listener.accept().await?; | ||
| 171 | + Ok(IpcClient::new(stream)) | ||
| 172 | + } | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | +pub struct IpcClient { | ||
| 176 | + reader: BufReader<tokio::net::unix::OwnedReadHalf>, | ||
| 177 | + writer: tokio::net::unix::OwnedWriteHalf, | ||
| 178 | +} | ||
| 179 | + | ||
| 180 | +impl IpcClient { | ||
| 181 | + pub fn new(stream: UnixStream) -> Self { | ||
| 182 | + let (read, write) = stream.into_split(); | ||
| 183 | + Self { | ||
| 184 | + reader: BufReader::new(read), | ||
| 185 | + writer: write, | ||
| 186 | + } | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + pub async fn read_request(&mut self) -> anyhow::Result<Option<Request>> { | ||
| 190 | + let mut line = String::new(); | ||
| 191 | + let n = self.reader.read_line(&mut line).await?; | ||
| 192 | + if n == 0 { | ||
| 193 | + return Ok(None); | ||
| 194 | + } | ||
| 195 | + Ok(Some(serde_json::from_str(&line)?)) | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + pub async fn send_response(&mut self, response: &Response) -> anyhow::Result<()> { | ||
| 199 | + let json = serde_json::to_string(response)?; | ||
| 200 | + self.writer.write_all(json.as_bytes()).await?; | ||
| 201 | + self.writer.write_all(b"\n").await?; | ||
| 202 | + self.writer.flush().await?; | ||
| 203 | + Ok(()) | ||
| 204 | + } | ||
| 205 | +} | ||
| 206 | +``` | ||
| 207 | + | ||
| 208 | +### 1.5 Wire Up Main Loop | ||
| 209 | + | ||
| 210 | +```rust | ||
| 211 | +// gardmd/src/main.rs | ||
| 212 | + | ||
| 213 | +async fn run() -> anyhow::Result<()> { | ||
| 214 | + let config = Config::load()?; | ||
| 215 | + let ipc = IpcServer::new("/run/gardm.sock").await?; | ||
| 216 | + let mut auth = AuthSession::new(); | ||
| 217 | + | ||
| 218 | + // Notify systemd we're ready | ||
| 219 | + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; | ||
| 220 | + | ||
| 221 | + tracing::info!("gardmd ready, listening for connections"); | ||
| 222 | + | ||
| 223 | + loop { | ||
| 224 | + let mut client = ipc.accept().await?; | ||
| 225 | + | ||
| 226 | + // Handle single client (greeter) | ||
| 227 | + while let Some(request) = client.read_request().await? { | ||
| 228 | + let response = match request { | ||
| 229 | + Request::CreateSession { username } => { | ||
| 230 | + auth.create_session(&username)? | ||
| 231 | + } | ||
| 232 | + Request::Authenticate { response } => { | ||
| 233 | + auth.authenticate(&response)? | ||
| 234 | + } | ||
| 235 | + Request::CancelSession => { | ||
| 236 | + auth.cancel(); | ||
| 237 | + Response::Success | ||
| 238 | + } | ||
| 239 | + // Power actions handled in later sprint | ||
| 240 | + _ => Response::Error { | ||
| 241 | + message: "Not implemented".to_string(), | ||
| 242 | + }, | ||
| 243 | + }; | ||
| 244 | + | ||
| 245 | + client.send_response(&response).await?; | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | +} | ||
| 249 | +``` | ||
| 250 | + | ||
| 251 | +### 1.6 Create Test Client | ||
| 252 | + | ||
| 253 | +For testing without the full greeter: | ||
| 254 | + | ||
| 255 | +```rust | ||
| 256 | +// gardm-greeter/src/bin/test-auth.rs | ||
| 257 | + | ||
| 258 | +use gardm_ipc::{Request, Response}; | ||
| 259 | +use std::io::{self, BufRead, Write}; | ||
| 260 | +use std::os::unix::net::UnixStream; | ||
| 261 | + | ||
| 262 | +fn main() -> anyhow::Result<()> { | ||
| 263 | + let mut stream = UnixStream::connect("/run/gardm.sock")?; | ||
| 264 | + | ||
| 265 | + // Get username | ||
| 266 | + print!("Username: "); | ||
| 267 | + io::stdout().flush()?; | ||
| 268 | + let mut username = String::new(); | ||
| 269 | + io::stdin().lock().read_line(&mut username)?; | ||
| 270 | + | ||
| 271 | + // Send create_session | ||
| 272 | + let req = Request::CreateSession { | ||
| 273 | + username: username.trim().to_string(), | ||
| 274 | + }; | ||
| 275 | + writeln!(stream, "{}", serde_json::to_string(&req)?)?; | ||
| 276 | + | ||
| 277 | + // Read response | ||
| 278 | + let mut response = String::new(); | ||
| 279 | + let mut reader = io::BufReader::new(&stream); | ||
| 280 | + reader.read_line(&mut response)?; | ||
| 281 | + println!("Response: {}", response); | ||
| 282 | + | ||
| 283 | + // Get password | ||
| 284 | + print!("Password: "); | ||
| 285 | + io::stdout().flush()?; | ||
| 286 | + let password = rpassword::read_password()?; | ||
| 287 | + | ||
| 288 | + // Send authenticate | ||
| 289 | + let req = Request::Authenticate { response: password }; | ||
| 290 | + writeln!(stream, "{}", serde_json::to_string(&req)?)?; | ||
| 291 | + | ||
| 292 | + // Read response | ||
| 293 | + response.clear(); | ||
| 294 | + reader.read_line(&mut response)?; | ||
| 295 | + println!("Response: {}", response); | ||
| 296 | + | ||
| 297 | + Ok(()) | ||
| 298 | +} | ||
| 299 | +``` | ||
| 300 | + | ||
| 301 | +## Acceptance Criteria | ||
| 302 | + | ||
| 303 | +1. Daemon accepts IPC connections from greeter | ||
| 304 | +2. `CreateSession` initiates PAM conversation | ||
| 305 | +3. `Authenticate` with correct password returns `Success` | ||
| 306 | +4. `Authenticate` with wrong password returns `AuthError` | ||
| 307 | +5. `CancelSession` resets state cleanly | ||
| 308 | +6. Multiple auth attempts work without daemon restart | ||
| 309 | +7. PAM config file is properly loaded | ||
| 310 | + | ||
| 311 | +## Pitfalls to Avoid | ||
| 312 | + | ||
| 313 | +1. **PAM is synchronous** - don't block the async runtime. Use `spawn_blocking` for PAM calls. | ||
| 314 | +2. **Memory safety with passwords** - zero memory after use (pam crate should handle this). | ||
| 315 | +3. **Don't store passwords** - only pass through to PAM immediately. | ||
| 316 | +4. **Handle PAM errors gracefully** - account locked, expired password, etc. have specific error types. | ||
| 317 | +5. **Test with real PAM** - mock tests are useful but test against real system too. | ||
| 318 | +6. **Remember PAM is stateful** - can't call authenticate twice on same session. | ||
| 319 | + | ||
| 320 | +## Testing | ||
| 321 | + | ||
| 322 | +```bash | ||
| 323 | +# Build and run daemon as root (required for PAM) | ||
| 324 | +sudo RUST_LOG=debug ./target/release/gardmd & | ||
| 325 | + | ||
| 326 | +# Test with CLI client | ||
| 327 | +./target/release/test-auth | ||
| 328 | + | ||
| 329 | +# Test wrong password | ||
| 330 | +# Test account lockout (if configured) | ||
| 331 | +# Test non-existent user | ||
| 332 | +``` | ||
| 333 | + | ||
| 334 | +## Security Considerations | ||
| 335 | + | ||
| 336 | +- Daemon must run as root for PAM access | ||
| 337 | +- IPC socket must be protected (0600 permissions) | ||
| 338 | +- Failed auth attempts should be rate-limited (PAM may do this) | ||
| 339 | +- Log auth attempts but NOT passwords | ||
| 340 | + | ||
| 341 | +## Dependencies for This Sprint | ||
| 342 | + | ||
| 343 | +```toml | ||
| 344 | +# gardmd/Cargo.toml | ||
| 345 | +[dependencies] | ||
| 346 | +pam = "0.8" | ||
| 347 | +nix = { version = "0.27", features = ["user"] } | ||
| 348 | + | ||
| 349 | +# gardm-greeter/Cargo.toml (for test client) | ||
| 350 | +[dependencies] | ||
| 351 | +rpassword = "7.0" | ||
| 352 | +``` | ||
| 353 | + | ||
| 354 | +## Next Sprint | ||
| 355 | + | ||
| 356 | +Sprint 2 will add X11 server management - starting Xorg and the greeter. | ||
docs/sprints/sprint-2-x11.mdadded@@ -0,0 +1,487 @@ | |||
| 1 | +# Sprint 2: X11 Server Management | ||
| 2 | + | ||
| 3 | +**Goal:** Start and manage Xorg server, launch greeter process, and handle session startup. | ||
| 4 | + | ||
| 5 | +## Objectives | ||
| 6 | + | ||
| 7 | +- Start Xorg on appropriate VT with correct permissions | ||
| 8 | +- Launch greeter as unprivileged user | ||
| 9 | +- Start user session after successful auth | ||
| 10 | +- Handle X server lifecycle (crash recovery, clean shutdown) | ||
| 11 | +- Integrate with systemd-logind for session registration | ||
| 12 | + | ||
| 13 | +## Background: X11 Display Manager Flow | ||
| 14 | + | ||
| 15 | +``` | ||
| 16 | +1. gardmd starts | ||
| 17 | +2. gardmd spawns Xorg on VT (e.g., :0 on vt1) | ||
| 18 | +3. gardmd waits for X to be ready (connect test) | ||
| 19 | +4. gardmd spawns greeter as unprivileged user | ||
| 20 | +5. Greeter authenticates user via IPC | ||
| 21 | +6. gardmd kills greeter, starts user session | ||
| 22 | +7. User session runs until logout | ||
| 23 | +8. gardmd restarts greeter (loop back to 4) | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +## Tasks | ||
| 27 | + | ||
| 28 | +### 2.1 X Server Launcher | ||
| 29 | + | ||
| 30 | +```rust | ||
| 31 | +// gardmd/src/x11.rs | ||
| 32 | + | ||
| 33 | +use nix::unistd::{fork, ForkResult, setsid, Pid}; | ||
| 34 | +use std::process::{Command, Child}; | ||
| 35 | +use std::time::Duration; | ||
| 36 | + | ||
| 37 | +pub struct XServer { | ||
| 38 | + process: Child, | ||
| 39 | + display: String, | ||
| 40 | + vt: u32, | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +impl XServer { | ||
| 44 | + /// Start Xorg server on specified display and VT | ||
| 45 | + pub fn start(display: &str, vt: u32) -> anyhow::Result<Self> { | ||
| 46 | + // Xorg arguments | ||
| 47 | + let mut cmd = Command::new("/usr/bin/Xorg"); | ||
| 48 | + cmd.arg(display) | ||
| 49 | + .arg(&format!("vt{}", vt)) | ||
| 50 | + .arg("-keeptty") | ||
| 51 | + .arg("-noreset") | ||
| 52 | + .arg("-novtswitch") | ||
| 53 | + .arg("-nolisten").arg("tcp"); | ||
| 54 | + | ||
| 55 | + // For multi-seat support (future): | ||
| 56 | + // cmd.arg("-seat").arg("seat0"); | ||
| 57 | + | ||
| 58 | + // Capture output for debugging | ||
| 59 | + cmd.stdout(std::process::Stdio::piped()) | ||
| 60 | + .stderr(std::process::Stdio::piped()); | ||
| 61 | + | ||
| 62 | + let process = cmd.spawn()?; | ||
| 63 | + | ||
| 64 | + tracing::info!("Started Xorg on {} (vt{}, pid={})", | ||
| 65 | + display, vt, process.id()); | ||
| 66 | + | ||
| 67 | + let server = Self { | ||
| 68 | + process, | ||
| 69 | + display: display.to_string(), | ||
| 70 | + vt, | ||
| 71 | + }; | ||
| 72 | + | ||
| 73 | + // Wait for X to be ready | ||
| 74 | + server.wait_ready(Duration::from_secs(10))?; | ||
| 75 | + | ||
| 76 | + Ok(server) | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + /// Wait for X server to be ready by attempting connection | ||
| 80 | + fn wait_ready(&self, timeout: Duration) -> anyhow::Result<()> { | ||
| 81 | + use std::time::Instant; | ||
| 82 | + | ||
| 83 | + let start = Instant::now(); | ||
| 84 | + while start.elapsed() < timeout { | ||
| 85 | + // Try to connect to X server | ||
| 86 | + match x11rb::connect(Some(&self.display)) { | ||
| 87 | + Ok(_) => { | ||
| 88 | + tracing::debug!("X server ready on {}", self.display); | ||
| 89 | + return Ok(()); | ||
| 90 | + } | ||
| 91 | + Err(_) => { | ||
| 92 | + std::thread::sleep(Duration::from_millis(100)); | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + anyhow::bail!("X server failed to become ready within {:?}", timeout); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /// Get the display string (e.g., ":0") | ||
| 101 | + pub fn display(&self) -> &str { | ||
| 102 | + &self.display | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + /// Get the VT number | ||
| 106 | + pub fn vt(&self) -> u32 { | ||
| 107 | + self.vt | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + /// Check if X server is still running | ||
| 111 | + pub fn is_running(&mut self) -> bool { | ||
| 112 | + match self.process.try_wait() { | ||
| 113 | + Ok(None) => true, | ||
| 114 | + _ => false, | ||
| 115 | + } | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + /// Stop the X server | ||
| 119 | + pub fn stop(&mut self) -> anyhow::Result<()> { | ||
| 120 | + tracing::info!("Stopping X server"); | ||
| 121 | + self.process.kill()?; | ||
| 122 | + self.process.wait()?; | ||
| 123 | + Ok(()) | ||
| 124 | + } | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +impl Drop for XServer { | ||
| 128 | + fn drop(&mut self) { | ||
| 129 | + let _ = self.stop(); | ||
| 130 | + } | ||
| 131 | +} | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +### 2.2 VT Allocation | ||
| 135 | + | ||
| 136 | +```rust | ||
| 137 | +// gardmd/src/vt.rs | ||
| 138 | + | ||
| 139 | +use std::fs::OpenOptions; | ||
| 140 | +use std::os::unix::io::AsRawFd; | ||
| 141 | +use nix::ioctl_read_bad; | ||
| 142 | + | ||
| 143 | +// ioctl for getting/setting VT | ||
| 144 | +const VT_GETSTATE: u64 = 0x5603; | ||
| 145 | +const VT_ACTIVATE: u64 = 0x5606; | ||
| 146 | +const VT_WAITACTIVE: u64 = 0x5607; | ||
| 147 | +const VT_OPENQRY: u64 = 0x5600; | ||
| 148 | + | ||
| 149 | +#[repr(C)] | ||
| 150 | +pub struct VtStat { | ||
| 151 | + pub v_active: u16, | ||
| 152 | + pub v_signal: u16, | ||
| 153 | + pub v_state: u16, | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +/// Find an unused VT | ||
| 157 | +pub fn find_unused_vt() -> anyhow::Result<u32> { | ||
| 158 | + let console = OpenOptions::new() | ||
| 159 | + .read(true) | ||
| 160 | + .write(true) | ||
| 161 | + .open("/dev/tty0")?; | ||
| 162 | + | ||
| 163 | + let mut vt: i32 = 0; | ||
| 164 | + unsafe { | ||
| 165 | + if libc::ioctl(console.as_raw_fd(), VT_OPENQRY as _, &mut vt) < 0 { | ||
| 166 | + anyhow::bail!("Failed to find unused VT"); | ||
| 167 | + } | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + if vt <= 0 { | ||
| 171 | + anyhow::bail!("No unused VT available"); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + Ok(vt as u32) | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +/// Switch to a specific VT | ||
| 178 | +pub fn switch_to_vt(vt: u32) -> anyhow::Result<()> { | ||
| 179 | + let console = OpenOptions::new() | ||
| 180 | + .read(true) | ||
| 181 | + .write(true) | ||
| 182 | + .open("/dev/tty0")?; | ||
| 183 | + | ||
| 184 | + unsafe { | ||
| 185 | + if libc::ioctl(console.as_raw_fd(), VT_ACTIVATE as _, vt) < 0 { | ||
| 186 | + anyhow::bail!("Failed to activate VT {}", vt); | ||
| 187 | + } | ||
| 188 | + if libc::ioctl(console.as_raw_fd(), VT_WAITACTIVE as _, vt) < 0 { | ||
| 189 | + anyhow::bail!("Failed to wait for VT {}", vt); | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + Ok(()) | ||
| 194 | +} | ||
| 195 | +``` | ||
| 196 | + | ||
| 197 | +### 2.3 Greeter Process Manager | ||
| 198 | + | ||
| 199 | +```rust | ||
| 200 | +// gardmd/src/greeter.rs | ||
| 201 | + | ||
| 202 | +use nix::unistd::{Uid, Gid, setuid, setgid, User}; | ||
| 203 | +use std::process::{Command, Child}; | ||
| 204 | + | ||
| 205 | +pub struct GreeterProcess { | ||
| 206 | + process: Child, | ||
| 207 | +} | ||
| 208 | + | ||
| 209 | +impl GreeterProcess { | ||
| 210 | + /// Start the greeter as the gardm user | ||
| 211 | + pub fn start(greeter_cmd: &str, display: &str) -> anyhow::Result<Self> { | ||
| 212 | + // Get gardm user info | ||
| 213 | + let user = User::from_name("gardm")? | ||
| 214 | + .ok_or_else(|| anyhow::anyhow!("gardm user not found"))?; | ||
| 215 | + | ||
| 216 | + let mut cmd = Command::new(greeter_cmd); | ||
| 217 | + cmd.env("DISPLAY", display) | ||
| 218 | + .env("XDG_SESSION_CLASS", "greeter") | ||
| 219 | + .env("XDG_SESSION_TYPE", "x11"); | ||
| 220 | + | ||
| 221 | + // Run as gardm user | ||
| 222 | + unsafe { | ||
| 223 | + cmd.pre_exec(move || { | ||
| 224 | + setgid(Gid::from_raw(user.gid.as_raw()))?; | ||
| 225 | + setuid(Uid::from_raw(user.uid.as_raw()))?; | ||
| 226 | + Ok(()) | ||
| 227 | + }); | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + let process = cmd.spawn()?; | ||
| 231 | + tracing::info!("Started greeter (pid={})", process.id()); | ||
| 232 | + | ||
| 233 | + Ok(Self { process }) | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + /// Check if greeter is still running | ||
| 237 | + pub fn is_running(&mut self) -> bool { | ||
| 238 | + matches!(self.process.try_wait(), Ok(None)) | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + /// Wait for greeter to exit | ||
| 242 | + pub fn wait(&mut self) -> anyhow::Result<std::process::ExitStatus> { | ||
| 243 | + Ok(self.process.wait()?) | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + /// Kill the greeter | ||
| 247 | + pub fn kill(&mut self) -> anyhow::Result<()> { | ||
| 248 | + self.process.kill()?; | ||
| 249 | + self.process.wait()?; | ||
| 250 | + Ok(()) | ||
| 251 | + } | ||
| 252 | +} | ||
| 253 | +``` | ||
| 254 | + | ||
| 255 | +### 2.4 Session Launcher | ||
| 256 | + | ||
| 257 | +```rust | ||
| 258 | +// gardmd/src/session.rs | ||
| 259 | + | ||
| 260 | +use nix::unistd::{Uid, Gid, setuid, setgid, User, initgroups, chdir}; | ||
| 261 | +use std::process::{Command, Child}; | ||
| 262 | +use std::ffi::CString; | ||
| 263 | + | ||
| 264 | +pub struct UserSession { | ||
| 265 | + process: Child, | ||
| 266 | + username: String, | ||
| 267 | +} | ||
| 268 | + | ||
| 269 | +impl UserSession { | ||
| 270 | + /// Start a user session | ||
| 271 | + pub fn start( | ||
| 272 | + username: &str, | ||
| 273 | + session_cmd: &[String], | ||
| 274 | + display: &str, | ||
| 275 | + vt: u32, | ||
| 276 | + ) -> anyhow::Result<Self> { | ||
| 277 | + let user = User::from_name(username)? | ||
| 278 | + .ok_or_else(|| anyhow::anyhow!("User {} not found", username))?; | ||
| 279 | + | ||
| 280 | + let home = user.dir.to_string_lossy().to_string(); | ||
| 281 | + let shell = user.shell.to_string_lossy().to_string(); | ||
| 282 | + let uid = user.uid; | ||
| 283 | + let gid = user.gid; | ||
| 284 | + let username_c = CString::new(username)?; | ||
| 285 | + | ||
| 286 | + // Build session command | ||
| 287 | + let (cmd_path, cmd_args) = if session_cmd.is_empty() { | ||
| 288 | + // Default to gar-session.sh | ||
| 289 | + ("/usr/local/bin/gar-session.sh".to_string(), vec![]) | ||
| 290 | + } else { | ||
| 291 | + (session_cmd[0].clone(), session_cmd[1..].to_vec()) | ||
| 292 | + }; | ||
| 293 | + | ||
| 294 | + let mut cmd = Command::new(&cmd_path); | ||
| 295 | + cmd.args(&cmd_args) | ||
| 296 | + .env("DISPLAY", display) | ||
| 297 | + .env("HOME", &home) | ||
| 298 | + .env("USER", username) | ||
| 299 | + .env("LOGNAME", username) | ||
| 300 | + .env("SHELL", &shell) | ||
| 301 | + .env("XDG_SESSION_TYPE", "x11") | ||
| 302 | + .env("XDG_VTNR", vt.to_string()) | ||
| 303 | + .env("XDG_SEAT", "seat0"); | ||
| 304 | + | ||
| 305 | + // Run as the user | ||
| 306 | + unsafe { | ||
| 307 | + cmd.pre_exec(move || { | ||
| 308 | + // Set groups | ||
| 309 | + initgroups(&username_c, gid)?; | ||
| 310 | + setgid(gid)?; | ||
| 311 | + setuid(uid)?; | ||
| 312 | + | ||
| 313 | + // Change to home directory | ||
| 314 | + let home_c = CString::new(home.as_str())?; | ||
| 315 | + chdir(&home_c)?; | ||
| 316 | + | ||
| 317 | + Ok(()) | ||
| 318 | + }); | ||
| 319 | + } | ||
| 320 | + | ||
| 321 | + let process = cmd.spawn()?; | ||
| 322 | + tracing::info!("Started session for {} (pid={})", username, process.id()); | ||
| 323 | + | ||
| 324 | + Ok(Self { | ||
| 325 | + process, | ||
| 326 | + username: username.to_string(), | ||
| 327 | + }) | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + /// Wait for session to end | ||
| 331 | + pub fn wait(&mut self) -> anyhow::Result<std::process::ExitStatus> { | ||
| 332 | + Ok(self.process.wait()?) | ||
| 333 | + } | ||
| 334 | +} | ||
| 335 | +``` | ||
| 336 | + | ||
| 337 | +### 2.5 Main Loop Integration | ||
| 338 | + | ||
| 339 | +```rust | ||
| 340 | +// gardmd/src/main.rs | ||
| 341 | + | ||
| 342 | +async fn run() -> anyhow::Result<()> { | ||
| 343 | + let config = Config::load()?; | ||
| 344 | + | ||
| 345 | + // Find VT to use | ||
| 346 | + let vt = if config.general.vt > 0 { | ||
| 347 | + config.general.vt | ||
| 348 | + } else { | ||
| 349 | + vt::find_unused_vt()? | ||
| 350 | + }; | ||
| 351 | + | ||
| 352 | + // Start X server | ||
| 353 | + let display = ":0"; | ||
| 354 | + let mut x_server = XServer::start(display, vt)?; | ||
| 355 | + | ||
| 356 | + // Switch to our VT | ||
| 357 | + vt::switch_to_vt(vt)?; | ||
| 358 | + | ||
| 359 | + // Start IPC server | ||
| 360 | + let ipc = IpcServer::new("/run/gardm.sock").await?; | ||
| 361 | + let mut auth = AuthSession::new(); | ||
| 362 | + | ||
| 363 | + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; | ||
| 364 | + | ||
| 365 | + loop { | ||
| 366 | + // Start greeter | ||
| 367 | + let mut greeter = GreeterProcess::start(&config.general.greeter, display)?; | ||
| 368 | + | ||
| 369 | + // Handle greeter authentication | ||
| 370 | + let session_info = handle_greeter_auth(&ipc, &mut auth).await?; | ||
| 371 | + | ||
| 372 | + // Kill greeter before starting session | ||
| 373 | + greeter.kill()?; | ||
| 374 | + | ||
| 375 | + // Start user session | ||
| 376 | + let mut session = UserSession::start( | ||
| 377 | + &session_info.username, | ||
| 378 | + &session_info.cmd, | ||
| 379 | + display, | ||
| 380 | + vt, | ||
| 381 | + )?; | ||
| 382 | + | ||
| 383 | + // Wait for session to end | ||
| 384 | + let status = session.wait()?; | ||
| 385 | + tracing::info!("Session ended with status: {:?}", status); | ||
| 386 | + | ||
| 387 | + // Loop back to greeter | ||
| 388 | + } | ||
| 389 | +} | ||
| 390 | + | ||
| 391 | +struct SessionInfo { | ||
| 392 | + username: String, | ||
| 393 | + cmd: Vec<String>, | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +async fn handle_greeter_auth( | ||
| 397 | + ipc: &IpcServer, | ||
| 398 | + auth: &mut AuthSession, | ||
| 399 | +) -> anyhow::Result<SessionInfo> { | ||
| 400 | + let mut client = ipc.accept().await?; | ||
| 401 | + | ||
| 402 | + loop { | ||
| 403 | + let request = client.read_request().await? | ||
| 404 | + .ok_or_else(|| anyhow::anyhow!("Greeter disconnected"))?; | ||
| 405 | + | ||
| 406 | + match request { | ||
| 407 | + Request::CreateSession { username } => { | ||
| 408 | + let response = auth.create_session(&username)?; | ||
| 409 | + client.send_response(&response.into()).await?; | ||
| 410 | + } | ||
| 411 | + Request::Authenticate { response } => { | ||
| 412 | + let result = auth.authenticate(&response)?; | ||
| 413 | + client.send_response(&result.clone().into()).await?; | ||
| 414 | + | ||
| 415 | + if matches!(result, AuthResponse::Success) { | ||
| 416 | + // Read StartSession request | ||
| 417 | + if let Some(Request::StartSession { cmd, .. }) = | ||
| 418 | + client.read_request().await? | ||
| 419 | + { | ||
| 420 | + return Ok(SessionInfo { | ||
| 421 | + username: auth.username().unwrap().to_string(), | ||
| 422 | + cmd, | ||
| 423 | + }); | ||
| 424 | + } | ||
| 425 | + } | ||
| 426 | + } | ||
| 427 | + Request::CancelSession => { | ||
| 428 | + auth.cancel(); | ||
| 429 | + client.send_response(&Response::Success).await?; | ||
| 430 | + } | ||
| 431 | + _ => { | ||
| 432 | + client.send_response(&Response::Error { | ||
| 433 | + message: "Unexpected request".to_string(), | ||
| 434 | + }).await?; | ||
| 435 | + } | ||
| 436 | + } | ||
| 437 | + } | ||
| 438 | +} | ||
| 439 | +``` | ||
| 440 | + | ||
| 441 | +## Acceptance Criteria | ||
| 442 | + | ||
| 443 | +1. Xorg starts on specified VT | ||
| 444 | +2. Greeter launches as unprivileged gardm user | ||
| 445 | +3. After auth success, greeter is killed and session starts | ||
| 446 | +4. Session runs as authenticated user with correct environment | ||
| 447 | +5. After session logout, greeter restarts | ||
| 448 | +6. X server crash triggers recovery (restart X + greeter) | ||
| 449 | + | ||
| 450 | +## Pitfalls to Avoid | ||
| 451 | + | ||
| 452 | +1. **Don't forget VT permissions** - Xorg needs access to /dev/tty* | ||
| 453 | +2. **X server takes time to start** - must wait for it to be ready | ||
| 454 | +3. **Environment inheritance** - don't leak root environment to session | ||
| 455 | +4. **Group membership** - user needs video, audio groups for hardware access | ||
| 456 | +5. **Home directory** - chdir to $HOME before starting session | ||
| 457 | +6. **Don't block on X** - use async/spawn for process management | ||
| 458 | + | ||
| 459 | +## Testing | ||
| 460 | + | ||
| 461 | +```bash | ||
| 462 | +# Test X server startup (need to be root, on real TTY) | ||
| 463 | +sudo ./target/release/gardmd | ||
| 464 | + | ||
| 465 | +# Alternative: Test in Xephyr (nested X) | ||
| 466 | +Xephyr -br -ac -noreset -screen 1280x720 :1 & | ||
| 467 | +DISPLAY=:1 ./target/release/gardm-greeter | ||
| 468 | + | ||
| 469 | +# Create gardm user for testing | ||
| 470 | +sudo useradd -r -s /usr/bin/nologin -d /var/lib/gardm gardm | ||
| 471 | +sudo mkdir -p /var/lib/gardm | ||
| 472 | +sudo chown gardm:gardm /var/lib/gardm | ||
| 473 | +``` | ||
| 474 | + | ||
| 475 | +## Dependencies for This Sprint | ||
| 476 | + | ||
| 477 | +```toml | ||
| 478 | +# gardmd/Cargo.toml | ||
| 479 | +[dependencies] | ||
| 480 | +x11rb = "0.13" | ||
| 481 | +libc = "0.2" | ||
| 482 | +nix = { version = "0.27", features = ["user", "process", "ioctl"] } | ||
| 483 | +``` | ||
| 484 | + | ||
| 485 | +## Next Sprint | ||
| 486 | + | ||
| 487 | +Sprint 3 will build the graphical greeter UI with Cairo/Pango. | ||
docs/sprints/sprint-3-greeter-ui.mdadded@@ -0,0 +1,586 @@ | |||
| 1 | +# Sprint 3: Greeter UI | ||
| 2 | + | ||
| 3 | +**Goal:** Build a sleek, centered login interface with blurred background using Cairo/Pango rendering. | ||
| 4 | + | ||
| 5 | +## Objectives | ||
| 6 | + | ||
| 7 | +- Create X11 window that covers the full screen | ||
| 8 | +- Render blurred background image | ||
| 9 | +- Implement centered login form (username, password inputs) | ||
| 10 | +- Add session selector dropdown | ||
| 11 | +- Add power buttons (shutdown, reboot, suspend) | ||
| 12 | +- Handle keyboard input and focus | ||
| 13 | +- Connect UI to daemon via IPC | ||
| 14 | + | ||
| 15 | +## Design Reference | ||
| 16 | + | ||
| 17 | +``` | ||
| 18 | +┌─────────────────────────────────────────────────────────────────────┐ | ||
| 19 | +│ │ | ||
| 20 | +│ [Blurred Background] │ | ||
| 21 | +│ │ | ||
| 22 | +│ ┌───────────────────────┐ │ | ||
| 23 | +│ │ User Avatar │ │ | ||
| 24 | +│ │ 👤 │ │ | ||
| 25 | +│ ├───────────────────────┤ │ | ||
| 26 | +│ │ Username: [ ] │ │ | ||
| 27 | +│ │ Password: [*******] │ │ | ||
| 28 | +│ │ │ │ | ||
| 29 | +│ │ Session: [gar ▼] │ │ | ||
| 30 | +│ │ │ │ | ||
| 31 | +│ │ [ Login ] │ │ | ||
| 32 | +│ │ │ │ | ||
| 33 | +│ │ Error message here │ │ | ||
| 34 | +│ └───────────────────────┘ │ | ||
| 35 | +│ │ | ||
| 36 | +│ 12:34 PM [⏻] [↻] [⏾] │ | ||
| 37 | +│ January 14, 2026 │ | ||
| 38 | +└─────────────────────────────────────────────────────────────────────┘ | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +## Tasks | ||
| 42 | + | ||
| 43 | +### 3.1 Window Setup | ||
| 44 | + | ||
| 45 | +```rust | ||
| 46 | +// gardm-greeter/src/window.rs | ||
| 47 | + | ||
| 48 | +use x11rb::connection::Connection; | ||
| 49 | +use x11rb::protocol::xproto::*; | ||
| 50 | +use x11rb::wrapper::ConnectionExt; | ||
| 51 | + | ||
| 52 | +pub struct GreeterWindow { | ||
| 53 | + conn: x11rb::rust_connection::RustConnection, | ||
| 54 | + screen_num: usize, | ||
| 55 | + window: Window, | ||
| 56 | + width: u16, | ||
| 57 | + height: u16, | ||
| 58 | + gc: Gcontext, | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +impl GreeterWindow { | ||
| 62 | + pub fn new() -> anyhow::Result<Self> { | ||
| 63 | + let (conn, screen_num) = x11rb::connect(None)?; | ||
| 64 | + let screen = &conn.setup().roots[screen_num]; | ||
| 65 | + | ||
| 66 | + let width = screen.width_in_pixels; | ||
| 67 | + let height = screen.height_in_pixels; | ||
| 68 | + let root = screen.root; | ||
| 69 | + let depth = screen.root_depth; | ||
| 70 | + let visual = screen.root_visual; | ||
| 71 | + | ||
| 72 | + // Create window | ||
| 73 | + let window = conn.generate_id()?; | ||
| 74 | + conn.create_window( | ||
| 75 | + depth, | ||
| 76 | + window, | ||
| 77 | + root, | ||
| 78 | + 0, 0, | ||
| 79 | + width, height, | ||
| 80 | + 0, | ||
| 81 | + WindowClass::INPUT_OUTPUT, | ||
| 82 | + visual, | ||
| 83 | + &CreateWindowAux::new() | ||
| 84 | + .background_pixel(screen.black_pixel) | ||
| 85 | + .event_mask( | ||
| 86 | + EventMask::EXPOSURE | ||
| 87 | + | EventMask::KEY_PRESS | ||
| 88 | + | EventMask::BUTTON_PRESS | ||
| 89 | + | EventMask::STRUCTURE_NOTIFY | ||
| 90 | + ), | ||
| 91 | + )?; | ||
| 92 | + | ||
| 93 | + // Make it fullscreen (bypass WM) | ||
| 94 | + let net_wm_state = conn.intern_atom(false, b"_NET_WM_STATE")?.reply()?.atom; | ||
| 95 | + let fullscreen = conn.intern_atom(false, b"_NET_WM_STATE_FULLSCREEN")?.reply()?.atom; | ||
| 96 | + conn.change_property32( | ||
| 97 | + PropMode::REPLACE, | ||
| 98 | + window, | ||
| 99 | + net_wm_state, | ||
| 100 | + AtomEnum::ATOM, | ||
| 101 | + &[fullscreen], | ||
| 102 | + )?; | ||
| 103 | + | ||
| 104 | + // Override redirect for greeter (no WM decorations) | ||
| 105 | + conn.change_window_attributes( | ||
| 106 | + window, | ||
| 107 | + &ChangeWindowAttributesAux::new().override_redirect(1), | ||
| 108 | + )?; | ||
| 109 | + | ||
| 110 | + // Create graphics context | ||
| 111 | + let gc = conn.generate_id()?; | ||
| 112 | + conn.create_gc(gc, window, &CreateGCAux::new())?; | ||
| 113 | + | ||
| 114 | + conn.map_window(window)?; | ||
| 115 | + conn.flush()?; | ||
| 116 | + | ||
| 117 | + Ok(Self { | ||
| 118 | + conn, | ||
| 119 | + screen_num, | ||
| 120 | + window, | ||
| 121 | + width, | ||
| 122 | + height, | ||
| 123 | + gc, | ||
| 124 | + }) | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + pub fn width(&self) -> u16 { self.width } | ||
| 128 | + pub fn height(&self) -> u16 { self.height } | ||
| 129 | + pub fn window(&self) -> Window { self.window } | ||
| 130 | + pub fn conn(&self) -> &x11rb::rust_connection::RustConnection { &self.conn } | ||
| 131 | +} | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +### 3.2 Cairo Rendering Surface | ||
| 135 | + | ||
| 136 | +```rust | ||
| 137 | +// gardm-greeter/src/render.rs | ||
| 138 | + | ||
| 139 | +use cairo::{Context, ImageSurface, Format}; | ||
| 140 | +use x11rb::protocol::xproto::*; | ||
| 141 | + | ||
| 142 | +pub struct Renderer { | ||
| 143 | + surface: ImageSurface, | ||
| 144 | + width: i32, | ||
| 145 | + height: i32, | ||
| 146 | +} | ||
| 147 | + | ||
| 148 | +impl Renderer { | ||
| 149 | + pub fn new(width: u16, height: u16) -> anyhow::Result<Self> { | ||
| 150 | + let surface = ImageSurface::create(Format::ARgb32, width as i32, height as i32)?; | ||
| 151 | + Ok(Self { | ||
| 152 | + surface, | ||
| 153 | + width: width as i32, | ||
| 154 | + height: height as i32, | ||
| 155 | + }) | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + pub fn context(&self) -> anyhow::Result<Context> { | ||
| 159 | + Ok(Context::new(&self.surface)?) | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + /// Get raw pixel data for X11 | ||
| 163 | + pub fn data(&self) -> Vec<u8> { | ||
| 164 | + let stride = self.surface.stride() as usize; | ||
| 165 | + let height = self.height as usize; | ||
| 166 | + let data = self.surface.data().unwrap(); | ||
| 167 | + | ||
| 168 | + // Cairo uses ARGB, X11 uses BGRA - but with same byte order on little-endian | ||
| 169 | + data[..stride * height].to_vec() | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + pub fn width(&self) -> i32 { self.width } | ||
| 173 | + pub fn height(&self) -> i32 { self.height } | ||
| 174 | +} | ||
| 175 | +``` | ||
| 176 | + | ||
| 177 | +### 3.3 Background with Blur | ||
| 178 | + | ||
| 179 | +```rust | ||
| 180 | +// gardm-greeter/src/background.rs | ||
| 181 | + | ||
| 182 | +use image::{RgbaImage, imageops}; | ||
| 183 | + | ||
| 184 | +/// Load and blur a background image | ||
| 185 | +pub fn load_blurred_background( | ||
| 186 | + path: &str, | ||
| 187 | + width: u32, | ||
| 188 | + height: u32, | ||
| 189 | + blur_radius: f32, | ||
| 190 | + brightness: f32, | ||
| 191 | +) -> anyhow::Result<RgbaImage> { | ||
| 192 | + // Load image | ||
| 193 | + let img = image::open(path)?.to_rgba8(); | ||
| 194 | + | ||
| 195 | + // Scale to screen size (cover mode) | ||
| 196 | + let scaled = scale_to_cover(&img, width, height); | ||
| 197 | + | ||
| 198 | + // Apply gaussian blur | ||
| 199 | + let blurred = imageops::blur(&scaled, blur_radius); | ||
| 200 | + | ||
| 201 | + // Adjust brightness (darken for better text contrast) | ||
| 202 | + let adjusted = adjust_brightness(&blurred, brightness); | ||
| 203 | + | ||
| 204 | + Ok(adjusted) | ||
| 205 | +} | ||
| 206 | + | ||
| 207 | +fn scale_to_cover(img: &RgbaImage, target_w: u32, target_h: u32) -> RgbaImage { | ||
| 208 | + let (src_w, src_h) = img.dimensions(); | ||
| 209 | + let scale = (target_w as f32 / src_w as f32) | ||
| 210 | + .max(target_h as f32 / src_h as f32); | ||
| 211 | + | ||
| 212 | + let new_w = (src_w as f32 * scale) as u32; | ||
| 213 | + let new_h = (src_h as f32 * scale) as u32; | ||
| 214 | + | ||
| 215 | + let resized = imageops::resize(img, new_w, new_h, imageops::FilterType::Lanczos3); | ||
| 216 | + | ||
| 217 | + // Crop to center | ||
| 218 | + let x = (new_w - target_w) / 2; | ||
| 219 | + let y = (new_h - target_h) / 2; | ||
| 220 | + | ||
| 221 | + imageops::crop_imm(&resized, x, y, target_w, target_h).to_image() | ||
| 222 | +} | ||
| 223 | + | ||
| 224 | +fn adjust_brightness(img: &RgbaImage, factor: f32) -> RgbaImage { | ||
| 225 | + let mut result = img.clone(); | ||
| 226 | + for pixel in result.pixels_mut() { | ||
| 227 | + pixel[0] = (pixel[0] as f32 * factor).min(255.0) as u8; | ||
| 228 | + pixel[1] = (pixel[1] as f32 * factor).min(255.0) as u8; | ||
| 229 | + pixel[2] = (pixel[2] as f32 * factor).min(255.0) as u8; | ||
| 230 | + } | ||
| 231 | + result | ||
| 232 | +} | ||
| 233 | +``` | ||
| 234 | + | ||
| 235 | +### 3.4 Login Form Widget | ||
| 236 | + | ||
| 237 | +```rust | ||
| 238 | +// gardm-greeter/src/widgets/login_form.rs | ||
| 239 | + | ||
| 240 | +use cairo::Context; | ||
| 241 | +use pango::{FontDescription, Layout}; | ||
| 242 | + | ||
| 243 | +pub struct LoginForm { | ||
| 244 | + pub username: String, | ||
| 245 | + pub password: String, | ||
| 246 | + pub focused_field: FocusedField, | ||
| 247 | + pub error_message: Option<String>, | ||
| 248 | + pub is_loading: bool, | ||
| 249 | + | ||
| 250 | + // Layout | ||
| 251 | + x: f64, | ||
| 252 | + y: f64, | ||
| 253 | + width: f64, | ||
| 254 | + height: f64, | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +#[derive(Clone, Copy, PartialEq)] | ||
| 258 | +pub enum FocusedField { | ||
| 259 | + Username, | ||
| 260 | + Password, | ||
| 261 | +} | ||
| 262 | + | ||
| 263 | +impl LoginForm { | ||
| 264 | + pub fn new(screen_width: f64, screen_height: f64) -> Self { | ||
| 265 | + let width = 400.0; | ||
| 266 | + let height = 300.0; | ||
| 267 | + | ||
| 268 | + Self { | ||
| 269 | + username: String::new(), | ||
| 270 | + password: String::new(), | ||
| 271 | + focused_field: FocusedField::Username, | ||
| 272 | + error_message: None, | ||
| 273 | + is_loading: false, | ||
| 274 | + x: (screen_width - width) / 2.0, | ||
| 275 | + y: (screen_height - height) / 2.0, | ||
| 276 | + width, | ||
| 277 | + height, | ||
| 278 | + } | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + pub fn render(&self, ctx: &Context, pango_ctx: &pango::Context) -> anyhow::Result<()> { | ||
| 282 | + // Background panel (semi-transparent) | ||
| 283 | + ctx.set_source_rgba(0.1, 0.1, 0.1, 0.85); | ||
| 284 | + rounded_rectangle(ctx, self.x, self.y, self.width, self.height, 16.0); | ||
| 285 | + ctx.fill()?; | ||
| 286 | + | ||
| 287 | + // Title | ||
| 288 | + let title_layout = Layout::new(pango_ctx); | ||
| 289 | + let mut font = FontDescription::new(); | ||
| 290 | + font.set_family("Sans"); | ||
| 291 | + font.set_size(24 * pango::SCALE); | ||
| 292 | + font.set_weight(pango::Weight::Bold); | ||
| 293 | + title_layout.set_font_description(Some(&font)); | ||
| 294 | + title_layout.set_text("Welcome"); | ||
| 295 | + | ||
| 296 | + ctx.set_source_rgb(1.0, 1.0, 1.0); | ||
| 297 | + ctx.move_to(self.x + self.width / 2.0 - 50.0, self.y + 30.0); | ||
| 298 | + pangocairo::show_layout(ctx, &title_layout); | ||
| 299 | + | ||
| 300 | + // Username field | ||
| 301 | + self.render_input_field( | ||
| 302 | + ctx, pango_ctx, | ||
| 303 | + "Username", | ||
| 304 | + &self.username, | ||
| 305 | + self.y + 100.0, | ||
| 306 | + self.focused_field == FocusedField::Username, | ||
| 307 | + false, | ||
| 308 | + )?; | ||
| 309 | + | ||
| 310 | + // Password field | ||
| 311 | + self.render_input_field( | ||
| 312 | + ctx, pango_ctx, | ||
| 313 | + "Password", | ||
| 314 | + &"•".repeat(self.password.len()), | ||
| 315 | + self.y + 160.0, | ||
| 316 | + self.focused_field == FocusedField::Password, | ||
| 317 | + true, | ||
| 318 | + )?; | ||
| 319 | + | ||
| 320 | + // Error message | ||
| 321 | + if let Some(ref msg) = self.error_message { | ||
| 322 | + ctx.set_source_rgb(1.0, 0.3, 0.3); | ||
| 323 | + let err_layout = Layout::new(pango_ctx); | ||
| 324 | + font.set_size(12 * pango::SCALE); | ||
| 325 | + font.set_weight(pango::Weight::Normal); | ||
| 326 | + err_layout.set_font_description(Some(&font)); | ||
| 327 | + err_layout.set_text(msg); | ||
| 328 | + ctx.move_to(self.x + 30.0, self.y + 230.0); | ||
| 329 | + pangocairo::show_layout(ctx, &err_layout); | ||
| 330 | + } | ||
| 331 | + | ||
| 332 | + // Login button | ||
| 333 | + self.render_button(ctx, pango_ctx, "Login", self.y + 260.0)?; | ||
| 334 | + | ||
| 335 | + Ok(()) | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + fn render_input_field( | ||
| 339 | + &self, | ||
| 340 | + ctx: &Context, | ||
| 341 | + pango_ctx: &pango::Context, | ||
| 342 | + label: &str, | ||
| 343 | + value: &str, | ||
| 344 | + y: f64, | ||
| 345 | + focused: bool, | ||
| 346 | + _is_password: bool, | ||
| 347 | + ) -> anyhow::Result<()> { | ||
| 348 | + let field_x = self.x + 30.0; | ||
| 349 | + let field_width = self.width - 60.0; | ||
| 350 | + let field_height = 40.0; | ||
| 351 | + | ||
| 352 | + // Label | ||
| 353 | + let mut font = FontDescription::new(); | ||
| 354 | + font.set_family("Sans"); | ||
| 355 | + font.set_size(11 * pango::SCALE); | ||
| 356 | + | ||
| 357 | + let label_layout = Layout::new(pango_ctx); | ||
| 358 | + label_layout.set_font_description(Some(&font)); | ||
| 359 | + label_layout.set_text(label); | ||
| 360 | + | ||
| 361 | + ctx.set_source_rgba(0.8, 0.8, 0.8, 1.0); | ||
| 362 | + ctx.move_to(field_x, y - 18.0); | ||
| 363 | + pangocairo::show_layout(ctx, &label_layout); | ||
| 364 | + | ||
| 365 | + // Input box | ||
| 366 | + if focused { | ||
| 367 | + ctx.set_source_rgba(0.3, 0.5, 0.8, 1.0); | ||
| 368 | + } else { | ||
| 369 | + ctx.set_source_rgba(0.3, 0.3, 0.3, 1.0); | ||
| 370 | + } | ||
| 371 | + rounded_rectangle(ctx, field_x, y, field_width, field_height, 8.0); | ||
| 372 | + ctx.fill()?; | ||
| 373 | + | ||
| 374 | + // Text value | ||
| 375 | + ctx.set_source_rgb(1.0, 1.0, 1.0); | ||
| 376 | + font.set_size(14 * pango::SCALE); | ||
| 377 | + let value_layout = Layout::new(pango_ctx); | ||
| 378 | + value_layout.set_font_description(Some(&font)); | ||
| 379 | + value_layout.set_text(if value.is_empty() { " " } else { value }); | ||
| 380 | + ctx.move_to(field_x + 10.0, y + 10.0); | ||
| 381 | + pangocairo::show_layout(ctx, &value_layout); | ||
| 382 | + | ||
| 383 | + // Cursor | ||
| 384 | + if focused { | ||
| 385 | + let (text_width, _) = value_layout.pixel_size(); | ||
| 386 | + ctx.set_source_rgb(1.0, 1.0, 1.0); | ||
| 387 | + ctx.rectangle(field_x + 10.0 + text_width as f64, y + 8.0, 2.0, 24.0); | ||
| 388 | + ctx.fill()?; | ||
| 389 | + } | ||
| 390 | + | ||
| 391 | + Ok(()) | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + fn render_button( | ||
| 395 | + &self, | ||
| 396 | + ctx: &Context, | ||
| 397 | + pango_ctx: &pango::Context, | ||
| 398 | + text: &str, | ||
| 399 | + y: f64, | ||
| 400 | + ) -> anyhow::Result<()> { | ||
| 401 | + let btn_width = 120.0; | ||
| 402 | + let btn_height = 36.0; | ||
| 403 | + let btn_x = self.x + (self.width - btn_width) / 2.0; | ||
| 404 | + | ||
| 405 | + // Button background | ||
| 406 | + ctx.set_source_rgba(0.2, 0.5, 0.8, 1.0); | ||
| 407 | + rounded_rectangle(ctx, btn_x, y, btn_width, btn_height, 8.0); | ||
| 408 | + ctx.fill()?; | ||
| 409 | + | ||
| 410 | + // Button text | ||
| 411 | + ctx.set_source_rgb(1.0, 1.0, 1.0); | ||
| 412 | + let mut font = FontDescription::new(); | ||
| 413 | + font.set_family("Sans"); | ||
| 414 | + font.set_size(14 * pango::SCALE); | ||
| 415 | + font.set_weight(pango::Weight::Bold); | ||
| 416 | + | ||
| 417 | + let layout = Layout::new(pango_ctx); | ||
| 418 | + layout.set_font_description(Some(&font)); | ||
| 419 | + layout.set_text(text); | ||
| 420 | + | ||
| 421 | + let (text_w, _) = layout.pixel_size(); | ||
| 422 | + ctx.move_to(btn_x + (btn_width - text_w as f64) / 2.0, y + 8.0); | ||
| 423 | + pangocairo::show_layout(ctx, &layout); | ||
| 424 | + | ||
| 425 | + Ok(()) | ||
| 426 | + } | ||
| 427 | + | ||
| 428 | + pub fn handle_key(&mut self, key: char) { | ||
| 429 | + match self.focused_field { | ||
| 430 | + FocusedField::Username => self.username.push(key), | ||
| 431 | + FocusedField::Password => self.password.push(key), | ||
| 432 | + } | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + pub fn handle_backspace(&mut self) { | ||
| 436 | + match self.focused_field { | ||
| 437 | + FocusedField::Username => { self.username.pop(); } | ||
| 438 | + FocusedField::Password => { self.password.pop(); } | ||
| 439 | + } | ||
| 440 | + } | ||
| 441 | + | ||
| 442 | + pub fn handle_tab(&mut self) { | ||
| 443 | + self.focused_field = match self.focused_field { | ||
| 444 | + FocusedField::Username => FocusedField::Password, | ||
| 445 | + FocusedField::Password => FocusedField::Username, | ||
| 446 | + }; | ||
| 447 | + } | ||
| 448 | +} | ||
| 449 | + | ||
| 450 | +fn rounded_rectangle(ctx: &Context, x: f64, y: f64, w: f64, h: f64, r: f64) { | ||
| 451 | + let degrees = std::f64::consts::PI / 180.0; | ||
| 452 | + ctx.new_sub_path(); | ||
| 453 | + ctx.arc(x + w - r, y + r, r, -90.0 * degrees, 0.0 * degrees); | ||
| 454 | + ctx.arc(x + w - r, y + h - r, r, 0.0 * degrees, 90.0 * degrees); | ||
| 455 | + ctx.arc(x + r, y + h - r, r, 90.0 * degrees, 180.0 * degrees); | ||
| 456 | + ctx.arc(x + r, y + r, r, 180.0 * degrees, 270.0 * degrees); | ||
| 457 | + ctx.close_path(); | ||
| 458 | +} | ||
| 459 | +``` | ||
| 460 | + | ||
| 461 | +### 3.5 Main Event Loop | ||
| 462 | + | ||
| 463 | +```rust | ||
| 464 | +// gardm-greeter/src/main.rs | ||
| 465 | + | ||
| 466 | +use x11rb::protocol::Event; | ||
| 467 | + | ||
| 468 | +fn main() -> anyhow::Result<()> { | ||
| 469 | + let window = GreeterWindow::new()?; | ||
| 470 | + let renderer = Renderer::new(window.width(), window.height())?; | ||
| 471 | + let mut form = LoginForm::new(window.width() as f64, window.height() as f64); | ||
| 472 | + | ||
| 473 | + // Load background | ||
| 474 | + let background = load_blurred_background( | ||
| 475 | + "/usr/share/gardm/backgrounds/default.jpg", | ||
| 476 | + window.width() as u32, | ||
| 477 | + window.height() as u32, | ||
| 478 | + 20.0, | ||
| 479 | + 0.7, | ||
| 480 | + )?; | ||
| 481 | + | ||
| 482 | + // Connect to daemon | ||
| 483 | + let mut daemon = DaemonClient::connect()?; | ||
| 484 | + | ||
| 485 | + loop { | ||
| 486 | + // Render frame | ||
| 487 | + { | ||
| 488 | + let ctx = renderer.context()?; | ||
| 489 | + render_background(&ctx, &background)?; | ||
| 490 | + form.render(&ctx, &pango_ctx)?; | ||
| 491 | + } | ||
| 492 | + | ||
| 493 | + // Copy to X11 | ||
| 494 | + window.put_image(&renderer.data())?; | ||
| 495 | + | ||
| 496 | + // Handle events | ||
| 497 | + let event = window.conn().wait_for_event()?; | ||
| 498 | + match event { | ||
| 499 | + Event::Expose(_) => { | ||
| 500 | + // Redraw handled above | ||
| 501 | + } | ||
| 502 | + Event::KeyPress(e) => { | ||
| 503 | + match e.detail { | ||
| 504 | + 9 => break, // Escape - exit (for testing) | ||
| 505 | + 36 => { // Enter - submit | ||
| 506 | + if form.focused_field == FocusedField::Password { | ||
| 507 | + // Authenticate | ||
| 508 | + daemon.create_session(&form.username)?; | ||
| 509 | + match daemon.authenticate(&form.password)? { | ||
| 510 | + Response::Success => { | ||
| 511 | + daemon.start_session(&["gar-session.sh"])?; | ||
| 512 | + break; | ||
| 513 | + } | ||
| 514 | + Response::AuthError { message } => { | ||
| 515 | + form.error_message = Some(message); | ||
| 516 | + } | ||
| 517 | + _ => {} | ||
| 518 | + } | ||
| 519 | + } else { | ||
| 520 | + form.handle_tab(); | ||
| 521 | + } | ||
| 522 | + } | ||
| 523 | + 23 => form.handle_tab(), // Tab | ||
| 524 | + 22 => form.handle_backspace(), // Backspace | ||
| 525 | + _ => { | ||
| 526 | + // Regular key | ||
| 527 | + if let Some(c) = keycode_to_char(e.detail, e.state) { | ||
| 528 | + form.handle_key(c); | ||
| 529 | + } | ||
| 530 | + } | ||
| 531 | + } | ||
| 532 | + } | ||
| 533 | + _ => {} | ||
| 534 | + } | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + Ok(()) | ||
| 538 | +} | ||
| 539 | +``` | ||
| 540 | + | ||
| 541 | +## Acceptance Criteria | ||
| 542 | + | ||
| 543 | +1. Greeter displays fullscreen with blurred background | ||
| 544 | +2. Login form is centered and styled | ||
| 545 | +3. Keyboard input works for username/password | ||
| 546 | +4. Tab switches between fields | ||
| 547 | +5. Enter submits form | ||
| 548 | +6. Error messages display correctly | ||
| 549 | +7. Successful auth triggers session start | ||
| 550 | + | ||
| 551 | +## Pitfalls to Avoid | ||
| 552 | + | ||
| 553 | +1. **X11 keycodes vary by layout** - use XKB for proper key mapping | ||
| 554 | +2. **Cairo surface format** - ensure ARGB matches X11 expectations | ||
| 555 | +3. **Font rendering** - initialize Pango context correctly | ||
| 556 | +4. **Fullscreen on all monitors** - may need per-monitor handling | ||
| 557 | +5. **Input focus** - greeter window must grab keyboard | ||
| 558 | +6. **Memory leaks** - Cairo contexts need proper cleanup | ||
| 559 | + | ||
| 560 | +## Testing | ||
| 561 | + | ||
| 562 | +```bash | ||
| 563 | +# Test in Xephyr | ||
| 564 | +Xephyr -br -ac -noreset -screen 1280x720 :1 & | ||
| 565 | +DISPLAY=:1 ./target/release/gardm-greeter | ||
| 566 | + | ||
| 567 | +# Test keyboard input | ||
| 568 | +# Test form submission | ||
| 569 | +# Test error display | ||
| 570 | +``` | ||
| 571 | + | ||
| 572 | +## Dependencies for This Sprint | ||
| 573 | + | ||
| 574 | +```toml | ||
| 575 | +# gardm-greeter/Cargo.toml | ||
| 576 | +[dependencies] | ||
| 577 | +x11rb = "0.13" | ||
| 578 | +cairo-rs = { version = "0.18", features = ["png"] } | ||
| 579 | +pango = "0.18" | ||
| 580 | +pangocairo = "0.18" | ||
| 581 | +image = "0.24" | ||
| 582 | +``` | ||
| 583 | + | ||
| 584 | +## Next Sprint | ||
| 585 | + | ||
| 586 | +Sprint 4 will add garbg integration for seamless wallpaper sync. | ||
docs/sprints/sprint-4-garbg-integration.mdadded@@ -0,0 +1,453 @@ | |||
| 1 | +# Sprint 4: garbg Integration | ||
| 2 | + | ||
| 3 | +**Goal:** Integrate with garbg to display the same wallpaper that will be shown after login, creating a seamless visual transition. | ||
| 4 | + | ||
| 5 | +## Objectives | ||
| 6 | + | ||
| 7 | +- Read garbg configuration to determine wallpaper source | ||
| 8 | +- Read garbg playlist state for current wallpaper | ||
| 9 | +- Apply consistent blur settings | ||
| 10 | +- Ensure seamless transition from greeter to gar session | ||
| 11 | +- Handle fallback when garbg config not available | ||
| 12 | + | ||
| 13 | +## Background: Seamless Transition | ||
| 14 | + | ||
| 15 | +The goal is that when a user logs in, the transition from greeter to desktop should feel smooth: | ||
| 16 | + | ||
| 17 | +``` | ||
| 18 | +┌─────────────────────────────────────────────────────────────────┐ | ||
| 19 | +│ Greeter │ | ||
| 20 | +│ - Shows blurred version of current garbg wallpaper │ | ||
| 21 | +│ - User enters credentials │ | ||
| 22 | +│ - Login succeeds │ | ||
| 23 | +└─────────────────────────────────────────────────────────────────┘ | ||
| 24 | + │ | ||
| 25 | + ▼ (fade out greeter) | ||
| 26 | +┌─────────────────────────────────────────────────────────────────┐ | ||
| 27 | +│ gar Session │ | ||
| 28 | +│ - garbg starts and sets SAME wallpaper (unblurred) │ | ||
| 29 | +│ - Only visible change: blur fades away, UI elements appear │ | ||
| 30 | +└─────────────────────────────────────────────────────────────────┘ | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +## Tasks | ||
| 34 | + | ||
| 35 | +### 4.1 Read garbg Configuration | ||
| 36 | + | ||
| 37 | +```rust | ||
| 38 | +// gardm-greeter/src/garbg.rs | ||
| 39 | + | ||
| 40 | +use serde::Deserialize; | ||
| 41 | +use std::path::PathBuf; | ||
| 42 | + | ||
| 43 | +#[derive(Debug, Deserialize)] | ||
| 44 | +pub struct GarbgConfig { | ||
| 45 | + #[serde(default)] | ||
| 46 | + pub general: GeneralConfig, | ||
| 47 | + #[serde(default)] | ||
| 48 | + pub default: DefaultConfig, | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +#[derive(Debug, Deserialize, Default)] | ||
| 52 | +pub struct GeneralConfig { | ||
| 53 | + #[serde(default = "default_mode")] | ||
| 54 | + pub mode: String, | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +#[derive(Debug, Deserialize, Default)] | ||
| 58 | +pub struct DefaultConfig { | ||
| 59 | + #[serde(default)] | ||
| 60 | + pub source: String, | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +fn default_mode() -> String { "fill".to_string() } | ||
| 64 | + | ||
| 65 | +impl GarbgConfig { | ||
| 66 | + /// Load garbg config from user or system location | ||
| 67 | + pub fn load(username: Option<&str>) -> Option<Self> { | ||
| 68 | + // Try user config first (if we know the username) | ||
| 69 | + if let Some(user) = username { | ||
| 70 | + if let Some(home) = get_user_home(user) { | ||
| 71 | + let user_config = home.join(".config/garbg/config.toml"); | ||
| 72 | + if let Ok(content) = std::fs::read_to_string(&user_config) { | ||
| 73 | + if let Ok(config) = toml::from_str(&content) { | ||
| 74 | + return Some(config); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + // Try system-wide default | ||
| 81 | + let system_config = PathBuf::from("/etc/garbg/config.toml"); | ||
| 82 | + if let Ok(content) = std::fs::read_to_string(&system_config) { | ||
| 83 | + if let Ok(config) = toml::from_str(&content) { | ||
| 84 | + return Some(config); | ||
| 85 | + } | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + None | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + /// Get the default wallpaper path | ||
| 92 | + pub fn default_wallpaper(&self) -> Option<String> { | ||
| 93 | + if self.default.source.is_empty() { | ||
| 94 | + None | ||
| 95 | + } else { | ||
| 96 | + Some(expand_path(&self.default.source)) | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +fn get_user_home(username: &str) -> Option<PathBuf> { | ||
| 102 | + use nix::unistd::User; | ||
| 103 | + User::from_name(username).ok()? | ||
| 104 | + .map(|u| PathBuf::from(u.dir)) | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +fn expand_path(path: &str) -> String { | ||
| 108 | + shellexpand::tilde(path).to_string() | ||
| 109 | +} | ||
| 110 | +``` | ||
| 111 | + | ||
| 112 | +### 4.2 Read garbg Playlist State | ||
| 113 | + | ||
| 114 | +```rust | ||
| 115 | +// gardm-greeter/src/garbg.rs (continued) | ||
| 116 | + | ||
| 117 | +#[derive(Debug, Deserialize)] | ||
| 118 | +pub struct PlaylistState { | ||
| 119 | + pub source: String, | ||
| 120 | + pub images: Vec<String>, | ||
| 121 | + pub current_index: usize, | ||
| 122 | + #[serde(default)] | ||
| 123 | + pub shuffled: bool, | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +impl PlaylistState { | ||
| 127 | + /// Load current playlist state from runtime directory | ||
| 128 | + pub fn load(username: Option<&str>) -> Option<Self> { | ||
| 129 | + // Playlist state is stored in XDG_RUNTIME_DIR | ||
| 130 | + let runtime_dir = std::env::var("XDG_RUNTIME_DIR").ok()?; | ||
| 131 | + let state_file = PathBuf::from(&runtime_dir).join("garbg-state.json"); | ||
| 132 | + | ||
| 133 | + // If that doesn't exist, try user-specific location | ||
| 134 | + if !state_file.exists() { | ||
| 135 | + if let Some(user) = username { | ||
| 136 | + // Try /run/user/<uid>/garbg-state.json | ||
| 137 | + if let Some(uid) = get_user_uid(user) { | ||
| 138 | + let user_runtime = format!("/run/user/{}/garbg-state.json", uid); | ||
| 139 | + if let Ok(content) = std::fs::read_to_string(&user_runtime) { | ||
| 140 | + if let Ok(state) = serde_json::from_str(&content) { | ||
| 141 | + return Some(state); | ||
| 142 | + } | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + } | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + let content = std::fs::read_to_string(&state_file).ok()?; | ||
| 149 | + serde_json::from_str(&content).ok() | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + /// Get the current wallpaper path | ||
| 153 | + pub fn current_wallpaper(&self) -> Option<&str> { | ||
| 154 | + self.images.get(self.current_index).map(|s| s.as_str()) | ||
| 155 | + } | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +fn get_user_uid(username: &str) -> Option<u32> { | ||
| 159 | + use nix::unistd::User; | ||
| 160 | + User::from_name(username).ok()? | ||
| 161 | + .map(|u| u.uid.as_raw()) | ||
| 162 | +} | ||
| 163 | +``` | ||
| 164 | + | ||
| 165 | +### 4.3 Wallpaper Resolution Logic | ||
| 166 | + | ||
| 167 | +```rust | ||
| 168 | +// gardm-greeter/src/garbg.rs (continued) | ||
| 169 | + | ||
| 170 | +/// Resolve which wallpaper to use for the greeter | ||
| 171 | +pub struct WallpaperResolver { | ||
| 172 | + fallback_path: String, | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | +impl WallpaperResolver { | ||
| 176 | + pub fn new(fallback: &str) -> Self { | ||
| 177 | + Self { | ||
| 178 | + fallback_path: fallback.to_string(), | ||
| 179 | + } | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + /// Resolve wallpaper path, trying multiple sources | ||
| 183 | + pub fn resolve(&self, username: Option<&str>) -> String { | ||
| 184 | + // Priority 1: Current playlist state (what user was last seeing) | ||
| 185 | + if let Some(state) = PlaylistState::load(username) { | ||
| 186 | + if let Some(current) = state.current_wallpaper() { | ||
| 187 | + let expanded = expand_path(current); | ||
| 188 | + if std::path::Path::new(&expanded).exists() { | ||
| 189 | + tracing::info!("Using wallpaper from playlist: {}", expanded); | ||
| 190 | + return expanded; | ||
| 191 | + } | ||
| 192 | + } | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + // Priority 2: garbg config default | ||
| 196 | + if let Some(config) = GarbgConfig::load(username) { | ||
| 197 | + if let Some(default) = config.default_wallpaper() { | ||
| 198 | + // If it's a directory, pick first image | ||
| 199 | + let path = std::path::Path::new(&default); | ||
| 200 | + if path.is_dir() { | ||
| 201 | + if let Some(first) = first_image_in_dir(&default) { | ||
| 202 | + tracing::info!("Using first image from garbg source dir: {}", first); | ||
| 203 | + return first; | ||
| 204 | + } | ||
| 205 | + } else if path.exists() { | ||
| 206 | + tracing::info!("Using wallpaper from garbg config: {}", default); | ||
| 207 | + return default; | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + // Priority 3: Fallback | ||
| 213 | + tracing::info!("Using fallback wallpaper: {}", self.fallback_path); | ||
| 214 | + self.fallback_path.clone() | ||
| 215 | + } | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +/// Get first image file in a directory | ||
| 219 | +fn first_image_in_dir(dir: &str) -> Option<String> { | ||
| 220 | + let extensions = ["jpg", "jpeg", "png", "webp"]; | ||
| 221 | + | ||
| 222 | + let mut entries: Vec<_> = std::fs::read_dir(dir).ok()? | ||
| 223 | + .filter_map(|e| e.ok()) | ||
| 224 | + .filter(|e| { | ||
| 225 | + let path = e.path(); | ||
| 226 | + if let Some(ext) = path.extension() { | ||
| 227 | + extensions.contains(&ext.to_string_lossy().to_lowercase().as_str()) | ||
| 228 | + } else { | ||
| 229 | + false | ||
| 230 | + } | ||
| 231 | + }) | ||
| 232 | + .collect(); | ||
| 233 | + | ||
| 234 | + entries.sort_by_key(|e| e.path()); | ||
| 235 | + entries.first().map(|e| e.path().to_string_lossy().to_string()) | ||
| 236 | +} | ||
| 237 | +``` | ||
| 238 | + | ||
| 239 | +### 4.4 Shared Visual Settings | ||
| 240 | + | ||
| 241 | +```rust | ||
| 242 | +// gardm-greeter/src/config.rs | ||
| 243 | + | ||
| 244 | +use serde::Deserialize; | ||
| 245 | + | ||
| 246 | +#[derive(Debug, Deserialize)] | ||
| 247 | +pub struct GreeterConfig { | ||
| 248 | + #[serde(default)] | ||
| 249 | + pub visual: VisualConfig, | ||
| 250 | + #[serde(default)] | ||
| 251 | + pub garbg: GarbgIntegration, | ||
| 252 | +} | ||
| 253 | + | ||
| 254 | +#[derive(Debug, Deserialize, Default)] | ||
| 255 | +pub struct VisualConfig { | ||
| 256 | + /// Blur radius for background (should match gar lock screen) | ||
| 257 | + #[serde(default = "default_blur_radius")] | ||
| 258 | + pub blur_radius: f32, | ||
| 259 | + | ||
| 260 | + /// Background brightness (0.0-1.0, lower = darker) | ||
| 261 | + #[serde(default = "default_brightness")] | ||
| 262 | + pub blur_brightness: f32, | ||
| 263 | + | ||
| 264 | + /// Corner radius for UI elements (match gar's corner_radius) | ||
| 265 | + #[serde(default = "default_corner_radius")] | ||
| 266 | + pub corner_radius: f64, | ||
| 267 | +} | ||
| 268 | + | ||
| 269 | +#[derive(Debug, Deserialize, Default)] | ||
| 270 | +pub struct GarbgIntegration { | ||
| 271 | + /// Whether to use garbg wallpaper | ||
| 272 | + #[serde(default = "default_true")] | ||
| 273 | + pub enabled: bool, | ||
| 274 | + | ||
| 275 | + /// Fallback wallpaper if garbg not configured | ||
| 276 | + #[serde(default = "default_fallback")] | ||
| 277 | + pub fallback: String, | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +fn default_blur_radius() -> f32 { 20.0 } | ||
| 281 | +fn default_brightness() -> f32 { 0.7 } | ||
| 282 | +fn default_corner_radius() -> f64 { 18.0 } | ||
| 283 | +fn default_true() -> bool { true } | ||
| 284 | +fn default_fallback() -> String { "/usr/share/gardm/backgrounds/default.jpg".to_string() } | ||
| 285 | +``` | ||
| 286 | + | ||
| 287 | +### 4.5 Integration with Greeter Main Loop | ||
| 288 | + | ||
| 289 | +```rust | ||
| 290 | +// gardm-greeter/src/main.rs (updated) | ||
| 291 | + | ||
| 292 | +fn main() -> anyhow::Result<()> { | ||
| 293 | + let config = GreeterConfig::load()?; | ||
| 294 | + let window = GreeterWindow::new()?; | ||
| 295 | + | ||
| 296 | + // Resolve wallpaper using garbg integration | ||
| 297 | + let wallpaper_path = if config.garbg.enabled { | ||
| 298 | + let resolver = WallpaperResolver::new(&config.garbg.fallback); | ||
| 299 | + // Initially no username known, use system defaults | ||
| 300 | + resolver.resolve(None) | ||
| 301 | + } else { | ||
| 302 | + config.garbg.fallback.clone() | ||
| 303 | + }; | ||
| 304 | + | ||
| 305 | + // Load and blur background | ||
| 306 | + let background = load_blurred_background( | ||
| 307 | + &wallpaper_path, | ||
| 308 | + window.width() as u32, | ||
| 309 | + window.height() as u32, | ||
| 310 | + config.visual.blur_radius, | ||
| 311 | + config.visual.blur_brightness, | ||
| 312 | + )?; | ||
| 313 | + | ||
| 314 | + let mut form = LoginForm::new( | ||
| 315 | + window.width() as f64, | ||
| 316 | + window.height() as f64, | ||
| 317 | + config.visual.corner_radius, | ||
| 318 | + ); | ||
| 319 | + | ||
| 320 | + // When username is entered, potentially update wallpaper | ||
| 321 | + // to user-specific one (optional enhancement) | ||
| 322 | + | ||
| 323 | + // ... rest of event loop ... | ||
| 324 | +} | ||
| 325 | +``` | ||
| 326 | + | ||
| 327 | +### 4.6 Session Transition Effect | ||
| 328 | + | ||
| 329 | +```rust | ||
| 330 | +// gardm-greeter/src/transition.rs | ||
| 331 | + | ||
| 332 | +use std::time::{Duration, Instant}; | ||
| 333 | + | ||
| 334 | +/// Fade out transition before starting session | ||
| 335 | +pub struct FadeOutTransition { | ||
| 336 | + start_time: Instant, | ||
| 337 | + duration: Duration, | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +impl FadeOutTransition { | ||
| 341 | + pub fn new(duration_ms: u64) -> Self { | ||
| 342 | + Self { | ||
| 343 | + start_time: Instant::now(), | ||
| 344 | + duration: Duration::from_millis(duration_ms), | ||
| 345 | + } | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + /// Get current opacity (1.0 -> 0.0) | ||
| 349 | + pub fn opacity(&self) -> f64 { | ||
| 350 | + let elapsed = self.start_time.elapsed(); | ||
| 351 | + if elapsed >= self.duration { | ||
| 352 | + 0.0 | ||
| 353 | + } else { | ||
| 354 | + 1.0 - (elapsed.as_secs_f64() / self.duration.as_secs_f64()) | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + pub fn is_complete(&self) -> bool { | ||
| 359 | + self.start_time.elapsed() >= self.duration | ||
| 360 | + } | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +/// Apply fade during session start | ||
| 364 | +pub fn render_with_fade( | ||
| 365 | + ctx: &cairo::Context, | ||
| 366 | + background: &image::RgbaImage, | ||
| 367 | + form: &LoginForm, | ||
| 368 | + fade: Option<&FadeOutTransition>, | ||
| 369 | +) -> anyhow::Result<()> { | ||
| 370 | + // Render background (always full opacity) | ||
| 371 | + render_background(ctx, background)?; | ||
| 372 | + | ||
| 373 | + // Render UI with fade | ||
| 374 | + if let Some(fade) = fade { | ||
| 375 | + ctx.push_group(); | ||
| 376 | + form.render(ctx, &pango_ctx)?; | ||
| 377 | + ctx.pop_group_to_source()?; | ||
| 378 | + ctx.paint_with_alpha(fade.opacity())?; | ||
| 379 | + } else { | ||
| 380 | + form.render(ctx, &pango_ctx)?; | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + Ok(()) | ||
| 384 | +} | ||
| 385 | +``` | ||
| 386 | + | ||
| 387 | +## Acceptance Criteria | ||
| 388 | + | ||
| 389 | +1. Greeter shows same wallpaper as garbg will after login | ||
| 390 | +2. Blur settings are consistent with gar lock screen | ||
| 391 | +3. Fallback works when garbg is not configured | ||
| 392 | +4. User-specific wallpaper is used when username is known | ||
| 393 | +5. Smooth fade transition when starting session | ||
| 394 | +6. No flash of different wallpaper during login | ||
| 395 | + | ||
| 396 | +## Pitfalls to Avoid | ||
| 397 | + | ||
| 398 | +1. **File permissions** - greeter runs as gardm user, may not access user home | ||
| 399 | +2. **Race conditions** - garbg state file might be stale | ||
| 400 | +3. **Directory vs file** - garbg source might be a directory | ||
| 401 | +4. **Missing files** - wallpaper might have been deleted | ||
| 402 | +5. **Large images** - blur on 4K images is slow, cache if needed | ||
| 403 | + | ||
| 404 | +## Testing | ||
| 405 | + | ||
| 406 | +```bash | ||
| 407 | +# Test wallpaper resolution | ||
| 408 | +# 1. With garbg configured and running | ||
| 409 | +garbg set ~/Pictures/wallpapers --random | ||
| 410 | +DISPLAY=:1 ./target/release/gardm-greeter | ||
| 411 | + | ||
| 412 | +# 2. With garbg config but no playlist | ||
| 413 | +rm ~/.local/state/garbg-state.json | ||
| 414 | +DISPLAY=:1 ./target/release/gardm-greeter | ||
| 415 | + | ||
| 416 | +# 3. Without any garbg config (should use fallback) | ||
| 417 | +mv ~/.config/garbg/config.toml ~/.config/garbg/config.toml.bak | ||
| 418 | +DISPLAY=:1 ./target/release/gardm-greeter | ||
| 419 | +``` | ||
| 420 | + | ||
| 421 | +## Dependencies for This Sprint | ||
| 422 | + | ||
| 423 | +```toml | ||
| 424 | +# gardm-greeter/Cargo.toml | ||
| 425 | +[dependencies] | ||
| 426 | +shellexpand = "3.0" | ||
| 427 | +toml = "0.8" | ||
| 428 | +serde_json = "1.0" | ||
| 429 | +``` | ||
| 430 | + | ||
| 431 | +## Integration Checklist | ||
| 432 | + | ||
| 433 | +- [ ] Greeter reads `~/.config/garbg/config.toml` | ||
| 434 | +- [ ] Greeter reads `$XDG_RUNTIME_DIR/garbg-state.json` | ||
| 435 | +- [ ] Blur radius matches gar lock screen (if implemented) | ||
| 436 | +- [ ] Corner radius matches gar's `corner_radius` setting | ||
| 437 | +- [ ] Fallback image is bundled with gardm package | ||
| 438 | +- [ ] Fade transition timing feels smooth (150-200ms) | ||
| 439 | + | ||
| 440 | +## Future Enhancements | ||
| 441 | + | ||
| 442 | +- [ ] Per-user wallpaper preview before login | ||
| 443 | +- [ ] Animated wallpaper support (match garbg animation) | ||
| 444 | +- [ ] Workspace-specific wallpaper from garbg config | ||
| 445 | +- [ ] Live wallpaper update if garbg changes during greeter | ||
| 446 | + | ||
| 447 | +## Next Steps | ||
| 448 | + | ||
| 449 | +After Sprint 4, consider: | ||
| 450 | +- Sprint 5: Power buttons and session selector | ||
| 451 | +- Sprint 6: User list with avatars | ||
| 452 | +- Sprint 7: Accessibility and theming | ||
| 453 | +- Sprint 8: Multi-monitor support | ||
etc/config.tomladded@@ -0,0 +1,34 @@ | |||
| 1 | +# gardm configuration | ||
| 2 | +# Place this file at /etc/gardm/config.toml | ||
| 3 | + | ||
| 4 | +[general] | ||
| 5 | +# Default session to launch (without .desktop extension) | ||
| 6 | +default_session = "gar" | ||
| 7 | +# Path to greeter executable | ||
| 8 | +greeter = "/usr/bin/gardm-greeter" | ||
| 9 | +# VT to use (0 = auto-select) | ||
| 10 | +vt = 0 | ||
| 11 | +# X11 display | ||
| 12 | +display = ":0" | ||
| 13 | + | ||
| 14 | +[greeter] | ||
| 15 | +# Background blur radius | ||
| 16 | +blur_radius = 20 | ||
| 17 | +# Background brightness (0.0-1.0) | ||
| 18 | +blur_brightness = 0.7 | ||
| 19 | +# Show power buttons (shutdown, reboot, suspend) | ||
| 20 | +show_power_buttons = true | ||
| 21 | +# Show session selector dropdown | ||
| 22 | +show_session_selector = true | ||
| 23 | +# Use garbg's current wallpaper | ||
| 24 | +use_garbg_wallpaper = true | ||
| 25 | +# Fallback wallpaper if garbg not available | ||
| 26 | +fallback_wallpaper = "/usr/share/gardm/backgrounds/default.jpg" | ||
| 27 | + | ||
| 28 | +[security] | ||
| 29 | +# Allow empty passwords (NOT recommended) | ||
| 30 | +allow_empty_password = false | ||
| 31 | +# Lock after N failed attempts (0 = disabled) | ||
| 32 | +lockout_attempts = 5 | ||
| 33 | +# Lockout duration in seconds | ||
| 34 | +lockout_duration = 300 | ||
etc/gardm.serviceadded@@ -0,0 +1,25 @@ | |||
| 1 | +[Unit] | ||
| 2 | +Description=gar Display Manager | ||
| 3 | +Documentation=https://github.com/mfwolffe/gardesk | ||
| 4 | +After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service | ||
| 5 | +Conflicts=getty@tty1.service | ||
| 6 | + | ||
| 7 | +[Service] | ||
| 8 | +Type=notify | ||
| 9 | +ExecStart=/usr/bin/gardmd | ||
| 10 | +ExecReload=/bin/kill -HUP $MAINPID | ||
| 11 | +Restart=always | ||
| 12 | +RestartSec=1 | ||
| 13 | + | ||
| 14 | +# Security hardening | ||
| 15 | +NoNewPrivileges=no | ||
| 16 | +ProtectSystem=strict | ||
| 17 | +ProtectHome=read-only | ||
| 18 | +PrivateTmp=yes | ||
| 19 | +ReadWritePaths=/run | ||
| 20 | + | ||
| 21 | +# PAM needs access to various system files | ||
| 22 | +ReadOnlyPaths=/etc/passwd /etc/shadow /etc/group /etc/pam.d | ||
| 23 | + | ||
| 24 | +[Install] | ||
| 25 | +Alias=display-manager.service | ||
gardm-greeter/Cargo.tomladded@@ -0,0 +1,18 @@ | |||
| 1 | +[package] | ||
| 2 | +name = "gardm-greeter" | ||
| 3 | +version.workspace = true | ||
| 4 | +edition.workspace = true | ||
| 5 | +license.workspace = true | ||
| 6 | +description = "gar display manager greeter" | ||
| 7 | + | ||
| 8 | +[[bin]] | ||
| 9 | +name = "gardm-greeter" | ||
| 10 | +path = "src/main.rs" | ||
| 11 | + | ||
| 12 | +[dependencies] | ||
| 13 | +gardm-ipc = { workspace = true } | ||
| 14 | +tokio = { workspace = true } | ||
| 15 | +tracing = { workspace = true } | ||
| 16 | +tracing-subscriber = { workspace = true } | ||
| 17 | +anyhow = { workspace = true } | ||
| 18 | +thiserror = { workspace = true } | ||
gardm-greeter/src/main.rsadded@@ -0,0 +1,35 @@ | |||
| 1 | +//! gardm-greeter - gar display manager greeter | ||
| 2 | +//! | ||
| 3 | +//! Graphical login UI that communicates with gardmd. | ||
| 4 | + | ||
| 5 | +use anyhow::Result; | ||
| 6 | +use gardm_ipc::{Client, Request}; | ||
| 7 | +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; | ||
| 8 | + | ||
| 9 | +#[tokio::main] | ||
| 10 | +async fn main() -> Result<()> { | ||
| 11 | + // Initialize logging | ||
| 12 | + tracing_subscriber::registry() | ||
| 13 | + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) | ||
| 14 | + .with(tracing_subscriber::fmt::layer()) | ||
| 15 | + .init(); | ||
| 16 | + | ||
| 17 | + tracing::info!("gardm-greeter starting"); | ||
| 18 | + | ||
| 19 | + // Connect to daemon | ||
| 20 | + let mut client = Client::connect().await?; | ||
| 21 | + tracing::info!("Connected to gardmd"); | ||
| 22 | + | ||
| 23 | + // TODO: Initialize X11/rendering | ||
| 24 | + // TODO: Main UI loop | ||
| 25 | + | ||
| 26 | + // For now, just test the connection | ||
| 27 | + let response = client.request(&Request::ListSessions).await?; | ||
| 28 | + tracing::info!(?response, "Got sessions"); | ||
| 29 | + | ||
| 30 | + let response = client.request(&Request::ListUsers).await?; | ||
| 31 | + tracing::info!(?response, "Got users"); | ||
| 32 | + | ||
| 33 | + tracing::info!("gardm-greeter exiting (UI not yet implemented)"); | ||
| 34 | + Ok(()) | ||
| 35 | +} | ||
gardm-ipc/Cargo.tomladded@@ -0,0 +1,12 @@ | |||
| 1 | +[package] | ||
| 2 | +name = "gardm-ipc" | ||
| 3 | +version.workspace = true | ||
| 4 | +edition.workspace = true | ||
| 5 | +license.workspace = true | ||
| 6 | +description = "IPC protocol types for gardm display manager" | ||
| 7 | + | ||
| 8 | +[dependencies] | ||
| 9 | +serde = { workspace = true } | ||
| 10 | +serde_json = { workspace = true } | ||
| 11 | +thiserror = { workspace = true } | ||
| 12 | +tokio = { workspace = true, features = ["net", "io-util"] } | ||
gardm-ipc/src/lib.rsadded@@ -0,0 +1,199 @@ | |||
| 1 | +//! IPC protocol for gardm display manager | ||
| 2 | +//! | ||
| 3 | +//! Defines the JSON-based protocol between gardmd (daemon) and gardm-greeter (UI). | ||
| 4 | + | ||
| 5 | +use serde::{Deserialize, Serialize}; | ||
| 6 | +use std::path::PathBuf; | ||
| 7 | +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; | ||
| 8 | +use tokio::net::UnixStream; | ||
| 9 | + | ||
| 10 | +/// Socket path for gardm IPC | ||
| 11 | +pub const SOCKET_PATH: &str = "/run/gardm.sock"; | ||
| 12 | + | ||
| 13 | +/// Requests from greeter to daemon | ||
| 14 | +#[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 15 | +#[serde(tag = "type", rename_all = "snake_case")] | ||
| 16 | +pub enum Request { | ||
| 17 | + /// Create a new authentication session for a user | ||
| 18 | + CreateSession { username: String }, | ||
| 19 | + | ||
| 20 | + /// Provide authentication response (password, OTP, etc.) | ||
| 21 | + Authenticate { response: String }, | ||
| 22 | + | ||
| 23 | + /// Start the user's session after successful auth | ||
| 24 | + StartSession { | ||
| 25 | + /// Session command (e.g., ["gar-session.sh"]) | ||
| 26 | + cmd: Vec<String>, | ||
| 27 | + /// Additional environment variables | ||
| 28 | + #[serde(default)] | ||
| 29 | + env: Vec<String>, | ||
| 30 | + }, | ||
| 31 | + | ||
| 32 | + /// Cancel the current authentication attempt | ||
| 33 | + CancelSession, | ||
| 34 | + | ||
| 35 | + /// Request system shutdown | ||
| 36 | + Shutdown, | ||
| 37 | + | ||
| 38 | + /// Request system reboot | ||
| 39 | + Reboot, | ||
| 40 | + | ||
| 41 | + /// Request system suspend | ||
| 42 | + Suspend, | ||
| 43 | + | ||
| 44 | + /// Get list of available sessions | ||
| 45 | + ListSessions, | ||
| 46 | + | ||
| 47 | + /// Get list of available users | ||
| 48 | + ListUsers, | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +/// Responses from daemon to greeter | ||
| 52 | +#[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 53 | +#[serde(tag = "type", rename_all = "snake_case")] | ||
| 54 | +pub enum Response { | ||
| 55 | + /// Operation completed successfully | ||
| 56 | + Success, | ||
| 57 | + | ||
| 58 | + /// PAM is requesting user input | ||
| 59 | + AuthPrompt { | ||
| 60 | + /// Prompt message (e.g., "Password:") | ||
| 61 | + prompt: String, | ||
| 62 | + /// Whether to echo input (false for passwords) | ||
| 63 | + echo: bool, | ||
| 64 | + }, | ||
| 65 | + | ||
| 66 | + /// PAM informational message | ||
| 67 | + AuthInfo { message: String }, | ||
| 68 | + | ||
| 69 | + /// Authentication failed | ||
| 70 | + AuthError { message: String }, | ||
| 71 | + | ||
| 72 | + /// General error | ||
| 73 | + Error { message: String }, | ||
| 74 | + | ||
| 75 | + /// List of available sessions | ||
| 76 | + Sessions { sessions: Vec<SessionInfo> }, | ||
| 77 | + | ||
| 78 | + /// List of available users | ||
| 79 | + Users { users: Vec<UserInfo> }, | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +/// Information about an available session | ||
| 83 | +#[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 84 | +pub struct SessionInfo { | ||
| 85 | + /// Session identifier (desktop file name without .desktop) | ||
| 86 | + pub id: String, | ||
| 87 | + /// Display name | ||
| 88 | + pub name: String, | ||
| 89 | + /// Optional comment/description | ||
| 90 | + pub comment: Option<String>, | ||
| 91 | + /// Exec command | ||
| 92 | + pub exec: String, | ||
| 93 | + /// Session type (x11, wayland) | ||
| 94 | + pub session_type: String, | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +/// Information about a user | ||
| 98 | +#[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 99 | +pub struct UserInfo { | ||
| 100 | + /// Username | ||
| 101 | + pub name: String, | ||
| 102 | + /// Full name (GECOS field) | ||
| 103 | + pub full_name: Option<String>, | ||
| 104 | + /// Home directory | ||
| 105 | + pub home: PathBuf, | ||
| 106 | + /// Path to user avatar (if available) | ||
| 107 | + pub avatar: Option<PathBuf>, | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +/// IPC client for connecting to gardmd | ||
| 111 | +pub struct Client { | ||
| 112 | + reader: BufReader<tokio::net::unix::OwnedReadHalf>, | ||
| 113 | + writer: tokio::net::unix::OwnedWriteHalf, | ||
| 114 | +} | ||
| 115 | + | ||
| 116 | +impl Client { | ||
| 117 | + /// Connect to the daemon | ||
| 118 | + pub async fn connect() -> Result<Self, std::io::Error> { | ||
| 119 | + Self::connect_to(SOCKET_PATH).await | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + /// Connect to a specific socket path | ||
| 123 | + pub async fn connect_to(path: &str) -> Result<Self, std::io::Error> { | ||
| 124 | + let stream = UnixStream::connect(path).await?; | ||
| 125 | + let (read, write) = stream.into_split(); | ||
| 126 | + Ok(Self { | ||
| 127 | + reader: BufReader::new(read), | ||
| 128 | + writer: write, | ||
| 129 | + }) | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + /// Send a request to the daemon | ||
| 133 | + pub async fn send(&mut self, request: &Request) -> Result<(), std::io::Error> { | ||
| 134 | + let json = serde_json::to_string(request).map_err(|e| { | ||
| 135 | + std::io::Error::new(std::io::ErrorKind::InvalidData, e) | ||
| 136 | + })?; | ||
| 137 | + self.writer.write_all(json.as_bytes()).await?; | ||
| 138 | + self.writer.write_all(b"\n").await?; | ||
| 139 | + self.writer.flush().await?; | ||
| 140 | + Ok(()) | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + /// Receive a response from the daemon | ||
| 144 | + pub async fn recv(&mut self) -> Result<Response, std::io::Error> { | ||
| 145 | + let mut line = String::new(); | ||
| 146 | + let n = self.reader.read_line(&mut line).await?; | ||
| 147 | + if n == 0 { | ||
| 148 | + return Err(std::io::Error::new( | ||
| 149 | + std::io::ErrorKind::UnexpectedEof, | ||
| 150 | + "daemon closed connection", | ||
| 151 | + )); | ||
| 152 | + } | ||
| 153 | + serde_json::from_str(&line).map_err(|e| { | ||
| 154 | + std::io::Error::new(std::io::ErrorKind::InvalidData, e) | ||
| 155 | + }) | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + /// Send request and wait for response | ||
| 159 | + pub async fn request(&mut self, request: &Request) -> Result<Response, std::io::Error> { | ||
| 160 | + self.send(request).await?; | ||
| 161 | + self.recv().await | ||
| 162 | + } | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +#[cfg(test)] | ||
| 166 | +mod tests { | ||
| 167 | + use super::*; | ||
| 168 | + | ||
| 169 | + #[test] | ||
| 170 | + fn test_request_serialization() { | ||
| 171 | + let req = Request::CreateSession { | ||
| 172 | + username: "testuser".to_string(), | ||
| 173 | + }; | ||
| 174 | + let json = serde_json::to_string(&req).unwrap(); | ||
| 175 | + assert!(json.contains("create_session")); | ||
| 176 | + assert!(json.contains("testuser")); | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + #[test] | ||
| 180 | + fn test_response_serialization() { | ||
| 181 | + let resp = Response::AuthPrompt { | ||
| 182 | + prompt: "Password:".to_string(), | ||
| 183 | + echo: false, | ||
| 184 | + }; | ||
| 185 | + let json = serde_json::to_string(&resp).unwrap(); | ||
| 186 | + assert!(json.contains("auth_prompt")); | ||
| 187 | + assert!(json.contains("Password:")); | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + #[test] | ||
| 191 | + fn test_request_deserialization() { | ||
| 192 | + let json = r#"{"type":"authenticate","response":"secret123"}"#; | ||
| 193 | + let req: Request = serde_json::from_str(json).unwrap(); | ||
| 194 | + match req { | ||
| 195 | + Request::Authenticate { response } => assert_eq!(response, "secret123"), | ||
| 196 | + _ => panic!("Wrong variant"), | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | +} | ||
gardmd/Cargo.tomladded@@ -0,0 +1,23 @@ | |||
| 1 | +[package] | ||
| 2 | +name = "gardmd" | ||
| 3 | +version.workspace = true | ||
| 4 | +edition.workspace = true | ||
| 5 | +license.workspace = true | ||
| 6 | +description = "gar display manager daemon" | ||
| 7 | + | ||
| 8 | +[[bin]] | ||
| 9 | +name = "gardmd" | ||
| 10 | +path = "src/main.rs" | ||
| 11 | + | ||
| 12 | +[dependencies] | ||
| 13 | +gardm-ipc = { workspace = true } | ||
| 14 | +tokio = { workspace = true } | ||
| 15 | +tracing = { workspace = true } | ||
| 16 | +tracing-subscriber = { workspace = true } | ||
| 17 | +anyhow = { workspace = true } | ||
| 18 | +thiserror = { workspace = true } | ||
| 19 | +serde = { workspace = true } | ||
| 20 | +serde_json = { workspace = true } | ||
| 21 | +toml = { workspace = true } | ||
| 22 | +nix = { workspace = true } | ||
| 23 | +sd-notify = "0.4" | ||
gardmd/src/config.rsadded@@ -0,0 +1,155 @@ | |||
| 1 | +//! Configuration for gardmd | ||
| 2 | + | ||
| 3 | +use serde::Deserialize; | ||
| 4 | +use std::path::PathBuf; | ||
| 5 | + | ||
| 6 | +/// Main configuration structure | ||
| 7 | +#[derive(Debug, Clone, Deserialize)] | ||
| 8 | +pub struct Config { | ||
| 9 | + #[serde(default)] | ||
| 10 | + pub general: GeneralConfig, | ||
| 11 | + #[serde(default)] | ||
| 12 | + pub greeter: GreeterConfig, | ||
| 13 | + #[serde(default)] | ||
| 14 | + pub security: SecurityConfig, | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +impl Default for Config { | ||
| 18 | + fn default() -> Self { | ||
| 19 | + Self { | ||
| 20 | + general: GeneralConfig::default(), | ||
| 21 | + greeter: GreeterConfig::default(), | ||
| 22 | + security: SecurityConfig::default(), | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +/// General daemon settings | ||
| 28 | +#[derive(Debug, Clone, Deserialize)] | ||
| 29 | +pub struct GeneralConfig { | ||
| 30 | + /// Default session if user hasn't selected one | ||
| 31 | + #[serde(default = "default_session")] | ||
| 32 | + pub default_session: String, | ||
| 33 | + | ||
| 34 | + /// Path to greeter executable | ||
| 35 | + #[serde(default = "default_greeter")] | ||
| 36 | + pub greeter: PathBuf, | ||
| 37 | + | ||
| 38 | + /// VT to use (0 = auto-select) | ||
| 39 | + #[serde(default)] | ||
| 40 | + pub vt: u32, | ||
| 41 | + | ||
| 42 | + /// X11 display to use | ||
| 43 | + #[serde(default = "default_display")] | ||
| 44 | + pub display: String, | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +impl Default for GeneralConfig { | ||
| 48 | + fn default() -> Self { | ||
| 49 | + Self { | ||
| 50 | + default_session: default_session(), | ||
| 51 | + greeter: default_greeter(), | ||
| 52 | + vt: 0, | ||
| 53 | + display: default_display(), | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +/// Greeter visual settings | ||
| 59 | +#[derive(Debug, Clone, Deserialize)] | ||
| 60 | +pub struct GreeterConfig { | ||
| 61 | + /// Blur radius for background | ||
| 62 | + #[serde(default = "default_blur_radius")] | ||
| 63 | + pub blur_radius: u32, | ||
| 64 | + | ||
| 65 | + /// Background brightness (0.0-1.0) | ||
| 66 | + #[serde(default = "default_blur_brightness")] | ||
| 67 | + pub blur_brightness: f32, | ||
| 68 | + | ||
| 69 | + /// Show power buttons | ||
| 70 | + #[serde(default = "default_true")] | ||
| 71 | + pub show_power_buttons: bool, | ||
| 72 | + | ||
| 73 | + /// Show session selector | ||
| 74 | + #[serde(default = "default_true")] | ||
| 75 | + pub show_session_selector: bool, | ||
| 76 | + | ||
| 77 | + /// Use garbg wallpaper | ||
| 78 | + #[serde(default = "default_true")] | ||
| 79 | + pub use_garbg_wallpaper: bool, | ||
| 80 | + | ||
| 81 | + /// Fallback wallpaper path | ||
| 82 | + #[serde(default = "default_fallback_wallpaper")] | ||
| 83 | + pub fallback_wallpaper: PathBuf, | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +impl Default for GreeterConfig { | ||
| 87 | + fn default() -> Self { | ||
| 88 | + Self { | ||
| 89 | + blur_radius: default_blur_radius(), | ||
| 90 | + blur_brightness: default_blur_brightness(), | ||
| 91 | + show_power_buttons: true, | ||
| 92 | + show_session_selector: true, | ||
| 93 | + use_garbg_wallpaper: true, | ||
| 94 | + fallback_wallpaper: default_fallback_wallpaper(), | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +/// Security settings | ||
| 100 | +#[derive(Debug, Clone, Deserialize)] | ||
| 101 | +pub struct SecurityConfig { | ||
| 102 | + /// Allow empty passwords | ||
| 103 | + #[serde(default)] | ||
| 104 | + pub allow_empty_password: bool, | ||
| 105 | + | ||
| 106 | + /// Lock after N failed attempts (0 = disabled) | ||
| 107 | + #[serde(default = "default_lockout_attempts")] | ||
| 108 | + pub lockout_attempts: u32, | ||
| 109 | + | ||
| 110 | + /// Lockout duration in seconds | ||
| 111 | + #[serde(default = "default_lockout_duration")] | ||
| 112 | + pub lockout_duration: u64, | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +impl Default for SecurityConfig { | ||
| 116 | + fn default() -> Self { | ||
| 117 | + Self { | ||
| 118 | + allow_empty_password: false, | ||
| 119 | + lockout_attempts: default_lockout_attempts(), | ||
| 120 | + lockout_duration: default_lockout_duration(), | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | +} | ||
| 124 | + | ||
| 125 | +// Default value functions | ||
| 126 | +fn default_session() -> String { "gar".to_string() } | ||
| 127 | +fn default_greeter() -> PathBuf { PathBuf::from("/usr/bin/gardm-greeter") } | ||
| 128 | +fn default_display() -> String { ":0".to_string() } | ||
| 129 | +fn default_blur_radius() -> u32 { 20 } | ||
| 130 | +fn default_blur_brightness() -> f32 { 0.7 } | ||
| 131 | +fn default_true() -> bool { true } | ||
| 132 | +fn default_fallback_wallpaper() -> PathBuf { | ||
| 133 | + PathBuf::from("/usr/share/gardm/backgrounds/default.jpg") | ||
| 134 | +} | ||
| 135 | +fn default_lockout_attempts() -> u32 { 5 } | ||
| 136 | +fn default_lockout_duration() -> u64 { 300 } | ||
| 137 | + | ||
| 138 | +impl Config { | ||
| 139 | + /// Load configuration from default path | ||
| 140 | + pub fn load() -> anyhow::Result<Self> { | ||
| 141 | + Self::load_from("/etc/gardm/config.toml") | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /// Load configuration from specified path | ||
| 145 | + pub fn load_from(path: &str) -> anyhow::Result<Self> { | ||
| 146 | + let path = PathBuf::from(path); | ||
| 147 | + if path.exists() { | ||
| 148 | + let content = std::fs::read_to_string(&path)?; | ||
| 149 | + Ok(toml::from_str(&content)?) | ||
| 150 | + } else { | ||
| 151 | + tracing::info!("Config file not found at {}, using defaults", path.display()); | ||
| 152 | + Ok(Config::default()) | ||
| 153 | + } | ||
| 154 | + } | ||
| 155 | +} | ||
gardmd/src/ipc.rsadded@@ -0,0 +1,99 @@ | |||
| 1 | +//! IPC server for gardmd | ||
| 2 | +//! | ||
| 3 | +//! Handles connections from the greeter and processes requests. | ||
| 4 | + | ||
| 5 | +use anyhow::Result; | ||
| 6 | +use gardm_ipc::{Request, Response, SOCKET_PATH}; | ||
| 7 | +use std::path::Path; | ||
| 8 | +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; | ||
| 9 | +use tokio::net::{UnixListener, UnixStream}; | ||
| 10 | +use tokio::sync::mpsc; | ||
| 11 | + | ||
| 12 | +/// IPC server for handling greeter connections | ||
| 13 | +pub struct Server { | ||
| 14 | + listener: UnixListener, | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +impl Server { | ||
| 18 | + /// Create a new IPC server | ||
| 19 | + pub async fn new() -> Result<Self> { | ||
| 20 | + Self::bind(SOCKET_PATH).await | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + /// Bind to a specific socket path | ||
| 24 | + pub async fn bind(path: &str) -> Result<Self> { | ||
| 25 | + let path = Path::new(path); | ||
| 26 | + | ||
| 27 | + // Remove stale socket if it exists | ||
| 28 | + if path.exists() { | ||
| 29 | + std::fs::remove_file(path)?; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + // Ensure parent directory exists | ||
| 33 | + if let Some(parent) = path.parent() { | ||
| 34 | + std::fs::create_dir_all(parent)?; | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + let listener = UnixListener::bind(path)?; | ||
| 38 | + tracing::info!("IPC server listening on {}", path.display()); | ||
| 39 | + | ||
| 40 | + Ok(Self { listener }) | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + /// Accept a new connection | ||
| 44 | + pub async fn accept(&self) -> Result<ClientConnection> { | ||
| 45 | + let (stream, _addr) = self.listener.accept().await?; | ||
| 46 | + tracing::debug!("New greeter connection"); | ||
| 47 | + Ok(ClientConnection::new(stream)) | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +/// A connected greeter client | ||
| 52 | +pub struct ClientConnection { | ||
| 53 | + reader: BufReader<tokio::net::unix::OwnedReadHalf>, | ||
| 54 | + writer: tokio::net::unix::OwnedWriteHalf, | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +impl ClientConnection { | ||
| 58 | + fn new(stream: UnixStream) -> Self { | ||
| 59 | + let (read, write) = stream.into_split(); | ||
| 60 | + Self { | ||
| 61 | + reader: BufReader::new(read), | ||
| 62 | + writer: write, | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /// Receive a request from the greeter | ||
| 67 | + pub async fn recv(&mut self) -> Result<Option<Request>> { | ||
| 68 | + let mut line = String::new(); | ||
| 69 | + let n = self.reader.read_line(&mut line).await?; | ||
| 70 | + if n == 0 { | ||
| 71 | + return Ok(None); | ||
| 72 | + } | ||
| 73 | + let request: Request = serde_json::from_str(&line)?; | ||
| 74 | + tracing::debug!(?request, "Received request"); | ||
| 75 | + Ok(Some(request)) | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + /// Send a response to the greeter | ||
| 79 | + pub async fn send(&mut self, response: &Response) -> Result<()> { | ||
| 80 | + let json = serde_json::to_string(response)?; | ||
| 81 | + self.writer.write_all(json.as_bytes()).await?; | ||
| 82 | + self.writer.write_all(b"\n").await?; | ||
| 83 | + self.writer.flush().await?; | ||
| 84 | + tracing::debug!(?response, "Sent response"); | ||
| 85 | + Ok(()) | ||
| 86 | + } | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +/// Commands from external sources (e.g., signal handlers) | ||
| 90 | +#[derive(Debug)] | ||
| 91 | +pub enum DaemonCommand { | ||
| 92 | + Shutdown, | ||
| 93 | + Reload, | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +/// Create a channel for daemon commands | ||
| 97 | +pub fn command_channel() -> (mpsc::Sender<DaemonCommand>, mpsc::Receiver<DaemonCommand>) { | ||
| 98 | + mpsc::channel(16) | ||
| 99 | +} | ||
gardmd/src/lib.rsadded@@ -0,0 +1,9 @@ | |||
| 1 | +//! gardmd - gar display manager daemon | ||
| 2 | +//! | ||
| 3 | +//! Handles PAM authentication, X11 server management, and session launching. | ||
| 4 | + | ||
| 5 | +pub mod config; | ||
| 6 | +pub mod ipc; | ||
| 7 | + | ||
| 8 | +pub use config::Config; | ||
| 9 | +pub use ipc::{ClientConnection, Server}; | ||
gardmd/src/main.rsadded@@ -0,0 +1,174 @@ | |||
| 1 | +//! gardmd - gar display manager daemon | ||
| 2 | +//! | ||
| 3 | +//! Main entry point with signal handling and systemd integration. | ||
| 4 | + | ||
| 5 | +use anyhow::Result; | ||
| 6 | +use gardmd::{config::Config, ipc}; | ||
| 7 | +use tokio::signal::unix::{signal, SignalKind}; | ||
| 8 | +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; | ||
| 9 | + | ||
| 10 | +#[tokio::main] | ||
| 11 | +async fn main() -> Result<()> { | ||
| 12 | + // Initialize logging | ||
| 13 | + tracing_subscriber::registry() | ||
| 14 | + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) | ||
| 15 | + .with(tracing_subscriber::fmt::layer()) | ||
| 16 | + .init(); | ||
| 17 | + | ||
| 18 | + tracing::info!("gardmd starting"); | ||
| 19 | + | ||
| 20 | + // Load configuration | ||
| 21 | + let config = Config::load()?; | ||
| 22 | + tracing::debug!(?config, "Loaded configuration"); | ||
| 23 | + | ||
| 24 | + // Create IPC server | ||
| 25 | + let server = ipc::Server::new().await?; | ||
| 26 | + | ||
| 27 | + // Set up signal handlers | ||
| 28 | + let mut sigterm = signal(SignalKind::terminate())?; | ||
| 29 | + let mut sigint = signal(SignalKind::interrupt())?; | ||
| 30 | + let mut sighup = signal(SignalKind::hangup())?; | ||
| 31 | + | ||
| 32 | + // Notify systemd we're ready | ||
| 33 | + if let Err(e) = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) { | ||
| 34 | + tracing::warn!("Failed to notify systemd: {}", e); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + tracing::info!("gardmd ready"); | ||
| 38 | + | ||
| 39 | + // Main event loop | ||
| 40 | + loop { | ||
| 41 | + tokio::select! { | ||
| 42 | + // Handle new greeter connections | ||
| 43 | + result = server.accept() => { | ||
| 44 | + match result { | ||
| 45 | + Ok(conn) => { | ||
| 46 | + tokio::spawn(handle_client(conn, config.clone())); | ||
| 47 | + } | ||
| 48 | + Err(e) => { | ||
| 49 | + tracing::error!("Failed to accept connection: {}", e); | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + // Handle SIGTERM | ||
| 55 | + _ = sigterm.recv() => { | ||
| 56 | + tracing::info!("Received SIGTERM, shutting down"); | ||
| 57 | + break; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + // Handle SIGINT | ||
| 61 | + _ = sigint.recv() => { | ||
| 62 | + tracing::info!("Received SIGINT, shutting down"); | ||
| 63 | + break; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + // Handle SIGHUP (reload config) | ||
| 67 | + _ = sighup.recv() => { | ||
| 68 | + tracing::info!("Received SIGHUP, reloading config"); | ||
| 69 | + // TODO: Implement config reload | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + // Notify systemd we're stopping | ||
| 75 | + let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Stopping]); | ||
| 76 | + | ||
| 77 | + tracing::info!("gardmd stopped"); | ||
| 78 | + Ok(()) | ||
| 79 | +} | ||
| 80 | + | ||
| 81 | +/// Handle a connected greeter client | ||
| 82 | +async fn handle_client(mut conn: ipc::ClientConnection, _config: Config) { | ||
| 83 | + loop { | ||
| 84 | + match conn.recv().await { | ||
| 85 | + Ok(Some(request)) => { | ||
| 86 | + let response = handle_request(request).await; | ||
| 87 | + if let Err(e) = conn.send(&response).await { | ||
| 88 | + tracing::error!("Failed to send response: {}", e); | ||
| 89 | + break; | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + Ok(None) => { | ||
| 93 | + tracing::debug!("Greeter disconnected"); | ||
| 94 | + break; | ||
| 95 | + } | ||
| 96 | + Err(e) => { | ||
| 97 | + tracing::error!("Error receiving request: {}", e); | ||
| 98 | + break; | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +/// Process a request and return a response | ||
| 105 | +async fn handle_request(request: gardm_ipc::Request) -> gardm_ipc::Response { | ||
| 106 | + use gardm_ipc::{Request, Response}; | ||
| 107 | + | ||
| 108 | + match request { | ||
| 109 | + Request::CreateSession { username } => { | ||
| 110 | + tracing::info!("Creating session for user: {}", username); | ||
| 111 | + // TODO: Implement PAM session creation | ||
| 112 | + Response::AuthPrompt { | ||
| 113 | + prompt: "Password:".to_string(), | ||
| 114 | + echo: false, | ||
| 115 | + } | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + Request::Authenticate { response: _ } => { | ||
| 119 | + // TODO: Implement PAM authentication | ||
| 120 | + Response::Error { | ||
| 121 | + message: "PAM not yet implemented".to_string(), | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + Request::StartSession { cmd, env } => { | ||
| 126 | + tracing::info!("Start session request: {:?} env={:?}", cmd, env); | ||
| 127 | + // TODO: Implement session start | ||
| 128 | + Response::Error { | ||
| 129 | + message: "Session start not yet implemented".to_string(), | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + Request::CancelSession => { | ||
| 134 | + tracing::info!("Session cancelled"); | ||
| 135 | + Response::Success | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + Request::Shutdown => { | ||
| 139 | + tracing::info!("Shutdown requested"); | ||
| 140 | + // TODO: Implement via logind | ||
| 141 | + Response::Error { | ||
| 142 | + message: "Shutdown not yet implemented".to_string(), | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + Request::Reboot => { | ||
| 147 | + tracing::info!("Reboot requested"); | ||
| 148 | + // TODO: Implement via logind | ||
| 149 | + Response::Error { | ||
| 150 | + message: "Reboot not yet implemented".to_string(), | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + Request::Suspend => { | ||
| 155 | + tracing::info!("Suspend requested"); | ||
| 156 | + // TODO: Implement via logind | ||
| 157 | + Response::Error { | ||
| 158 | + message: "Suspend not yet implemented".to_string(), | ||
| 159 | + } | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + Request::ListSessions => { | ||
| 163 | + tracing::debug!("Listing sessions"); | ||
| 164 | + // TODO: Enumerate /usr/share/xsessions and /usr/share/wayland-sessions | ||
| 165 | + Response::Sessions { sessions: vec![] } | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + Request::ListUsers => { | ||
| 169 | + tracing::debug!("Listing users"); | ||
| 170 | + // TODO: Enumerate users from /etc/passwd | ||
| 171 | + Response::Users { users: vec![] } | ||
| 172 | + } | ||
| 173 | + } | ||
| 174 | +} | ||