gardesk/gardm / 3b0e644

Browse files

greeter Cairo/Pango UI with login form

- X11 window with fullscreen override redirect
- Cairo rendering surface for drawing
- Background loader with blur and brightness adjustment
- Login form widget with username/password fields
- Keyboard input handling with X11 keycodes
- IPC integration with gardmd for authentication
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3b0e644b984b0a71ae545887858db8c8d7e25083
Parents
5bb512e
Tree
4d7f5ef

9 changed files

StatusFile+-
M Cargo.lock 619 16
M gardm-greeter/Cargo.toml 7 0
A gardm-greeter/src/background.rs 109 0
A gardm-greeter/src/keyboard.rs 121 0
M gardm-greeter/src/main.rs 237 11
A gardm-greeter/src/render.rs 78 0
A gardm-greeter/src/widgets/login_form.rs 319 0
A gardm-greeter/src/widgets/mod.rs 5 0
A gardm-greeter/src/window.rs 176 0
Cargo.lockmodified
@@ -2,6 +2,12 @@
22
 # It is not intended for manual editing.
33
 version = 4
44
 
5
+[[package]]
6
+name = "adler2"
7
+version = "2.0.1"
8
+source = "registry+https://github.com/rust-lang/crates.io-index"
9
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
+
511
 [[package]]
612
 name = "aho-corasick"
713
 version = "1.1.4"
@@ -73,6 +79,12 @@ version = "1.0.1"
7379
 source = "registry+https://github.com/rust-lang/crates.io-index"
7480
 checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
7581
 
82
+[[package]]
83
+name = "autocfg"
84
+version = "1.5.0"
85
+source = "registry+https://github.com/rust-lang/crates.io-index"
86
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
87
+
7688
 [[package]]
7789
 name = "bindgen"
7890
 version = "0.69.5"
@@ -93,6 +105,12 @@ dependencies = [
93105
  "syn 2.0.114",
94106
 ]
95107
 
108
+[[package]]
109
+name = "bit_field"
110
+version = "0.10.3"
111
+source = "registry+https://github.com/rust-lang/crates.io-index"
112
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
113
+
96114
 [[package]]
97115
 name = "bitflags"
98116
 version = "1.3.2"
@@ -105,12 +123,49 @@ version = "2.10.0"
105123
 source = "registry+https://github.com/rust-lang/crates.io-index"
106124
 checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
107125
 
126
+[[package]]
127
+name = "bytemuck"
128
+version = "1.24.0"
129
+source = "registry+https://github.com/rust-lang/crates.io-index"
130
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
131
+
132
+[[package]]
133
+name = "byteorder"
134
+version = "1.5.0"
135
+source = "registry+https://github.com/rust-lang/crates.io-index"
136
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
137
+
108138
 [[package]]
109139
 name = "bytes"
110140
 version = "1.11.0"
111141
 source = "registry+https://github.com/rust-lang/crates.io-index"
112142
 checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
113143
 
144
+[[package]]
145
+name = "cairo-rs"
146
+version = "0.18.5"
147
+source = "registry+https://github.com/rust-lang/crates.io-index"
148
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
149
+dependencies = [
150
+ "bitflags 2.10.0",
151
+ "cairo-sys-rs",
152
+ "glib",
153
+ "libc",
154
+ "once_cell",
155
+ "thiserror",
156
+]
157
+
158
+[[package]]
159
+name = "cairo-sys-rs"
160
+version = "0.18.2"
161
+source = "registry+https://github.com/rust-lang/crates.io-index"
162
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
163
+dependencies = [
164
+ "glib-sys",
165
+ "libc",
166
+ "system-deps",
167
+]
168
+
114169
 [[package]]
115170
 name = "cexpr"
116171
 version = "0.6.0"
@@ -120,6 +175,16 @@ dependencies = [
120175
  "nom",
121176
 ]
122177
 
178
+[[package]]
179
+name = "cfg-expr"
180
+version = "0.15.8"
181
+source = "registry+https://github.com/rust-lang/crates.io-index"
182
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
183
+dependencies = [
184
+ "smallvec",
185
+ "target-lexicon",
186
+]
187
+
123188
 [[package]]
124189
 name = "cfg-if"
125190
 version = "1.0.4"
@@ -164,7 +229,7 @@ version = "4.5.49"
164229
 source = "registry+https://github.com/rust-lang/crates.io-index"
165230
 checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
