-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathaudit-findings.json
More file actions
1314 lines (1314 loc) · 198 KB
/
Copy pathaudit-findings.json
File metadata and controls
1314 lines (1314 loc) · 198 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"summary": "Exhaustively review every config in the bazzite-rice repo, adversarially verify findings",
"agentCount": 75,
"logs": [
"Launching 12 domain reviewers across every config file",
"Round 1: 88 findings confirmed, 3 refuted",
"Critic found 3 coverage gaps: lua-port-unreviewed; hyprlock-conf-unreviewed; remaining-user-bin-and-small-configs",
"Final: 106 confirmed findings (14 bugs, 16 breakages)"
],
"result": {
"summary": {
"total": 106,
"bySeverity": {
"bug": 14,
"breakage": 16,
"inconsistency": 24,
"improvement": 36,
"hygiene": 16
},
"refutedCount": 3
},
"findings": [
{
"file": "dotfiles/home/.local/bin/playing",
"line": 68,
"severity": "bug",
"title": "Pipe-delimited playerctl format breaks on metadata containing '|'",
"detail": "get_state() joins seven fields with '|' (lines 60-64) and splits on '|' (line 68: 'parts = (raw.split(\"|\") + [\"\"] * 7)[:7]'). Any title/artist/album containing a pipe — common in real metadata, e.g. 'Artist | Topic' YouTube channels or titles like 'A | B' — shifts every subsequent field, so status, position and length get parsed from the wrong columns (position/length then _int() to 0, status becomes part of the title, glyph lookup fails).",
"fix": "Use the ASCII unit separator instead of '|': build the format with SEP = '\\x1f', e.g. fmt = SEP.join(['{{playerName}}','{{status}}','{{xesam:title}}','{{xesam:artist}}','{{xesam:album}}','{{position}}','{{mpris:length}}']) and split with raw.split('\\x1f'). playerctl passes the literal byte through.",
"dimension": "user-bin",
"verdict": {
"confidence": "high",
"isReal": true,
"reason": "Confirmed against the live system. Lines 60-68 of dotfiles/home/.local/bin/playing join 7 playerctl fields with '|' and naively split on '|'. playerctl 2.4.1 substitutes metadata verbatim (no escaping), and the running firefox player is playing YouTube content where '|' in titles/channel names is common. A pipe in any field shifts all subsequent columns: e.g. a pipe in the title truncates the title, pushes its remainder into artist, real artist into album, and makes pos/length parse from wrong columns (album name -> _int() -> 0). Minor inaccuracy in the finding's detail: status (field 1) precedes title so it is NOT corrupted by a pipe in the title — but the core field-shift bug is real. The suggested fix was verified empirically: `playerctl metadata --format \"$(printf 'A\\x1f{{playerName}}\\x1fB')\" | od -c` shows the literal 0x1f byte passes through playerctl's template engine unchanged. Also checked the one subtlety: Python's str.strip() at line 54 strips \\x1f ('\\x1f'.isspace() is True), so trailing-empty fields lose separators — but the existing `+ [\"\"] * 7` padding makes that benign (they correctly default to \"\"/0), and playerName (the only leading field) is never empty. The fix as proposed is correct and complete.",
"revisedFix": null
}
},
{
"file": "dotfiles/home/.local/bin/playing",
"line": 213,
"severity": "bug",
"title": "Card layout uses len() as visible width — CJK/fullwidth titles misalign the card borders",
"detail": "truncate() (line 92), marquee() (line 96), card_line() padding (lines 213-217), and status_vis (line 221) all equate string length with terminal columns. East-Asian wide characters occupy 2 columns, so a Japanese/Chinese/Korean track title (this library demonstrably contains CJK-named media — the repo's own wallpapers include CJK filenames) renders the title/artist/album rows wider than card_w, breaking the right border '│' alignment of the card. The '⏸' glyph (U+23F8, ambiguous/emoji-presentation width) can also shift the status row by one column.",
"fix": "Add a display-width helper using unicodedata: def wcwidth_str(s): return sum(2 if unicodedata.east_asian_width(c) in ('W','F') else 1 for c in s). Use it for visible_len in card_line callers, and make truncate/marquee operate on accumulated column width rather than character count.",
"dimension": "user-bin",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed against the live system. card_line() (lines 213-217) pads with card_w-2-visible_len where every caller passes len() (lines 238-249), and truncate()/marquee() (lines 92, 96-107) slice by character count. glibc wcwidth and unicodedata.east_asian_width both confirm CJK/fullwidth chars occupy 2 cells in kitty (the terminal this script runs in, per waybar/modules.jsonc:176 \"kitty --class=playing-tui -- playing\"), so a CJK title/artist/album renders wider than padded, shifting the right border; a long CJK title makes marquee() emit a title_w-char slice rendering up to ~2x title_w columns, which can exceed the terminal width and wrap, corrupting the frame. The trigger is present in practice: the repo itself contains CJK-titled media (wallpapers/【東方】Bad Apple!! PV【影絵】.mp4). One sub-claim is wrong but harmless: U+23F8 ⏸ is EAW 'N' (Neutral, not Ambiguous) and renders width 1 in kitty without VS16, so status_vis at line 221 does NOT misalign; the suggested fix's W/F-only rule handles it correctly anyway.",
"revisedFix": "In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/home/.local/bin/playing: add `import unicodedata` and a column-width helper:\n\ndef vwidth(s: str) -> int:\n return sum(2 if unicodedata.east_asian_width(c) in (\"W\", \"F\") else 1 for c in s)\n\nMake truncate() width-aware (accumulate columns, reserve 1 col for the ellipsis):\n\ndef truncate(s: str, n: int) -> str:\n if vwidth(s) <= n:\n return s\n out, w = [], 0\n for c in s:\n cw = 2 if unicodedata.east_asian_width(c) in (\"W\", \"F\") else 1\n if w + cw > n - 1:\n break\n out.append(c); w += cw\n return \"\".join(out) + \"…\"\n\nMake marquee() step and window by columns: keep `if vwidth(s) <= n: return s`; advance `step` over len(full) characters as now, but build the visible slice by accumulating character widths from (full+full)[step:] until adding the next char would exceed n columns, then pad with a space if the accumulated width is n-1 (wide char straddling the boundary).\n\nThen fix the callers: line 240 `card_line(title_str, vwidth(title))`, line 241 `card_line(artist_str, vwidth(artist))`, line 244 `card_line(album_str, vwidth(album))`, line 249 `card_line(player_str, 4 + vwidth(player))`. Line 221 status_vis can stay as-is (⏸ is width 1 in kitty), or use vwidth(glyph)+2+len(status) which is equivalent under the W/F rule. Note pad in card_line can go negative if a marquee slice lands exactly at n-1 with a trailing wide char unless the space-padding above is applied."
}
},
{
"file": "dotfiles/hypr/brightness.sh",
"line": 65,
"severity": "bug",
"title": "read from failed ddcutil getvcp aborts entire script under set -e, blocking brightness for all displays",
"detail": "Line 65: `read -r _ _ _ cur _ < <(ddcutil --display \"$d\" \"${dc[@]}\" getvcp 10 --terse 2>/dev/null)`. If ddcutil produces no output (display asleep/DPMS-off, unresponsive DDC, stale display number in the cache — the cache signature at line 36 is keyed only to DRM connector status, which does not change on DPMS sleep), `read` hits EOF and returns 1, and `set -euo pipefail` (line 5) kills the whole script. Verified the bash semantics: `set -e; read -r _ < <(true)` exits 1 before the next statement. This path is hit exactly when level files are missing (first run, or right after a re-detect rm'd them at line 42), and one dead panel then silently blocks brightness adjustment and the OSD for every panel — the existing `cur=${cur:-50}` fallback on line 66 is unreachable.",
"fix": "Append `|| true` so the empty-output case falls through to the default: `read -r _ _ _ cur _ < <(ddcutil --display \"$d\" \"${dc[@]}\" getvcp 10 --terse 2>/dev/null) || true` — line 66's `cur=${cur:-50}` then handles it as designed.",
"dimension": "watchers-startup",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Empirically confirmed on the live system. (1) Bash semantics: under `set -euo pipefail`, `read -r _ _ _ cur _ < <(true)` exits 1 before the next statement (tested; 'reached' never printed), so /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/brightness.sh line 65 aborts the script whenever ddcutil emits no stdout, making line 66's `cur=${cur:-50}` fallback dead code for that case. (2) Installed ddcutil 2.2.1 confirmed to emit nothing on stdout for a non-responding display (`ddcutil --display 7 getvcp 10 --terse 2>/dev/null` -> rc=1, empty stdout; errors go to stderr which the script discards). (3) Trigger path exists in practice: line 42 rm's all level-* files on every re-detect (connector-set change), forcing the getvcp on line 65 for every cached display (cache currently holds displays 1 and 2); an unresponsive panel at that moment (post-replug DDC warm-up, standby panel, or silent failure from the aggressive --sleep-multiplier .2 the script itself warns about at line 49) kills the script mid-loop, skipping all later displays and the OSD, and since no level file gets written the failure repeats on every keypress. The comments (lines 52-58) show the getvcp-failure fallback is intended behavior that set -e defeats. Suggested fix verified to work: with `|| true` appended, the script reaches line 66 and defaults cur to 50 under set -euo pipefail. Only nit: panels earlier in the loop than the dead one do get their backgrounded setvcp before the abort, so 'blocks every panel' slightly overstates it — but the OSD is always lost and later panels plus repeat presses are blocked, so the bug and fix stand as written.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/cycle-wallpaper.lua",
"line": 452,
"severity": "bug",
"title": "apply_spanned_video never stops old mpvpaper instances when frame extraction fails (reproduced from .sh)",
"detail": "stop_mpvpaper_for is only invoked via apply_spanned_image(frame) at line 442. If extract_video_frame returns nil (ffmpeg missing from PATH, or ffmpeg fails on that specific file — corrupt/truncated video), the spawn loop at lines 449–457 starts new mpvpaper instances WITHOUT killing the previous ones (the comment at line 452 'apply_spanned_image already stopped mpvpaper for this monitor' is only true when a frame was extracted). Result: two mpvpaper processes fighting per output, and the pidfile at line 185 is overwritten so the old PID can never be cleaned via the pidfile. cycle-wallpaper.sh:430 has the identical hole; the port reproduces it.",
"fix": "Insert an unconditional, idempotent stop at the top of the spawn loop body (before line 453): `stop_mpvpaper_for(r.name)` — stop_mpvpaper_for is already safe to call when nothing is running. Delete the now-wrong comment on line 452.",
"dimension": "gap:lua-port-unreviewed",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified in /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/cycle-wallpaper.lua: in apply_spanned_video, stop_mpvpaper_for is reached only via apply_spanned_image(frame) (lua:441-443 -> lua:385), which is skipped when extract_video_frame returns nil; the spawn loop (lua:449-457) then spawns new mpvpaper instances and overwrites the pidfile (lua:185) without killing old ones. The asymmetry with the non-spanned apply_video — which calls stop_mpvpaper_for(mon) unconditionally at lua:208 even when frame is nil — proves the missing stop is a bug, and the comment at lua:452 asserts an invariant that only holds on extraction success. cycle-wallpaper.sh has the identical hole (comment sh:430, spawn sh:432-435, no stop in that loop; only indirect stop at sh:343). Two corrections to the finding's framing: (1) the 'ffmpeg missing from PATH' trigger is largely gated on this system because ffmpeg/ffprobe both live in /usr/bin and apply_spanned_video bails early when ffprobe is absent (lua:420-423); the realistic triggers are ffmpeg failing on a specific file, and an additional unmentioned path: apply_spanned_image's return value is ignored at lua:442 and it can return false BEFORE its stop loop (ImageMagick missing lua:344, identify failure lua:350), also leaving stale mpvpaper running — the unconditional stop fixes that too. (2) The orphan is not permanent: the pkill_f fallback inside a future stop_mpvpaper_for reaps it on the next wallpaper change; until then two instances fight per output. The suggested fix is correct and safe: stop_mpvpaper_for is idempotent (posix.read_file returns nil on missing pidfile, unlink ignores ENOENT, pkill_f is best-effort).",
"revisedFix": "Original fix is correct for the Lua file; apply it to both implementations. In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/cycle-wallpaper.lua, replace line 452 (\" -- apply_spanned_image already stopped mpvpaper for this monitor.\") with \" stop_mpvpaper_for(r.name)\" (before the spawn_mpvpaper call at line 453). In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/cycle-wallpaper.sh, replace line 430 (\" # apply_spanned_image already stopped mpvpaper for this monitor.\") with \" stop_mpvpaper_for \\\"$mon\\\"\" (before the 'rm -f \"$sock\"' at line 431). stop_mpvpaper_for is idempotent in both versions, so the call is a no-op when apply_spanned_image already stopped the instance."
}
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 460,
"severity": "bug",
"title": "Bare `wait` in apply_spanned_video blocks forever on the mpvpaper children",
"detail": "The mpvpaper instances are started with `setsid mpvpaper ... &` (lines 432-434). Because the script runs without job control, util-linux setsid does not fork (pid != pgid), so each mpvpaper IS a direct background child of the script. The bare `wait` on line 460 therefore waits not only for the `_mpv_unpause` jobs but also for the never-exiting mpvpaper processes (loop-file=inf). Verified empirically: `bash -c 'set -euo pipefail; setsid sleep 30 & true & wait; echo done'` never prints done. Consequences: (a) `cycle-wallpaper.sh span <video>` run in a terminal hangs until the NEXT wallpaper change kills mpvpaper; (b) the success notify-send at line 473 never fires; (c) every spanned-video pick from pick-wallpaper.sh leaves a lingering detached bash process until the next wallpaper change.",
"fix": "Wait only on the unpause jobs. Replace lines 457-460:\n for s in \"${sockets[@]}\"; do\n _mpv_unpause \"$s\" &\n done\n wait\nwith:\n local -a unpause_pids=()\n for s in \"${sockets[@]}\"; do\n _mpv_unpause \"$s\" & unpause_pids+=(\"$!\")\n done\n wait \"${unpause_pids[@]}\" 2>/dev/null || true",
"dimension": "wallpaper-system",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified empirically on this system (bash 5.3.9, util-linux setsid 2.42, mpvpaper without -f): `bash -c 'set -euo pipefail; setsid sleep 30 & true & wait; echo done'` never prints done (timeout exit 124), and a setsid'd background child retains the script as PPID (setsid does not fork because non-interactive background children are not process-group leaders), so it remains a waitable job. In cycle-wallpaper.sh, lines 432-434 spawn `setsid mpvpaper -o \"... loop-file=inf pause ...\" ... &` (never exits) and line 460 is a bare `wait` with no disown, so apply_spanned_video blocks until the mpvpaper children are killed by the next wallpaper change. Consequences confirmed: terminal `span <video>` invocation hangs; the success notify-send at line 473 never fires for spanned videos; pick-wallpaper.sh line 209 (`setsid \"$CYCLE\" ... & disown`) leaves a lingering blocked bash per spanned-video pick. The suggested fix pattern was tested and returns immediately while still reaping the unpause jobs; `|| true` correctly guards set -e against a failed unpause exit status.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 146,
"severity": "bug",
"title": "set -e kills the script when _extract_video_frame fails; the has_frame=0 fallback is dead code",
"detail": "`frame=$(_extract_video_frame \"$wall\")` (line 146 in apply_video, line 404 in apply_spanned_video) is a plain assignment, so under `set -euo pipefail` a nonzero return from _extract_video_frame (ffmpeg missing from PATH, undecodable/corrupt video, zero-byte output frame — all of which make the function `return 1`) aborts the entire script before mpvpaper is ever spawned. The carefully written `has_frame=0` fallback path (lines 148/405 `[[ -n \"$frame\" ]]`) is unreachable on failure. Verified: `bash -c 'set -euo pipefail; f(){ return 1; }; x=$(f); echo after'` exits 1 without printing. Net effect: any video ffmpeg can't extract a frame from silently fails to become a wallpaper at all instead of just skipping the pre-fade. Additionally, if this is fixed, the has_frame=0 path in apply_spanned_video never calls stop_mpvpaper_for (the comment on line 430 assumes apply_spanned_image ran), so old mpvpaper instances would be orphaned.",
"fix": "Line 146 and line 404: change to `frame=$(_extract_video_frame \"$wall\" || true)`. In apply_spanned_video, also replace the line 430-431 pair:\n # apply_spanned_image already stopped mpvpaper for this monitor.\n rm -f \"$sock\"\nwith:\n (( has_frame )) || stop_mpvpaper_for \"$mon\"\n rm -f \"$sock\"",
"dimension": "wallpaper-system",
"verdict": {
"confidence": "medium",
"isReal": true,
"reason": "Mechanism verified on the live system (bash 5.3.9): under set -euo pipefail, frame=$(_extract_video_frame ...) failing aborts the script, and the has_frame=0 fallback is dead code — but ONLY in the 'pick' (line 490) and 'span' (line 469) flows, which call apply_video/apply_spanned_video with errexit active. These are real user flows (pick-wallpaper.sh SUPER+W picker, detached via setsid, so failure is fully silent). The finding is overbroad in one respect: the common next/prev/init flow (line 526, results+=(\"$(apply_to_monitor ...)\")) is immune because bash unsets errexit inside command-substitution subshells by default — empirically verified the fallback works there. The trigger is also latent rather than active: Hyprland's session PATH (/usr/local/sbin:/usr/local/bin:/usr/bin) contains /usr/bin/ffmpeg 8.1.1 (full-codec Bazzite build), which I verified extracts frames from both the HEVC .mov aerials and the CJK-named h264 .mp4 in wallpapers/. So no current repo file aborts the script today; it fires only on a future corrupt/unsupported video or ffmpeg removal — exactly the contingency the fallback was written for. The orphaned-mpvpaper observation about line 430 is correct (pidfile overwritten at line 435, old instance never killed in the has_frame=0 path). The suggested fix is correct and complete: `|| true` inside the substitutions at lines 146/404 is safe, and `(( has_frame )) || stop_mpvpaper_for \"$mon\"` is errexit-safe as an || list.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 285,
"severity": "bug",
"title": "Dimension-probe `read` under set -e makes the explicit error branches unreachable (silent exit)",
"detail": "Line 285: `read -r src_w src_h < <(identify -format '%w %h\\n' \"$wall\" 2>/dev/null | head -1)` — if identify fails (unreadable/corrupt image) the process substitution produces no output, `read` returns 1, and errexit kills the script silently. The friendly error branch on lines 286-289 (`couldn't read dimensions of $wall`) can never run for the total-failure case. Same pattern at line 384: `IFS=, read -r src_w src_h < <(ffprobe ...)` — ffprobe failure causes a silent exit instead of reaching lines 386-389. Verified: `bash -c 'set -euo pipefail; read -r a b < <(true); echo after'` exits 1 without printing.",
"fix": "Append `|| true` to both reads so the existing [[ -z ]] checks do their job:\nline 285: `read -r src_w src_h < <(identify -format '%w %h\\n' \"$wall\" 2>/dev/null | head -1) || true`\nline 384: `IFS=, read -r src_w src_h < <(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 \"$wall\" 2>/dev/null) || true`",
"dimension": "wallpaper-system",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system (bash 5.3.9, /usr/bin/identify, linuxbrew ffprobe). Reproduced both failure paths with corrupt files: `read -r src_w src_h < <(identify ... | head -1)` (line 285) and `IFS=, read ... < <(ffprobe ...)` (line 384) get zero bytes when the probe fails (the pipeline itself exits 0 since stderr is suppressed and `head`/process substitution mask the status), so `read` returns 1 on EOF and `set -e` kills the script silently — the friendly `couldn't read dimensions` branches at 286-289/386-389 are unreachable for total probe failure. Errexit is active at both sites because all call chains (lines 469, 471, 406) are unchecked plain statements. The path is reachable in practice via pick-wallpaper.sh line 223 (`run_cycle span \"$abs\"`, ENTER in the picker). The suggested fix was also verified: with `|| true`, read assigns empty strings to the variables on EOF, so the existing [[ -z ]] checks work correctly under set -u and the diagnostic prints.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/hyprland.conf",
"line": 130,
"severity": "bug",
"title": "SUPER+C clipboard bind clears the clipboard when rofi is cancelled",
"detail": "`bind = $mod, C, exec, cliphist list | rofi -dmenu -p clip | cliphist decode | wl-copy` runs the whole pipeline unconditionally. When the user presses Esc in rofi, rofi exits 1 with no output; `cliphist decode` on empty stdin was verified on this system to exit 1 and emit 0 bytes (`extracting id: input not prefixed with id`). `wl-copy` then still runs with empty stdin and takes ownership of the clipboard with empty contents, so opening the history popup and cancelling it wipes whatever was on the clipboard.",
"fix": "Replace line 130 with: bind = $mod, C, exec, sh -c 'sel=$(cliphist list | rofi -dmenu -p clip) && [ -n \"$sel\" ] && printf \"%s\" \"$sel\" | cliphist decode | wl-copy'",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed end-to-end. Line 130 of dotfiles/hypr/hyprland.conf is exactly the unconditional pipeline claimed. On rofi Esc-cancel, rofi exits 1 with no output; verified on this system that `printf '' | cliphist decode` (cliphist 2.0.0) exits 1 emitting 0 bytes (\"extracting id: input not prefixed with id\"); sh runs all pipeline stages regardless, so wl-copy executes with empty stdin. Checked the wl-clipboard source matching the installed wl-clipboard-2.2.1^git20251124: wl-copy has no empty-input special case — it takes selection ownership and offers zero bytes, destroying the prior clipboard contents. The suggested fix was also validated: cliphist decode accepts a list line via printf without trailing newline (decoded a real entry, exit 0), the `sel=$(...) &&` chain aborts on cancel (assignment propagates rofi's exit 1, verified), the command contains no commas so Hyprland bind parsing is unaffected, `$sel` is not a defined hyprlang variable so it reaches the shell literally, and the direct decode|wl-copy pipe stays binary-safe for image entries. Only residual edge (typing non-matching free text + Enter still wipes) is far narrower than the reported bug and does not invalidate the fix.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/reload-watcher.sh",
"line": 10,
"severity": "bug",
"title": "pkill -f workspace-watcher.sh kills any process with the filename in argv, including the editor used to edit RULES",
"detail": "Line 10's `pkill -f workspace-watcher.sh` matches every process whose full cmdline contains that (unescaped-dot) regex. The documented workflow (CLAUDE.md: edit the RULES table, then press SUPER+SHIFT+R) makes a terminal editor with the file open — e.g. `nvim dotfiles/hypr/workspace-watcher.sh`, `less`, `code <path>` — a likely concurrent process; pressing the reload bind kills it (potential data loss with unsaved edits). Verified live that the legitimate targets are exactly three processes with cmdline `bash /home/Hahafoot/.config/hypr/workspace-watcher.sh` (PIDs 4056-4058: main shell, if-branch subshell, while-loop subshell). Secondary: the socket reader (currently `python3 -c ...`, PID 4059; would be socat if installed) has no 'workspace-watcher.sh' in argv, so it survives the pkill and lingers connected to .socket2.sock until the next Hyprland event triggers a broken-pipe exit — harmless but worth knowing.",
"fix": "Anchor the pattern to the interpreter invocation so only the watcher's shells match: replace line 10 with `pkill -f '^bash [^ ]*/workspace-watcher\\.sh$' 2>/dev/null || true`. This still matches all three bash processes (identical cmdlines) but not editors, pagers, or reload-watcher itself.",
"dimension": "watchers-startup",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Empirically confirmed on the live system. reload-watcher.sh line 10 is `pkill -f workspace-watcher.sh`, bound to SUPER+SHIFT+R (hyprland.conf:110), and CLAUDE.md documents pressing it right after editing the RULES table. pkill/pgrep -f match the unanchored regex against the full cmdline: a simulated editor process `python3 ... workspace-watcher.sh` (stand-in for `nvim dotfiles/hypr/workspace-watcher.sh`) was matched by `pgrep -f 'workspace-watcher.sh'` alongside the three legitimate watcher shells (PIDs 4056-4058, cmdline `bash /home/Hahafoot/.config/hypr/workspace-watcher.sh`) — even my own wrapper shell matched. So pressing the reload bind while the file is open in a terminal editor SIGTERMs the editor. Secondary claim also verified: PID 4059 is the python3 fallback socket reader (socat not installed), has no 'workspace-watcher.sh' in argv, survives the pkill, and lingers until a broken-pipe write — harmless as stated. The suggested fix was verified live: `pgrep -f '^bash [^ ]*/workspace-watcher\\.sh$'` matched exactly PIDs 4056-4058 and nothing else (not the fake editor, not the invoking shell, not reload-watcher itself), and both launch paths in this repo (exec-once at hyprland.conf:33 and the dispatch exec in reload-watcher.sh:12) produce that exact cmdline shape via the env shebang, so the anchored pattern remains correct after restart.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/startup-apps.sh",
"line": 9,
"severity": "bug",
"title": "Commands containing '|' are silently truncated and the truncated prefix is executed",
"detail": "Line 9 uses `while IFS='|' read -r state name cmd rest`, so `cmd` only receives the third pipe-delimited field and `rest` absorbs everything after it. Verified: for the line `on|x|echo a | grep b`, cmd=[echo a ] and rest=[ grep b] — line 20 then runs `setsid sh -c \"echo a \"`, executing a wrong, truncated command. startup-apps.conf's header explicitly documents the third field as a \"shell command\" and invites editing the file directly, and shell commands routinely contain pipes. toggle-startup-apps.sh only guards against pipes for entries added via rofi (line 134), not manual edits. The same 4-field read appears in toggle-startup-apps.sh build_menu (line 43), where a truncated cmd only degrades icon resolution.",
"fix": "In dotfiles/hypr/startup-apps.sh line 9, drop the `rest` catch-all so `cmd` absorbs the remainder of the line including pipes: `while IFS='|' read -r state name cmd; do` (with read, the last variable receives the rest of the line verbatim). Apply the same change to `build_menu` in dotfiles/hypr/toggle-startup-apps.sh line 43 (`while IFS='|' read -r state name cmd; do`). The pipe-rejection guard at toggle-startup-apps.sh line 134 can then be deleted.",
"dimension": "watchers-startup",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Empirically reproduced: startup-apps.sh:9 uses `IFS='|' read -r state name cmd rest`, so for a conf line `on|x|echo a | grep b` bash yields cmd='echo a ' and rest=' grep b', and line 20 runs `setsid sh -c \"echo a \"` — the truncated prefix executes silently. startup-apps.conf explicitly documents field 3 as a 'shell command' and invites direct editing, while the pipe-rejection guard (toggle-startup-apps.sh:134) only covers rofi-added entries, leaving manual edits unprotected. The suggested fix is verified correct: with `read -r state name cmd` the last variable absorbs the remainder including embedded pipes (tested: cmd='echo a | grep b'), existing entries are unaffected, and the build_menu read at toggle-startup-apps.sh:43 is the only other site where truncation matters (the third 4-var read at line 177 never uses cmd, so it needs no change). Deleting the line-134 guard is safe since the only other field-3 parser (awk -F'|' at line 138) reads only field 2. Caveat: no current conf entry contains a pipe, so this is a latent bug triggered by the documented manual-edit workflow rather than the present file contents.",
"revisedFix": null
}
},
{
"file": "dotfiles/waybar/modules.jsonc",
"line": 25,
"severity": "bug",
"title": "41 icon values are literally empty strings — window-rewrite matches suppress the :default: icon, pulseaudio/network show bare spaces",
"detail": "Verified by hexdump: e.g. line 212 `\"format-icons\": { \"default\": [\"\", \"\", \"\"] }` is three empty strings (bytes 5b 22 22 2c ...), line 199 format-wifi is `\" {essid}\"` (two plain spaces, no glyph). grep counts 41 values matching `: \"\"` in this file. Consequences on the live bar: (a) ~37 window-rewrite entries (chromium, librewolf, zed, whatsapp, teams, nautilus, nemo, thunar, pavucontrol, zathura, evince, lutris, bottles, retroarch, prismlauncher, keybinds-cheatsheet, all the title<nvim/vim/htop/btop/ssh/git> rules, etc.) match a window and rewrite it to NOTHING, actively overriding the `\"window-rewrite-default\": \":default:\"` fallback — Nautilus windows (the system file manager) render as a blank gap in the workspace pill; (b) pulseaudio renders as ' 45%' and network as ' offline' with stray leading spaces. The glyphs were evidently lost when the file was written (only one real glyph survived: U+F02A0 for heroic on line 103).",
"fix": "Either delete every entry whose value is \"\" so window-rewrite-default \":default:\" applies (and drop the leading double-spaces / empty format-icons from network/pulseaudio formats), or restore real icons: sketchybar-app-font ligature names (\":nautilus:\" style) for window-rewrite, and Nerd Font glyphs for pulseaudio/network after installing a Nerd Font (see the font finding).",
"dimension": "waybar",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Hexdump-verified: 44 literal empty-string values in dotfiles/waybar/modules.jsonc (41 single values + 3 in the line 212 format-icons array). grep -P for non-ASCII confirms the only glyphs left in the file are line 15 (⌘), line 103 (heroic U+F02A0, the lone survivor as the finding states), and ↓↑ arrows on lines 200-203 — format-wifi/disconnected/muted and cpu/memory have two plain 0x20 spaces. The file is the live config (~/.config/waybar/modules.jsonc symlinks to it; Waybar v0.15.0 running as PID 4040). Waybar's window-rewrite returns a matched rule's value as-is, so empty-valued matches override window-rewrite-default ':default:' — chromium/librewolf/zed/whatsapp/teams/nautilus/pavucontrol/title<nvim|htop|btop|ssh|git> etc. render as nothing in the workspace pill, and pulseaudio/network show bare leading spaces unconditionally. sketchybar-app-font is installed and the surviving :name: ligature entries work, so both halves of the suggested fix (delete empty entries to fall back to :default:, or restore ligatures/Nerd Font glyphs) are valid.",
"revisedFix": null
}
},
{
"file": "dotfiles/waybar/scripts/cpu-stats.sh",
"line": 54,
"severity": "bug",
"title": "warning/critical CSS class never applies: class emitted as single space-separated string",
"detail": "Lines 53-55 build class=\"cpu warning\"/\"cpu critical\" and line 60-61 pass it to jq as one JSON string. Waybar adds a custom module's string class verbatim as ONE GTK style class (man 5 waybar-custom: \"class is a CSS class\"; arrays are required for multiple classes). GTK adds the literal class name \"cpu warning\", so the selectors `#custom-cpu.warning` / `#custom-cpu.critical` in style.css lines 101-102 never match — the yellow/red high-load coloring has never worked.",
"fix": "Emit the class as an array by splitting in jq. Replace the final jq invocation with: jq -nc --arg text \"$text\" --arg tt \"$tooltip\" --arg cls \"$class\" '{text: $text, tooltip: $tt, class: ($cls | split(\" \"))}'",
"dimension": "waybar",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed against the installed Waybar (waybar-0.15.0-2.fc44, GTK3) and its actual source at tag 0.15.0: src/modules/custom.cpp parseOutputJson() does `class_.push_back(parsed[\"class\"].asString())` when class is a JSON string, and update() calls `style->add_class(c)` on that whole string. GTK3's add_class registers \"cpu warning\" verbatim as a single style class; CSS identifiers cannot contain spaces, so the selectors at /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/waybar/style.css:101-102 (#custom-cpu.warning / #custom-cpu.critical) can never match. cpu-stats.sh lines 53-55 build the space-separated string and lines 60-61 emit it as one JSON string, exactly as described. Waybar does support class arrays (parseOutputJson iterates isArray()), and the proposed jq split was tested locally: it emits {\"class\":[\"cpu\",\"warning\"]} / {\"class\":[\"cpu\"]}. The fix is correct for cpu-stats.sh but incomplete for the described symptom: gpu-stats.sh has the identical bug (lines 62-64 build \"gpu warning\"/\"gpu critical\", lines 70-71 emit one string), leaving #custom-gpu.warning/.critical equally dead.",
"revisedFix": "Apply the array fix to BOTH scripts. In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/waybar/scripts/cpu-stats.sh lines 60-61 replace with: jq -nc --arg text \"$text\" --arg tt \"$tooltip\" --arg cls \"$class\" '{text: $text, tooltip: $tt, class: ($cls | split(\" \"))}'. In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/waybar/scripts/gpu-stats.sh lines 70-71 make the same replacement (its class strings \"gpu warning\"/\"gpu critical\" at lines 62-64 have the identical problem, so #custom-gpu.warning/#custom-gpu.critical in style.css lines 101-102 also never match). The \"no amdgpu\" fallback at gpu-stats.sh line 24 emits a single-word class and may stay as-is."
}
},
{
"file": "dotfiles/waybar/scripts/gpu-stats.sh",
"line": 63,
"severity": "bug",
"title": "warning/critical CSS class never applies: class emitted as single space-separated string",
"detail": "Same defect as cpu-stats.sh: lines 62-64 build class=\"gpu warning\"/\"gpu critical\" as one string and lines 70-71 emit it as a JSON string. Waybar/GTK treats \"gpu warning\" as a single literal class name, so `#custom-gpu.warning` / `#custom-gpu.critical` in style.css never match.",
"fix": "Replace the final jq invocation with: jq -nc --arg text \"$text\" --arg tt \"$tooltip\" --arg cls \"$class\" '{text: $text, tooltip: $tt, class: ($cls | split(\" \"))}'",
"dimension": "waybar",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed against the exact installed software. gpu-stats.sh lines 62-64/70-71 emit class as the single JSON string \"gpu warning\"/\"gpu critical\". The installed waybar-0.15.0-2.fc44 (GTK3) was checked at source level via the Fedora src.rpm: src/modules/custom.cpp lines 278-282 push a string-typed \"class\" as ONE element (class_.push_back(parsed[\"class\"].asString())) and call style->add_class() with it verbatim; splitting only occurs for JSON arrays. GTK3 class matching is exact-string/quark based and CSS idents cannot contain spaces, so style.css lines 101-102 (#custom-gpu.warning / #custom-gpu.critical) can never match. The module is live (modules.jsonc line 191, return-type json) and the system has amdgpu cards with gpu_busy_percent, so the broken path is the one that actually runs. The suggested jq fix was executed with the installed jq 1.8.1 and produces {\"class\":[\"gpu\",\"warning\"]} / {\"class\":[\"gpu\"]}, which waybar's array branch handles correctly. Same defect exists in cpu-stats.sh lines 53-55 as the finding notes.",
"revisedFix": null
}
},
{
"file": "dotfiles/waybar/scripts/media-now.py",
"line": 21,
"severity": "bug",
"title": "ICONS for mpv/firefox/chromium/chrome are empty strings; vlc icon is an uncovered CJK codepoint",
"detail": "Lines 23-26: \"mpv\": \"\", \"firefox\": \"\", \"chromium\": \"\", \"chrome\": \"\" — verified literal empty strings in the file. Playing media in any of these players renders text as ' Title — Artist' (empty icon + double space) instead of an icon. Line 27 \"vlc\": \"嗢\" is U+55E2, a CJK ideograph from old icon-font hacks; no installed font covers it (`fc-list ':charset=55e2'` empty) so VLC playback shows a tofu box.",
"fix": "Replace the empty/broken entries with covered characters, e.g. \"mpv\": \"▶\", \"firefox\": \"▶\", \"chromium\": \"▶\", \"chrome\": \"▶\", \"vlc\": \"▶\" (U+25B6 is covered by Noto Sans Mono), or with real Nerd Font glyphs once a Nerd Font is installed — and simply delete entries that duplicate \"default\".",
"dimension": "waybar",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified byte-for-byte via od: ICONS entries for mpv/firefox/chromium/chrome at lines 23-26 of dotfiles/waybar/scripts/media-now.py are literal empty strings, and line 27 vlc is U+55E2 (bytes e5 97 a2). Line 85-86 renders f\"{icon} {body}\" while Playing, so these players show \" Title — Artist\" with no icon; the empty entries also shadow the \"default\": \"▶\" fallback since dict.get only falls back on missing keys. Live-system check: fc-list ':charset=55e2' returns nothing (no font covers U+55E2 → tofu for VLC), no Nerd Font is installed (fc-list | grep -i nerd empty, even though style.css line 16 lists \"Symbols Nerd Font Mono\"), and U+25B6 is covered by Noto Sans Mono. The script is live: modules.jsonc line 171 execs it. Finding fully confirmed.",
"revisedFix": "Simpler exact fix: delete the five broken entries so lookups fall through to the existing default via ICONS.get(player.lower(), ICONS[\"default\"]). Replace lines 21-29 of /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/waybar/scripts/media-now.py with:\n\nICONS = {\n \"spotify\": \"♪\",\n \"default\": \"▶\",\n}\n\n(Equivalent to the original suggestion of setting all five to \"▶\", but without duplicating the default. If distinct per-player icons are wanted later, install a Nerd Font first — style.css already references \"Symbols Nerd Font Mono\" but none is installed — then use real PUA glyphs.)"
}
},
{
"file": "dotfiles/home/.local/bin/music",
"line": 2,
"severity": "breakage",
"title": "music execs spotify_player, which exists only via unprovisioned linuxbrew",
"detail": "Line 2 is `exec spotify_player \"$@\"`. On the live system spotify_player exists only at /home/linuxbrew/.linuxbrew/bin/spotify_player (rpm -q confirms no system package; it appears nowhere in recipes/recipe.yml or README.md). The script is advertised as a terminal widget in dotfiles/hypr/keybinds.txt:110. It only works on this machine because dotfiles/home/.zshrc:2 hardcodes `eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"`. On a fresh machine following the README flow (rebase + install.sh), or from any context without the brew PATH, it fails with `exec: spotify_player: not found` (exit 127). Same unprovisioned-linuxbrew class as the .zshrc/kitty.conf findings. (No hyprland bind, rofi entry, or startup-apps.conf line invokes music, so the failure is terminal-only.)",
"fix": "Make the wrapper self-sufficient and fail clearly: replace the file body with:\n#!/usr/bin/env bash\nPATH=\"$PATH:/home/linuxbrew/.linuxbrew/bin\"\ncommand -v spotify_player >/dev/null 2>&1 || { echo \"music: spotify_player not found (brew install spotify_player)\" >&2; exit 127; }\nexec spotify_player \"$@\"\nAdditionally document the linuxbrew bootstrap (brew install spotify_player cava) in README.md, since the image recipe does not provision it.",
"dimension": "gap:remaining-user-bin-and-small-configs",
"verdict": {
"confidence": "high",
"isReal": true,
"reason": "Verified on the live system: dotfiles/home/.local/bin/music line 2 is `exec spotify_player \"$@\"`; spotify_player exists only at /home/linuxbrew/.linuxbrew/bin/spotify_player (rpm -q: not installed; zero mentions of spotify_player or brew in recipes/recipe.yml and README.md). install.sh symlinks the wrapper to ~/.local/bin/music (symlink confirmed on disk), and keybinds.txt:110 advertises it as a terminal widget. It only resolves because dotfiles/home/.zshrc:2 evals brew shellenv; under a bare PATH `command -v spotify_player` fails, so on a fresh machine following the README flow (or from any non-zsh context) the wrapper exits 127. The finding's scoping is also accurate: no hyprland bind, rofi entry, or startup-apps.conf line invokes music. The companion claim that cava is also brew-only (relevant to the suggested README doc line) verified too.",
"revisedFix": null
}
},
{
"file": "dotfiles/home/.local/bin/viz",
"line": 2,
"severity": "breakage",
"title": "viz execs cava, which exists only via unprovisioned linuxbrew",
"detail": "Line 2 is `exec cava \"$@\"`. cava exists only at /home/linuxbrew/.linuxbrew/bin/cava (rpm -q cava: not installed; not in recipes/recipe.yml or README.md). Advertised in dotfiles/hypr/keybinds.txt:111 as a terminal widget. Works on this machine solely because .zshrc loads brew shellenv; breaks with `exec: cava: not found` (exit 127) on any machine provisioned per the README, or in any shell without brew in PATH. Same unprovisioned-linuxbrew breakage class as music/.zshrc/kitty.conf.",
"fix": "Replace the file body with:\n#!/usr/bin/env bash\nPATH=\"$PATH:/home/linuxbrew/.linuxbrew/bin\"\ncommand -v cava >/dev/null 2>&1 || { echo \"viz: cava not found (brew install cava)\" >&2; exit 127; }\nexec cava \"$@\"\nAnd document the brew dependency in README.md (or add cava as an rpm in recipes/recipe.yml — cava is packaged in Fedora repos, unlike spotify_player).",
"dimension": "gap:remaining-user-bin-and-small-configs",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system: dotfiles/home/.local/bin/viz:2 is `exec cava \"$@\"`; cava resolves only to /home/linuxbrew/.linuxbrew/bin/cava (rpm -q cava: not installed); grep finds no cava/brew provisioning in recipes/recipe.yml or README.md; keybinds.txt:111 advertises viz as a terminal widget. It works on this machine solely because dotfiles/home/.zshrc:2 evals brew shellenv, so any machine provisioned per the README (or any shell without brew in PATH) gets exit 127. Severity 'breakage' is correct, and the suggested fix (PATH fallback + command -v guard + documenting or rpm-provisioning cava, which is packaged in Fedora) is accurate and complete.",
"revisedFix": null
}
},
{
"file": "dotfiles/home/.zshrc",
"line": 2,
"severity": "breakage",
"title": ".zshrc hard-requires Homebrew, oh-my-zsh, and powerlevel10k, none of which the repo installs or documents",
"detail": "install.sh symlinks this .zshrc into $HOME (backing up the user's real one), but on a fresh system following README.md's install flow the file errors on every new shell: line 2 `eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"` fails with '.zshrc:2: no such file or directory' when linuxbrew is absent, and line 76 `source $ZSH/oh-my-zsh.sh` fails the same way when ~/.oh-my-zsh is absent, leaving the shell with no completion setup and no prompt theme (ZSH_THEME=powerlevel10k/powerlevel10k at line 12 additionally needs the theme cloned into ~/.oh-my-zsh/custom/themes/powerlevel10k). recipe.yml only installs the zsh rpm; install.sh and README.md never mention installing brew/oh-my-zsh/p10k. Verified all three exist on this live machine (so startup is currently clean, `zsh -ic true` exits 0 under a pty), but a fresh consumer of the repo gets a degraded, error-spewing shell.",
"fix": "Guard both hard dependencies so the file degrades gracefully: replace line 2 with `[[ -x /home/linuxbrew/.linuxbrew/bin/brew ]] && eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"` and line 76 with `[[ -r $ZSH/oh-my-zsh.sh ]] && source \"$ZSH/oh-my-zsh.sh\"`. Additionally document the manual prereqs in README.md's 'Apply the dotfiles' section (install Homebrew, oh-my-zsh, and `git clone --depth=1 https://github.com/romkatv/powerlevel10k ~/.oh-my-zsh/custom/themes/powerlevel10k`) or have install.sh perform/offer these installs.",
"dimension": "home-shell",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Partially confirmed, with one major correction. CONFIRMED: dotfiles/home/.zshrc line 76 (`source $ZSH/oh-my-zsh.sh`) and line 12 (ZSH_THEME=powerlevel10k/powerlevel10k) hard-depend on oh-my-zsh and the p10k theme clone, which nothing in the repo installs or documents (recipe.yml installs only the zsh rpm; install.sh only symlinks + a font; README's 'Apply the dotfiles' section is silent). On this live machine both exist only because they were manually git-cloned (verified remotes ohmyzsh/ohmyzsh and romkatv/powerlevel10k). Empirically reproduced in a throwaway $HOME: every interactive shell prints '.zshrc:source:76: no such file or directory'; omz completion/plugins never load and the theme line is dead (shell continues — ~/.p10k.zsh is internally guarded and fastfetch ships in Bazzite base). REFUTED: the line 2 brew claim. Bazzite's base image ships /usr/share/homebrew.tar.zst (131 MB) plus brew-setup.service (enabled, WantedBy=multi-user.target, ConditionPathExists=!/var/home/linuxbrew/.linuxbrew), which auto-installs Homebrew on first boot; README line 23 makes Bazzite the explicit prereq, so /home/linuxbrew/.linuxbrew/bin/brew exists for every consumer of the documented flow and line 2 never errors there. The suggested fix is therefore wrong where it tells README to instruct users to install Homebrew manually.",
"revisedFix": "In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/home/.zshrc, guard the genuinely missing dependency at line 76: replace `source $ZSH/oh-my-zsh.sh` with `[[ -r $ZSH/oh-my-zsh.sh ]] && source \"$ZSH/oh-my-zsh.sh\"`. (Guarding line 2 with `[[ -x /home/linuxbrew/.linuxbrew/bin/brew ]] && eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"` is harmless defensive hardening but not required — Bazzite's brew-setup.service installs Homebrew automatically on first boot.) In README.md's 'Apply the dotfiles' section, document only the two real manual prereqs, noting that oh-my-zsh must be installed BEFORE running install.sh (its installer overwrites ~/.zshrc, which install.sh will then back up and replace): `sh -c \"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\" \"\" --unattended` followed by `git clone --depth=1 https://github.com/romkatv/powerlevel10k ~/.oh-my-zsh/custom/themes/powerlevel10k`. Do NOT add a Homebrew install step — the Bazzite base image provides it."
}
},
{
"file": "dotfiles/hypr/hyprlock.conf",
"line": 3,
"severity": "breakage",
"title": "path = screenshot captures DPMS-off displays on every idle/suspend lock, rendering a black lock screen",
"detail": "hyprlock 0.9.5 grabs the screenshot via wlr-screencopy once at startup (verified: /usr/bin/hyprlock contains the screenshot/screencopy code paths including a 'Missing screenshot for output' fallback). But dotfiles/hypr/hypridle.conf orders the idle chain as: dpms off at 300 s (lines 7-11), loginctl lock-session at 600 s (lines 13-16), suspend at 1800 s with before_sleep_cmd = loginctl lock-session (line 3). hypridle is live (running, pid verified; launched by exec-once at hyprland.conf:28), so every non-manual lock starts hyprlock while DP-1 and DP-2 are dpms-off; a blanked output yields no fresh screencopy frame and the background falls back to default black. The blur_passes/blur_size/contrast/brightness tuning on lines 4-7 is therefore a no-op for the idle path — only the manual SUPER+L bind (hyprland.conf:104) ever shows the intended blurred-desktop background.",
"fix": "Reorder hypridle so the session locks before displays blank. In dotfiles/hypr/hypridle.conf replace lines 7-16 with:\n\nlistener {\n timeout = 300\n on-timeout = loginctl lock-session\n}\n\nlistener {\n timeout = 330\n on-timeout = hyprctl dispatch dpms off\n on-resume = hyprctl dispatch dpms on\n}\n\nAdditionally, as a fallback in hyprlock.conf, insert 'color = rgb(1e1e2e)' after line 3 so a failed screencopy degrades to the Mocha base color instead of black.",
"dimension": "gap:hyprlock-conf-unreviewed",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system, including direct evidence of the failure in this boot's Hyprland log. Config facts confirmed: dotfiles/hypr/hyprlock.conf:3 'path = screenshot'; dotfiles/hypr/hypridle.conf orders 'hyprctl dispatch dpms off' at timeout 300 (lines 7-11) before 'loginctl lock-session' at timeout 600 (lines 13-16), with before_sleep_cmd = loginctl lock-session (line 3); hypridle is running (pid 4046, stderr to /dev/null) via hyprland.conf:28; manual hyprlock bind at hyprland.conf:104; hyprlock v0.9.5, Hyprland 0.55.3. Source verification (Hyprland v0.55.3): screenshare frame copies run only in CScreenshareManager::onOutputCommit, and CMonitor::commitDPMSState calls m_output->state->setEnabled(false), so a dpms-off monitor produces no commits and a pending screencopy stalls. Decisive live evidence: /run/user/1000/hypr/unknown_1781220164_49168497/hyprland.log records this exact failure twice this boot (lines ~31305-31486 and ~36938-37065): 'dispatcher dpms : off' -> DRM 'backup slot ... assigned to disabled DP-1/DP-2' -> hyprlock binds CScreencopyProtocol, allocates 2560x1440 and 1920x1080 copy buffers, session locks -> screencopy frames stall the entire dpms-off period -> at wake ('Connector DP-1/DP-2 enabledState changed false -> true') both stalled copies fail with 'ERR ]: [src/managers/screenshare/ScreenshareFrame.cpp:180] Invalid source texture', so hyprlock takes its 'Missing screenshot for output' fallback (default near-black background color; string confirmed in /usr/bin/hyprlock). One mechanism nuance vs the finding text: the screencopy does not capture a black frame, it stalls and then errors at dpms-on — but the user-visible outcome is exactly as claimed: every idle/suspend lock renders the fallback near-black background, and the blur_passes/blur_size/contrast/brightness tuning (hyprlock.conf:4-7) only ever applies to the manual SUPER+L lock. The suggested fix is correct: locking before dpms-off (300/330) matches the upstream hypridle README example, and adding 'color = rgb(1e1e2e)' to the background block is a valid hyprlock option giving a Mocha-base fallback consistent with the input-field's rgba(1e1e2eaa).",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/monitor-arranger.py",
"line": 1,
"severity": "breakage",
"title": "Shebang resolves to linuxbrew python3, which has no 'gi' module — crashes when run from a terminal",
"detail": "Shebang is '#!/usr/bin/env python3'. In interactive shells on this machine, python3 resolves to /home/linuxbrew/.linuxbrew/bin/python3, which cannot 'import gi' (verified: ModuleNotFoundError: No module named 'gi'). /usr/bin/python3 has GTK4 bindings (verified ok). The SUPER+SHIFT+D keybinding happens to work only because Hyprland's exec PATH is '/usr/local/sbin:/usr/local/bin:/usr/bin' (verified from /proc/<Hyprland pid>/environ), so env finds /usr/bin/python3. But CLAUDE.md's documented workflow 'Or run ~/.config/hypr/monitor-arranger.py' from a terminal crashes immediately with ModuleNotFoundError at line 8.",
"fix": "Change line 1 from '#!/usr/bin/env python3' to '#!/usr/bin/python3' to pin the system interpreter that has the gi/GTK4 bindings.",
"dimension": "user-bin",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Every claim reproduced on the live system: line 1 of dotfiles/hypr/monitor-arranger.py is '#!/usr/bin/env python3'; in a user shell python3 resolves to /home/linuxbrew/.linuxbrew/bin/python3 (brew 3.14.5), which fails 'import gi' with ModuleNotFoundError; /usr/bin/python3 imports gi/Gtk 4.0 successfully. Running Hyprland's PATH is /usr/local/sbin:/usr/local/bin:/usr/bin with no python3 in /usr/local, so the SUPER+SHIFT+D bind (hyprland.conf:114) coincidentally uses /usr/bin/python3 and masks the bug, but the CLAUDE.md-documented terminal invocation of ~/.config/hypr/monitor-arranger.py crashes at line 8. The suggested fix ('#!/usr/bin/python3') is correct and complete for this immutable Bazzite system."
}
},
{
"file": "dotfiles/hypr/monitor-arranger.py",
"line": 1,
"severity": "breakage",
"title": "`#!/usr/bin/env python3` resolves to linuxbrew python3 (no gi module) in interactive shells — script crashes when run from a terminal",
"detail": "~/.zshrc puts linuxbrew first in PATH, so in any interactive shell `env python3` resolves to /home/linuxbrew/.linuxbrew/bin/python3, which has no `gi` bindings (verified: ModuleNotFoundError on `import gi`). monitor-arranger.py imports gi at module top level (lines 8-10), so running `~/.config/hypr/monitor-arranger.py` from a terminal — the exact workflow CLAUDE.md describes (\"Or run ~/.config/hypr/monitor-arranger.py\") — crashes immediately. The SUPER+SHIFT+D keybind still works only because Hyprland's PATH is /usr/local/sbin:/usr/local/bin:/usr/bin, where python3 is the system interpreter (verified Gtk 4.0 and Atspi 2.0 import OK under /usr/bin/python3). dotfiles/hypr/open-terminal-here.py:1 has the same shebang; it degrades silently instead of crashing (gi import is inside a caught try), losing the Nautilus-directory feature under brew python.",
"fix": "Change the shebang in both dotfiles/hypr/monitor-arranger.py and dotfiles/hypr/open-terminal-here.py from `#!/usr/bin/env python3` to `#!/usr/bin/python3` so they always use the system interpreter that has the GI bindings.",
"dimension": "dependency-audit",
"verdict": {
"confidence": "high",
"isReal": true,
"reason": "Verified live: (1) both scripts use `#!/usr/bin/env python3`; (2) ~/.zshrc (dotfiles/home/.zshrc line 2) evals brew shellenv, and an interactive zsh resolves python3 to /home/linuxbrew/.linuxbrew/bin/python3 (confirmed with `zsh -ic 'command -v python3'`); (3) that interpreter raises ModuleNotFoundError on `import gi` (confirmed), so monitor-arranger.py crashes at its top-level `import gi` (line 8) when run from a terminal — the exact workflow CLAUDE.md line 27 documents; (4) /usr/bin/python3 imports Gtk 4.0 and Atspi 2.0 successfully, and the running Hyprland process has PATH=/usr/local/sbin:/usr/local/bin:/usr/bin with no python3 in /usr/local, so the SUPER+SHIFT+D keybind still works — matching the finding; (5) open-terminal-here.py's gi import is inside nautilus_dir(), called under try/except Exception in main(), so under brew python it silently loses the Nautilus-directory feature rather than crashing, also as described. The suggested fix (`#!/usr/bin/python3` in both files) is correct: /usr/bin/python3 exists (symlink to python3.14, part of the ostree image) and has the required GI bindings.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/reload-watcher.lua",
"line": 26,
"severity": "breakage",
"title": "Kills the live workspace watcher, then execs a path that does not exist in ~/.config",
"detail": "Line 24 `proc.pkill_f(\"workspace-watcher\")` matches and kills the currently-running workspace-watcher.sh (verified: 3 bash processes with that cmdline are live). Line 26 then dispatches exec of `~/.config/hypr/workspace-watcher.lua`, but that symlink does not exist: install.sh was last run 2026-06-10 17:51 (verified via `ls -la ~/.config/hypr/` — zero .lua entries), before any Lua file was created. workspace-watcher.lua DOES now exist in the repo (created 2026-06-11 23:12, refining the task seed) but is unsymlinked AND mode 0644 (see separate finding). Net effect if this script is ever run (manually, or once SUPER+SHIFT+R is repointed from reload-watcher.sh): workspace assignment silently dies until next login. Currently unreachable — hyprland.conf:110 still binds reload-watcher.sh — so this is a latent landmine in untracked code.",
"fix": "Before wiring this script anywhere: (1) `chmod +x dotfiles/hypr/workspace-watcher.lua`, (2) re-run `bash dotfiles/install.sh` to create the ~/.config/hypr symlinks, (3) update hyprland.conf:110 to `bind = $mod SHIFT, R, exec, ~/.config/hypr/reload-watcher.lua` in the same commit. Optionally make line 26 defensive: `local target = os.getenv(\"HOME\") .. \"/.config/hypr/workspace-watcher.lua\"; if not posix.exists(target) then target = target:gsub(\"%.lua$\", \".sh\") end; hypr.dispatch(\"exec\", target)`.",
"dimension": "gap:lua-port-unreviewed",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system: reload-watcher.lua:24 (proc.pkill_f(\"workspace-watcher\") -> pkill -f, per lib/proc.lua:213) kills the 3 running workspace-watcher.sh processes (PIDs 4056-4058 confirmed via pgrep), then line 26 dispatches exec of ~/.config/hypr/workspace-watcher.lua, which does not exist — ~/.config/hypr/ has zero .lua symlinks (install.sh last ran 2026-06-10 17:51, before the Lua files were created 2026-06-11). hyprland.conf:110 still binds reload-watcher.sh and nothing references reload-watcher.lua, so the script is currently unreachable, but running it (manually or once wired) kills workspace assignment until next login. One detail in the finding is wrong: workspace-watcher.lua is mode 755, not 0644, so the chmod step in the original fix is unneeded. The original fix also omits repointing exec-once (hyprland.conf:33), which still launches workspace-watcher.sh at login.",
"revisedFix": "Before wiring reload-watcher.lua anywhere: (1) re-run `bash /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/install.sh` to create the missing ~/.config/hypr/*.lua and ~/.config/hypr/lib/*.lua symlinks (workspace-watcher.lua is already executable, mode 755 — no chmod needed); (2) in the same commit, update dotfiles/hypr/hyprland.conf line 110 to `bind = $mod SHIFT, R, exec, ~/.config/hypr/reload-watcher.lua` AND line 33 to `exec-once = ~/.config/hypr/workspace-watcher.lua &` (otherwise login starts the .sh watcher while SUPER+SHIFT+R swaps in the .lua one), plus the comment at line 188 and the matching entries in dotfiles/hypr/keybinds.txt and CLAUDE.md. Optionally make reload-watcher.lua:26 defensive by falling back to the .sh path if the .lua target is missing, e.g.: `local target = os.getenv(\"HOME\") .. \"/.config/hypr/workspace-watcher.lua\"; if not io.open(target) then target = target:gsub(\"%.lua$\", \".sh\") end; hypr.dispatch(\"exec\", target)`."
}
},
{
"file": "dotfiles/install.sh",
"line": 33,
"severity": "breakage",
"title": "No pruning of dangling symlinks from removed/renamed dotfiles; two broken links exist on the live system now",
"detail": "install.sh only creates links; it never removes links whose repo source has since been deleted or renamed. Verified on the live system: ~/.local/bin/playing-tui -> /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/home/.local/bin/playing-tui and ~/.config/waybar/scripts/media-status.py -> /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/waybar/scripts/media-status.py are both dangling (targets gone; the scripts were renamed to 'playing' and 'media-now.py'). 'find ~/.config ~/.local/bin -xtype l' confirms both. A dangling executable in ~/.local/bin shadows PATH lookups with 'command not found' noise, and every future rename of a dotfile will silently accumulate more dead links.",
"fix": "Add a cleanup pass after the link loop (after line 33), before the font install: \n\n# prune symlinks left behind by removed/renamed dotfiles\nfind \"$DEST\" \"$HOME/.local\" -xtype l -lname \"$SRC/*\" -print -delete 2>/dev/null || true\nfind \"$HOME\" -maxdepth 1 -xtype l -lname \"$SRC/*\" -print -delete\n\nAlso run once now to clear the two existing dead links (~/.local/bin/playing-tui, ~/.config/waybar/scripts/media-status.py).",
"dimension": "install-hygiene",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified live: /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/install.sh only creates symlinks (lines 15-33) and never prunes; both claimed dangling links exist now (~/.local/bin/playing-tui and ~/.config/waybar/scripts/media-status.py, targets renamed to 'playing' and 'media-now.py' in the repo). However the suggested fix is broken on this system: find's -lname matches the literal link-content string, and the stale links store /var/home/... targets while the README-documented invocation (bash ~/Documents/bazzite-rice/dotfiles/install.sh with HOME=/home/Hahafoot, /home -> var/home on ostree) yields SRC=/home/Hahafoot/..., so -lname \"$SRC/*\" matches nothing — confirmed by dry run (the /home-prefixed pattern matched 0 links, the /var/home one matched both). The fix must canonicalize paths; also find on ~/.local exits nonzero from permission errors, which would abort under set -euo pipefail in the fix's second line.",
"revisedFix": "Insert after line 33 of /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/install.sh (after the link loop, before the font install), using canonicalized-path comparison instead of find -lname so /home vs /var/home spelling does not matter, and -xtype l so only broken links are ever deleted:\n\n# prune symlinks left behind by removed/renamed dotfiles\nSRC_REAL=\"$(readlink -f \"$SRC\")\"\nwhile IFS= read -r -d '' link; do\n tgt=\"$(readlink -m -- \"$link\")\"\n if [[ \"$tgt\" == \"$SRC_REAL\"/* ]]; then\n rm -v -- \"$link\"\n fi\ndone < <(\n find \"$DEST\" \"$HOME/.local\" -xtype l -print0 2>/dev/null\n find \"$HOME\" -maxdepth 1 -xtype l -print0 2>/dev/null\n)\n\nVerified on the live system: readlink -m on both dangling links and readlink -f on SRC all resolve to matching /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/... prefixes, so this prunes both existing dead links regardless of which path spelling install.sh is invoked from. The process-substitution form also keeps find's nonzero exit (permission errors observed under ~/.local) from aborting the script under set -euo pipefail. Running install.sh once after this change clears the two existing dead links."
}
},
{
"file": "dotfiles/kitty/kitty.conf",
"line": 1,
"severity": "breakage",
"title": "shell hardcodes Homebrew zsh path that the image/README install flow never creates",
"detail": "Line 1 is 'shell /home/linuxbrew/.linuxbrew/bin/zsh --login'. This works on this machine only because the user happened to 'brew install zsh' (symlink to ../Cellar/zsh/5.9/bin/zsh exists). But the image recipe already installs zsh as an RPM (recipes/recipe.yml line 36; /usr/bin/zsh is zsh 5.9, identical version to the brew one), and README.md never instructs users to install zsh via brew. A fresh rebase onto ghcr.io/hahafoot/bazzite-rice:latest plus 'bash dotfiles/install.sh' yields a kitty that fails to spawn its shell in every new window until the user manually brew-installs zsh. The brew path buys nothing over the baked-in /usr/bin/zsh (both 5.9).",
"fix": "Replace line 1 with: 'shell /usr/bin/zsh --login' (guaranteed by the image recipe). If brew zsh is preferred when present, kitty cannot express a fallback in 'shell', so the portable RPM path is the right choice.",
"dimension": "terminal-launcher-theme",
"verdict": {
"confidence": "high",
"isReal": true,
"reason": "Verified on the live system: kitty.conf line 1 hardcodes /home/linuxbrew/.linuxbrew/bin/zsh, which exists here only because the brew zsh formula was manually installed (confirmed via brew list and the Cellar symlink). The repo's install flow never creates it: README.md, instructions.md, and dotfiles/install.sh contain zero brew references, while recipes/recipe.yml:36 already bakes zsh into the image (/usr/bin/zsh, rpm zsh-5.9-20.fc44 — same 5.9 as the brew build). install.sh generically symlinks dotfiles/kitty/kitty.conf into ~/.config/kitty/kitty.conf for every user, and kitty (0.47.1, recipe.yml:35) has no fallback when the configured shell path fails to exec, so on a fresh rebase + install.sh every new kitty window fails to spawn its shell. Severity 'breakage' is accurate and the suggested fix (shell /usr/bin/zsh --login) is correct and complete — valid kitty syntax, path guaranteed by the image recipe.",
"revisedFix": null
}
},
{
"file": "dotfiles/rofi/config.rasi",
"line": 8,
"severity": "breakage",
"title": "Font \"JetBrains Mono 11\" is unresolvable on the live system; rofi renders in proportional Noto Sans",
"detail": "The jetbrains-mono-fonts RPM is installed (files present in /usr/share/fonts/jetbrains-mono-fonts/*.otf, package jetbrains-mono-fonts-2.304-10.fc44), but fontconfig cannot see it: 'fc-list | grep -i jetbrains' returns 0 matches and 'fc-match \"JetBrains Mono\"' resolves to \"Noto Sans\" (a proportional font). Root cause: stale fontconfig caches — only 5 of ~35 directories under /usr/share/fonts are visible to fc-list (liberation-*, google-noto-vf, abattis-cantarell-vf-fonts); nerd-fonts and fira-code are also invisible (this additionally breaks any Nerd Font glyphs elsewhere in the rice, e.g. waybar). On ostree systems all files have mtime epoch (Dec 31 1969), so fontconfig considers old caches in /usr/lib/fontconfig/cache and ~/.cache/fontconfig valid even after font packages are layered into the image. The rofi config itself is correct; the system state breaks it.",
"fix": "Rebuild the user font cache once on the live system: 'fc-cache -rf'. For rebased users, add a build step to recipes/recipe.yml that regenerates the system cache after font packages are installed, e.g. a script module running 'fc-cache -fs'. Verify afterwards with 'fc-match \"JetBrains Mono\"'.",
"dimension": "terminal-launcher-theme",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Reproduced end-to-end on the live system. dotfiles/rofi/config.rasi:8 requests \"JetBrains Mono 11\" (live via ~/.config/rofi symlink). jetbrains-mono-fonts-2.304-10.fc44 is installed and fc-scan confirms the OTFs are valid \"JetBrains Mono\", yet fc-list shows 0 JetBrains entries and fc-match \"JetBrains Mono\" resolves to \"Noto Sans\" (also \"Fira Code\" and \"JetBrainsMono Nerd Font\" -> Noto Sans, breaking waybar/hyprlock fonts too). Only 5 of 34 /usr/share/fonts subdirs are visible to fontconfig, exactly as claimed. Mechanism verified with FC_DEBUG=16 and cache-file forensics: ostree sets all /usr mtimes to epoch so the runtime dir checksum is 0; stale checksum-0 caches covering only the 5 base dirs validate (\"FcCacheTimeValid ... cache checksum 0 dir checksum 0\"), while the caches generated during the image build (e.g. /usr/lib/fontconfig/cache/3830d5c3...-le64.cache-9, which correctly lists all 34 dirs including jetbrains-mono-fonts) recorded build-time mtimes (checksum 1781009351) and are rejected at runtime; fontconfig apps do not rescan invalid dirs. The only inaccuracy in the finding is the recipe-level fix: fc-cache already runs during the image build (RPM file triggers produced those 34-dir caches) and its output is invalidated when ostree zeroes mtimes at commit, so a bare \"fc-cache -fs\" build step would not help — the font dir mtimes must be zeroed first.",
"revisedFix": "Live system (immediate): run as the user `fc-cache -rf`, then verify with `fc-match \"JetBrains Mono\"` (should report JetBrainsMono-Regular, not Noto Sans). This writes fresh caches to ~/.cache/fontconfig recording checksum 0 (current epoch mtimes), which validate and take effect for rofi/waybar/hyprlock on next launch. Image (durable — without this, fonts added in future image updates will go invisible again because the user's checksum-0 cache for /usr/share/fonts stays \"valid\" while listing stale subdirs): add a script/run module to recipes/recipe.yml that zeroes font directory mtimes BEFORE regenerating the system cache, so the recorded checksums match the deployed ostree system (where all mtimes are epoch): `find /usr/share/fonts -type d -exec touch -d @0 {} + && fc-cache -fs`. A bare `fc-cache -fs` build step is insufficient: fc-cache already runs at build time via RPM file triggers, and its caches (keyed to real build-time mtimes) are invalidated when ostree normalizes mtimes to epoch at commit — that is precisely the broken state currently on disk in /usr/lib/fontconfig/cache."
}
},
{
"file": "dotfiles/rofi/config.rasi",
"line": 9,
"severity": "breakage",
"title": "icon-theme \"Papirus-Dark\" is not installed anywhere on the system or in the image recipe",
"detail": "Line 9 sets 'icon-theme: \"Papirus-Dark\";' with 'show-icons: true', but Papirus-Dark does not exist in /usr/share/icons (only Adwaita, AdwaitaLegacy, Bluecurve, breeze, breeze-dark, hicolor, locolor, oxygen), ~/.local/share/icons (distrobox, hicolor), ~/.icons, or the flatpak exports (/var/lib/flatpak/exports/share/icons has only Adwaita, breeze, hicolor). 'rpm -q papirus-icon-theme' confirms it is not installed, and recipes/recipe.yml does not include it. rofi silently falls back to the default theme, so drun entries whose icons only exist in Papirus render as generic/missing icons.",
"fix": "Either add 'papirus-icon-theme' to the packages list in recipes/recipe.yml (it is in the Fedora repos) and rebase/layer it, or change line 9 to an installed theme: 'icon-theme: \"Adwaita\";'.",
"dimension": "terminal-launcher-theme",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system: dotfiles/rofi/config.rasi line 9 sets icon-theme \"Papirus-Dark\" with show-icons true, and the file is symlinked into ~/.config/rofi/config.rasi consumed by installed rofi 2.0.0. Papirus is absent from every icon lookup location in XDG_DATA_DIRS (/usr/share/icons, /usr/local/share/icons, both flatpak exports), ~/.local/share/icons, and ~/.icons (nonexistent); a filesystem-wide find for '*papirus*' returns nothing and rpm -q confirms papirus-icon-theme is not installed. recipes/recipe.yml contains no papirus or icon-theme package. rofi silently falls back to the default theme chain, so the configured theme has no effect — a genuine dangling reference/missing dependency matching the 'breakage' definition. dnf repoquery confirms papirus-icon-theme-20250501-2.fc44.noarch exists in Fedora 44 repos, so the suggested fix (add to recipe packages or layer it, or change line 9 to an installed theme like Adwaita) is valid and complete.",
"revisedFix": null
}
},
{
"file": "dotfiles/rofi/config.rasi",
"line": 9,
"severity": "breakage",
"title": "rofi icon-theme \"Papirus-Dark\" is not installed anywhere on the system",
"detail": "config.rasi sets icon-theme: \"Papirus-Dark\", but the theme is absent: nothing matching Papirus* in /usr/share/icons (only Adwaita/breeze/hicolor/oxygen etc.), ~/.local/share/icons, or ~/.icons, and `rpm -q papirus-icon-theme` reports not installed. It is also not in recipes/recipe.yml. rofi falls back to the default theme chain, so any app icon shipped only in Papirus naming renders as missing; the configured theme is a dangling reference.",
"fix": "Add `papirus-icon-theme` to the packages list in recipes/recipe.yml (next to the other GUI helpers), or change config.rasi line 9 to an installed theme, e.g. icon-theme: \"Adwaita\";",
"dimension": "dependency-audit",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Every factual claim verified on the live system: dotfiles/rofi/config.rasi:9 sets icon-theme \"Papirus-Dark\" (and show-icons: true makes it active; the file is symlinked to ~/.config/rofi/config.rasi). Papirus exists nowhere — /usr/share/icons, ~/.local/share/icons, ~/.icons, and flatpak exports all lack it; rpm -q papirus-icon-theme reports not installed; filesystem-wide find for *papirus* returns nothing; grep -ri papirus recipes/ is empty. The suggested fix is viable: papirus-icon-theme-20250501-2.fc44.noarch is available in the Fedora 44 repos (verified via dnf repoquery), and the alternative \"Adwaita\" theme is present in /usr/share/icons. Minor nuance: rofi 2.0.0 falls back through hicolor/Adwaita/gnome so most app icons still render — only Papirus-exclusive icon names go missing — which is exactly what the finding states. Dangling-reference breakage confirmed; original fix is correct (note the recipe route only takes effect after image rebuild + rpm-ostree upgrade, while editing config.rasi fixes the live session immediately).",
"revisedFix": null
}
},
{
"file": "dotfiles/waybar/modules.jsonc",
"line": 206,
"severity": "breakage",
"title": "network on-click-right runs gnome-control-center, which refuses to start under Hyprland",
"detail": "gnome-control-center 50.1 (installed) contains the hard check \"Running gnome-control-center is only supported under GNOME and Unity, exiting\" keyed off XDG_CURRENT_DESKTOP; the live waybar process (pid 4040) has XDG_CURRENT_DESKTOP=Hyprland in its environment, so right-clicking the network module silently does nothing.",
"fix": "Change line 206 to: \"on-click-right\": \"env XDG_CURRENT_DESKTOP=GNOME gnome-control-center network\"",
"dimension": "waybar",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified end-to-end on the live system: modules.jsonc:206 runs \"gnome-control-center network\" on right-click; installed gnome-control-center 50.1 contains the hard check string and, when run with XDG_CURRENT_DESKTOP=Hyprland, prints \"Running gnome-control-center is only supported under GNOME and Unity, exiting\" and exits 1 (reproduced); the live waybar process (pid 4040) has XDG_CURRENT_DESKTOP=Hyprland in /proc/4040/environ, so the click handler inherits it and silently fails. The suggested fix was also verified: with XDG_CURRENT_DESKTOP=GNOME the desktop gate no longer fires (process proceeds to display connection), and waybar executes on-click commands via sh -c, so the \"env XDG_CURRENT_DESKTOP=GNOME gnome-control-center network\" form works as written.",
"revisedFix": null
}
},
{
"file": "dotfiles/waybar/modules.jsonc",
"line": 206,
"severity": "breakage",
"title": "Network module right-click `gnome-control-center network` refuses to run under Hyprland",
"detail": "Verified on this system: running gnome-control-center with XDG_CURRENT_DESKTOP=Hyprland prints \"Running gnome-control-center is only supported under GNOME and Unity, exiting\" and exits before opening any window. Waybar (and everything in the Hyprland session) has XDG_CURRENT_DESKTOP=Hyprland, so the right-click silently does nothing. keybinds.txt:101 documents this binding as functional.",
"fix": "Change line 206 to: \"on-click-right\": \"XDG_CURRENT_DESKTOP=GNOME gnome-control-center network\" (the env override is the standard workaround and works in waybar's sh -c invocation), or drop the binding since nm-connection-editor is already on left-click.",
"dimension": "dependency-audit",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Reproduced on the live system: gnome-control-center 50.1 run with XDG_CURRENT_DESKTOP=Hyprland prints \"Running gnome-control-center is only supported under GNOME and Unity, exiting\" and exits 1 before opening any window (verified with a bogus display so the check order is provable). The running waybar's /proc environ confirms XDG_CURRENT_DESKTOP=Hyprland, so the on-click-right at modules.jsonc:206 silently does nothing. keybinds.txt (~line 101) documents the binding as functional (\"right-click → GNOME Control Center → Network\"), confirming the inconsistency. The suggested fix is verified to work: with XDG_CURRENT_DESKTOP=GNOME the desktop gate is bypassed (it proceeded to display connection in testing), and waybar spawns click handlers via /bin/sh -c (string confirmed in waybar 0.15.0 binary), so the env-prefix syntax \"XDG_CURRENT_DESKTOP=GNOME gnome-control-center network\" is valid as written.",
"revisedFix": null
}
},
{
"file": "dotfiles/waybar/style.css",
"line": 16,
"severity": "breakage",
"title": "Font stack names two fonts that are not installed; the few real Nerd-Font glyphs in scripts render as tofu",
"detail": "`font-family: sans-serif, \"JetBrains Mono\", \"Symbols Nerd Font Mono\"` — neither JetBrains Mono nor any Nerd Font is installed on this system (`fc-list | grep -ci nerd` = 0; `fc-match 'JetBrains Mono'` falls back to Noto Sans; full family list is 49 Noto/Liberation/Cantarell families + sketchybar-app-font). The PUA glyphs that DO exist in the repo therefore have no covering font and render as tofu boxes on the live bar: U+F4BC (CPU icon, cpu-stats.sh line 57), U+F08AE (GPU icon, gpu-stats.sh lines 24 and 66), U+F02A0 (Heroic icon, modules.jsonc line 103). Verified with `fc-list ':charset=f4bc'` etc., all empty.",
"fix": "Install a Nerd Font, e.g. add to recipes/recipe.yml or run: mkdir -p ~/.local/share/fonts && curl -L https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.tar.xz | tar -xJ -C ~/.local/share/fonts && fc-cache -f. Then optionally reorder the stack so the specific fonts precede the generic: font-family: \"JetBrains Mono\", \"Symbols Nerd Font Mono\", sans-serif;",
"dimension": "waybar",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Every factual claim verified on the live system. style.css:16 names \"JetBrains Mono\" and \"Symbols Nerd Font Mono\"; fc-list | grep -ci nerd = 0 and fc-match 'JetBrains Mono' falls back to Noto Sans, so neither is installed. The three PUA glyphs exist exactly where cited (U+F4BC cpu-stats.sh:57, U+F08AE gpu-stats.sh:24/66, U+F02A0 modules.jsonc:103) and a full PUA scan of dotfiles/waybar/ confirms these are the only three. fc-list ':charset=f4bc/f08ae/f02a0' all return empty (control query ':charset=41' returns fonts, validating the method), so no installed font — including sketchybar-app-font — covers them; they render as tofu. Waybar is the native host rpm (waybar-0.15.0-2.fc44, running pid 4040), so host fontconfig applies. The cpu/gpu modules are wired into modules-right and the Heroic icon is in the active window-rewrite map, so the tofu appears on the live bar.",
"revisedFix": "Install a Nerd Font at user level (Fedora ships no Nerd Font package, so the recipe.yml route would need a COPR or files/script module): mkdir -p ~/.local/share/fonts && curl -L https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.tar.xz | tar -xJ -C ~/.local/share/fonts && fc-cache -f ~/.local/share/fonts (the lighter NerdFontsSymbolsOnly.tar.xz also covers all three codepoints). Installing the font alone fixes the tofu — Pango falls through the font-family list for missing glyphs even with sans-serif first — so reordering the stack in style.css:16 is optional and only changes the body-text font; keep sans-serif first if Noto Sans body text is intended."
}
},
{
"file": "dotfiles/waypaper/config.ini",
"line": 11,
"severity": "breakage",
"title": "waypaper (SUPER+SHIFT+P) cannot replace an active mpvpaper video wallpaper — empty post_command",
"detail": "config.ini pins `backend = swww` and `post_command =` (empty). Verified in the installed waypaper 2.7 source (/usr/lib/python3.14/site-packages/waypaper/changer.py): `change_with_swww` only kills swaybg/hyprpaper, never mpvpaper, and its mpvpaper kill helper matches waypaper's own `mpvpaper -f socket-<mon>` pattern, which never matches this repo's mpvpaper invocations. Since the mpvpaper layer surface is created after swww-daemon's, it stacks above it: picking a wallpaper in waypaper while a video/gif wallpaper is playing visibly does nothing, and mpvpaper keeps decoding (GPU burn). The rice's own scripts always call stop_mpvpaper_for before `swww img` for exactly this reason; waypaper bypasses that.",
"fix": "Set line 11 to `post_command = pkill mpvpaper` (waypaper runs it after applying, so the swww image becomes visible and orphaned video wallpapers die). Alternatively document that waypaper only works when no video wallpaper is active.",
"dimension": "wallpaper-system",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified against installed waypaper 2.7 source and live system. config.ini (symlinked to ~/.config/waypaper/config.ini) pins backend=swww with empty post_command (line 11); hyprland.conf line 112 binds SUPER+SHIFT+P to waypaper. /usr/lib/python3.14/site-packages/waypaper/changer.py change_with_swww (lines 118-157) kills only swaybg and hyprpaper, never mpvpaper, and its mpvpaper killer (line 42) matches 'mpvpaper -f socket-<mon>' which never matches this repo's invocation ('setsid mpvpaper -o \"no-audio loop-file=inf ...\" \"$mon\" \"$wall\"', cycle-wallpaper.sh lines ~163/~432). The repo ships many video/GIF wallpapers (.mp4/.mov/.gif in wallpapers/), and the rice's own scripts call stop_mpvpaper_for before every swww img (lines 78/147/343) precisely because mpvpaper's later-created background-layer surface stacks above swww's — so waypaper's swww img while a video wallpaper plays is invisible and mpvpaper keeps decoding. The suggested fix works: waypaper's config.py sets use_post_command=True by default and changer.py lines 249-255 execute post_command via shell Popen after applying, so 'post_command = pkill mpvpaper' kills the video layer and reveals the swww image. Stale pidfiles left in ~/.local/state/hypr are already tolerated by stop_mpvpaper_for. A $monitor-scoped pkill would be wrong here since monitors=All expands $monitor to the literal 'All'."
}
},
{
"file": ".gitignore",
"line": 3,
"severity": "inconsistency",
"title": "wallpapers/c8c78924-...webp is listed in .gitignore but is already tracked, so the ignore is a no-op and the file ships in the public repo",
"detail": "The webp was committed in 7116963 ('Add wallpaper cycling script...'); the .gitignore entry was added later in 527bd46 ('Update .gitignore to include specific wallpaper files'). gitignore never applies to tracked files, so 'git check-ignore' (index mode) does not match it and 'git ls-files' confirms wallpapers/c8c78924-51dd-4533-8c63-4a30cf9fd0b3_TABLET_LANDSCAPE_LARGE_16_9.webp is still tracked and published — directly contradicting the stated intent of 527bd46. Additionally, lines 4-5 (wallpapers/wall.png, wallpapers/image0.gif) reference files that exist neither on disk nor in the index — dead entries.",
"fix": "If the file should be excluded as intended: git rm --cached 'wallpapers/c8c78924-51dd-4533-8c63-4a30cf9fd0b3_TABLET_LANDSCAPE_LARGE_16_9.webp' && git commit (note: it remains in history; use git-filter-repo if it must be scrubbed). If it should stay, delete line 3 instead. Either way, delete the stale lines 4-5 (wallpapers/wall.png, wallpapers/image0.gif).",
"dimension": "install-hygiene",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live repo: the webp at .gitignore line 3 is tracked (git ls-files confirms), was committed in 7116963 (2026-05-21) before the ignore entry was added in 527bd46 (2026-05-22, message states intent to exclude wallpaper files). gitignore does not apply to tracked files: 'git check-ignore' exits 1 for the path while '--no-index' matches it at .gitignore:3, so the entry is a confirmed no-op and the file still ships publicly, contradicting 527bd46's intent. Lines 4-5 (wallpapers/wall.png, wallpapers/image0.gif) exist neither in the index nor anywhere on disk (recursive find over wallpapers/ found nothing) and no script generates them — dead entries. The suggested fix (git rm --cached to untrack, or delete line 3 if it should stay; delete stale lines 4-5; filter-repo caveat for history) is accurate and complete.",
"revisedFix": null
}
},
{
"file": "CLAUDE.md",
"line": 51,
"severity": "inconsistency",
"title": "CLAUDE.md pins the windowrule rationale to 'Hyprland 0.54' but the system runs 0.55.3",
"detail": "Line 51 says \"Hyprland 0.54's `windowrule` block silently *accepts but ignores* `workspace=`...\". 'hyprctl version' on the live compositor reports Hyprland 0.55.3 (image package hyprland-0.55.3-1.fc44 from the nett00n COPR). The workspace-watcher is still running (pgrep confirms), so the workaround is still in use, but the version-specific claim is stale and will mislead future edits that check behavior against 0.54 docs. (Note: instructions.md's 0.55 Lua-config claims check out — the installed Hyprland links liblua-5.4 and ships /usr/share/hypr/hyprland.lua.)",
"fix": "Reword line 51 to: \"Hyprland 0.54+'s `windowrule` block (still true on the 0.55.x this system runs) silently *accepts but ignores* `workspace=`/`assign=`/`move=` ...\" — or re-verify on each compositor bump and drop the version number.",
"dimension": "image-ci-docs",
"verdict": {
"confidence": "medium",
"isReal": true,
"reason": "Confirmed: CLAUDE.md line 51 pins the windowrule-ignores-workspace rationale to \"Hyprland 0.54\", while hyprctl version and rpm -q hyprland both report 0.55.3 on the live system. The workspace-watcher workaround is still running (pgrep PIDs 4056-4058) and hyprland.conf:188 delegates workspace assignment to it, so the doc's guidance is operative but its version-specific justification is stale — a genuine doc-vs-reality inconsistency. Confidence is medium rather than high only because the sentence is historically accurate about 0.54 and the impact is limited to potentially misleading future edits. The original suggested fix is flawed: it would insert \"(still true on the 0.55.x this system runs)\", which nothing on this system verifies — no workspace= windowrules remain in the config to observe being ignored, and 0.55 deprecated the .conf syntax entirely (per instructions.md:175-178), so the bug's persistence on 0.55.3 is untested, only the workaround's continued use is.",
"revisedFix": "In /var/home/Hahafoot/Documents/bazzite-rice/CLAUDE.md line 51, replace \"Hyprland 0.54's `windowrule` block silently *accepts but ignores* `workspace=`/`assign=`/`move=` — so this script\" with \"Hyprland's `windowrule` block silently *accepts but ignores* `workspace=`/`assign=`/`move=` (observed on 0.54; the workaround remains in use on the 0.55.3 this system now runs, and has not been retested) — so this script\". This removes the stale version pin without asserting the unverified claim that 0.55.x still has the bug."
}
},
{
"file": "dotfiles/fastfetch/config.jsonc",
"line": 6,
"severity": "inconsistency",
"title": "Logo color \"#ffa7b8\" drifts from the canonical #ffa7b9 accent",
"detail": "Line 6 sets '\"color\": { \"1\": \"#ffa7b8\" }' while the accent everywhere else in the rice is #ffa7b9 (hyprland.conf border, hyprlock.conf, waybar style.css, rofi config.rasi). Same single-bit typo as dotfiles/wlogout/style.css:24. The hex value itself works fine — verified fastfetch emits the truecolor escape ESC[38;2;255;167;184m and substitutes the $1 placeholder in ascii.txt — so this is purely a palette consistency issue.",
"fix": "Change line 6 to '\"color\": { \"1\": \"#ffa7b9\" }'.",
"dimension": "terminal-launcher-theme",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system: dotfiles/fastfetch/config.jsonc:6 sets \"#ffa7b8\" while the accent everywhere else in the rice is #ffa7b9 (hyprland.conf:43, hyprlock.conf:16, rofi/config.rasi:19-20, waybar/style.css:9-10 — 6 occurrences vs 2 of the b8 typo, the other being wlogout/style.css:24 as the finding notes). The config is symlinked into ~/.config/fastfetch/ and fastfetch 2.63.1 emits ESC[38;2;255;167;184m (blue=0xb8) for it, confirming the value renders and the drift is real but functional — correctly classified as an inconsistency, not a bug. A 1/255 channel difference has no plausible intentional purpose. The suggested fix is exact and complete.",
"revisedFix": null
}
},
{
"file": "dotfiles/home/.zshrc",
"line": 2,
"severity": "inconsistency",
"title": ".zshrc hard-depends on linuxbrew and oh-my-zsh, neither provisioned by recipe nor documented in README",
"detail": "Line 2 unconditionally evals `/home/linuxbrew/.linuxbrew/bin/brew shellenv` and line 76 sources `$ZSH/oh-my-zsh.sh` (~/.oh-my-zsh). Neither linuxbrew nor oh-my-zsh/powerlevel10k appears in recipes/recipe.yml, README.md, or instructions.md, so on a fresh system following the documented install flow every zsh start errors (`no such file or directory: /home/linuxbrew/.linuxbrew/bin/brew`, failed omz source). The brew-first PATH from line 2 is also what shadows /usr/bin/python3 with a gi-less brew python3 (see monitor-arranger.py finding). Several keybinds.txt 'terminal widgets' (music -> spotify_player, viz -> cava, pomo -> uv) are likewise brew/~/.local-only and undocumented.",
"fix": "Guard the optional pieces: `[[ -x /home/linuxbrew/.linuxbrew/bin/brew ]] && eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"` and `[[ -d $ZSH ]] && source $ZSH/oh-my-zsh.sh`; add a short 'optional extras' section to README.md listing linuxbrew (mpv/ffmpeg are already in the image; brew provides cava, spotify_player), oh-my-zsh + powerlevel10k, and uv (for pomo).",
"dimension": "dependency-audit",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Half-real. The brew claim is refuted: the Bazzite base image ships /usr/lib/systemd/system/brew-setup.service (enabled) + /usr/share/homebrew.tar.zst, which auto-extract Homebrew to /home/linuxbrew/.linuxbrew on first boot, so .zshrc:2 `eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"` works on any fresh install without user action — the claimed 'no such file or directory: .../bin/brew' error will not occur. The oh-my-zsh claim is confirmed: ~/.oh-my-zsh (+ powerlevel10k in custom/themes) is user-installed and appears nowhere in recipe.yml, install.sh, README.md, or instructions.md, so on a fresh system .zshrc:76 `source $ZSH/oh-my-zsh.sh` errors and the prompt/plugins are broken (non-fatal; zsh continues). The widget claim is partially right: music/viz/pomo wrappers ARE provisioned by install.sh (dotfiles/home/.local/bin/), but their backends (spotify_player, cava via `brew install`; uv at ~/.local/bin for pomo's `uv run --script` shebang) are manual and undocumented. The bigger undocumented brew dependency the finding missed: dotfiles/kitty/kitty.conf:1 hardcodes shell `/home/linuxbrew/.linuxbrew/bin/zsh --login` and the user's login shell is that same brew zsh — brew's zsh is NOT in the base tarball, so a fresh install needs `brew install zsh` or kitty's shell fails to launch.",
"revisedFix": "1) /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/home/.zshrc line 76: replace `source $ZSH/oh-my-zsh.sh` with `[[ -f $ZSH/oh-my-zsh.sh ]] && source $ZSH/oh-my-zsh.sh` (guard the file, not just the directory). The brew guard on line 2 is optional/harmless but unnecessary — linuxbrew is provisioned by the Bazzite base image's brew-setup.service. 2) Add a short \"Manual extras\" section to README.md that documents what install.sh does NOT provide: oh-my-zsh + powerlevel10k (`git clone https://github.com/ohmyzsh/ohmyzsh ~/.oh-my-zsh && git clone --depth=1 https://github.com/romkatv/powerlevel10k ~/.oh-my-zsh/custom/themes/powerlevel10k` — .zshrc:12 sets ZSH_THEME=powerlevel10k/powerlevel10k), `brew install zsh cava spotify_player` (brew's zsh is hardcoded as kitty's shell in dotfiles/kitty/kitty.conf:1 and as the login shell; cava/spotify_player back the `viz`/`music` widgets), and uv (https://docs.astral.sh/uv/, installs to ~/.local/bin) for the `pomo` widget. Do NOT document brew itself as a manual install — it ships with Bazzite."
}
},
{
"file": "dotfiles/hypr/cycle-wallpaper.lua",
"line": 51,
"severity": "inconsistency",
"title": "is_video() routes ALL GIFs to mpvpaper, contradicting the file header and CLAUDE.md (reproduces the .sh bug)",
"detail": "Lines 7–9 of this file say GIFs 'animate fine in swww. Only real video containers go through mpvpaper -- except when spanning', and CLAUDE.md says GIFs route through mpvpaper 'when spanning'. But `is_video` (line 51: `return VIDEO_EXTS[e] or e == \"gif\"`) is also called from the non-span path `apply_wallpaper` (line 227), so plain next/prev/init/pick of any of the repo's 3 GIFs (caption.gif, gtfotw.gif, ezgif-6422010faf15fc55.gif) spawns a persistent mpvpaper process instead of using swww. Combined with brew-only ffmpeg (see ffprobe finding), the pre-fade frame is skipped in Hyprland-spawned contexts, so cycling onto a GIF hard-cuts/black-flashes. This reproduces the same defect in cycle-wallpaper.sh:44-53 verbatim.",
"fix": "Make spanning explicit: `local function is_video(path, spanning)\\n local e = fs.ext(path)\\n return VIDEO_EXTS[e] or (spanning and e == \"gif\") or false\\nend` ; change line 487 to `if is_video(span_path, true) then` and leave line 227 as `if is_video(wall) then`. Apply the equivalent fix to cycle-wallpaper.sh if both are kept.",
"dimension": "gap:lua-port-unreviewed",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed. cycle-wallpaper.lua:51 (`return VIDEO_EXTS[e] or e == \"gif\"`) routes ALL GIFs to mpvpaper; it is called from the non-span path apply_wallpaper (line 227), which serves next/prev/init (line 536) and pick (line 507). This contradicts (a) the file's own header lines 7-9 (\"Only real video containers go through mpvpaper -- except when spanning ... (see is_video below)\") and (b) CLAUDE.md (\"swww for stills (incl. animated GIFs in non-span mode)\"). The repo has 3 GIF wallpapers, so plain cycling triggers it. Identical defect verified in cycle-wallpaper.sh:44-53 (unconditional `*.gif) return 0` vs header lines 7-8 \"Only real video containers go through mpvpaper\"); commit d722abe added the gif line with a span-only justification comment and simultaneously added animated-GIF coalesce support to apply_spanned_image that the unconditional routing renders dead — evidence the non-span rerouting was unintended. Secondary claim also verified: running Hyprland's PATH is /usr/local/sbin:/usr/local/bin:/usr/bin (no linuxbrew), while ffmpeg/ffprobe live only in /home/linuxbrew/.linuxbrew/bin, so extract_video_frame fails in Hyprland-spawned runs and GIF/video swaps hard-cut without the pre-fade. Note: the .lua is untracked; the .sh is what hyprland.conf exec-once (line 27) and pick-wallpaper.sh actually invoke, so the .sh fix is mandatory, not \"if both are kept\".",
"revisedFix": "Lua (dotfiles/hypr/cycle-wallpaper.lua): replace lines 49-52 with:\nlocal function is_video(path, spanning)\n local e = fs.ext(path)\n return VIDEO_EXTS[e] or (spanning and e == \"gif\") or false\nend\nand change line 487 from `if is_video(span_path) then` to `if is_video(span_path, true) then`. Leave line 227 (`if is_video(wall) then`) unchanged.\n\nShell (dotfiles/hypr/cycle-wallpaper.sh — REQUIRED, this is the version wired into hyprland.conf exec-once and pick-wallpaper.sh): replace lines 44-53 with:\nis_video() {\n case \"${1,,}\" in\n *.mp4|*.webm|*.mkv|*.mov) return 0 ;;\n # When spanning, GIFs route through mpvpaper: swww's per-monitor\n # animation workers run on independent clocks and drift out of sync,\n # while parallel mpv processes started together stay frame-aligned.\n *.gif) [[ \"${2:-}\" == \"span\" ]] && return 0; return 1 ;;\n *) return 1 ;;\n esac\n}\nand change the span dispatch (line 468) from `if is_video \"$span_path\"; then` to `if is_video \"$span_path\" span; then`. All other is_video call sites stay one-argument. Also update cycle-wallpaper.sh header line 8 to mention the span exception, matching the .lua header."
}
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 7,
"severity": "inconsistency",
"title": "Header comment (and CLAUDE.md) claim non-span GIFs use swww, but is_video routes ALL gifs to mpvpaper",
"detail": "Lines 7-8 say: 'animated gifs are treated as images and animate fine in swww. Only real video containers go through mpvpaper.' But `is_video()` (line 50) returns 0 for `*.gif` unconditionally, so a plain `next`/`prev`/`pick` of a .gif on one monitor also goes through mpvpaper, never swww. CLAUDE.md makes the same stale claim ('swww for stills (incl. animated GIFs in non-span mode)' and 'Animated GIFs route through mpvpaper rather than swww when spanning'). keybinds.txt line 50 likewise says 'Images use swww, videos (.mp4/.webm/.mkv/.mov) use mpvpaper' omitting gif->mpvpaper. The code's own inline comment at lines 47-49 documents the real (deliberate) behavior.",
"fix": "Update the docs to match the code. Replace lines 7-8 of cycle-wallpaper.sh with: '# Note: animated gifs route through mpvpaper in ALL modes (see is_video).' Update CLAUDE.md's wallpaper paragraph to say gifs always use mpvpaper, and keybinds.txt line 50 to 'Images use swww, videos and GIFs (.mp4/.webm/.mkv/.mov/.gif) use mpvpaper.'",
"dimension": "wallpaper-system",
"verdict": {
"confidence": "high",
"isReal": true,
"reason": "Verified against the live file: is_video() (cycle-wallpaper.sh lines 44-53) returns 0 for *.gif unconditionally and apply_wallpaper routes those to apply_video/mpvpaper, so non-span gifs never use swww — contradicting header lines 3 and 7-8, CLAUDE.md line 49 (\"swww for stills (incl. animated GIFs in non-span mode)\"), and keybinds.txt line 50. Git history confirms the docs are the stale side: e65a1cd added the gif->swww header when gif was removed from is_video; d722abe later re-added *.gif to is_video (with the span-drift inline comment) without updating the header, and the same commit updated pick-wallpaper.sh to correctly say gifs live in the \"videos\" bucket — proving the unconditional gif->mpvpaper routing is deliberate. CLAUDE.md was written 5 days after the code change and repeats the stale claim. The suggested fix's direction (update docs to match code) is right but incomplete: it leaves line 3 of the header still listing .gif under the swww image bucket.",
"revisedFix": "In /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/cycle-wallpaper.sh, replace lines 3-4 with:\n# - Images (.png .jpg .jpeg .webp .jxl) -> swww\n# - Videos/GIFs (.mp4 .webm .mkv .mov .gif) -> mpvpaper (live wallpaper)\nand replace lines 7-8 with:\n# Note: animated gifs route through mpvpaper in ALL modes (see is_video):\n# swww's per-monitor animation clocks drift, mpv instances stay frame-aligned.\n\nIn /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/keybinds.txt line 50, replace with:\n Images use swww, videos and GIFs (.mp4/.webm/.mkv/.mov/.gif) use mpvpaper.\n\nIn /var/home/Hahafoot/Documents/bazzite-rice/CLAUDE.md line 49, change \"`swww` for stills (incl. animated GIFs in non-span mode), `mpvpaper` for videos\" to \"`swww` for still images, `mpvpaper` for videos and animated GIFs\", and change the last sentence \"Animated GIFs route through `mpvpaper` rather than `swww` when spanning, because swww's per-monitor animation workers run on independent clocks and drift.\" to \"Animated GIFs always route through `mpvpaper` rather than `swww`, because swww's per-monitor animation workers run on independent clocks and drift when spanning.\""
}
},
{
"file": "dotfiles/hypr/hyprland.conf",
"line": 113,
"severity": "inconsistency",
"title": "SUPER+SHIFT+A described as 'toggle startup apps on/off' but the script removes entries permanently",
"detail": "Both the inline comment on hyprland.conf line 113 and keybinds.txt line 15 say 'toggle startup apps on/off'. toggle-startup-apps.sh never toggles anything to an `off` state: selecting an existing entry deletes the line from startup-apps.conf entirely (lines 171-190) and pkills the process; the only other action is adding a new `on` entry. The conf format has a state field but the script never writes `off`. CLAUDE.md describes the real behavior (remove or add), so the bind comment and cheatsheet are the outliers.",
"fix": "Reword hyprland.conf line 113 comment and keybinds.txt line 15 to: `add/remove startup apps (rofi menu; removing also kills the process)`. Alternatively, change toggle-startup-apps.sh to flip the state field to `off` instead of deleting the line, to match the existing wording and conf format.",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed by reading the files. hyprland.conf:113 and keybinds.txt:15 both say 'toggle startup apps on/off', but toggle-startup-apps.sh never writes an 'off' state: selecting an entry deletes its line from startup-apps.conf (lines 171-190) and pkills the process (line 192); the only write is appending 'on|...' (line 143). build_menu (line 46) even filters to state=='on', so a hand-set 'off' entry could never be re-enabled via the menu. The script's own header (lines 2-6) and CLAUDE.md describe the real remove/add behavior. The finding is actually understated: startup-apps.conf lines 13-14 also claim 'selecting an entry flips its state' — a third doc location with the same false toggle claim, missed by the original fix.",
"revisedFix": "Update all three doc locations (the original fix missed the third):\n1. /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/hyprland.conf line 113: change trailing comment from `# toggle startup apps on/off` to `# add/remove startup apps (rofi menu; removing also kills the process)`.\n2. /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/keybinds.txt line 15: change `SUPER + SHIFT + A toggle startup apps on/off (rofi menu)` to `SUPER + SHIFT + A add/remove startup apps (rofi menu; removing kills the process)`.\n3. /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/startup-apps.conf lines 13-14: change `# Edit this file directly, or use SUPER+SHIFT+A for a rofi toggle menu --\\n# selecting an entry flips its state AND starts/kills the app immediately.` to `# Edit this file directly, or use SUPER+SHIFT+A for a rofi menu --\\n# selecting an entry removes it AND kills the app; \"+ add new app...\" appends\\n# and launches a new entry.`\n(Also note the conf comment 'off = skip' remains valid since startup-apps.sh honors it for hand-edited entries — keep it. The alternative of making the script flip state to 'off' would require also changing build_menu's `[[ \"$state\" == \"on\" ]] || continue` filter at line 46 to list off entries for re-enabling; the doc reword is the smaller, safer fix.)"
}
},
{
"file": "dotfiles/hypr/hyprland.conf",
"line": 117,
"severity": "inconsistency",
"title": "SUPER+W picker: 'Enter applies to all monitors' contradicts actual span behavior",
"detail": "The comment above the SUPER+W bind (hyprland.conf lines 116-117) and keybinds.txt lines 44-45 say Enter 'applies to all monitors'. pick-wallpaper.sh line 223 actually runs `cycle-wallpaper.sh span`, which per cycle-wallpaper.sh line 16 stretches ONE image across the combined monitor area (each monitor shows a slice), not the full wallpaper on each monitor. The picker's own in-window fzf header (pick-wallpaper.sh line 170) correctly says 'ENTER: span across monitors'.",
"fix": "Change hyprland.conf line 117 comment to 'Enter spans the wallpaper across all monitors' and keybinds.txt line 45 to `Enter span across all monitors (one image stretched over both)`.",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified all four claims. hyprland.conf:117 says 'Enter applies to all monitors' and keybinds.txt:45 says 'apply to all monitors', but pick-wallpaper.sh:223 runs `cycle-wallpaper.sh span` when no target monitor is set (the SUPER+W case), and cycle-wallpaper.sh's span action (lines 465-474 via _span_layout/apply_spanned_image) stretches ONE image across the combined monitor area, giving each monitor a cropped slice. 'Apply to all monitors' instead describes cycle-wallpaper.sh's `pick PATH` with no monitor arg (full image replicated per monitor), a distinct behavior the script also implements — so the wording is genuinely misleading, not just vague. The picker's own fzf header (pick-wallpaper.sh:170) correctly says 'ENTER: span across monitors', contradicting the two docs. Doc-vs-reality and doc-vs-doc inconsistency confirmed; suggested fix is correct and complete."
}
},
{
"file": "dotfiles/hypr/hyprlock.conf",
"line": 32,
"severity": "inconsistency",
"title": "'JetBrains Mono missing' breakage is a false positive — the font is installed and resolvable by hyprlock (and by rofi/waybar)",
"detail": "The 'fc-list | grep -ci jetbrains returns 0' evidence comes from linuxbrew's fc-list/fc-match shadowing /usr/bin in PATH: FC_DEBUG shows it loads /home/linuxbrew/.linuxbrew/etc/fonts/fonts.conf, whose view covers only 5 font dirs (242 google-noto-vf files, liberation, cantarell, ~/.local/share/fonts) and misses /usr/share/fonts/jetbrains-mono-fonts entirely. The system fontconfig that hyprlock actually links (ldd /usr/bin/hyprlock -> /lib64/libfontconfig.so.1) resolves the font fine: jetbrains-mono-fonts-2.304-10.fc44 is installed (pulled in by recipes/recipe.yml line 38), /usr/bin/fc-list reports 16 'JetBrains Mono' faces, /usr/bin/fc-match 'JetBrains Mono' returns JetBrainsMono-Regular.otf, and the running Hyprland session environment contains no FONTCONFIG_FILE/FONTCONFIG_PATH override. So font_family at lines 32 and 43 renders in real JetBrains Mono — the lock-screen clock/date are not falling back to Noto Sans. The same reasoning invalidates the parallel 'confirmed' missing-font findings for dotfiles/rofi/config.rasi:8 and dotfiles/waybar/style.css:16: rofi-2.0.0-2.fc44 and waybar-0.15.0-2.fc44 are system rpms at /usr/bin linking the same system libfontconfig. (Verified clean in the same pass: line 20's '##cdd6f4aa' un-escapes to '#cdd6f4aa' per hyprlang, which Pango 1.57.1 accepts as #RRGGBBAA in span foreground; line 16's rgba(ffa7b9ee) matches the canonical #ffa7b9 accent used in hyprland.conf:43, waybar/style.css:9-10, rofi/config.rasi:19-20 — no drift; the file is live via hypridle.conf:2 and hyprland.conf:104.)",
"fix": "No change to hyprlock.conf — keep 'font_family = JetBrains Mono' at lines 32 and 43 as the canonical rice font, and withdraw the corresponding missing-font findings for dotfiles/rofi/config.rasi and dotfiles/waybar/style.css. To stop the misleading diagnostics in interactive shells, use /usr/bin/fc-list explicitly or unlink the brew fontconfig (brew unlink fontconfig) so PATH fc-* tools reflect what GUI apps actually see.",
"dimension": "gap:hyprlock-conf-unreviewed",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system: the 'JetBrains Mono missing' breakage was indeed a false positive caused by linuxbrew's fc-* tools shadowing /usr/bin in PATH. Evidence: (1) jetbrains-mono-fonts-2.304-10.fc44.noarch is installed, sourced from recipes/recipe.yml:38; (2) /usr/bin/fc-list shows 16 'JetBrains Mono' faces in /usr/share/fonts/jetbrains-mono-fonts/ and /usr/bin/fc-match 'JetBrains Mono' resolves JetBrainsMono-Regular.otf, while PATH fc-list (/home/linuxbrew/.linuxbrew/bin/fc-list) returns 0 matches and brew fc-match falls back to Noto Sans, with FC_DEBUG=1024 confirming it loads /home/linuxbrew/.linuxbrew/etc/fonts/fonts.conf; (3) hyprlock-0.9.5-5.fc44, rofi-2.0.0-2.fc44, and waybar-0.15.0-2.fc44 are system rpms whose ldd all show libfontconfig.so.1 => /lib64/libfontconfig.so.1; (4) no FONTCONFIG_FILE/FONTCONFIG_PATH in the running Hyprland process environ or systemd user environment; (5) hyprlock.conf:32/:43 use font_family = JetBrains Mono and the file is live via hypridle.conf lock_cmd and hyprland.conf:104. Therefore hyprlock/rofi (config.rasi:8) /waybar (style.css:16) all resolve the real JetBrains Mono, and the parallel missing-font findings should be withdrawn. The suggested fix (no change to hyprlock.conf; use /usr/bin/fc-list for diagnostics) is correct.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 85,
"severity": "inconsistency",
"title": "SUPER+SHIFT+D (monitor arranger) bind is undocumented in keybinds.txt",
"detail": "hyprland.conf line 114 binds `$mod SHIFT, D, exec, ~/.config/hypr/monitor-arranger.py` (verified registered via `hyprctl binds`), and both CLAUDE.md and the comment at hyprland.conf lines 2-3 advertise SUPER+SHIFT+D as the way to edit monitor layout. keybinds.txt — the cheatsheet opened by SUPER+\\ — never mentions it. Every other SUPER bind in hyprland.conf is documented; this is the only missing one.",
"fix": "Add a line to the MAINTENANCE section of dotfiles/hypr/keybinds.txt after line 85 ('SUPER + CTRL + SHIFT + R ...'):\n SUPER + SHIFT + D GUI monitor arranger (drag/rotate; Save writes monitors.conf)",
"dimension": "hyprland-core",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed: hyprland.conf line 114 binds SUPER+SHIFT+D to ~/.config/hypr/monitor-arranger.py and the bind is registered in the running compositor (verified via hyprctl binds). The comment at hyprland.conf lines 2-3 and CLAUDE.md both advertise this shortcut, yet /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/keybinds.txt (the cheatsheet opened by SUPER+\\, per line 108 of hyprland.conf) contains no mention of it. Cross-checked every other bind in hyprland.conf against keybinds.txt: all are documented; SUPER+SHIFT+D is the only omission. The suggested fix is correct and fits the MAINTENANCE section's format.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 71,
"severity": "inconsistency",
"title": "Bound XF86AudioStop key missing from MEDIA / VOLUME section",
"detail": "hyprland.conf line 144 binds `bindl = , XF86AudioStop, exec, playerctl stop`, but keybinds.txt line 71 only documents 'Play / Pause / Next / Prev'. The Stop media key is bound and works but is undocumented.",
"fix": "Change keybinds.txt line 71 from:\n Play / Pause / Next / Prev playerctl (whichever player is active)\nto:\n Play / Pause / Next / Prev / Stop playerctl (whichever player is active)",
"dimension": "hyprland-core",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed against both files. /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/hyprland.conf:144 contains `bindl = , XF86AudioStop, exec, playerctl stop`, while /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/keybinds.txt:71 documents only `Play / Pause / Next / Prev playerctl (whichever player is active)` and the word \"Stop\" appears nowhere else in the cheatsheet's media section. A bound, working media key is undocumented in the user-facing cheatsheet (SUPER+\\), which is a genuine config-vs-doc inconsistency. The suggested one-line fix is correct; only cosmetic caveat is it lengthens the left column past the section's alignment (a separate `Stop` line at the same indent would preserve the two-column layout), but that does not invalidate the fix.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 58,
"severity": "inconsistency",
"title": "Region screenshot alias annotation names a non-existent bind (SHIFT+Print)",
"detail": "keybinds.txt line 58 reads 'SUPER + SHIFT + S region (alias of SHIFT+Print)'. Plain SHIFT+Print is not bound anywhere in hyprland.conf; the actual sibling bind is SUPER+SHIFT+Print (hyprland.conf line 127). A user pressing SHIFT+Print based on this doc gets nothing.",
"fix": "Change line 58 to:\n SUPER + SHIFT + S region (alias of SUPER+SHIFT+Print)",
"dimension": "hyprland-core",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed: keybinds.txt line 58 annotates SUPER+SHIFT+S as \"(alias of SHIFT+Print)\", but plain SHIFT+Print is bound nowhere — hyprland.conf's only region screenshot binds are \"$mod SHIFT, Print\" (line 127) and \"$mod SHIFT, S\" (line 105), and the live compositor's `hyprctl binds` shows Print only with modmask 0, 64, and 65 (no SHIFT-only modmask 1). The suggested fix correctly renames the alias target to SUPER+SHIFT+Print.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 3,
"severity": "inconsistency",
"title": "Cheatsheet header claims Esc closes the window, but the pager is less (Esc does nothing)",
"detail": "keybinds.txt line 3 says '(press q or Esc to close this window)'. The cheatsheet is opened via hyprland.conf line 108 as `kitty --class=keybinds-cheatsheet -- less ~/.config/hypr/keybinds.txt`. In less, Esc is a prefix key and pressing it alone neither quits nor closes the window; only q quits. Users following the header instruction will see nothing happen.",
"fix": "Change keybinds.txt line 3 to:\n (press q to close this window)\nAlternatively, keep the doc and make Esc work by adding a lesskey binding, but editing the doc line is the simplest correct fix.",
"dimension": "hyprland-core",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified: keybinds.txt line 3 says \"press q or Esc to close this window\", but hyprland.conf line 108 opens it with `kitty ... -- less ~/.config/hypr/keybinds.txt`. On this system (less 692) Esc is only a command prefix and does not quit; there is no lesskey file, no LESS env var, no kitty Esc mapping, and the only Hyprland rule matching ^(keybinds-cheatsheet)$ merely floats/centers the window (no Escape close bind — the sole ESCAPE bind is SUPER+ESCAPE → wlogout). The documented Esc behavior therefore does not exist, exactly as the finding describes. The suggested fix (change line 3 to \"(press q to close this window)\", keeping the leading whitespace for centering) is correct and sufficient."
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 45,
"severity": "inconsistency",
"title": "keybinds.txt says ENTER 'apply to all monitors' but the picker actually SPANS",
"detail": "keybinds.txt line 45 documents the picker's ENTER as 'apply to all monitors'. In pick-wallpaper.sh the no-target ENTER runs `run_cycle span \"$abs\"` (line 223), which stretches ONE image/video across both monitors — functionally different from 'apply to all monitors' (which is what `pick` with no monitor does: same wallpaper duplicated per monitor). The picker's own fzf header (pick-wallpaper.sh line 170) correctly says 'ENTER: span across monitors'.",
"fix": "Change keybinds.txt line 45 from 'Enter apply to all monitors' to 'Enter span across all monitors'.",
"dimension": "wallpaper-system",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified: keybinds.txt line 45 says ENTER = 'apply to all monitors', but SUPER+W (hyprland.conf line 119) launches pick-wallpaper.sh with no monitor target, so plain ENTER runs `run_cycle span \"$abs\"` (pick-wallpaper.sh line 223). cycle-wallpaper.sh's span action (lines 466-474) crops one image/video across both monitors via apply_spanned_image/apply_spanned_video ('spanned across monitors' notification) — functionally different from `pick` with no monitor (lines 477-494), which duplicates the wallpaper per monitor and is what 'apply to all monitors' would mean. The picker's own fzf header (pick-wallpaper.sh line 170) says 'ENTER: span across monitors', so keybinds.txt contradicts both the code and the in-UI help. The suggested fix is correct as written.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": null,
"severity": "inconsistency",
"title": "SUPER+SHIFT+D (monitor arranger) bound in hyprland.conf but absent from keybinds.txt",
"detail": "hyprland.conf line 114 binds `$mod SHIFT, D` to ~/.config/hypr/monitor-arranger.py, and both the hyprland.conf header comment (line 3) and CLAUDE.md reference SUPER+SHIFT+D as the arranger shortcut, but the cheatsheet (keybinds.txt) has no entry for it in any section.",
"fix": "Add to the MAINTENANCE section of keybinds.txt (after line 85): ` SUPER + SHIFT + D monitor arranger (GUI: drag/resize/rotate, Save writes monitors.conf)`",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified: hyprland.conf line 114 binds $mod SHIFT, D to ~/.config/hypr/monitor-arranger.py (and the bind is live per hyprctl binds), the shortcut is documented in hyprland.conf's header comment (line 3) and CLAUDE.md (line 27), yet keybinds.txt (read in full, 136 lines) contains no entry for it in any section — including MAINTENANCE (lines 81-85). The user-facing cheatsheet shown by SUPER+\\ omits a real keybind, a genuine doc/config inconsistency.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 71,
"severity": "inconsistency",
"title": "XF86AudioStop bind missing from keybinds.txt media section",
"detail": "hyprland.conf line 144 binds `bindl = , XF86AudioStop, exec, playerctl stop`, but keybinds.txt line 71 only lists 'Play / Pause / Next / Prev playerctl (whichever player is active)' — the Stop key is undocumented.",
"fix": "Change keybinds.txt line 71 to: ` Play / Pause / Next / Prev / Stop playerctl (whichever player is active)`",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed: hyprland.conf line 144 binds XF86AudioStop to `playerctl stop`, but keybinds.txt line 71 (the only media-key line) lists only Play/Pause/Next/Prev. keybinds.txt is the live cheatsheet opened by SUPER+\\ (hyprland.conf line 108), so the Stop bind is genuinely undocumented — a real doc-vs-config inconsistency, though minor.",
"revisedFix": "Replace keybinds.txt line 71 with: ` Play/Pause/Next/Prev/Stop playerctl (whichever player is active)` — the original suggested text (`Play / Pause / Next / Prev / Stop` + 3 spaces) overflows the section's 28-character key column (descriptions align at column 33, cf. lines 68-70) and would break the cheatsheet's column alignment; the compact form keeps the description aligned."
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 58,
"severity": "inconsistency",
"title": "SUPER+SHIFT+S described as 'alias of SHIFT+Print', but SHIFT+Print is not bound",
"detail": "keybinds.txt line 58 says `SUPER + SHIFT + S region (alias of SHIFT+Print)`. The actual region bind it aliases is SUPER+SHIFT+Print (hyprland.conf line 127). Plain SHIFT+Print has no bind at all, so the cheatsheet points users at a nonexistent combo.",
"fix": "Change line 58 to: ` SUPER + SHIFT + S region (alias of SUPER+SHIFT+Print)`",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Confirmed in both config and live compositor state. keybinds.txt line 58 says SUPER+SHIFT+S is an \"alias of SHIFT+Print\", but hyprland.conf has no SHIFT+Print bind: the region screenshot is bound to `$mod SHIFT, Print` (line 127, $mod = SUPER) and `$mod SHIFT, S` (line 105). `hyprctl binds` on the running system shows `hyprshot -m region` only on key S modmask 65 and key Print modmask 65 (SUPER+SHIFT); no SHIFT-only Print bind exists. The cheatsheet therefore directs users to a nonexistent combo. The suggested fix (change line 58 to reference SUPER+SHIFT+Print) is accurate.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/keybinds.txt",
"line": 3,
"severity": "inconsistency",
"title": "Cheatsheet claims 'press q or Esc to close' but Esc does not quit less",
"detail": "SUPER+backslash (hyprland.conf line 108) opens this file in `less` inside kitty. keybinds.txt line 3 says '(press q or Esc to close this window)', but plain Esc is only a prefix key in less and does nothing on its own; Hyprland's environment has no LESS env var that would change this (verified via /proc/<hyprland>/environ). Only q closes the window.",
"fix": "Either change line 3 to '(press q to close this window)', or make Esc work by changing the bind on hyprland.conf line 108 to use `less --quit-on-intr` plus a kitty map, simplest being the doc fix.",
"dimension": "keybind-matrix",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified on the live system: keybinds.txt line 3 claims 'press q or Esc to close', but the SUPER+backslash bind (hyprland.conf line 108) execs `kitty ... -- less ~/.config/hypr/keybinds.txt` directly (no shell). Hyprland's environ has no LESS* variable, there are no lesskey files (~/.lesskey, ~/.config/lesskey), and no kitty/Hyprland Escape binding affects the window. Behavioral pty test against the installed less 692 confirmed: 'q' exits less immediately (exit 0), while ESC alone leaves less running indefinitely (killed only by timeout) — ESC is just a command prefix in less. So the cheatsheet's claim that Esc closes the window is false; only q works.</parameter>\n<parameter name=\"revisedFix\">Change /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/hypr/keybinds.txt line 3 from '(press q or Esc to close this window)' to '(press q to close this window)'. Note: the finding's alternative ('less --quit-on-intr plus a kitty map') would NOT make Esc quit — --quit-on-intr (-K) only quits on Ctrl-C/SIGINT, not Esc. If Esc support is actually wanted, use a lesskey binding instead, e.g. create dotfiles/hypr/lesskey-cheatsheet containing '#command' and '\\e quit', and change the bind to: kitty --class=keybinds-cheatsheet -- less --lesskey-src ~/.config/hypr/lesskey-cheatsheet ~/.config/hypr/keybinds.txt. The simple doc fix remains the recommended option."
}
},
{
"file": "dotfiles/hypr/lib/fs.lua",
"line": 45,
"severity": "inconsistency",
"title": "Byte-order sort diverges from the bash script's locale-collated sort — wallpaper index state files are NOT compatible between the two implementations",
"detail": "cycle-wallpaper.lua:61-62 explicitly claims 'State files keep the 0-based indices the bash version wrote, so existing state carries over.' This is false on the live system: cycle-wallpaper.sh:35 pipes find through plain `sort -z`, which uses LANG=en_US.UTF-8 collation, while fs.walk_files uses Lua table.sort (bytewise/LC_ALL=C order). Verified empirically against the live wallpapers/ tree (107 files): en_US.UTF-8 puts `c8c78924-...webp` and `caption.gif` at indices 0–1 (digit-before-letter, case-insensitive collation), C order puts `COPYRIGHTED/Big Sur Graphic-00:00.png` first, and every index between the two displaced blocks shifts by 2 — so ~100 of 107 saved indices resolve to a DIFFERENT wallpaper after switching implementation. The walker itself is correct: fs.walk_files output diffed byte-identical against `find ... -print0 | LC_ALL=C sort -z` (verified), including the space/colon/CJK names (`COPYRIGHTED/Big Sur Graphic-12:45.png`, `【東方】Bad Apple!! PV【影絵】.mp4`).",
"fix": "Align the bash side to the port's deterministic byte order: in cycle-wallpaper.sh line 35 change `-print0 | sort -z` to `-print0 | LC_ALL=C sort -z` (one-time index shift, but then both implementations and all locales agree forever). Longer term, store the wallpaper PATH instead of an index in ~/.local/state/hypr/wallpaper-index-<MON> so adds/removes/sort changes can't remap saved wallpapers.",
"dimension": "gap:lua-port-unreviewed",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified empirically on the live system. (1) cycle-wallpaper.sh:35 uses plain `sort -z` and the session locale is LANG=en_US.UTF-8 with LC_ALL/LC_COLLATE unset, so the bash wallpaper list is locale-collated. (2) fs.lua:45 uses Lua table.sort (bytewise); running fs.walk_files with cycle-wallpaper.lua's exact arguments produced output byte-identical to `find ... -print0 | LC_ALL=C sort -z` (107 files, including the space/colon/CJK names) — the walker is correct, as claimed. (3) Diffing the two orderings: 93 of 107 indices hold different files (finding said ~100; minor overcount, substance unchanged). Locale order starts with c8c78924-...webp and caption.gif; C order starts with COPYRIGHTED/Big Sur Graphic-00:00.png — exactly as described. (4) Therefore cycle-wallpaper.lua:61-62's claim that 'existing state carries over' is false for 93/107 saved indices. (5) Additional corroboration: pick-wallpaper.sh:35 already uses `LC_ALL=C sort`, so plain `sort -z` in cycle-wallpaper.sh is inconsistent even within the bash side of the repo. The bash script is the live implementation (hyprland.conf:27 runs cycle-wallpaper.sh init; nothing references the .lua ports yet), so the remap fires when the Lua port is adopted — a genuine inconsistency. The suggested fix (add LC_ALL=C to the sort at cycle-wallpaper.sh:35) is correct, minimal, and matches both fs.lua's order and pick-wallpaper.sh's existing convention.",
"revisedFix": null
}
},
{
"file": "dotfiles/hypr/startup-apps.conf",
"line": 13,
"severity": "inconsistency",
"title": "Docs claim SUPER+SHIFT+A 'flips state' / 'toggles on/off' — the script removes entries entirely and never writes 'off'",
"detail": "startup-apps.conf lines 13-14 say 'selecting an entry flips its state AND starts/kills the app immediately'; keybinds.txt line 15 says 'toggle startup apps on/off (rofi menu)'; hyprland.conf line 113 comment says '# toggle startup apps on/off'. The actual behavior in toggle-startup-apps.sh is: selecting an entry deletes its line from the conf (lines 171-190) and pkills the process (line 192) — it never starts a stopped app and never writes `off`. Compounding this, build_menu (line 46) filters to `on` entries only, while the add-time duplicate check (line 138, `$2==n` on any non-comment line) matches `off` entries too — so a manually-set `off` entry is invisible in the menu AND blocked from re-adding via the picker: a soft-lock fixable only by hand-editing the conf. CLAUDE.md and the script's own header (lines 2-6) describe the real remove-or-add behavior correctly.",
"fix": "Pick one: (a) fix the docs — startup-apps.conf lines 13-14 -> 'selecting an entry REMOVES it (and kills the app); \"+ add new app...\" appends a new entry'; keybinds.txt line 15 -> 'manage startup apps: remove entry or add new (rofi menu)'; hyprland.conf line 113 comment -> '# manage startup apps (remove/add)'; or (b) implement real toggling in toggle-startup-apps.sh: list off entries too, and on selection rewrite the state field on<->off (starting/killing accordingly) instead of deleting the line. If keeping removal semantics, also change the line-138 duplicate check to only match enabled entries: `'!/^#/ && $1==\"on\" && $2==n {found=1}'`.",
"dimension": "watchers-startup",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified directly against the files. startup-apps.conf:13-14 claims selecting an entry \"flips its state AND starts/kills the app immediately\", keybinds.txt:15 says \"toggle startup apps on/off\", and hyprland.conf:113 comments \"# toggle startup apps on/off\" — but toggle-startup-apps.sh actually deletes the selected line from the conf (lines 171-190) and pkills the process (line 192); it never writes 'off', never flips state, and never starts a stopped app from the menu. The script's own header (lines 2-6) and CLAUDE.md describe the real remove/add behavior, confirming the doc split. The secondary soft-lock claim is also real: 'off' is a documented state honored by startup-apps.sh:12, yet build_menu line 46 filters the menu to 'on' entries only while the duplicate check at line 138 ('!/^#/ && $2==n') matches any state — so a manually-set 'off' entry is invisible in the menu and blocked from re-adding via the picker, fixable only by hand-editing the conf. The suggested fix (update the three doc strings to remove/add semantics, or implement true toggling; and tighten the line-138 awk to '$1==\"on\" && $2==n' if keeping removal semantics) is accurate and complete."
}
},
{
"file": "dotfiles/kitty/kitty.conf",
"line": 1,
"severity": "inconsistency",
"title": "kitty shell hardcodes linuxbrew zsh, which neither the recipe nor README provisions",
"detail": "`shell /home/linuxbrew/.linuxbrew/bin/zsh --login` works on this machine (brew zsh 5.9 exists) but linuxbrew is never mentioned in recipe.yml, README.md, or instructions.md — the documented install flow (rebase image + run install.sh) leaves every kitty window unable to start a shell, while the image's own /usr/bin/zsh (installed by recipe.yml line 36) goes unused. SUPER+RETURN would fail for anyone following the README.",
"fix": "Change kitty.conf line 1 to `shell /usr/bin/zsh --login` (the recipe-installed zsh; brew shellenv still loads via ~/.zshrc when present).",
"dimension": "dependency-audit",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified: kitty.conf line 1 hardcodes /home/linuxbrew/.linuxbrew/bin/zsh, which on this machine is a user-installed brew formula (Cellar/zsh/5.9). Full-repo grep confirms nothing in recipe.yml, README.md, instructions.md, or install.sh provisions brew zsh — the only linuxbrew references are kitty.conf and .zshrc's brew shellenv. The recipe installs zsh as an rpm (/usr/bin/zsh exists, zsh-5.9-20.fc44, README line 15 lists it). install.sh symlinks this kitty.conf into ~/.config/kitty/, and hyprland.conf line 97 binds SUPER+RETURN to kitty, so anyone following the documented install flow gets terminals whose shell binary doesn't exist (Bazzite ships brew itself but not the zsh formula). The suggested fix is correct: /usr/bin/zsh --login works, is recipe-provisioned, and preserves behavior on the live machine since .zshrc still loads brew shellenv.",
"revisedFix": null
}
},
{
"file": "dotfiles/waypaper/config.ini",
"line": 3,
"severity": "inconsistency",
"title": "Committed waypaper config hardcodes /home/Hahafoot paths, contradicting the repo's no-hardcoded-paths convention",
"detail": "Line 3 `folder = /home/Hahafoot/Documents/bazzite-rice/wallpapers` and line 25 `stylesheet = /home/Hahafoot/.config/waypaper/style.css` bake this user's paths into a tracked dotfile. CLAUDE.md's conventions section explicitly says not to hardcode ~/Documents/bazzite-rice because users may clone elsewhere; every other wallpaper component resolves the repo via readlink. The referenced style.css also does not exist (~/.config/waypaper contains only the config.ini symlink), so it is additionally a dangling reference. Works on this machine only because /home -> /var/home and the clone location matches.",
"fix": "Either untrack the file (waypaper regenerates it; add dotfiles/waypaper/config.ini to .gitignore) or neutralize the user-specific lines: set `folder = ~/Documents/bazzite-rice/wallpapers` (waypaper expands ~) and blank the `stylesheet =` line.",
"dimension": "dependency-audit",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified: dotfiles/waypaper/config.ini is git-tracked and symlinked to ~/.config/waypaper/config.ini; lines 3 and 25 hardcode /home/Hahafoot paths, which only resolve because /home -> var/home and the clone location matches — directly contradicting CLAUDE.md's \"don't hardcode ~/Documents/bazzite-rice\" convention. ~/.config/waypaper/style.css confirmed absent (dangling, though waypaper 2.7 silently ignores it via try/except OSError in app.py:56-60, and it equals the built-in default path). Bonus confirmation: because use_xdg_state=True, waypaper's read_state() (\\_\\_main\\_\\_.py:23) overrides the folder with ~/.local/state/waypaper/state.ini's value (currently ~/Downloads), so line 3 is inert at runtime — pure dead, non-portable config. Severity \"inconsistency\" is accurate; no functional breakage on this machine.",
"revisedFix": "Preferred: untrack the file — `git rm --cached dotfiles/waypaper/config.ini` and add `dotfiles/waypaper/config.ini` to .gitignore (waypaper regenerates a default config, and this is the only option immune to waypaper rewriting the tracked file through the symlink: cf.save() at app.py:586/605/751 rewrites config.ini on GUI wallpaper changes, so the tracked copy will keep accruing machine-local diffs). Note fresh clones then lose the curated defaults (backend=swww, use_xdg_state=True, subfolders=True). If keeping it tracked instead: (a) line 3 -> `folder = ~/Documents/bazzite-rice/wallpapers` (waypaper expanduser()s it; config.py:77) — note it is overridden by the state file when use_xdg_state=True, so it only matters as a first-run hint; (b) line 25 -> `stylesheet =` (blank the VALUE but KEEP the key — if the line is deleted, waypaper falls back to the absolute default Path and its save() writes `/var/home/Hahafoot/.config/waypaper/style.css` back into the tracked file via config.py:248; an empty string round-trips safely and the missing-file open is caught by try/except OSError)."
}
},
{
"file": "dotfiles/wlogout/style.css",
"line": 24,
"severity": "inconsistency",
"title": "Hover accent color #ffa7b8 is a one-off; the rice accent everywhere else is #ffa7b9",
"detail": "Line 24 sets 'background-color: #ffa7b8;' for button focus/hover. The canonical accent used by the rest of the rice is #ffa7b9: dotfiles/hypr/hyprland.conf:43 (col.active_border rgba(ffa7b9ee)), dotfiles/hypr/hyprlock.conf:16 (outer_color rgba(ffa7b9ee)), dotfiles/waybar/style.css:9-10 (@define-color blue/mauve #ffa7b9), dotfiles/rofi/config.rasi:19-20 (blue/mauve #ffa7b9). The b8/b9 difference looks like a typo and will bite anyone re-theming by search-and-replace on the accent value.",
"fix": "Change line 24 to 'background-color: #ffa7b9;'.",
"dimension": "terminal-launcher-theme",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified: dotfiles/wlogout/style.css:24 uses #ffa7b8 while the canonical rice accent is #ffa7b9 (hyprland.conf:43, hyprlock.conf:16, waybar/style.css:9-10, rofi/config.rasi:19-20). The file is live — symlinked to ~/.config/wlogout/style.css and wlogout is installed. Git history (-S 'ffa7b8') shows no intent behind the variant. However, the finding's 'one-off' framing is wrong: #ffa7b8 also appears in dotfiles/fastfetch/config.jsonc:6, so the suggested fix is incomplete relative to its own search-and-replace re-theming rationale.",
"revisedFix": "Normalize both occurrences to the canonical accent: (1) in /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/wlogout/style.css line 24 change 'background-color: #ffa7b8;' to 'background-color: #ffa7b9;'; (2) in /var/home/Hahafoot/Documents/bazzite-rice/dotfiles/fastfetch/config.jsonc line 6 change '\"color\": { \"1\": \"#ffa7b8\" }' to '\"color\": { \"1\": \"#ffa7b9\" }'."
}
},
{
"file": "instructions.md",
"line": 112,
"severity": "inconsistency",
"title": "COPR named 'nett00n/hyprland-copr' does not exist; the real project is 'nett00n/hyprland'",
"detail": "instructions.md line 112 lists the COPR as '`nett00n/hyprland-copr`' and the illustrative recipe comment on line 77 says 'or ashbuk/Hyprland-Fedora, nett00n/hyprland-copr'. The Copr API confirms: project 'nett00n/hyprland-copr' returns {\"error\": \"Project nett00n/hyprland-copr does not exist.\"} while 'nett00n/hyprland' exists (and is what recipes/recipe.yml correctly uses, verified installed: hyprland vendor 'Fedora Copr - user nett00n'). 'hyprland-copr' is only the GitHub source repo name (line 378's URL is fine). A reader copying the doc's name into a recipe or 'dnf copr enable' gets a failure.",
"fix": "Line 77: change 'nett00n/hyprland-copr' to 'nett00n/hyprland'. Line 112: change '- **`nett00n/hyprland-copr`** — reproducible builds...' to '- **`nett00n/hyprland`** (source: github.com/nett00n/hyprland-copr) — reproducible builds...'.",
"dimension": "image-ci-docs",
"verdict": {
"isReal": true,
"confidence": "high",
"reason": "Verified directly: instructions.md lines 77 and 112 both name the COPR as 'nett00n/hyprland-copr'. Live Copr API query returns {\"error\": \"Project nett00n/hyprland-copr does not exist.\"} while 'nett00n/hyprland' exists (its description even states it is generated from the github.com/nett00n/hyprland-copr source repo). recipes/recipe.yml correctly uses 'nett00n/hyprland' and the installed hyprland 0.55.3 package vendor is 'Fedora Copr - user nett00n'. Line 378's GitHub URL is correct as-is. The doc-vs-reality inconsistency is genuine and would cause a real failure for anyone copying the doc's COPR name; the suggested fix is accurate and complete.",
"revisedFix": null
}
},
{
"file": ".github/workflows/build.yml",
"line": 7,
"severity": "improvement",
"title": "Every dotfiles/wallpapers push triggers a full image build and publish",
"detail": "paths-ignore only lists '**.md', but per CLAUDE.md 'the image and the dotfiles are independent: the image only provides binaries'. Pushes touching only dotfiles/ or wallpapers/ (e.g. recent commits 739d93d 'Add keybindings...' and 5c07f5b 'added wallpapers') rebuild and republish ghcr.io/hahafoot/bazzite-rice:latest with identical package content, wasting ~6 min of CI per push and churning image digests for every rebased machine's next update. The cron itself ('05 10 * * *' = 10:05 UTC) matches CLAUDE.md and '**.md' does not skip any build that should run.",
"fix": "Extend the filter:\n paths-ignore:\n - \"**.md\"\n - \"dotfiles/**\"\n - \"wallpapers/**\"\n(Revisit if a files module ever bakes dotfiles into the image.) Also update CLAUDE.md line 41's '(paths-ignore: **.md)' to match.",
"dimension": "image-ci-docs"
},
{
"file": ".github/workflows/build.yml",
"line": null,
"severity": "improvement",
"title": "No concurrency block — overlapping runs can race pushing :latest",
"detail": "The workflow has push, nightly schedule, and workflow_dispatch triggers but no concurrency group. Two builds running simultaneously (e.g. a push landing near 10:05 UTC, or rapid consecutive pushes) can finish out of order, so an older build can overwrite a newer image at ghcr.io/hahafoot/bazzite-rice:latest. The upstream BlueBuild template ships a concurrency block for exactly this reason.",
"fix": "Add under the top-level 'on:' section:\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref || github.run_id }}\n cancel-in-progress: true",
"dimension": "image-ci-docs"
},
{
"file": "dotfiles/home/.local/bin/cube",
"line": 73,
"severity": "improvement",
"title": "cube clobbers the user's terminal contents instead of using the alternate screen buffer",
"detail": "Line 73 clears the main screen on start (`write(\"\\x1b[?25l\\x1b[2J\")`) and restore() at line 60 clears it again on exit (`\\x1b[0m\\x1b[?25h\\x1b[2J\\x1b[H`), so quitting the animation leaves a blank terminal — whatever shell output was on screen before launching is permanently wiped. Everything else about teardown is correct (termios restored in a finally block at line 167-168 plus SIGINT/SIGTERM handlers at lines 69-70; size re-read each frame so no SIGWINCH handler needed; imports are stdlib-only so the linuxbrew python3 shebang is benign; no Nerd Font glyphs). Switching to the alternate screen buffer, as full-screen TUIs conventionally do, preserves prior terminal content with a 2-character change.",
"fix": "Line 73: write(\"\\x1b[?1049h\\x1b[?25l\\x1b[2J\")\nLine 60 (in restore()): write(\"\\x1b[0m\\x1b[?25h\\x1b[2J\\x1b[H\\x1b[?1049l\")",
"dimension": "gap:remaining-user-bin-and-small-configs"
},
{
"file": "dotfiles/home/.local/bin/pomo",
"line": 124,
"severity": "improvement",
"title": "refresh_tasks sets ListView.index synchronously after clear()/append(), which Textual processes asynchronously",
"detail": "ListView.clear() and ListView.append() return awaitables in Textual (>=0.85 per the script header); the actual DOM removal/mount completes on the message pump after refresh_tasks() returns. Setting 'lv.index = target' (line 134) immediately after can be validated against a stale or partially-updated child list, so the selection/highlight is intermittently lost or lands on the wrong row after adding, cycling, or deleting a task.",
"fix": "Make refresh_tasks async and await the mutations: 'await lv.clear()' then 'await lv.extend(items)' before assigning lv.index, and call it via self.call_after_refresh(...) or from async action_ methods (Textual actions may be 'async def').",
"dimension": "user-bin"
},
{
"file": "dotfiles/home/.local/bin/portal",
"line": 54,
"severity": "improvement",
"title": "portal clobbers the user's terminal contents instead of using the alternate screen buffer",
"detail": "Identical issue to cube: line 54 clears the main screen on start and restore() at line 40 clears it again on exit, wiping the user's prior shell output. Teardown is otherwise correct (finally-block termios restore at lines 129-130, SIGINT/SIGTERM handlers at lines 49-50, per-frame size re-read, stdlib-only imports, ASCII-only character ramp).",
"fix": "Line 54: write(\"\\x1b[?1049h\\x1b[?25l\\x1b[2J\")\nLine 40 (in restore()): write(\"\\x1b[0m\\x1b[?25h\\x1b[2J\\x1b[H\\x1b[?1049l\")",
"dimension": "gap:remaining-user-bin-and-small-configs"
},
{
"file": "dotfiles/home/.zshrc",
"line": 112,
"severity": "improvement",
"title": "Unconditional fastfetch run pollutes any programmatic interactive shell",
"detail": "`fastfetch` is invoked unguarded as the last line of .zshrc, so every interactive zsh — including programmatic invocations like `zsh -ic '<cmd>'` used by scripts/tools to capture output — gets ~35 lines of ASCII-art system info prepended to its output (observed directly: a sandboxed `zsh -ic true` here emitted the full fastfetch banner). It also re-renders in every nested shell. Guarding on stdin being a tty keeps the banner for real terminals while silencing scripted shells.",
"fix": "Replace line 112 `fastfetch` with `[[ -t 0 ]] && fastfetch` (stdin-is-a-tty guard; real terminal sessions still show the banner, `zsh -ic` from scripts without a pty skips it).",
"dimension": "home-shell"
},
{
"file": "dotfiles/hypr/cycle-wallpaper.lua",
"line": 118,
"severity": "improvement",
"title": "Stale-PID kill reproduced from .sh: pidfile PID killed without verifying it is still mpvpaper",
"detail": "stop_mpvpaper_for (lines 116-122) reads ~/.local/state/hypr/mpvpaper-<MON>.pid and kills that PID unconditionally. State files persist across reboots, and mpvpaper instances die with the session, so after a reboot (or mpvpaper crash) the recorded PID can belong to an unrelated process, which gets SIGTERMed on the next wallpaper change. The trailing `pkill -f` makes the pidfile kill redundant in the common case, so the unverified kill is pure downside. Identical defect exists in cycle-wallpaper.sh:65-74; the port reproduces rather than fixes it.",
"fix": "Verify the process identity before killing: `local pid = tonumber((posix.read_file(pidfile) or \"\"):match(\"%d+\"))\\nif pid then\\n local comm = (posix.read_file(\"/proc/\" .. pid .. \"/comm\") or \"\"):gsub(\"%s+$\", \"\")\\n if comm == \"mpvpaper\" then proc.kill(pid) end\\nend",
"dimension": "gap:lua-port-unreviewed"
},
{
"file": "dotfiles/hypr/cycle-wallpaper.lua",
"line": 359,
"severity": "improvement",
"title": "Unbounded caches reproduced: spanned-cache and video-frames grow forever",
"detail": "Cache keys for spanned slices (line 361) include source mtime AND the monitor-layout hash, so every layout change (monitor-arranger drag, scale change, plug/unplug) and every file touch strands the previous full-resolution PNG slices (one per monitor, multi-MB each at 2560x1440) in ~/.local/state/hypr/spanned-cache forever; video-frames/ (line 167) likewise accumulates one PNG per video per mtime. Nothing ever prunes either directory. Reproduced from cycle-wallpaper.sh (lines 117, 300); the port was the opportunity to fix it.",
"fix": "After a successful cache write in apply_spanned_image and extract_video_frame, prune entries not accessed recently, e.g. `proc.call({ \"find\", cache_dir, \"-type\", \"f\", \"-atime\", \"+30\", \"-delete\" })`, or keep only the N most-recent keys per monitor by mtime.",
"dimension": "gap:lua-port-unreviewed"
},
{
"file": "dotfiles/hypr/cycle-wallpaper.lua",
"line": 532,
"severity": "improvement",
"title": "Span state not persisted (reproduced): `init` degrades a spanned wallpaper to independent per-monitor crops",
"detail": "Both span paths call save_index(mon, wall) per monitor (lines 387, 456), storing only the flat wallpaper index. On next login, `cycle-wallpaper.lua init` (hyprland.conf:27 equivalent) replays each index through apply_wallpaper → apply_image/apply_video per monitor, so a wallpaper the user explicitly spanned comes back as N independent crop-to-fill copies instead of one spanned image. Reproduced exactly from cycle-wallpaper.sh:348-352/439-443 — the known .sh limitation carried over.",
"fix": "Persist a span marker, e.g. in the span action write `posix.write_file(STATE_DIR .. \"/wallpaper-span\", span_path .. \"\\n\")` and have apply_image/apply_video/next/prev unlink it; in the init path, before the per-monitor loop: `local sp = (posix.read_file(STATE_DIR .. \"/wallpaper-span\") or \"\"):match(\"[^\\n]+\"); if action == \"init\" and sp and posix.exists(sp) then if is_video(sp, true) then apply_spanned_video(sp) else apply_spanned_image(sp) end os.exit(0) end`.",
"dimension": "gap:lua-port-unreviewed"
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 514,
"severity": "improvement",
"title": "Span state is not persisted — `init` re-applies a spanned wallpaper unspanned after Hyprland restart",
"detail": "The per-monitor state files (`wallpaper-index-<MON>`) record only the index into the walls array (written at lines 348-352, 439-443, 488, 519). When a wallpaper was applied with `span`, nothing records that fact, so the `exec-once ... cycle-wallpaper.sh init` at Hyprland startup (hyprland.conf line 27) replays it via apply_to_monitor -> apply_wallpaper, putting the FULL image/video letterboxed on each monitor instead of the per-monitor spanned crops the user had before restart.",
"fix": "On span, write the spanned path to `$STATE_DIR/wallpaper-span` (and remove it in the `pick`/`next`/`prev` paths). In the `init` action, check that file first: if it exists and the path is still present, call apply_spanned_video/apply_spanned_image and exit instead of per-monitor apply.",
"dimension": "wallpaper-system"
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 70,
"severity": "improvement",
"title": "stop_mpvpaper_for kills a stale PID without verifying it is mpvpaper",
"detail": "`$STATE_DIR/mpvpaper-$mon.pid` persists across reboots/sessions (STATE_DIR is ~/.local/state/hypr). At session start `cycle-wallpaper.sh init` runs and `kill \"$pid\"` (line 70) signals whatever process currently holds that PID — after a reboot PID reuse can hit an unrelated user process. The pkill fallback on line 73 is name-checked, but the pidfile kill is not.",
"fix": "Verify the process name before killing: replace line 70 with:\n [[ -n \"$pid\" && \"$(ps -o comm= -p \"$pid\" 2>/dev/null)\" == mpvpaper ]] && kill \"$pid\" 2>/dev/null || true",
"dimension": "wallpaper-system"
},
{
"file": "dotfiles/hypr/cycle-wallpaper.sh",
"line": 300,
"severity": "improvement",
"title": "spanned-cache and video-frames caches grow without bound (already 420 MB on this machine)",
"detail": "Cache keys include file mtime and (for spans) the monitor-layout hash, so every monitor rearrangement and every wallpaper-file change strands the old entries forever. Current sizes on this system: ~/.local/state/hypr/spanned-cache = 249 MB, ~/.local/state/hypr/video-frames = 171 MB, plus ~/.cache/wallpaper-picker/thumbs (888 KB). Nothing ever prunes them, and they live in XDG_STATE (not cache), so system cache cleaners won't touch them.",
"fix": "Add a cheap prune at script start (or after a successful apply), e.g.:\n find \"$STATE_DIR/spanned-cache\" \"$STATE_DIR/video-frames\" -type f -atime +30 -delete 2>/dev/null || true\nOptionally move both dirs under ${XDG_CACHE_HOME:-$HOME/.cache} since they are regenerable caches, not state.",
"dimension": "wallpaper-system"
},
{
"file": "dotfiles/hypr/hypridle.conf",
"line": 7,
"severity": "improvement",
"title": "Screen blanks 5 minutes before the session locks, leaving an unlocked dark-screen window",
"detail": "Listener 1 (lines 7-11) runs 'hyprctl dispatch dpms off' at 300s, but the lock listener (lines 13-16) only fires 'loginctl lock-session' at 600s. Between 300s and 600s the displays are dark but the session is unlocked: any keypress/mouse move triggers on-resume 'dpms on' and lands directly on the unlocked desktop. The conventional hypridle chain locks first (or simultaneously) and blanks shortly after, so a dark screen always implies a locked session. The rest of the chain is sound (suspend at 1800s, before_sleep_cmd locks before suspend, after_sleep_cmd restores DPMS, lock_cmd is guarded with pidof).",
"fix": "Reorder so lock precedes blank, e.g. replace the two listeners with:\nlistener {\n timeout = 300\n on-timeout = loginctl lock-session\n}\n\nlistener {\n timeout = 330\n on-timeout = hyprctl dispatch dpms off\n on-resume = hyprctl dispatch dpms on\n}",
"dimension": "lock-idle-session"
},
{
"file": "dotfiles/hypr/hyprland.conf",
"line": null,
"severity": "improvement",
"title": "Audit coverage summary: 64/64 binds parsed, 63 functionally verified correct",
"detail": "Informational coverage entry. All 64 bind/bindm/bindl/bindle/binde/bindel lines in hyprland.conf register in the running Hyprland 0.55.3 (hyprctl binds count matches 64; hyprctl configerrors empty). Zero duplicate modmask+key combos (verified via hyprctl binds matrix). Zero submaps defined or registered. All exec targets exist: kitty, nautilus, rofi, hyprlock, hyprshot, wlogout, steam, waybar, waypaper, hyprctl, cliphist, wl-copy, wpctl, playerctl, killall, less all in /usr/bin; all 6 repo scripts symlinked into ~/.config/hypr and executable; flock used by brightness.sh exists at /usr/bin/flock (within Hyprland's PATH=/usr/local/sbin:/usr/local/bin:/usr/bin). DP-1/DP-2 in `swapactiveworkspaces` and pick-wallpaper Alt-1/Alt-2 match live `hyprctl monitors -j`. XF86 coverage complete: RaiseVolume/LowerVolume (bindel), Mute/MicMute/Play/Pause/Next/Prev/Stop (bindl), MonBrightnessUp/Down (bindel); brightness binds match brightness.sh's `up|down [step]` interface. The only functionally incorrect bind is SUPER+C's cancel path (separate bug finding); the remaining findings are documentation mismatches.",
"fix": "No action required — informational.",
"dimension": "keybind-matrix"
},
{
"file": "dotfiles/hypr/lib/json.lua",
"line": 99,
"severity": "improvement",
"title": "JSON null inside arrays silently collapses/shifts elements",
"detail": "parse_value returns Lua nil for null, and parse_array does `arr[#arr + 1] = v`, so `[1,null,2]` decodes to `{1,2}` (verified by running the parser: len=2, element positions shifted left). The doc comment at lines 142-143 only acknowledges null OBJECT values vanishing; array-element shifting is a different and nastier failure (later elements change index). Currently latent: live `hyprctl monitors -j` and `hyprctl clients -j` were decoded correctly (2 monitors incl. transform=1 rotation, 6 clients) and contain zero nulls; unicode escapes, surrogate pairs, floats, and nested arrays all verified correct. But any future consumer whose payload can contain nulls (playerctl/waybar JSON) gets silently corrupted arrays.",
"fix": "In parse_array, use an explicit counter so positions are preserved: `local arr, n, i = {}, 0, skip_ws(str, pos + 1)` ... in the loop: `v, i = parse_value(str, i); n = n + 1; arr[n] = v` and return `arr` with its length documented (or introduce a `json.null` sentinel table returned by parse_value for null and document it). Update the comment at lines 142-143 accordingly.",
"dimension": "gap:lua-port-unreviewed"
},
{
"file": "dotfiles/hypr/monitor-arranger.py",
"line": 427,
"severity": "improvement",
"title": "Current mode is always inserted as a duplicate entry in the Mode dropdown",
"detail": "In _on_canvas_select (lines 425-434), 'current' is mon.mode_str (e.g. '2560x1440@165' from refresh 165.00000 with :g formatting) while available_modes entries carry an 'Hz' suffix and two decimals (verified from hyprctl monitors all -j: '2560x1440@165.00Hz'). 'current not in modes' is therefore always true, so the current mode is unconditionally inserted at index 0, and the 'seen' dedup key ('2560x1440@165' vs '2560x1440@165.00') never matches — every monitor's dropdown shows the active mode twice in two spellings.",
"fix": "Dedup on the parsed tuple instead of the string: key = parse_mode(m.replace('Hz','')); skip entries whose (w,h,round(r,2)) tuple is already in 'seen', and compare against parse_mode(current) when deciding whether to insert the current mode.",
"dimension": "user-bin"
},
{
"file": "dotfiles/hypr/open-terminal-here.py",
"line": 1,
"severity": "improvement",
"title": "Same brew-python shadowing: Nautilus-folder detection silently disabled when run outside Hyprland",
"detail": "Shebang '#!/usr/bin/env python3' resolves to brew python3 (no 'gi') in interactive shells. The script degrades gracefully — the ImportError from 'import gi' inside nautilus_dir() is swallowed by the try/except in main() (lines 118-121), so it falls back to plain kitty — but that makes terminal testing (including the provided '--dry' debug flag) silently return None even when the AT-SPI path would work, which is misleading when debugging. Via the keybinding it uses /usr/bin/python3 where gi+Atspi import fine (verified; dry run exits 0).",
"fix": "Change line 1 to '#!/usr/bin/python3' so the script behaves identically from the keybinding and from a terminal/--dry invocation.",
"dimension": "user-bin"
},
{
"file": "dotfiles/hypr/pick-wallpaper.sh",
"line": 170,
"severity": "improvement",
"title": "Monitor names DP-1/DP-2 hardcoded in picker header and ALT-1/ALT-2 handlers",
"detail": "Lines 170 (header text) and 217-218 (`alt-1) run_cycle pick \"$abs\" DP-1` / `alt-2) ... DP-2`) hardcode the current monitor names, while every other code path queries `hyprctl monitors -j`. This matches the live system today (hyprctl reports DP-1 and DP-2), but if a monitor is unplugged or renamed, ALT-N silently targets a nonexistent output and cycle-wallpaper.sh then dies on the swww error under set -e with no notification (the detached video path swallows it entirely).",
"fix": "Build the monitor list dynamically: `mapfile -t mons < <(hyprctl monitors -j | jq -r '.[].name')`, generate `--expect=alt-1,alt-2,...` and the header from it, and map `alt-N` back to `${mons[N-1]}` in the final case statement.",
"dimension": "wallpaper-system"
},
{
"file": "dotfiles/hypr/reload-watcher.lua",
"line": 24,
"severity": "improvement",
"title": "Widened extensionless pkill pattern can kill the user's editor during the documented edit-then-reload workflow",
"detail": "The pattern was deliberately widened from `workspace-watcher.sh` to `workspace-watcher` (comment, lines 8-9) to also reap the bash-era watcher. But `pkill -f -- workspace-watcher` matches ANY cmdline containing the substring — including a terminal editor session like `nvim workspace-watcher.lua`. The documented workflow (workspace-watcher.lua:9, CLAUDE.md) is precisely 'edit RULES, then press SUPER+SHIFT+R', so a user editing the rules in a terminal editor gets their editor SIGTERMed by the reload they were told to press. (The .sh had the same hazard for `vim workspace-watcher.sh`; the wider pattern extends it to the .lua too.)",
"fix": "Anchor the pattern to an interpreter-launched script: `proc.pkill_f(\"^[^ ]*(bash|luajit) [^ ]*workspace-watcher\")` — still matches the running `bash /home/.../workspace-watcher.sh` and a future `luajit .../workspace-watcher.lua`, but not editors/pagers/greps whose argv[0] differs.",
"dimension": "gap:lua-port-unreviewed"
},
{
"file": "dotfiles/hypr/toggle-startup-apps.sh",
"line": 127,