166231
 dependencies = [
167
- "heck",
232
+ "heck 0.5.0",
168233
  "proc-macro2",
169234
  "quote",
170235
  "syn 2.0.114",
@@ -176,12 +241,58 @@ version = "0.7.7"
176241
 source = "registry+https://github.com/rust-lang/crates.io-index"
177242
 checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
178243
 
244
+[[package]]
245
+name = "color_quant"
246
+version = "1.1.0"
247
+source = "registry+https://github.com/rust-lang/crates.io-index"
248
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
249
+
179250
 [[package]]
180251
 name = "colorchoice"
181252
 version = "1.0.4"
182253
 source = "registry+https://github.com/rust-lang/crates.io-index"
183254
 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
184255
 
256
+[[package]]
257
+name = "crc32fast"
258
+version = "1.5.0"
259
+source = "registry+https://github.com/rust-lang/crates.io-index"
260
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
261
+dependencies = [
262
+ "cfg-if",
263
+]
264
+
265
+[[package]]
266
+name = "crossbeam-deque"
267
+version = "0.8.6"
268
+source = "registry+https://github.com/rust-lang/crates.io-index"
269
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
270
+dependencies = [
271
+ "crossbeam-epoch",
272
+ "crossbeam-utils",
273
+]
274
+
275
+[[package]]
276
+name = "crossbeam-epoch"
277
+version = "0.9.18"
278
+source = "registry+https://github.com/rust-lang/crates.io-index"
279
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
280
+dependencies = [
281
+ "crossbeam-utils",
282
+]
283
+
284
+[[package]]
285
+name = "crossbeam-utils"
286
+version = "0.8.21"
287
+source = "registry+https://github.com/rust-lang/crates.io-index"
288
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
289
+
290
+[[package]]
291
+name = "crunchy"
292
+version = "0.2.4"
293
+source = "registry+https://github.com/rust-lang/crates.io-index"
294
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
295
+
185296
 [[package]]
186297
 name = "either"
187298
 version = "1.15.0"
@@ -215,18 +326,120 @@ dependencies = [
215326
  "windows-sys 0.61.2",
216327
 ]
217328
 
329
+[[package]]
330
+name = "exr"
331
+version = "1.74.0"
332
+source = "registry+https://github.com/rust-lang/crates.io-index"
333
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
334
+dependencies = [
335
+ "bit_field",
336
+ "half",
337
+ "lebe",
338
+ "miniz_oxide",
339
+ "rayon-core",
340
+ "smallvec",
341
+ "zune-inflate",
342
+]
343
+
344
+[[package]]
345
+name = "fdeflate"
346
+version = "0.3.7"
347
+source = "registry+https://github.com/rust-lang/crates.io-index"
348
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
349
+dependencies = [
350
+ "simd-adler32",
351
+]
352
+
353
+[[package]]
354
+name = "flate2"
355
+version = "1.1.8"
356
+source = "registry+https://github.com/rust-lang/crates.io-index"
357
+checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
358
+dependencies = [
359
+ "crc32fast",
360
+ "miniz_oxide",
361
+]
362
+
363
+[[package]]
364
+name = "futures-channel"
365
+version = "0.3.31"
366
+source = "registry+https://github.com/rust-lang/crates.io-index"
367
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
368
+dependencies = [
369
+ "futures-core",
370
+]
371
+
372
+[[package]]
373
+name = "futures-core"
374
+version = "0.3.31"
375
+source = "registry+https://github.com/rust-lang/crates.io-index"
376
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
377
+
378
+[[package]]
379
+name = "futures-executor"
380
+version = "0.3.31"
381
+source = "registry+https://github.com/rust-lang/crates.io-index"
382
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
383
+dependencies = [
384
+ "futures-core",
385
+ "futures-task",
386
+ "futures-util",
387
+]
388
+
389
+[[package]]
390
+name = "futures-io"
391
+version = "0.3.31"
392
+source = "registry+https://github.com/rust-lang/crates.io-index"
393
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
394
+
395
+[[package]]
396
+name = "futures-macro"
397
+version = "0.3.31"
398
+source = "registry+https://github.com/rust-lang/crates.io-index"
399
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
400
+dependencies = [
401
+ "proc-macro2",
402
+ "quote",
403
+ "syn 2.0.114",
404
+]
405
+
406
+[[package]]
407
+name = "futures-task"
408
+version = "0.3.31"
409
+source = "registry+https://github.com/rust-lang/crates.io-index"
410
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
411
+
412
+[[package]]
413
+name = "futures-util"
414
+version = "0.3.31"
415
+source = "registry+https://github.com/rust-lang/crates.io-index"
416
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
417
+dependencies = [
418
+ "futures-core",
419
+ "futures-macro",
420
+ "futures-task",
421
+ "pin-project-lite",
422
+ "pin-utils",
423
+ "slab",
424
+]
425
+
218426
 [[package]]
219427
 name = "gardm-greeter"
220428
 version = "0.1.0"
221429
 dependencies = [
222430
  "anyhow",
431
+ "cairo-rs",
223432
  "gardm-ipc",
433
+ "image",
434
+ "pango",
435
+ "pangocairo",
224436
  "rpassword 7.4.0",
225437
  "serde_json",
226438
  "thiserror",
227439
  "tokio",
228440
  "tracing",
229441
  "tracing-subscriber",
442
+ "x11rb",
230443
 ]
231444
 
232445
 [[package]]
@@ -270,24 +483,159 @@ dependencies = [
270483
  "windows-link",
271484
 ]
272485
 
486
+[[package]]
487
+name = "gif"
488
+version = "0.13.3"
489
+source = "registry+https://github.com/rust-lang/crates.io-index"
490
+checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
491
+dependencies = [
492
+ "color_quant",
493
+ "weezl",
494
+]
495
+
496
+[[package]]
497
+name = "gio"
498
+version = "0.18.4"
499
+source = "registry+https://github.com/rust-lang/crates.io-index"
500
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
501
+dependencies = [
502
+ "futures-channel",
503
+ "futures-core",
504
+ "futures-io",
505
+ "futures-util",
506
+ "gio-sys",
507
+ "glib",
508
+ "libc",
509
+ "once_cell",
510
+ "pin-project-lite",
511
+ "smallvec",
512
+ "thiserror",
513
+]
514
+
515
+[[package]]
516
+name = "gio-sys"
517
+version = "0.18.1"
518
+source = "registry+https://github.com/rust-lang/crates.io-index"
519
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
520
+dependencies = [
521
+ "glib-sys",
522
+ "gobject-sys",
523
+ "libc",
524
+ "system-deps",
525
+ "winapi",
526
+]
527
+
528
+[[package]]
529
+name = "glib"
530
+version = "0.18.5"
531
+source = "registry+https://github.com/rust-lang/crates.io-index"
532
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
533
+dependencies = [
534
+ "bitflags 2.10.0",
535
+ "futures-channel",
536
+ "futures-core",
537
+ "futures-executor",
538
+ "futures-task",
539
+ "futures-util",
540
+ "gio-sys",
541
+ "glib-macros",
542
+ "glib-sys",
543
+ "gobject-sys",
544
+ "libc",
545
+ "memchr",
546
+ "once_cell",
547
+ "smallvec",
548
+ "thiserror",
549
+]
550
+
551
+[[package]]
552
+name = "glib-macros"
553
+version = "0.18.5"
554
+source = "registry+https://github.com/rust-lang/crates.io-index"
555
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
556
+dependencies = [
557
+ "heck 0.4.1",
558
+ "proc-macro-crate",
559
+ "proc-macro-error",
560
+ "proc-macro2",
561
+ "quote",
562
+ "syn 2.0.114",
563
+]
564
+
565
+[[package]]
566
+name = "glib-sys"
567
+version = "0.18.1"
568
+source = "registry+https://github.com/rust-lang/crates.io-index"
569
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
570
+dependencies = [
571
+ "libc",
572
+ "system-deps",
573
+]
574
+
273575
 [[package]]
274576
 name = "glob"
275577
 version = "0.3.3"
276578
 source = "registry+https://github.com/rust-lang/crates.io-index"
277579
 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
278580
 
581
+[[package]]
582
+name = "gobject-sys"
583
+version = "0.18.0"
584
+source = "registry+https://github.com/rust-lang/crates.io-index"
585
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
586
+dependencies = [
587
+ "glib-sys",
588
+ "libc",
589
+ "system-deps",
590
+]
591
+
592
+[[package]]
593
+name = "half"
594
+version = "2.7.1"
595
+source = "registry+https://github.com/rust-lang/crates.io-index"
596
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
597
+dependencies = [
598
+ "cfg-if",
599
+ "crunchy",
600
+ "zerocopy",
601
+]
602
+
279603
 [[package]]
280604
 name = "hashbrown"
281605
 version = "0.16.1"
282606
 source = "registry+https://github.com/rust-lang/crates.io-index"
283607
 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
284608
 
609
+[[package]]
610
+name = "heck"
611
+version = "0.4.1"
612
+source = "registry+https://github.com/rust-lang/crates.io-index"
613
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
614
+
285615
 [[package]]
286616
 name = "heck"
287617
 version = "0.5.0"
288618
 source = "registry+https://github.com/rust-lang/crates.io-index"
289619
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
290620
 
621
+[[package]]
622
+name = "image"
623
+version = "0.24.9"
624
+source = "registry+https://github.com/rust-lang/crates.io-index"
625
+checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
626
+dependencies = [
627
+ "bytemuck",
628
+ "byteorder",
629
+ "color_quant",
630
+ "exr",
631
+ "gif",
632
+ "jpeg-decoder",
633
+ "num-traits",
634
+ "png",
635
+ "qoi",
636
+ "tiff",
637
+]
638
+
291639
 [[package]]
292640
 name = "indexmap"
293641
 version = "2.13.0"
@@ -319,6 +667,15 @@ version = "1.0.17"
319667
 source = "registry+https://github.com/rust-lang/crates.io-index"
320668
 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
321669
 
670
+[[package]]
671
+name = "jpeg-decoder"
672
+version = "0.3.2"
673
+source = "registry+https://github.com/rust-lang/crates.io-index"
674
+checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
675
+dependencies = [
676
+ "rayon",
677
+]
678
+
322679
 [[package]]
323680
 name = "lazy_static"
324681
 version = "1.5.0"
@@ -331,6 +688,12 @@ version = "1.3.0"
331688
 source = "registry+https://github.com/rust-lang/crates.io-index"
332689
 checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
333690
 
691
+[[package]]
692
+name = "lebe"
693
+version = "0.5.3"
694
+source = "registry+https://github.com/rust-lang/crates.io-index"
695
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
696
+
334697
 [[package]]
335698
 name = "libc"
336699
 version = "0.2.180"
@@ -379,6 +742,16 @@ version = "0.2.1"
379742
 source = "registry+https://github.com/rust-lang/crates.io-index"
380743
 checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
381744
 
745
+[[package]]
746
+name = "miniz_oxide"
747
+version = "0.8.9"
748
+source = "registry+https://github.com/rust-lang/crates.io-index"
749
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
750
+dependencies = [
751
+ "adler2",
752
+ "simd-adler32",
753
+]
754
+
382755
 [[package]]
383756
 name = "mio"
384757
 version = "1.1.1"
@@ -420,6 +793,15 @@ dependencies = [
420793
  "windows-sys 0.61.2",
421794
 ]
422795
 
796
+[[package]]
797
+name = "num-traits"
798
+version = "0.2.19"
799
+source = "registry+https://github.com/rust-lang/crates.io-index"
800
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
801
+dependencies = [
802
+ "autocfg",
803
+]
804
+
423805
 [[package]]
424806
 name = "once_cell"
425807
 version = "1.21.3"
@@ -456,6 +838,57 @@ dependencies = [
456838
  "libc",
457839
 ]
458840
 
841
+[[package]]
842
+name = "pango"
843
+version = "0.18.3"
844
+source = "registry+https://github.com/rust-lang/crates.io-index"
845
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
846
+dependencies = [
847
+ "gio",
848
+ "glib",
849
+ "libc",
850
+ "once_cell",
851
+ "pango-sys",
852
+]
853
+
854
+[[package]]
855
+name = "pango-sys"
856
+version = "0.18.0"
857
+source = "registry+https://github.com/rust-lang/crates.io-index"
858
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
859
+dependencies = [
860
+ "glib-sys",
861
+ "gobject-sys",
862
+ "libc",
863
+ "system-deps",
864
+]
865
+
866
+[[package]]
867
+name = "pangocairo"
868
+version = "0.18.0"
869
+source = "registry+https://github.com/rust-lang/crates.io-index"
870
+checksum = "57036589a9cfcacf83f9e606d15813fc6bf03f0e9e69aa2b5e3bb85af86b38a5"
871
+dependencies = [
872
+ "cairo-rs",
873
+ "glib",
874
+ "libc",
875
+ "pango",
876
+ "pangocairo-sys",
877
+]
878
+
879
+[[package]]
880
+name = "pangocairo-sys"
881
+version = "0.18.0"
882
+source = "registry+https://github.com/rust-lang/crates.io-index"
883
+checksum = "fc3c8ff676a37e7a72ec1d5fc029f91c407278083d2752784ff9f5188c108833"
884
+dependencies = [
885
+ "cairo-sys-rs",
886
+ "glib-sys",
887
+ "libc",
888
+ "pango-sys",
889
+ "system-deps",
890
+]
891
+
459892
 [[package]]
460893
 name = "parking_lot"
461894
 version = "0.12.5"
@@ -485,6 +918,65 @@ version = "0.2.16"
485918
 source = "registry+https://github.com/rust-lang/crates.io-index"
486919
 checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
487920
 
921
+[[package]]
922
+name = "pin-utils"
923
+version = "0.1.0"
924
+source = "registry+https://github.com/rust-lang/crates.io-index"
925
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
926
+
927
+[[package]]
928
+name = "pkg-config"
929
+version = "0.3.32"
930
+source = "registry+https://github.com/rust-lang/crates.io-index"
931
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
932
+
933
+[[package]]
934
+name = "png"
935
+version = "0.17.16"
936
+source = "registry+https://github.com/rust-lang/crates.io-index"
937
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
938
+dependencies = [
939
+ "bitflags 1.3.2",
940
+ "crc32fast",
941
+ "fdeflate",
942
+ "flate2",
943
+ "miniz_oxide",
944
+]
945
+
946
+[[package]]
947
+name = "proc-macro-crate"
948
+version = "2.0.2"
949
+source = "registry+https://github.com/rust-lang/crates.io-index"
950
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
951
+dependencies = [
952
+ "toml_datetime",
953
+ "toml_edit",
954
+]
955
+
956
+[[package]]
957
+name = "proc-macro-error"
958
+version = "1.0.4"
959
+source = "registry+https://github.com/rust-lang/crates.io-index"
960
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
961
+dependencies = [
962
+ "proc-macro-error-attr",
963
+ "proc-macro2",
964
+ "quote",
965
+ "syn 1.0.109",
966
+ "version_check",
967
+]
968
+
969
+[[package]]
970
+name = "proc-macro-error-attr"
971
+version = "1.0.4"
972
+source = "registry+https://github.com/rust-lang/crates.io-index"
973
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
974
+dependencies = [
975
+ "proc-macro2",
976
+ "quote",
977
+ "version_check",
978
+]
979
+
488980
 [[package]]
489981
 name = "proc-macro2"
490982
 version = "1.0.105"
@@ -494,6 +986,15 @@ dependencies = [
494986
  "unicode-ident",
495987
 ]
496988
 
989
+[[package]]
990
+name = "qoi"
991
+version = "0.4.1"
992
+source = "registry+https://github.com/rust-lang/crates.io-index"
993
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
994
+dependencies = [
995
+ "bytemuck",
996
+]
997
+
497998
 [[package]]
498999
 name = "quote"
4991000
 version = "1.0.43"
@@ -503,6 +1004,26 @@ dependencies = [
5031004
  "proc-macro2",
5041005
 ]
5051006
 
1007
+[[package]]
1008
+name = "rayon"
1009
+version = "1.11.0"
1010
+source = "registry+https://github.com/rust-lang/crates.io-index"
1011
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
1012
+dependencies = [
1013
+ "either",
1014
+ "rayon-core",
1015
+]
1016
+
1017
+[[package]]
1018
+name = "rayon-core"
1019
+version = "1.13.0"
1020
+source = "registry+https://github.com/rust-lang/crates.io-index"
1021
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
1022
+dependencies = [
1023
+ "crossbeam-deque",
1024
+ "crossbeam-utils",
1025
+]
1026
+
5061027
 [[package]]
5071028
 name = "redox_syscall"
5081029
 version = "0.5.18"
@@ -691,6 +1212,18 @@ dependencies = [
6911212
  "libc",
6921213
 ]
6931214
 
1215
+[[package]]
1216
+name = "simd-adler32"
1217
+version = "0.3.8"
1218
+source = "registry+https://github.com/rust-lang/crates.io-index"
1219
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
1220
+
1221
+[[package]]
1222
+name = "slab"
1223
+version = "0.4.11"
1224
+source = "registry+https://github.com/rust-lang/crates.io-index"
1225
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
1226
+
6941227
 [[package]]
6951228
 name = "smallvec"
6961229
 version = "1.15.1"
@@ -735,6 +1268,25 @@ dependencies = [
7351268
  "unicode-ident",
7361269
 ]
7371270
 
1271
+[[package]]
1272
+name = "system-deps"
1273
+version = "6.2.2"
1274
+source = "registry+https://github.com/rust-lang/crates.io-index"
1275
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
1276
+dependencies = [
1277
+ "cfg-expr",
1278
+ "heck 0.5.0",
1279
+ "pkg-config",
1280
+ "toml",
1281
+ "version-compare",
1282
+]
1283
+
1284
+[[package]]
1285
+name = "target-lexicon"
1286
+version = "0.12.16"
1287
+source = "registry+https://github.com/rust-lang/crates.io-index"
1288
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
1289
+
7381290
 [[package]]
7391291
 name = "thiserror"
7401292
 version = "1.0.69"
@@ -764,6 +1316,17 @@ dependencies = [
7641316
  "cfg-if",
7651317
 ]
7661318
 
1319
+[[package]]
1320
+name = "tiff"
1321
+version = "0.9.1"
1322
+source = "registry+https://github.com/rust-lang/crates.io-index"
1323
+checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
1324
+dependencies = [
1325
+ "flate2",
1326
+ "jpeg-decoder",
1327
+ "weezl",
1328
+]
1329
+
7671330
 [[package]]
7681331
 name = "tokio"
7691332
 version = "1.49.0"
@@ -794,9 +1357,9 @@ dependencies = [
7941357
 
7951358
 [[package]]
7961359
 name = "toml"
797
-version = "0.8.23"
1360
+version = "0.8.2"
7981361
 source = "registry+https://github.com/rust-lang/crates.io-index"
799
-checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
1362
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
8001363
 dependencies = [
8011364
  "serde",
8021365
  "serde_spanned",
@@ -806,33 +1369,26 @@ dependencies = [
8061369
 
8071370
 [[package]]
8081371
 name = "toml_datetime"
809
-version = "0.6.11"
1372
+version = "0.6.3"
8101373
 source = "registry+https://github.com/rust-lang/crates.io-index"
811
-checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
1374
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
8121375
 dependencies = [
8131376
  "serde",
8141377
 ]
8151378
 
8161379
 [[package]]
8171380
 name = "toml_edit"
818
-version = "0.22.27"
1381
+version = "0.20.2"
8191382
 source = "registry+https://github.com/rust-lang/crates.io-index"
820
-checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
1383
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
8211384
 dependencies = [
8221385
  "indexmap",
8231386
  "serde",
8241387
  "serde_spanned",
8251388
  "toml_datetime",
826
- "toml_write",
8271389
  "winnow",
8281390
 ]
8291391
 
830
-[[package]]
831
-name = "toml_write"
832
-version = "0.1.2"
833
-source = "registry+https://github.com/rust-lang/crates.io-index"
834
-checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
835
-
8361392
 [[package]]
8371393
 name = "tracing"
8381394
 version = "0.1.44"
@@ -912,12 +1468,30 @@ version = "0.1.1"
9121468
 source = "registry+https://github.com/rust-lang/crates.io-index"
9131469
 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
9141470
 
1471
+[[package]]
1472
+name = "version-compare"
1473
+version = "0.2.1"
1474
+source = "registry+https://github.com/rust-lang/crates.io-index"
1475
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
1476
+
1477
+[[package]]
1478
+name = "version_check"
1479
+version = "0.9.5"
1480
+source = "registry+https://github.com/rust-lang/crates.io-index"
1481
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1482
+
9151483
 [[package]]
9161484
 name = "wasi"
9171485
 version = "0.11.1+wasi-snapshot-preview1"
9181486
 source = "registry+https://github.com/rust-lang/crates.io-index"
9191487
 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
9201488
 
1489
+[[package]]
1490
+name = "weezl"
1491
+version = "0.1.12"
1492
+source = "registry+https://github.com/rust-lang/crates.io-index"
1493
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
1494
+
9211495
 [[package]]
9221496
 name = "winapi"
9231497
 version = "0.3.9"
@@ -1113,9 +1687,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
11131687
 
11141688
 [[package]]
11151689
 name = "winnow"
1116
-version = "0.7.14"
1690
+version = "0.5.40"
11171691
 source = "registry+https://github.com/rust-lang/crates.io-index"
1118
-checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
1692
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
11191693
 dependencies = [
11201694
  "memchr",
11211695
 ]
@@ -1139,8 +1713,37 @@ version = "0.13.2"
11391713
 source = "registry+https://github.com/rust-lang/crates.io-index"
11401714
 checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
11411715
 
1716
+[[package]]
1717
+name = "zerocopy"
1718
+version = "0.8.33"
1719
+source = "registry+https://github.com/rust-lang/crates.io-index"
1720
+checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
1721
+dependencies = [
1722
+ "zerocopy-derive",
1723
+]
1724
+
1725
+[[package]]
1726
+name = "zerocopy-derive"
1727
+version = "0.8.33"
1728
+source = "registry+https://github.com/rust-lang/crates.io-index"
1729
+checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
1730
+dependencies = [
1731
+ "proc-macro2",
1732
+ "quote",
1733
+ "syn 2.0.114",
1734
+]
1735
+
11421736
 [[package]]
11431737
 name = "zmij"
11441738
 version = "1.0.14"
11451739
 source = "registry+https://github.com/rust-lang/crates.io-index"
11461740
 checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
1741
+
1742
+[[package]]
1743
+name = "zune-inflate"
1744
+version = "0.2.54"
1745
+source = "registry+https://github.com/rust-lang/crates.io-index"
1746
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
1747
+dependencies = [
1748
+ "simd-adler32",
1749
+]
gardm-greeter/Cargo.tomlmodified
@@ -22,3 +22,10 @@ anyhow = { workspace = true }
2222
 thiserror = { workspace = true }
2323
 rpassword = { workspace = true }
2424
 serde_json = { workspace = true }
25
+
26
+# X11 and graphics
27
+x11rb = { workspace = true }
28
+cairo-rs = { workspace = true }
29
+pango = { workspace = true }
30
+pangocairo = { workspace = true }
31
+image = { workspace = true }
gardm-greeter/src/background.rsadded
@@ -0,0 +1,109 @@
1
+//! Background image loading and processing for the greeter
2
+//!
3
+//! Loads background images, scales to fit screen, applies blur and brightness adjustments.
4
+
5
+use anyhow::{Context, Result};
6
+use image::{imageops, RgbaImage};
7
+
8
+/// Load and process a background image for the greeter
9
+pub fn load_blurred_background(
10
+    path: &str,
11
+    width: u32,
12
+    height: u32,
13
+    blur_radius: f32,
14
+    brightness: f32,
15
+) -> Result<RgbaImage> {
16
+    let img = image::open(path)
17
+        .with_context(|| format!("Failed to open background image: {}", path))?
18
+        .to_rgba8();
19
+
20
+    // Scale to screen size (cover mode - fills screen, may crop)
21
+    let scaled = scale_to_cover(&img, width, height);
22
+
23
+    // Apply gaussian blur
24
+    let blurred = imageops::blur(&scaled, blur_radius);
25
+
26
+    // Adjust brightness (typically darken for better text contrast)
27
+    let adjusted = adjust_brightness(&blurred, brightness);
28
+
29
+    Ok(adjusted)
30
+}
31
+
32
+/// Generate a solid color fallback background
33
+pub fn solid_background(width: u32, height: u32, r: u8, g: u8, b: u8) -> RgbaImage {
34
+    RgbaImage::from_fn(width, height, |_, _| image::Rgba([r, g, b, 255]))
35
+}
36
+
37
+/// Scale image to cover the target dimensions (may crop)
38
+fn scale_to_cover(img: &RgbaImage, target_w: u32, target_h: u32) -> RgbaImage {
39
+    let (src_w, src_h) = img.dimensions();
40
+
41
+    // Calculate scale to cover entire target
42
+    let scale = (target_w as f32 / src_w as f32).max(target_h as f32 / src_h as f32);
43
+
44
+    let new_w = (src_w as f32 * scale).ceil() as u32;
45
+    let new_h = (src_h as f32 * scale).ceil() as u32;
46
+
47
+    let resized = imageops::resize(img, new_w, new_h, imageops::FilterType::Lanczos3);
48
+
49
+    // Crop to center
50
+    let x = (new_w.saturating_sub(target_w)) / 2;
51
+    let y = (new_h.saturating_sub(target_h)) / 2;
52
+
53
+    imageops::crop_imm(&resized, x, y, target_w, target_h).to_image()
54
+}
55
+
56
+/// Adjust image brightness by a factor (0.0-1.0 darkens, >1.0 brightens)
57
+fn adjust_brightness(img: &RgbaImage, factor: f32) -> RgbaImage {
58
+    let mut result = img.clone();
59
+    for pixel in result.pixels_mut() {
60
+        pixel[0] = (pixel[0] as f32 * factor).min(255.0) as u8;
61
+        pixel[1] = (pixel[1] as f32 * factor).min(255.0) as u8;
62
+        pixel[2] = (pixel[2] as f32 * factor).min(255.0) as u8;
63
+        // Alpha unchanged
64
+    }
65
+    result
66
+}
67
+
68
+/// Convert RGBA image to BGRA for Cairo/X11 (little-endian ARGB32)
69
+pub fn rgba_to_bgra(img: &RgbaImage) -> Vec<u8> {
70
+    let (width, height) = img.dimensions();
71
+    let stride = (width * 4) as usize;
72
+    let mut data = vec![0u8; stride * height as usize];
73
+
74
+    for (y, row) in img.rows().enumerate() {
75
+        for (x, pixel) in row.enumerate() {
76
+            let offset = y * stride + x * 4;
77
+            // RGBA -> BGRA
78
+            data[offset] = pixel[2]; // B
79
+            data[offset + 1] = pixel[1]; // G
80
+            data[offset + 2] = pixel[0]; // R
81
+            data[offset + 3] = pixel[3]; // A
82
+        }
83
+    }
84
+
85
+    data
86
+}
87
+
88
+/// Render background image to Cairo context
89
+pub fn render_to_cairo(
90
+    ctx: &cairo::Context,
91
+    img: &RgbaImage,
92
+) -> Result<()> {
93
+    let (width, height) = img.dimensions();
94
+    let bgra_data = rgba_to_bgra(img);
95
+
96
+    let surface = cairo::ImageSurface::create_for_data(
97
+        bgra_data,
98
+        cairo::Format::ARgb32,
99
+        width as i32,
100
+        height as i32,
101
+        (width * 4) as i32,
102
+    )
103
+    .context("Failed to create Cairo surface from background")?;
104
+
105
+    ctx.set_source_surface(&surface, 0.0, 0.0)?;
106
+    ctx.paint()?;
107
+
108
+    Ok(())
109
+}
gardm-greeter/src/keyboard.rsadded
@@ -0,0 +1,121 @@
1
+//! Keyboard input handling for the greeter
2
+//!
3
+//! Converts X11 keycodes to characters with basic shift support.
4
+
5
+use x11rb::protocol::xproto::KeyButMask;
6
+
7
+/// Common X11 keycodes (evdev-based, typical for modern Linux)
8
+pub mod keycodes {
9
+    pub const ESCAPE: u8 = 9;
10
+    pub const BACKSPACE: u8 = 22;
11
+    pub const TAB: u8 = 23;
12
+    pub const RETURN: u8 = 36;
13
+    pub const SHIFT_L: u8 = 50;
14
+    pub const SHIFT_R: u8 = 62;
15
+    pub const CAPS_LOCK: u8 = 66;
16
+}
17
+
18
+/// Convert a keycode to a character, considering shift state
19
+pub fn keycode_to_char(keycode: u8, state: KeyButMask) -> Option<char> {
20
+    let shifted = state.contains(KeyButMask::SHIFT);
21
+
22
+    // Main alphanumeric keys (evdev keycodes)
23
+    let base = match keycode {
24
+        // Number row
25
+        10 => '1',
26
+        11 => '2',
27
+        12 => '3',
28
+        13 => '4',
29
+        14 => '5',
30
+        15 => '6',
31
+        16 => '7',
32
+        17 => '8',
33
+        18 => '9',
34
+        19 => '0',
35
+        20 => '-',
36
+        21 => '=',
37
+
38
+        // Top row (QWERTY)
39
+        24 => 'q',
40
+        25 => 'w',
41
+        26 => 'e',
42
+        27 => 'r',
43
+        28 => 't',
44
+        29 => 'y',
45
+        30 => 'u',
46
+        31 => 'i',
47
+        32 => 'o',
48
+        33 => 'p',
49
+        34 => '[',
50
+        35 => ']',
51
+
52
+        // Home row (ASDF)
53
+        38 => 'a',
54
+        39 => 's',
55
+        40 => 'd',
56
+        41 => 'f',
57
+        42 => 'g',
58
+        43 => 'h',
59
+        44 => 'j',
60
+        45 => 'k',
61
+        46 => 'l',
62
+        47 => ';',
63
+        48 => '\'',
64
+        51 => '\\',
65
+
66
+        // Bottom row (ZXCV)
67
+        52 => 'z',
68
+        53 => 'x',
69
+        54 => 'c',
70
+        55 => 'v',
71
+        56 => 'b',
72
+        57 => 'n',
73
+        58 => 'm',
74
+        59 => ',',
75
+        60 => '.',
76
+        61 => '/',
77
+
78
+        // Space
79
+        65 => ' ',
80
+
81
+        // Grave/tilde
82
+        49 => '`',
83
+
84
+        _ => return None,
85
+    };
86
+
87
+    // Apply shift transformations
88
+    let c = if shifted {
89
+        match base {
90
+            // Numbers to symbols
91
+            '1' => '!',
92
+            '2' => '@',
93
+            '3' => '#',
94
+            '4' => '$',
95
+            '5' => '%',
96
+            '6' => '^',
97
+            '7' => '&',
98
+            '8' => '*',
99
+            '9' => '(',
100
+            '0' => ')',
101
+            '-' => '_',
102
+            '=' => '+',
103
+            '[' => '{',
104
+            ']' => '}',
105
+            ';' => ':',
106
+            '\'' => '"',
107
+            '\\' => '|',
108
+            ',' => '<',
109
+            '.' => '>',
110
+            '/' => '?',
111
+            '`' => '~',
112
+            // Letters to uppercase
113
+            c if c.is_ascii_lowercase() => c.to_ascii_uppercase(),
114
+            c => c,
115
+        }
116
+    } else {
117
+        base
118
+    };
119
+
120
+    Some(c)
121
+}
gardm-greeter/src/main.rsmodified
@@ -2,9 +2,31 @@
22
 //!
33
 //! Graphical login UI that communicates with gardmd.
44
 
5
-use anyhow::Result;
6
-use gardm_ipc::{Client, Request};
5
+mod background;
6
+mod keyboard;
7
+mod render;
8
+mod widgets;
9
+mod window;
10
+
11
+use anyhow::{Context, Result};
12
+use gardm_ipc::{Client, Request, Response};
13
+use std::time::{Duration, Instant};
714
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
15
+use x11rb::connection::Connection;
16
+use x11rb::protocol::xproto::ConnectionExt;
17
+use x11rb::protocol::Event;
18
+
19
+use background::{load_blurred_background, render_to_cairo, solid_background};
20
+use keyboard::{keycode_to_char, keycodes};
21
+use render::Renderer;
22
+use widgets::{FocusedField, LoginForm};
23
+use window::GreeterWindow;
24
+
25
+/// Default background image path
26
+const DEFAULT_BACKGROUND: &str = "/usr/share/gardm/backgrounds/default.jpg";
27
+
28
+/// Cursor blink interval
29
+const CURSOR_BLINK_MS: u64 = 500;
830
 
931
 #[tokio::main]
1032
 async fn main() -> Result<()> {
@@ -16,20 +38,224 @@ async fn main() -> Result<()> {
1638
 
1739
     tracing::info!("gardm-greeter starting");
1840
 
41
+    // Create X11 window
42
+    let window = GreeterWindow::new().context("Failed to create window")?;
43
+    let width = window.width();
44
+    let height = window.height();
45
+    tracing::info!(width, height, "Window created");
46
+
47
+    // Create renderer
48
+    let mut renderer = Renderer::new(width, height).context("Failed to create renderer")?;
49
+
50
+    // Load background image (with fallback to solid color)
51
+    let background = match load_blurred_background(
52
+        DEFAULT_BACKGROUND,
53
+        width as u32,
54
+        height as u32,
55
+        25.0, // blur radius
56
+        0.6,  // brightness (darken for contrast)
57
+    ) {
58
+        Ok(bg) => {
59
+            tracing::info!("Loaded background image");
60
+            bg
61
+        }
62
+        Err(e) => {
63
+            tracing::warn!("Failed to load background: {}, using solid color", e);
64
+            solid_background(width as u32, height as u32, 30, 30, 40)
65
+        }
66
+    };
67
+
68
+    // Create login form
69
+    let mut form = LoginForm::new(width as f64, height as f64);
70
+
1971
     // Connect to daemon
20
-    let mut client = Client::connect().await?;
72
+    let mut client = Client::connect().await.context("Failed to connect to gardmd")?;
2173
     tracing::info!("Connected to gardmd");
2274
 
23
-    // TODO: Initialize X11/rendering
24
-    // TODO: Main UI loop
75
+    // Fetch available sessions
76
+    let sessions = match client.request(&Request::ListSessions).await? {
77
+        Response::Sessions { sessions } => sessions,
78
+        _ => Vec::new(),
79
+    };
80
+    tracing::debug!(?sessions, "Available sessions");
81
+
82
+    // Get default session command
83
+    let default_session = sessions.first().map(|s| s.exec.clone()).unwrap_or_else(|| {
84
+        "gar-session.sh".to_string()
85
+    });
86
+
87
+    // Create Pango context for text rendering
88
+    let pango_ctx = pangocairo::functions::create_context(&renderer.context()?);
89
+
90
+    // Timing for cursor blink
91
+    let mut last_cursor_toggle = Instant::now();
92
+
93
+    // Main event loop
94
+    tracing::info!("Entering main loop");
95
+    loop {
96
+        // Toggle cursor blink
97
+        if last_cursor_toggle.elapsed() >= Duration::from_millis(CURSOR_BLINK_MS) {
98
+            form.toggle_cursor();
99
+            last_cursor_toggle = Instant::now();
100
+        }
101
+
102
+        // Render frame
103
+        {
104
+            let ctx = renderer.context()?;
105
+
106
+            // Draw background
107
+            render_to_cairo(&ctx, &background)?;
108
+
109
+            // Draw login form
110
+            form.render(&ctx, &pango_ctx)?;
111
+        }
112
+
113
+        // Copy rendered frame to X11 window
114
+        let data = renderer.data()?;
115
+        window.put_image(&data)?;
116
+
117
+        // Poll for X11 events (non-blocking)
118
+        while let Some(event) = window.poll_for_event()? {
119
+            match event {
120
+                Event::Expose(_) => {
121
+                    // Already rendering every frame
122
+                }
123
+
124
+                Event::KeyPress(e) => {
125
+                    match e.detail {
126
+                        keycodes::ESCAPE => {
127
+                            tracing::info!("Escape pressed, exiting");
128
+                            return Ok(());
129
+                        }
130
+
131
+                        keycodes::RETURN => {
132
+                            if form.focused_field == FocusedField::Password && form.can_submit() {
133
+                                // Attempt login
134
+                                handle_login(&mut client, &mut form, &default_session).await?;
135
+                            } else if form.focused_field == FocusedField::Username {
136
+                                form.handle_tab();
137
+                            }
138
+                        }
139
+
140
+                        keycodes::TAB => {
141
+                            form.handle_tab();
142
+                        }
143
+
144
+                        keycodes::BACKSPACE => {
145
+                            form.handle_backspace();
146
+                        }
147
+
148
+                        _ => {
149
+                            // Regular character key
150
+                            if let Some(c) = keycode_to_char(e.detail, e.state) {
151
+                                form.handle_key(c);
152
+                            }
153
+                        }
154
+                    }
155
+                }
156
+
157
+                Event::FocusOut(_) => {
158
+                    // Regrab focus if we lose it
159
+                    let _ = window.conn().set_input_focus(
160
+                        x11rb::protocol::xproto::InputFocus::POINTER_ROOT,
161
+                        window.window(),
162
+                        x11rb::CURRENT_TIME,
163
+                    );
164
+                    let _ = window.conn().flush();
165
+                }
166
+
167
+                _ => {}
168
+            }
169
+        }
170
+
171
+        // Small sleep to avoid busy-spinning
172
+        std::thread::sleep(Duration::from_millis(16)); // ~60fps
173
+    }
174
+}
175
+
176
+/// Handle login attempt
177
+async fn handle_login(client: &mut Client, form: &mut LoginForm, default_session: &str) -> Result<()> {
178
+    form.is_loading = true;
179
+    form.clear_messages();
180
+
181
+    // Create session for user
182
+    tracing::info!(username = %form.username, "Creating auth session");
183
+    let response = client
184
+        .request(&Request::CreateSession {
185
+            username: form.username.clone(),
186
+        })
187
+        .await?;
188
+
189
+    match response {
190
+        Response::AuthPrompt { prompt, .. } => {
191
+            tracing::debug!(prompt, "Got auth prompt");
192
+        }
193
+        Response::Error { message } => {
194
+            tracing::warn!(message, "Session creation failed");
195
+            form.set_error(message);
196
+            form.is_loading = false;
197
+            return Ok(());
198
+        }
199
+        _ => {}
200
+    }
201
+
202
+    // Send password
203
+    tracing::debug!("Sending password");
204
+    let response = client
205
+        .request(&Request::Authenticate {
206
+            response: form.password.clone(),
207
+        })
208
+        .await?;
209
+
210
+    match response {
211
+        Response::Success => {
212
+            tracing::info!("Authentication successful");
213
+            form.set_info("Starting session...".to_string());
25214
 
26
-    // For now, just test the connection
27
-    let response = client.request(&Request::ListSessions).await?;
28
-    tracing::info!(?response, "Got sessions");
215
+            // Start session
216
+            let session_cmd = vec![default_session.to_string()];
217
+            let response = client
218
+                .request(&Request::StartSession {
219
+                    cmd: session_cmd,
220
+                    env: vec![],
221
+                })
222
+                .await?;
29223
 
30
-    let response = client.request(&Request::ListUsers).await?;
31
-    tracing::info!(?response, "Got users");
224
+            match response {
225
+                Response::Success => {
226
+                    tracing::info!("Session started, greeter will exit");
227
+                    // The daemon will kill us, but exit cleanly just in case
228
+                    std::process::exit(0);
229
+                }
230
+                Response::Error { message } => {
231
+                    tracing::error!(message, "Failed to start session");
232
+                    form.set_error(message);
233
+                }
234
+                _ => {
235
+                    form.set_error("Unexpected response".to_string());
236
+                }
237
+            }
238
+        }
239
+        Response::AuthError { message } => {
240
+            tracing::warn!(message, "Authentication failed");
241
+            form.set_error(message);
242
+            form.clear_password();
243
+        }
244
+        Response::AuthPrompt { prompt, .. } => {
245
+            // PAM wants more input (e.g., OTP)
246
+            form.set_info(prompt);
247
+        }
248
+        Response::AuthInfo { message } => {
249
+            form.set_info(message);
250
+        }
251
+        Response::Error { message } => {
252
+            form.set_error(message);
253
+        }
254
+        _ => {
255
+            form.set_error("Unexpected response".to_string());
256
+        }
257
+    }
32258
 
33
-    tracing::info!("gardm-greeter exiting (UI not yet implemented)");
259
+    form.is_loading = false;
34260
     Ok(())
35261
 }
gardm-greeter/src/render.rsadded
@@ -0,0 +1,78 @@
1
+//! Cairo rendering for the greeter
2
+//!
3
+//! Provides a Cairo surface that can be rendered to X11.
4
+
5
+use anyhow::{Context, Result};
6
+use cairo::{Context as CairoContext, Format, ImageSurface};
7
+
8
+/// Cairo-based renderer
9
+pub struct Renderer {
10
+    surface: ImageSurface,
11
+    width: i32,
12
+    height: i32,
13
+}
14
+
15
+impl Renderer {
16
+    /// Create a new renderer with the given dimensions
17
+    pub fn new(width: u16, height: u16) -> Result<Self> {
18
+        let surface = ImageSurface::create(Format::ARgb32, width as i32, height as i32)
19
+            .context("Failed to create Cairo surface")?;
20
+
21
+        Ok(Self {
22
+            surface,
23
+            width: width as i32,
24
+            height: height as i32,
25
+        })
26
+    }
27
+
28
+    /// Get a Cairo context for drawing
29
+    pub fn context(&self) -> Result<CairoContext> {
30
+        CairoContext::new(&self.surface).context("Failed to create Cairo context")
31
+    }
32
+
33
+    /// Get the raw pixel data for X11 (BGRA format on little-endian)
34
+    pub fn data(&mut self) -> Result<Vec<u8>> {
35
+        self.surface.flush();
36
+
37
+        let stride = self.surface.stride() as usize;
38
+        let height = self.height as usize;
39
+
40
+        let data = self
41
+            .surface
42
+            .data()
43
+            .context("Failed to get surface data")?;
44
+
45
+        // Cairo uses ARGB, which on little-endian is BGRA in memory
46
+        // X11 with depth 24/32 typically expects the same
47
+        Ok(data[..stride * height].to_vec())
48
+    }
49
+
50
+    /// Get the width
51
+    pub fn width(&self) -> i32 {
52
+        self.width
53
+    }
54
+
55
+    /// Get the height
56
+    pub fn height(&self) -> i32 {
57
+        self.height
58
+    }
59
+
60
+    /// Clear the surface with a solid color
61
+    pub fn clear(&self, r: f64, g: f64, b: f64) -> Result<()> {
62
+        let ctx = self.context()?;
63
+        ctx.set_source_rgb(r, g, b);
64
+        ctx.paint()?;
65
+        Ok(())
66
+    }
67
+}
68
+
69
+/// Draw a rounded rectangle path
70
+pub fn rounded_rectangle(ctx: &CairoContext, x: f64, y: f64, w: f64, h: f64, r: f64) {
71
+    let degrees = std::f64::consts::PI / 180.0;
72
+    ctx.new_sub_path();
73
+    ctx.arc(x + w - r, y + r, r, -90.0 * degrees, 0.0);
74
+    ctx.arc(x + w - r, y + h - r, r, 0.0, 90.0 * degrees);
75
+    ctx.arc(x + r, y + h - r, r, 90.0 * degrees, 180.0 * degrees);
76
+    ctx.arc(x + r, y + r, r, 180.0 * degrees, 270.0 * degrees);
77
+    ctx.close_path();
78
+}
gardm-greeter/src/widgets/login_form.rsadded
@@ -0,0 +1,319 @@
1
+//! Login form widget for the greeter
2
+//!
3
+//! Renders username/password fields, login button, and handles keyboard input.
4
+
5
+use crate::render::rounded_rectangle;
6
+use anyhow::Result;
7
+use cairo::Context;
8
+use pango::{FontDescription, Layout, Weight};
9
+
10
+/// Which field currently has focus
11
+#[derive(Clone, Copy, PartialEq, Eq)]
12
+pub enum FocusedField {
13
+    Username,
14
+    Password,
15
+}
16
+
17
+/// Login form widget state and rendering
18
+pub struct LoginForm {
19
+    pub username: String,
20
+    pub password: String,
21
+    pub focused_field: FocusedField,
22
+    pub error_message: Option<String>,
23
+    pub info_message: Option<String>,
24
+    pub is_loading: bool,
25
+    pub cursor_visible: bool,
26
+
27
+    // Layout dimensions
28
+    x: f64,
29
+    y: f64,
30
+    width: f64,
31
+    height: f64,
32
+}
33
+
34
+impl LoginForm {
35
+    /// Create a new login form centered on the screen
36
+    pub fn new(screen_width: f64, screen_height: f64) -> Self {
37
+        let width = 400.0;
38
+        let height = 320.0;
39
+
40
+        Self {
41
+            username: String::new(),
42
+            password: String::new(),
43
+            focused_field: FocusedField::Username,
44
+            error_message: None,
45
+            info_message: None,
46
+            is_loading: false,
47
+            cursor_visible: true,
48
+            x: (screen_width - width) / 2.0,
49
+            y: (screen_height - height) / 2.0,
50
+            width,
51
+            height,
52
+        }
53
+    }
54
+
55
+    /// Render the login form
56
+    pub fn render(&self, ctx: &Context, pango_ctx: &pango::Context) -> Result<()> {
57
+        // Background panel (semi-transparent dark)
58
+        ctx.set_source_rgba(0.1, 0.1, 0.1, 0.85);
59
+        rounded_rectangle(ctx, self.x, self.y, self.width, self.height, 16.0);
60
+        ctx.fill()?;
61
+
62
+        // Title
63
+        self.render_title(ctx, pango_ctx)?;
64
+
65
+        // Username field
66
+        self.render_input_field(
67
+            ctx,
68
+            pango_ctx,
69
+            "Username",
70
+            &self.username,
71
+            self.y + 100.0,
72
+            self.focused_field == FocusedField::Username,
73
+        )?;
74
+
75
+        // Password field (masked)
76
+        let masked_password = "•".repeat(self.password.len());
77
+        self.render_input_field(
78
+            ctx,
79
+            pango_ctx,
80
+            "Password",
81
+            &masked_password,
82
+            self.y + 170.0,
83
+            self.focused_field == FocusedField::Password,
84
+        )?;
85
+
86
+        // Error message
87
+        if let Some(ref msg) = self.error_message {
88
+            self.render_message(ctx, pango_ctx, msg, (1.0, 0.3, 0.3))?;
89
+        } else if let Some(ref msg) = self.info_message {
90
+            self.render_message(ctx, pango_ctx, msg, (0.7, 0.7, 0.7))?;
91
+        }
92
+
93
+        // Login button
94
+        self.render_button(ctx, pango_ctx)?;
95
+
96
+        Ok(())
97
+    }
98
+
99
+    fn render_title(&self, ctx: &Context, pango_ctx: &pango::Context) -> Result<()> {
100
+        let layout = Layout::new(pango_ctx);
101
+        let mut font = FontDescription::new();
102
+        font.set_family("Sans");
103
+        font.set_size(24 * pango::SCALE);
104
+        font.set_weight(Weight::Bold);
105
+        layout.set_font_description(Some(&font));
106
+        layout.set_text("Welcome");
107
+
108
+        let (text_width, _) = layout.pixel_size();
109
+
110
+        ctx.set_source_rgb(1.0, 1.0, 1.0);
111
+        ctx.move_to(
112
+            self.x + (self.width - text_width as f64) / 2.0,
113
+            self.y + 30.0,
114
+        );
115
+        pangocairo::functions::show_layout(ctx, &layout);
116
+
117
+        Ok(())
118
+    }
119
+
120
+    fn render_input_field(
121
+        &self,
122
+        ctx: &Context,
123
+        pango_ctx: &pango::Context,
124
+        label: &str,
125
+        value: &str,
126
+        y: f64,
127
+        focused: bool,
128
+    ) -> Result<()> {
129
+        let field_x = self.x + 30.0;
130
+        let field_width = self.width - 60.0;
131
+        let field_height = 40.0;
132
+
133
+        // Label
134
+        let mut font = FontDescription::new();
135
+        font.set_family("Sans");
136
+        font.set_size(11 * pango::SCALE);
137
+
138
+        let label_layout = Layout::new(pango_ctx);
139
+        label_layout.set_font_description(Some(&font));
140
+        label_layout.set_text(label);
141
+
142
+        ctx.set_source_rgba(0.8, 0.8, 0.8, 1.0);
143
+        ctx.move_to(field_x, y - 18.0);
144
+        pangocairo::functions::show_layout(ctx, &label_layout);
145
+
146
+        // Input box background
147
+        if focused {
148
+            ctx.set_source_rgba(0.2, 0.4, 0.6, 1.0);
149
+        } else {
150
+            ctx.set_source_rgba(0.25, 0.25, 0.25, 1.0);
151
+        }
152
+        rounded_rectangle(ctx, field_x, y, field_width, field_height, 8.0);
153
+        ctx.fill()?;
154
+
155
+        // Input box border
156
+        if focused {
157
+            ctx.set_source_rgba(0.3, 0.6, 0.9, 1.0);
158
+            rounded_rectangle(ctx, field_x, y, field_width, field_height, 8.0);
159
+            ctx.set_line_width(2.0);
160
+            ctx.stroke()?;
161
+        }
162
+
163
+        // Text value
164
+        ctx.set_source_rgb(1.0, 1.0, 1.0);
165
+        font.set_size(14 * pango::SCALE);
166
+        let value_layout = Layout::new(pango_ctx);
167
+        value_layout.set_font_description(Some(&font));
168
+        value_layout.set_text(if value.is_empty() { " " } else { value });
169
+        ctx.move_to(field_x + 12.0, y + 10.0);
170
+        pangocairo::functions::show_layout(ctx, &value_layout);
171
+
172
+        // Cursor (blinking)
173
+        if focused && self.cursor_visible {
174
+            let (text_width, _) = value_layout.pixel_size();
175
+            let cursor_x = if value.is_empty() {
176
+                field_x + 12.0
177
+            } else {
178
+                field_x + 12.0 + text_width as f64
179
+            };
180
+            ctx.set_source_rgb(1.0, 1.0, 1.0);
181
+            ctx.rectangle(cursor_x, y + 8.0, 2.0, 24.0);
182
+            ctx.fill()?;
183
+        }
184
+
185
+        Ok(())
186
+    }
187
+
188
+    fn render_message(
189
+        &self,
190
+        ctx: &Context,
191
+        pango_ctx: &pango::Context,
192
+        msg: &str,
193
+        color: (f64, f64, f64),
194
+    ) -> Result<()> {
195
+        ctx.set_source_rgb(color.0, color.1, color.2);
196
+
197
+        let mut font = FontDescription::new();
198
+        font.set_family("Sans");
199
+        font.set_size(12 * pango::SCALE);
200
+
201
+        let layout = Layout::new(pango_ctx);
202
+        layout.set_font_description(Some(&font));
203
+        layout.set_text(msg);
204
+        layout.set_width((self.width - 60.0) as i32 * pango::SCALE);
205
+
206
+        ctx.move_to(self.x + 30.0, self.y + 240.0);
207
+        pangocairo::functions::show_layout(ctx, &layout);
208
+
209
+        Ok(())
210
+    }
211
+
212
+    fn render_button(&self, ctx: &Context, pango_ctx: &pango::Context) -> Result<()> {
213
+        let btn_width = 120.0;
214
+        let btn_height = 36.0;
215
+        let btn_x = self.x + (self.width - btn_width) / 2.0;
216
+        let btn_y = self.y + 275.0;
217
+
218
+        // Button background
219
+        if self.is_loading {
220
+            ctx.set_source_rgba(0.3, 0.3, 0.3, 1.0);
221
+        } else {
222
+            ctx.set_source_rgba(0.2, 0.5, 0.8, 1.0);
223
+        }
224
+        rounded_rectangle(ctx, btn_x, btn_y, btn_width, btn_height, 8.0);
225
+        ctx.fill()?;
226
+
227
+        // Button text
228
+        ctx.set_source_rgb(1.0, 1.0, 1.0);
229
+        let mut font = FontDescription::new();
230
+        font.set_family("Sans");
231
+        font.set_size(14 * pango::SCALE);
232
+        font.set_weight(Weight::Bold);
233
+
234
+        let layout = Layout::new(pango_ctx);
235
+        layout.set_font_description(Some(&font));
236
+        layout.set_text(if self.is_loading { "..." } else { "Login" });
237
+
238
+        let (text_w, _) = layout.pixel_size();
239
+        ctx.move_to(btn_x + (btn_width - text_w as f64) / 2.0, btn_y + 8.0);
240
+        pangocairo::functions::show_layout(ctx, &layout);
241
+
242
+        Ok(())
243
+    }
244
+
245
+    /// Handle a character key press
246
+    pub fn handle_key(&mut self, key: char) {
247
+        if self.is_loading {
248
+            return;
249
+        }
250
+
251
+        match self.focused_field {
252
+            FocusedField::Username => self.username.push(key),
253
+            FocusedField::Password => self.password.push(key),
254
+        }
255
+        self.clear_messages();
256
+    }
257
+
258
+    /// Handle backspace key
259
+    pub fn handle_backspace(&mut self) {
260
+        if self.is_loading {
261
+            return;
262
+        }
263
+
264
+        match self.focused_field {
265
+            FocusedField::Username => {
266
+                self.username.pop();
267
+            }
268
+            FocusedField::Password => {
269
+                self.password.pop();
270
+            }
271
+        }
272
+        self.clear_messages();
273
+    }
274
+
275
+    /// Handle tab key (switch focus)
276
+    pub fn handle_tab(&mut self) {
277
+        if self.is_loading {
278
+            return;
279
+        }
280
+
281
+        self.focused_field = match self.focused_field {
282
+            FocusedField::Username => FocusedField::Password,
283
+            FocusedField::Password => FocusedField::Username,
284
+        };
285
+    }
286
+
287
+    /// Toggle cursor visibility for blinking effect
288
+    pub fn toggle_cursor(&mut self) {
289
+        self.cursor_visible = !self.cursor_visible;
290
+    }
291
+
292
+    /// Set error message
293
+    pub fn set_error(&mut self, msg: String) {
294
+        self.error_message = Some(msg);
295
+        self.info_message = None;
296
+    }
297
+
298
+    /// Set info message
299
+    pub fn set_info(&mut self, msg: String) {
300
+        self.info_message = Some(msg);
301
+        self.error_message = None;
302
+    }
303
+
304
+    /// Clear all messages
305
+    pub fn clear_messages(&mut self) {
306
+        self.error_message = None;
307
+        self.info_message = None;
308
+    }
309
+
310
+    /// Clear password field
311
+    pub fn clear_password(&mut self) {
312
+        self.password.clear();
313
+    }
314
+
315
+    /// Check if form is ready to submit
316
+    pub fn can_submit(&self) -> bool {
317
+        !self.is_loading && !self.username.is_empty() && !self.password.is_empty()
318
+    }
319
+}
gardm-greeter/src/widgets/mod.rsadded
@@ -0,0 +1,5 @@
1
+//! UI widgets for the greeter
2
+
3
+mod login_form;
4
+
5
+pub use login_form::{FocusedField, LoginForm};
gardm-greeter/src/window.rsadded
@@ -0,0 +1,176 @@
1
+//! X11 window management for the greeter
2
+//!
3
+//! Creates a fullscreen window for displaying the login interface.
4
+
5
+use anyhow::{Context, Result};
6
+use x11rb::connection::Connection;
7
+use x11rb::protocol::xproto::*;
8
+use x11rb::rust_connection::RustConnection;
9
+use x11rb::wrapper::ConnectionExt as _;
10
+
11
+/// Greeter window wrapping X11 connection and window handle
12
+pub struct GreeterWindow {
13
+    conn: RustConnection,
14
+    screen_num: usize,
15
+    window: Window,
16
+    gc: Gcontext,
17
+    width: u16,
18
+    height: u16,
19
+    depth: u8,
20
+}
21
+
22
+impl GreeterWindow {
23
+    /// Create a new fullscreen greeter window
24
+    pub fn new() -> Result<Self> {
25
+        let (conn, screen_num) = x11rb::connect(None).context("Failed to connect to X server")?;
26
+
27
+        let screen = &conn.setup().roots[screen_num];
28
+        let width = screen.width_in_pixels;
29
+        let height = screen.height_in_pixels;
30
+        let root = screen.root;
31
+        let depth = screen.root_depth;
32
+        let visual = screen.root_visual;
33
+
34
+        // Create window
35
+        let window = conn.generate_id().context("Failed to generate window ID")?;
36
+        conn.create_window(
37
+            depth,
38
+            window,
39
+            root,
40
+            0,
41
+            0,
42
+            width,
43
+            height,
44
+            0,
45
+            WindowClass::INPUT_OUTPUT,
46
+            visual,
47
+            &CreateWindowAux::new()
48
+                .background_pixel(screen.black_pixel)
49
+                .event_mask(
50
+                    EventMask::EXPOSURE
51
+                        | EventMask::KEY_PRESS
52
+                        | EventMask::KEY_RELEASE
53
+                        | EventMask::BUTTON_PRESS
54
+                        | EventMask::BUTTON_RELEASE
55
+                        | EventMask::STRUCTURE_NOTIFY
56
+                        | EventMask::FOCUS_CHANGE,
57
+                ),
58
+        )
59
+        .context("Failed to create window")?;
60
+
61
+        // Set fullscreen hint
62
+        let net_wm_state = conn
63
+            .intern_atom(false, b"_NET_WM_STATE")
64
+            .context("Failed to intern _NET_WM_STATE")?
65
+            .reply()
66
+            .context("Failed to get _NET_WM_STATE reply")?
67
+            .atom;
68
+
69
+        let fullscreen = conn
70
+            .intern_atom(false, b"_NET_WM_STATE_FULLSCREEN")
71
+            .context("Failed to intern fullscreen atom")?
72
+            .reply()
73
+            .context("Failed to get fullscreen atom reply")?
74
+            .atom;
75
+
76
+        conn.change_property32(PropMode::REPLACE, window, net_wm_state, AtomEnum::ATOM, &[
77
+            fullscreen,
78
+        ])
79
+        .context("Failed to set fullscreen property")?;
80
+
81
+        // Override redirect - no window manager decorations
82
+        conn.change_window_attributes(
83
+            window,
84
+            &ChangeWindowAttributesAux::new().override_redirect(1),
85
+        )
86
+        .context("Failed to set override redirect")?;
87
+
88
+        // Create graphics context for image rendering
89
+        let gc = conn.generate_id().context("Failed to generate GC ID")?;
90
+        conn.create_gc(gc, window, &CreateGCAux::new())
91
+            .context("Failed to create GC")?;
92
+
93
+        // Map and flush
94
+        conn.map_window(window).context("Failed to map window")?;
95
+        conn.flush().context("Failed to flush X connection")?;
96
+
97
+        // Grab keyboard focus
98
+        conn.set_input_focus(InputFocus::POINTER_ROOT, window, x11rb::CURRENT_TIME)
99
+            .context("Failed to set input focus")?;
100
+        conn.flush()?;
101
+
102
+        tracing::info!(width, height, "Created greeter window");
103
+
104
+        Ok(Self {
105
+            conn,
106
+            screen_num,
107
+            window,
108
+            gc,
109
+            width,
110
+            height,
111
+            depth,
112
+        })
113
+    }
114
+
115
+    /// Get window width
116
+    pub fn width(&self) -> u16 {
117
+        self.width
118
+    }
119
+
120
+    /// Get window height
121
+    pub fn height(&self) -> u16 {
122
+        self.height
123
+    }
124
+
125
+    /// Get the X11 connection
126
+    pub fn conn(&self) -> &RustConnection {
127
+        &self.conn
128
+    }
129
+
130
+    /// Get the window ID
131
+    pub fn window(&self) -> Window {
132
+        self.window
133
+    }
134
+
135
+    /// Put an ARGB image to the window
136
+    pub fn put_image(&self, data: &[u8]) -> Result<()> {
137
+        self.conn
138
+            .put_image(
139
+                ImageFormat::Z_PIXMAP,
140
+                self.window,
141
+                self.gc,
142
+                self.width,
143
+                self.height,
144
+                0,
145
+                0,
146
+                0,
147
+                self.depth,
148
+                data,
149
+            )
150
+            .context("Failed to put image")?;
151
+
152
+        self.conn.flush().context("Failed to flush after put_image")?;
153
+        Ok(())
154
+    }
155
+
156
+    /// Wait for and return the next X11 event
157
+    pub fn wait_for_event(&self) -> Result<x11rb::protocol::Event> {
158
+        self.conn
159
+            .wait_for_event()
160
+            .context("Failed to wait for X11 event")
161
+    }
162
+
163
+    /// Poll for event without blocking
164
+    pub fn poll_for_event(&self) -> Result<Option<x11rb::protocol::Event>> {
165
+        self.conn
166
+            .poll_for_event()
167
+            .context("Failed to poll for X11 event")
168
+    }
169
+}
170
+
171
+impl Drop for GreeterWindow {
172
+    fn drop(&mut self) {
173
+        let _ = self.conn.destroy_window(self.window);
174
+        let _ = self.conn.flush();
175
+    }
176
+}