Repository navigation
Expand file tree
/
Copy pathtest.sh
More file actions
executable file
·608 lines (605 loc) · 87.1 KB
/
Copy pathtest.sh
File metadata and controls
executable file
·608 lines (605 loc) · 87.1 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
#!/usr/bin/env bash
# Smoke test, run inside the built image: docker run --rm -v ./test.sh:/t.sh fennek:dev bash /t.sh
set -euo pipefail
fail() { echo "FAIL: $*"; exit 1; }
x86() { [ "$(uname -m)" = x86_64 ]; } # .exe runner (umu): x86-64 only so far (ARM: FEX)
grep -q '^SHELL=/bin/zsh' /etc/default/useradd || fail "default shell"
[ "$(zsh -ic 'alias dir' 2>/dev/null)" = "dir='ls -lah'" ] || fail "zsh aliases"
[ "$(bash --rcfile /etc/skel/.bashrc -ic 'alias cls' 2>/dev/null)" = "alias cls='clear'" ] || fail "bash aliases"
[ "$(zsh -ic 'a=$SAVEHIST; history-off >/dev/null; b=$SAVEHIST; history-on >/dev/null; print "$a $b $SAVEHIST"' 2>/dev/null)" = "10000 0 10000" ] \
|| fail "terminal history on/off"
zsh -ic 'typeset -f _zsh_autosuggest_start' >/dev/null 2>&1 || fail "zsh autosuggestions"
if x86; then command -v umu-run >/dev/null || fail "umu-run"; else command -v FEXInterpreter >/dev/null && command -v muvm >/dev/null || fail "FEX (x86-64 programs on ARM)"; fi
grep -q '^Exec=fennek-run-exe %f' /usr/share/applications/fennek-run-exe.desktop || fail "exe launcher"
fennek-run-exe --selftest >/dev/null && grep -q "^Exec=fennek-run-exe --choose %f" /usr/share/kio/servicemenus/fennek-run-exe-choose.desktop || fail "run-exe selftest (remembered choices) + Choose how to run"
grep -q '^application/x-ms-dos-executable=fennek-run-exe.desktop' /etc/xdg/mimeapps.list || fail "exe default"
[ -x /usr/libexec/fennek-open-image ] && grep -q '^application/vnd.efi.iso=fennek-open-image.desktop' /etc/xdg/mimeapps.list || fail "disc images open like a disc"
/usr/libexec/fennek-package-help --selftest >/dev/null && grep -q '^application/x-rpm=fennek-package-help.desktop' /etc/xdg/mimeapps.list && grep -q '^application/vnd.appimage=fennek-package-help.desktop' /etc/xdg/mimeapps.list && ! grep -q '^application/vnd.appimage application/x-executable' /usr/share/mime/subclasses && [ -e /usr/lib64/libfuse.so.2 ] || fail "downloaded Linux programs: a clear answer, and AppImages can start"
grep -q '^AutomaticUpdatePolicy=stage' /etc/rpm-ostreed.conf || fail "update policy"
[ -L /etc/systemd/system/timers.target.wants/rpm-ostreed-automatic.timer ] || fail "update timer"
grep -q 'Indexing-Enabled=false' /etc/xdg/baloofilerc || fail "baloo off"
grep -q '^NAME="Fennek OS"' /usr/lib/os-release || fail "os-release name"
grep -q '^ID=fedora' /usr/lib/os-release || fail "os-release id must stay fedora"
! rpm -q plasma-welcome-fedora >/dev/null || fail "fedora welcome pages"
! grep -q "Fennek image build" /etc/dnf/dnf.conf || fail "build-time dnf settings left in the image"
[ -s /usr/share/wallpapers/Fennek/contents/images/3840x2160.jpg ] || fail "wallpaper"
[ "$(magick /usr/share/wallpapers/Fennek/contents/images/3840x2160.jpg -crop 1x1+1920+1290 -format '%[fx:r>b && r>0.6]' info:)" = 1 ] \
|| fail "wallpaper isn't the desert at dusk (warm horizon)"
grep -q '^Image=Fennek' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/defaults || fail "lnf wallpaper"
[ "$(kreadconfig6 --file /usr/share/plasma/desktoptheme/fennek-dark/colors --group Colors:Selection --key BackgroundNormal)" = 109,69,184 ] && ! grep -q "29,153,243" /usr/share/plasma/desktoptheme/fennek-dark/colors || fail "panel accent is Fennek violet"
# Desktop update scripts run once and silently: at least check they are well-formed (brackets, quotes).
python3 - <<'PY' || fail "a desktop update script is malformed"
import glob, re
for f in glob.glob("/usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fennek-*.js"):
# strings and comments in one pass: "preferred://x" is a string, not a comment
code = re.sub(r'"(?:\\.|[^"\\])*"|//[^\n]*', lambda m: '""' if m.group(0)[0] == '"' else "", open(f).read())
stack, pairs = [], {")": "(", "]": "[", "}": "{"}
for ch in code:
if ch in "([{": stack.append(ch)
elif ch in ")]}": assert stack and stack.pop() == pairs[ch], f
assert not stack and code.count("'") % 2 == 0, f
PY
[ -f /usr/share/icons/Fennek/scalable/places/folder-documents-symbolic.svg ] || fail "monochrome folder icons in the Fennek set"
[ -s /usr/share/fennek/files-first-window-state ] && grep -q "files-first-window-state" /usr/libexec/fennek-first-login || fail "Files: first window with menu bar, side panels closed"
fam=$(mktemp -d)
grep -q "zsh-familiar.zsh" /etc/zshrc \
&& [[ $(echo n | zsh -c "source /usr/share/fennek/zsh-familiar.zsh; md '$fam/a b'" 2>&1) == *"this is: mkdir -p $fam/a\\ b"* ]] && [ ! -e "$fam/a b" ] \
&& echo y | zsh -c "source /usr/share/fennek/zsh-familiar.zsh; md '$fam/a b'" >/dev/null 2>&1 && [ -d "$fam/a b" ] \
&& ! zsh -c "source /usr/share/fennek/zsh-familiar.zsh; nosuchcommand" 2>/dev/null \
|| fail "terminal: classic command hints: $(echo n | zsh -c "source /usr/share/fennek/zsh-familiar.zsh; md '$fam/a b'" 2>&1 | tr '\n' '|') / y: $(echo y | zsh -c "source /usr/share/fennek/zsh-familiar.zsh; md '$fam/c'" 2>&1 | tr '\n' '|') $(ls "$fam")"
grep -q "zsh-prompt.zsh" /etc/zshrc &&zsh -c "source /usr/share/fennek/zsh-prompt.zsh; [[ \$PROMPT == *❯* ]]" || fail "terminal prompt"
command -v kioclient >/dev/null && for id in $(grep -o 'id: "[a-z.-]*"' /usr/share/plasma/plasmoids/org.fennek.searchbox/contents/ui/main.qml | cut -d\" -f2); do
[ -f "/usr/share/applications/$id.desktop" ] || fail "search starter app missing: $id"; done || fail "kioclient (search starter apps)"
# Existing desktops follow layout changes by themselves (Plasma update scripts, once per user): no reset needed.
u=/usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fennek-2026-10-16-taskbar.js
[ -f "$u" ] && grep -q 'org.fennek.taskview' "$u" && grep -q 'hideSearchField' "$u" && [ "$(kreadconfig6 --file /etc/xdg/plasmarc --group Theme --key name)" = fennek-light ] && grep -q "^name=fennek-light" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/defaults && grep -q "^name=fennek-dark" /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults && [ -f /usr/share/plasma/desktoptheme/fennek-light/widgets/tasks.svg ] && [ -f /usr/share/plasma/desktoptheme/fennek-dark/widgets/tasks.svg ] && python3 -c "import json; assert json.load(open('/usr/share/plasma/desktoptheme/fennek-dark/metadata.json'))['KPlugin']['Id'] == 'fennek-dark'" || fail "taskbar update for existing desktops / light and dark panels"
for app in org.kde.kmenuedit systemsettings kdesystemsettings org.kde.khelpcenter org.kde.kinfocenter firewall-config org.kde.kjournaldbrowser org.kde.plasma-welcome; do
f=/usr/share/applications/$app.desktop; [ ! -f "$f" ] || { [ "$(grep -c '^NoDisplay=' "$f")" = 1 ] && grep -q '^NoDisplay=true' "$f"; } || fail "Start list still shows $app"
done
grep -q '^Name=Terminal$' /usr/share/applications/org.kde.konsole.desktop && grep -q '^Name=Store$' /usr/share/applications/org.kde.discover.desktop && ! grep -q '^Name\[en_GB\]=' /usr/share/applications/org.kde.konsole.desktop || fail "plain app names in Start"
grep -q 'tray.writeConfig("extraItems", shown.concat(hidden))' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js && [ -f /usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fennek-2026-11-02b-tray.js ] && ! grep -q "SystrayContainmentId" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/*.js /usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fennek-*.js || fail "tray shown/hidden icons, on the tray widget itself (new accounts and existing desktops)"
for p in org.fennek.searchbox org.fennek.showdesktop org.fennek.clock org.fennek.actioncenter org.fennek.keyboard; do
python3 -c "import json; json.load(open('/usr/share/plasma/plasmoids/$p/metadata.json'))" && [ -s /usr/share/plasma/plasmoids/$p/contents/ui/main.qml ] \
&& grep -q "\"$p\"" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js \
|| fail "taskbar widget $p"
done
[ -f /usr/lib64/qt6/qml/org/kde/plasma/workspace/keyboardlayout/qmldir ] || fail "language list: keyboard layout module missing"
grep -q 'panel.addWidget("org.fennek.keyboard")' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js \
&& ! grep -q NotificationArea /usr/share/plasma/plasmoids/org.fennek.keyboard/metadata.json \
&& ! grep -q '"org.kde.plasma.keyboardlayout"' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js \
&& [ -f /usr/share/plasma/shells/org.kde.plasma.desktop/contents/updates/fennek-2026-10-19-language-by-clock.js ] \
|| fail "language: one indicator, next to the clock (its panel fits its rows), stock one never loads"
grep -q 'panel.hiding = "none"' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || fail "taskbar always visible"
grep -q 'panel.height = 40' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || fail "40 px taskbar"
grep -q '^ButtonsOnRight=IAX' /etc/xdg/kwinrc && grep -q '^LayoutName=thumbnail_grid' /etc/xdg/kwinrc || fail "classic window buttons / Alt+Tab"
grep -q '^TitleAlignment=AlignLeft' /etc/xdg/breezerc || fail "titles on the left"
a=/usr/share/aurorae/themes/FennekClassic
grep -q '^theme=__aurorae__svg__FennekClassic' /etc/xdg/kwinrc && [ -f "$a/FennekClassicrc" ] || fail "Fennek Classic window frame"
lib=$(sed -n 's/^library=//p' /etc/xdg/kwinrc)
[ -f "/usr/lib64/qt6/plugins/org.kde.kdecoration3/$lib.so" ] || fail "window frame engine $lib not installed"
grep -q '^Shadow=true' "$a/FennekClassicrc" && grep -q 'id="decoration-bottomright"' "$a/decoration.svg" || fail "window shadow"
for b in minimize maximize restore close; do grep -q 'id="hover-center"' "$a/$b.svg" || fail "frame button $b"; done
grep -q 'id="decoration-top"' "$a/decoration.svg" && grep -q 'id="decoration-inactive-top"' "$a/decoration.svg" || fail "frame svg"
grep -q '^BorderlessMaximizedWindows=false' /etc/xdg/kwinrc || fail "title bar on maximized windows"
r=/usr/share/kxmlgui5/dolphin/dolphinui.rc
grep -q 'name="go_up"' "$r" && grep -q 'version="10[0-9][0-9]"' "$r" && grep -q '<Menu' "$r" || fail "Files toolbar (Back/Forward/Up)"
python3 -c "import sys, xml.dom.minidom as x; x.parse(sys.argv[1])" "$r" || fail "Files toolbar file is not valid XML"
[ -s /usr/share/fennek/apps/firewall/Page.qml ] && [ ! -e /usr/share/fennek/apps/firewall/main.qml ] && grep -q '^Exec=fennek-app settings firewall' /usr/share/applications/fennek-firewall.desktop || fail "Firewall window"
fennek-firewall list --json | python3 -c 'import json,sys; assert json.load(sys.stdin) == []' || fail "firewall list --json"
# Dark theme icons: Fennek's own icons over Breeze Dark (light symbols), used by the dark look.
grep -q '^Inherits=breeze-dark' /usr/share/icons/Fennek-Dark/index.theme && [ -e /usr/share/icons/Fennek-Dark/scalable/apps/fennek-settings.svg ] \
&& grep -q '^Theme=Fennek-Dark$' /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults \
&& grep -q '^Theme=Fennek$' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/defaults || fail "dark theme icons"
# Classic command names: each one linked, every program they open is installed.
for c in calc notepad control taskmgr ncpa.cpl appwiz.cpl; do
[ "$(readlink -f "/usr/bin/$c")" = /usr/libexec/fennek-classic-command ] || fail "classic command $c"
done
for p in gio dolphin konsole fennek-app plasma-systemmonitor spectacle kinfocenter plasma-discover partitionmanager systemsettings; do
command -v "$p" >/dev/null || fail "classic commands: $p missing"
done
# Desktop Ctrl+wheel = display size: stand-in screen tool (125% -> 150%, never past 300%), plugin + binding present.
us=$(mktemp -d) && cat >"$us/kscreen-doctor" <<'SH' && chmod +x "$us/kscreen-doctor" && printf '#!/bin/sh\n' >"$us/gdbus" && chmod +x "$us/gdbus"
#!/bin/sh
if [ "$1" = -j ]; then echo "{\"outputs\":[{\"name\":\"Virtual-1\",\"enabled\":true,\"connected\":true,\"scale\":$SCALE}]}"; else echo "$*" >>"$OUT"; fi
SH
PATH="$us:$PATH" SCALE=1.25 OUT="$us/set" XDG_RUNTIME_DIR="$us" /usr/libexec/fennek-ui-scale up \
&& PATH="$us:$PATH" SCALE=3 OUT="$us/set" XDG_RUNTIME_DIR="$us" /usr/libexec/fennek-ui-scale up \
&& [ "$(cat "$us/set")" = "output.Virtual-1.scale.1.5" ] \
&& ls /usr/lib64/qt6/plugins/plasma/containmentactions/org.fennek.uiscale.so >/dev/null \
&& grep -q '^wheel:Vertical;ControlModifier=org.fennek.uiscale' /usr/share/plasma/shells/org.kde.plasma.desktop/contents/defaults \
|| fail "desktop Ctrl+wheel display size"
# Start "Open file location": stand-in Files, so only the lookup runs.
ol=$(mktemp -d) && printf '#!/bin/sh\necho "$*"\n' >"$ol/dolphin" && chmod +x "$ol/dolphin" \
&& [ "$(PATH="$ol:$PATH" XDG_DATA_DIRS=/usr/share /usr/libexec/fennek-open-location org.kde.dolphin.desktop)" = "--select /usr/share/applications/org.kde.dolphin.desktop" ] \
&& ! PATH="$ol:$PATH" /usr/libexec/fennek-open-location '../../etc/passwd' >/dev/null || fail "Start: open file location"
grep -q "fennek-permissions:" /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileItem.qml || fail "Start tile right-click: App permissions"
/usr/libexec/fennek-reset-settings --selftest >/dev/null && grep -q 'fennek-reset-settings --now' /etc/xdg/plasma-workspace/env/fennek-reset.sh && grep -q 'fennek-reset-settings' /usr/share/fennek/apps/settings/catalog.js || fail "Reset desktop settings (keep files and apps)"
# Go back (undo last update): stand-in rpm-ostree/pkexec, so only the decision logic runs.
gb=$(mktemp -d) && printf '#!/bin/sh\necho "$GB_JSON"\n' >"$gb/rpm-ostree" && printf '#!/bin/sh\necho "pkexec $*"\n' >"$gb/pkexec" && chmod +x "$gb"/* \
&& [[ $(GB_JSON='{"deployments":[{"booted":true,"timestamp":1760000000}]}' PATH="$gb:$PATH" fennek-go-back --yes 2>&1) == *"no previous version"* ]] \
&& [[ $(GB_JSON='{"deployments":[{"staged":true},{"booted":true},{"timestamp":1760000000}]}' PATH="$gb:$PATH" fennek-go-back --yes) == *"pkexec bootc rollback"* ]] \
&& ! GB_JSON='{"deployments":[{"booted":true},{"timestamp":1}]}' PATH="$gb:$PATH" fennek-go-back >/dev/null 2>&1 || fail "go back (rollback) logic"
echo '{"digest": "bad"}' >"$gb/failed" \
&& [[ $(FENNEK_ROLLED_BACK="$gb/failed" GB_JSON='{"deployments":[{"booted":true},{"timestamp":1,"container-image-reference-digest":"bad"}]}' PATH="$gb:$PATH" fennek-go-back --yes 2>&1) == *"no previous version"* ]] \
&& [[ $(FENNEK_ROLLED_BACK="$gb/failed" GB_JSON='{"deployments":[{"booted":true},{"timestamp":1,"container-image-reference-digest":"ok"}]}' PATH="$gb:$PATH" fennek-go-back --yes) == *"pkexec bootc rollback"* ]] || fail "go back: never to a version that failed to start here"
rm -rf "$gb"
# Everyday apps: installed once after the first start (never in the live session); --list = what's still missing.
ea=$(/usr/libexec/fennek-essential-apps --list) && echo "everyday apps to fetch: $ea" | tr '\n' ' ' && echo \
&& [ -L /etc/systemd/system/multi-user.target.wants/fennek-essential-apps.service ] \
&& grep -q '^ConditionKernelCommandLine=!rd.live.image' /usr/lib/systemd/system/fennek-essential-apps.service \
&& [ -z "$(grep -vxE 'org\.kde\.[a-z]*' <<<"$ea")" ] && [ -x /usr/libexec/fennek-get-app ] && grep -q 'fennek-get-app org.libreoffice.LibreOffice' /usr/share/applications/fennek-get-libreoffice.desktop \
&& grep -q '^application/msword=org.libreoffice' /etc/xdg/mimeapps.list || fail "everyday apps installer; office on request"
# Alt+F4 script (desktop: Shut down) + shortcut update for existing users (runs before the window manager).
# It must sit where KWin's own scripts are, or Alt+F4 would do nothing at all.
kd=$(ls -d /usr/share/kwin*/scripts/minimizeall 2>/dev/null | head -1)
[ -n "$kd" ] && [ ! -L "${kd%/minimizeall}/fennekaltf4" ] && [ -f "${kd%/minimizeall}/fennekaltf4/contents/code/main.js" ] && [ -f "${kd%/minimizeall}/fenneksnapassist/contents/code/main.js" ] || fail "Alt+F4 script not where KWin loads scripts (${kd:-none found})"
python3 -c "import json; d=json.load(open('/usr/share/kwin-wayland/scripts/fennekaltf4/metadata.json')); assert d['KPlugin']['Id']=='fennekaltf4'" && grep -q 'registerShortcut("Fennek Close or Shut Down"' /usr/share/kwin-wayland/scripts/fennekaltf4/contents/code/main.js && [ "$(kreadconfig6 --file /etc/xdg/kwinrc --group Plugins --key fennekaltf4Enabled)" = true ] && [ "$(kreadconfig6 --file /etc/xdg/kglobalshortcutsrc --group kwin --key 'Window Close')" = "none,Alt+F4,Close Window" ] || fail "Alt+F4 on the desktop"
[ -f /usr/share/kwin-wayland/scripts/fennekf11/contents/code/main.js ] && grep -q 'registerShortcut("Fennek F11"' /usr/share/kwin-wayland/scripts/fennekf11/contents/code/main.js && [ "$(kreadconfig6 --file /etc/xdg/kwinrc --group Plugins --key fennekf11Enabled)" = true ] && python3 -c "import json; assert json.load(open('/usr/share/kwin-wayland/scripts/fennekf11/metadata.json'))['KPlugin']['Id'] == 'fennekf11'" || fail "F11: full screen / normal window for any app"
sh=$(mktemp -d) && printf '[kwin]\nOverview=Meta+W,Meta+W,Toggle Overview\nWindow Close=Alt+F4,Alt+F4,Close Window\n' >"$sh/kglobalshortcutsrc" \
&&XDG_CONFIG_HOME=$sh XDG_STATE_HOME=$sh /usr/libexec/fennek-shortcuts-update && [ "$(kreadconfig6 --file "$sh/kglobalshortcutsrc" --group kwin --key 'Window Close')" = "none,Alt+F4,Close Window" ] && [ "$(kreadconfig6 --file "$sh/kglobalshortcutsrc" --group kwin --key Overview | cut -f1)" = Meta+Tab ] && [ -L /etc/systemd/user/plasma-workspace.target.wants/fennek-shortcuts-update.service ] || fail "shortcut update for existing users"
rm -rf "$sh"
# Desktop right-click for existing accounts: nothing before the desktop made its own settings, KDE's menu becomes Fennek's once, a later own choice stays, a menu Plasma dropped comes back.
dm=$(mktemp -d) && dmk=(--file "$dm/plasma-org.kde.plasma.desktop-appletsrc" --group ActionPlugins --group 0 --key 'RightButton;NoModifier') \
&& XDG_CONFIG_HOME=$dm XDG_STATE_HOME=$dm /usr/libexec/fennek-shortcuts-update && [ -z "$(kreadconfig6 "${dmk[@]}")" ] \
&& kwriteconfig6 "${dmk[@]}" org.kde.contextmenu && XDG_CONFIG_HOME=$dm XDG_STATE_HOME=$dm /usr/libexec/fennek-shortcuts-update && [ "$(kreadconfig6 "${dmk[@]}")" = org.fennek.desktopmenu ] \
&& kwriteconfig6 "${dmk[@]}" org.kde.contextmenu && XDG_CONFIG_HOME=$dm XDG_STATE_HOME=$dm /usr/libexec/fennek-shortcuts-update && [ "$(kreadconfig6 "${dmk[@]}")" = org.kde.contextmenu ] \
&& kwriteconfig6 "${dmk[@]}" --delete && XDG_CONFIG_HOME=$dm XDG_STATE_HOME=$dm /usr/libexec/fennek-shortcuts-update && [ "$(kreadconfig6 "${dmk[@]}")" = org.fennek.desktopmenu ] \
|| fail "desktop right-click menu for existing accounts"
rm -rf "$dm"
[ "$(kreadconfig6 --file /etc/xdg/ksmserverrc --group General --key confirmLogout)" = false ] || fail "power menu acts without a second confirmation"
grep -q 'Fennek OS: no line over an open button' /usr/share/plasma/shells/org.kde.plasma.desktop/contents/applet/CompactApplet.qml || fail "no line over open taskbar buttons"
# Everyday apps get plain names in Start (Okular -> PDF Reader); the copy goes when the app goes.
an=$(mktemp -d) && mkdir -p "$an/exp" && printf '[Desktop Entry]\nName=Okular\nExec=x\n' >"$an/exp/org.kde.okular.desktop" \
&& FENNEK_FLATPAK_EXPORTS="$an/exp" XDG_DATA_HOME="$an" /usr/libexec/fennek-app-names \
&& grep -q '^Name=PDF Reader$' "$an/applications/org.kde.okular.desktop" && rm "$an/exp/org.kde.okular.desktop" \
&& FENNEK_FLATPAK_EXPORTS="$an/exp" XDG_DATA_HOME="$an" /usr/libexec/fennek-app-names && [ ! -e "$an/applications/org.kde.okular.desktop" ] \
&& [ -L /etc/systemd/user/graphical-session.target.wants/fennek-app-names.service ] || fail "plain names for everyday apps"
an=$(mktemp -d) && mkdir -p "$an/exp" && printf '[Desktop Entry]\nName=KClock\n' > "$an/exp/org.kde.kclock.desktop" && FENNEK_FLATPAK_EXPORTS="$an/exp" XDG_DATA_HOME="$an" /usr/libexec/fennek-app-names && grep -qx 'Name=Alarms & Clock' "$an/applications/org.kde.kclock.desktop" && /usr/libexec/fennek-essential-apps --list | grep -q org.kde.kamoso || fail "Camera and Alarms & Clock among the everyday apps"; rm -rf "$an"
rm -rf "$an"
# Fennek colour schemes: violet instead of Breeze blue; system default for new users; existing users switched once.
grep -q '^Name=Fennek Light$' /usr/share/color-schemes/FennekLight.colors && ! grep -q '61,174,233' /usr/share/color-schemes/FennekLight.colors && grep -q '^Name=Fennek Dark$' /usr/share/color-schemes/FennekDark.colors && ! grep -q '61,174,233' /usr/share/color-schemes/FennekDark.colors && [ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group Colors:Selection --key BackgroundNormal)" = 109,69,184 ] && grep -q '^ColorScheme=FennekDark' /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults || fail "Fennek colour schemes"
cs=$(mktemp -d) && mkdir -p "$cs/bin" && printf '#!/bin/sh\necho "$1" >"%s/applied"\n' "$cs" >"$cs/bin/plasma-apply-colorscheme" \
&& chmod +x "$cs/bin/plasma-apply-colorscheme" && printf '[General]\nColorScheme=BreezeDark\n' >"$cs/kdeglobals" \
&& PATH="$cs/bin:$PATH" XDG_CONFIG_HOME="$cs" XDG_STATE_HOME="$cs" /usr/libexec/fennek-shortcuts-update \
&& [ "$(cat "$cs/applied")" = FennekDark ] || fail "existing users move to the Fennek colour scheme"
# A stock theme that took over (Breeze Dark via the Dark mode switch) is replaced by Fennek Dark, once.
lf=$(mktemp -d) && mkdir -p "$lf/bin" && cp "$cs/bin/plasma-apply-colorscheme" "$lf/bin/" \
&& printf '#!/bin/sh\necho "$2" >"%s/lnf"\n' "$lf" >"$lf/bin/plasma-apply-lookandfeel" && chmod +x "$lf/bin/plasma-apply-lookandfeel" \
&& printf '[KDE]\nLookAndFeelPackage=org.kde.breezedark.desktop\nDefaultDarkLookAndFeel=org.kde.breezedark.desktop\n' >"$lf/kdeglobals" \
&& PATH="$lf/bin:$PATH" XDG_CONFIG_HOME="$lf" XDG_STATE_HOME="$lf" XDG_DATA_HOME="$lf" /usr/libexec/fennek-shortcuts-update \
&& [ "$(cat "$lf/lnf")" = org.fennek.dark.desktop ] \
&& [ "$(kreadconfig6 --file "$lf/kdeglobals" --group KDE --key DefaultDarkLookAndFeel)" = org.fennek.dark.desktop ] || fail "stock theme replaced by Fennek's"
rm -rf "$lf"
rm -rf "$cs"
# Backup to a USB drive: logic, hourly check for everyone, and a real two-pass copy that keeps the older version.
/usr/libexec/fennek-own-partition --selftest >/dev/null && [ -s /usr/lib/udev/rules.d/85-fennek-own-partition.rules ] || fail "Fennek's own partitions stay out of the drive lists"
fennek-backup --selftest >/dev/null && [ -L /etc/systemd/user/timers.target.wants/fennek-backup.timer ] && python3 - <<'PY' || fail "backup to a USB drive"
import importlib.machinery, importlib.util, pathlib, subprocess, tempfile
spec = importlib.util.spec_from_loader("fb", importlib.machinery.SourceFileLoader("fb", "/usr/bin/fennek-backup"))
fb = importlib.util.module_from_spec(spec); spec.loader.exec_module(fb)
t = pathlib.Path(tempfile.mkdtemp()); src = t / "Documents"; src.mkdir(); (src / "a.txt").write_text("v1")
cur, old = t / "drive/Current", t / "drive/Older versions"
cur.mkdir(parents=True)
assert subprocess.run(fb.rsync_args(str(src), str(cur), str(old / "2026-10-17 10.00/Documents"))).returncode == 0
(src / "a.txt").write_text("v2"); import os, time; os.utime(src / "a.txt", (time.time() + 60, time.time() + 60)) # a later edit
assert subprocess.run(fb.rsync_args(str(src), str(cur), str(old / "2026-10-17 11.00/Documents"))).returncode == 0
assert (cur / "Documents/a.txt").read_text() == "v2"
assert (old / "2026-10-17 11.00/Documents/a.txt").read_text() == "v1"
# Restoring: what's missing comes back plain, nothing that's here is replaced, a link on the drive isn't followed.
mine = t / "mine"; mine.mkdir(); (mine / "kept.txt").write_text("mine")
back = t / "m/Current/mine"; back.mkdir(parents=True); (back / "kept.txt").write_text("old")
(back / "run.desktop").write_text("x"); os.chmod(back / "run.desktop", 0o777)
(t / "m/Current/evil").symlink_to("/etc")
fb.destination = lambda m, fresh=False: t / "m"; fb.sources = lambda: [str(mine)]
fb.restore(str(t / "m"))
assert (mine / "kept.txt").read_text() == "mine" and (mine / "run.desktop").stat().st_mode & 0o777 == 0o644
assert not (pathlib.Path.home() / "evil").exists()
home = t / "home"; home.mkdir(); os.environ["HOME"] = str(home)
danger = t / "m/Current/.config/autostart"; danger.mkdir(parents=True); (danger / "x.desktop").write_text("x")
other = t / "m/Current/Dokumente"; other.mkdir(); (other / "d.txt").write_text("d")
fb.restore(str(t / "m"))
assert not (home / ".config").exists(), "a drive's folder names never choose a place in home"
assert (home / "Restored from backup/Dokumente/d.txt").is_file()
PY
# "Update ready" note: once per downloaded update, enabled for every user.
ur=$(mktemp -d) && echo one >"$ur/staged" && export FENNEK_STAGED="$ur/staged" XDG_STATE_HOME="$ur" FENNEK_DRY_RUN=1 \
&& [ "$(/usr/libexec/fennek-update-ready)" = "would notify" ] && [ -z "$(/usr/libexec/fennek-update-ready)" ] \
&& echo two >"$ur/staged" && [ "$(/usr/libexec/fennek-update-ready)" = "would notify" ] \
&& [ -L /etc/systemd/user/graphical-session.target.wants/fennek-update-ready.path ] \
&& [ -L /etc/systemd/user/graphical-session.target.wants/fennek-update-ready.service ] || fail "update ready notification"
unset FENNEK_STAGED XDG_STATE_HOME FENNEK_DRY_RUN; rm -rf "$ur"
command -v ss >/dev/null && fennek-firewall active --json | python3 -c 'import json,sys; assert isinstance(json.load(sys.stdin), list)' || fail "firewall active --json"
grep -qF "fennek-firewall profile [home|sharing|public]" /usr/bin/fennek-firewall || fail "firewall network profile command"
[ -x /usr/libexec/fennek-first-login ] && grep -q '^Exec=/usr/libexec/fennek-first-login' /etc/xdg/autostart/fennek-first-login.desktop && grep -q "apply org.fennek.dark.desktop" /usr/libexec/fennek-first-login || fail "first-login Fennek look (dark by default)"
cmp -s /usr/share/plasma/look-and-feel/org.fennek.{desktop,dark.desktop}/contents/layouts/org.kde.plasma.desktop-layout.js || fail "Fennek Dark has the Fennek layout"
grep -q "^startPage=applications.page" /etc/xdg/systemmonitorrc || fail "Task Manager opens on apps"
[ -f /usr/share/icons/Fennek/scalable/places/folder.svg ] && [ -f /usr/share/icons/Fennek/scalable/apps/org.kde.dolphin.svg ] && [ -f /usr/share/icons/hicolor/scalable/apps/fennek-firewall.svg ] && [ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group Icons --key Theme)" = Fennek ] && grep -q "^Theme=Fennek" /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults || fail "Fennek icons"
grep -q "Ctrl+Shift+V; Ctrl+V" /usr/share/kxmlgui5/konsole/sessionui.rc && grep -q "hidden=\"true\" name=\"sessionToolbar\"" /usr/share/kxmlgui5/konsole/sessionui.rc && grep -q "hidden=\"true\" name=\"mainToolBar\"" /usr/share/kxmlgui5/konsole/konsoleui.rc && grep -q "^TabBarVisibility=AlwaysShowTabBar" /etc/xdg/konsolerc || fail "Terminal: classic tabs, no toolbars"
ls /usr/lib64/qt6/plugins/plasma/containmentactions/org.fennek.taskbarmenu.so /usr/lib64/qt6/plugins/kf6/kio/kio_thispc.so >/dev/null && grep -q "^RightButton;NoModifier=org.fennek.taskbarmenu" /usr/share/plasma/shells/org.kde.plasma.desktop/contents/defaults && ! rpm -q gcc-c++ >/dev/null || fail "taskbar menu + This PC plugins (and no compiler left in the image)"
ls /usr/lib64/qt6/plugins/plasma/containmentactions/org.fennek.desktopmenu.so >/dev/null && grep -q "^RightButton;NoModifier=org.fennek.desktopmenu" /usr/share/plasma/shells/org.kde.plasma.desktop/contents/defaults || fail "desktop right-click: Fennek's classic menu"
grep -q "^AccentColor=109,69,184" /etc/xdg/kdeglobals && grep -q "sidebarFollowsTheme\", true" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || fail "Fennek accent colour + visible Start strip"
grep -q "^ExpandableFolders=false" /etc/xdg/dolphinrc && grep -q "browser.tabs.inTitlebar" /etc/firefox/policies/policies.json || fail "classic details rows + Firefox system title bar"
for c in left_ptr pointer xterm watch left_ptr_watch sb_h_double_arrow; do [ "$(head -c4 "/usr/share/icons/Fennek-Cursors/cursors/$c" 2>/dev/null)" = Xcur ] || fail "Fennek pointer $c"; done
[ -f /usr/share/icons/Breeze_Light/cursors/left_ptr ] && grep -q "^cursorTheme=Fennek-Cursors" /etc/xdg/kcminputrc && grep -q "^CursorTheme=Fennek-Cursors" /etc/sddm.conf.d/10-fennek.conf || fail "Fennek pointers default (Breeze Light behind)"
! grep -q '^Hidden=' /usr/share/icons/Fennek-Cursors/index.theme && grep -q '^Hidden=true' /usr/share/icons/Breeze_Light/index.theme && [ -f /usr/share/icons/Fennek-Cursors/cursors/left_ptr ] || fail "pointer list: Fennek alone, stock themes hidden"
grep -q "^font=Inter,11" /etc/xdg/kdeglobals && grep -q "^fixed=Cascadia Mono" /etc/xdg/kdeglobals && [ -n "$(fc-list :family=Inter)" ] && [ -n "$(fc-list ':family=Cascadia Mono')" ] || fail "Inter + Cascadia Mono fonts"
[ "$(kreadconfig6 --file /etc/xdg/kscreenlockerrc --group Daemon --key Timeout)" = 30 ] && [ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group KDE --key AnimationDurationFactor)" = 0.5 ] || fail "auto-lock after 30 min / snappy animations"
systemctl is-enabled -q flatpak-system-update.timer && systemctl is-enabled -q rpm-ostreed-automatic.timer || fail "automatic updates: system and Store apps (daily, in the background)"
/usr/libexec/fennek-update-settings --selftest >/dev/null && /usr/libexec/fennek-update-settings get | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["auto"] and d["apps_auto"] and d["paused_until"] == ""' && grep -q '^ExecCondition=/usr/libexec/fennek-update-settings allowed' /usr/lib/systemd/system/rpm-ostreed-automatic.service.d/fennek-pause.conf /usr/lib/systemd/system/flatpak-system-update.service.d/fennek-pause.conf /usr/lib/systemd/user/flatpak-user-update.service.d/fennek-pause.conf && [ -s /usr/share/polkit-1/actions/org.fennek.update-settings.policy ] && grep -q '^Exec=fennek-app settings updates' /usr/share/applications/fennek-updates.desktop || fail "Updates window: settings helper, pause, launcher"
grep -q '^IMAGE_VERSION=[0-9]' /usr/lib/os-release && grep -q '^BUILD_ID=' /usr/lib/os-release && grep -q '^PRETTY_NAME="Fennek OS [0-9.]* · ' /usr/lib/os-release && grep -q '^VERSION_ID=' /usr/lib/os-release && python3 -c 'import json; m=json.load(open("/usr/share/fennek/milestones.json")); assert m and m[0]["name"]' && command -v skopeo >/dev/null || fail "Trails: version, build id, milestones, earlier versions list"
/usr/libexec/fennek-switch-version --selftest >/dev/null && grep -q '<allow_active>auth_admin</allow_active>' /usr/share/polkit-1/actions/org.fennek.switch-version.policy && [ -s /usr/share/fennek/oldest-allowed-build ] && ! /usr/libexec/fennek-update-settings set version latest 2>/dev/null || fail "earlier versions: own helper, fresh password, age limit and cutoff"
grep -q '^Theme=org.fennek.desktop' /etc/xdg/ksplashrc && [ -s /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/splash/Splash.qml ] && grep -q '^Theme=org.fennek.desktop' /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults || fail "Fennek splash after signing in"
[ -f /usr/lib64/qt6/plugins/styles/libfennekstyle.so ] && grep -q '^widgetStyle=Fennek' /etc/xdg/kdeglobals && grep -aq KItemListContainer /usr/lib64/qt6/plugins/styles/libfennekstyle.so && { rc=0; QT_QPA_PLATFORM=offscreen QT_STYLE_OVERRIDE=Fennek HOME=$(mktemp -d) timeout 5 kdialog --combobox Pick a b >/tmp/style.log 2>&1 || rc=$?; [ $rc -eq 124 ] && ! grep -qi "invalid style" /tmp/style.log; } || fail "Fennek widget style (wheel scrolls the page) loads"
! grep -rnE 'on(DragActive|ContainsMouse|Expanded|Checked|Visible)Changed: function\([a-z]' /usr/share/plasma/plasmoids/ /usr/share/fennek/apps/ || fail "QML: a property-change handler takes an argument (always undefined in Qt 6)"
! grep -rnE '^[[:space:]]*on(Entered|PositionChanged|Dropped): [{]' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/ || fail "Start tiles: drop handlers must name their event (an app dropped from the list must become a tile)"
grep -q '"cmd": "/usr/libexec/fennek-reset-layout"' /usr/share/fennek/apps/settings/catalog.js && grep -q -- "--resetLayout" /usr/libexec/fennek-reset-layout && grep -q "warningcontinuecancel" /usr/libexec/fennek-reset-layout || fail "Settings: Restore taskbar & Start (asks first, keeps a backup)"
/usr/libexec/fennek-update-run --selftest >/dev/null && grep -q fennek-update-run /usr/share/fennek/apps/updates/Page.qml && command -v script >/dev/null || fail "Updates: install progress (steps, parts downloaded)"
grep -q '^Exec=/usr/libexec/fennek-update-notify' /etc/xdg/autostart/fennek-update-notify.desktop || fail "Updates: a notice after an update was installed"
grep -q '^ShowSelectionToggle=false' /etc/xdg/dolphinrc || fail "File Explorer: no + box on hover"
grep -q '^LeftPadding=0' /etc/xdg/dolphinrc && grep -q '^UseShortRelativeDates=false' /etc/xdg/dolphinrc && grep -q '^IconSize=22' /etc/xdg/dolphinrc && [ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group Locale --key BinaryUnitDialect)" = 1 ] || fail "File Explorer: bigger rows, real dates, KB/MB, no side strips"
/usr/libexec/fennek-greeter-lang --selftest >/dev/null && systemctl is-enabled -q fennek-greeter-lang.service || fail "Sign-in screen in the last user's language"
sh=$(mktemp -d) && XDG_CONFIG_HOME=$sh XDG_STATE_HOME=$sh /usr/libexec/fennek-shortcuts-update && [ "$(kreadconfig6 --file "$sh/kglobalshortcutsrc" --group kwin --key 'Switch to Desktop 4')" = "$(printf 'Meta+F4,Meta+F4\tCtrl+F4,Switch to Desktop 4')" ] && [ "$(kreadconfig6 --file "$sh/kglobalshortcutsrc" --group kwin --key Overview | cut -f1)" = Meta+Tab ] || fail "classic keys: a new account's shortcuts are written too"
rm -rf "$sh"
for w in wallpaper brightness wifi ethernet "dark mode" monitor lid "power button" "screen saver" "screen reader" magnifier; do grep -qiw "$w" /usr/share/fennek/settings-index.tsv || fail "setting word $w"; done
[ "$(kreadconfig6 --file /etc/xdg/krunnerrc --group Plugins --key baloosearchEnabled)" = false ] && grep -q 'runnerModel.runners = filters' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/SearchModel.qml && grep -q 'runnerModel.runners = tab.runners' /usr/share/plasma/plasmoids/org.fennek.searchbox/contents/ui/main.qml || fail "search: the tabs really filter"
grep -q '^Window Operations Menu=Alt+Space' /etc/xdg/kglobalshortcutsrc && ! grep -q '^X-KDE-Shortcuts=.*Alt+Space' /usr/share/kglobalaccel/org.kde.krunner.desktop && [ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group Shortcuts --key Close)" = "Ctrl+W; Ctrl+F4" ] || fail "Alt+Space window menu, Ctrl+F4 closes a document"
grep -q 'slotWindowQuickTileTopLeft' /usr/share/kwin-wayland/scripts/fennekarrows/contents/code/main.js && grep -q 'viewOpen' /usr/share/kwin-wayland/scripts/fenneksnapassist/contents/code/main.js || fail "Super+Up and Down make quarters; Esc ends snap assist"
grep -q '^Switch to Next Keyboard Layout=Meta+Space' /etc/xdg/kglobalshortcutsrc || fail "Super+Space switches the input language"
[ "$(readlink -f /usr/bin/kinfocenter)" = /usr/libexec/fennek/systemsettings ] && [ -x /usr/libexec/fennek/systemsettings ] && grep -q 'fennek-app settings' /usr/bin/systemsettings || fail "one Settings: KDE's links land in Fennek's, KDE's window and About stay"
[ "$(kreadconfig6 --file /etc/xdg/krunnerrc --group Plugins --key krunner_systemsettingsEnabled)" = false ] && grep -q 'name: "Settings", runners: \["fennek-settings"\]' /usr/share/plasma/plasmoids/org.fennek.searchbox/contents/ui/main.qml || fail "search finds Fennek's settings, not KDE's window"
grep -q 'Fennek Bring Forward' /usr/share/kwin-wayland/scripts/fennekarrows/contents/code/main.js && ! grep -q 'fsplevel' /etc/xdg/kwinrulesrc && ldd /usr/bin/fennek-app | grep -q DBusAddons || fail "Fennek's apps: one window each, which comes forward when opened again"
grep -q '^maxsizerule=2' /etc/xdg/kwinrulesrc && grep -q '^wmclass=' /etc/xdg/kwinrulesrc || fail "Windows programs and games can be maximized (button, drag to top)"
grep -q '^Exec=/usr/libexec/fennek-snip' /usr/share/applications/org.kde.spectacle.desktop && grep -q 'action=save=Save' /usr/libexec/fennek-printscreen && command -v wl-copy >/dev/null && grep -q '^rememberSelectionRect=Never' /etc/xdg/spectaclerc && grep -q '^autoSaveImage=false' /etc/xdg/spectaclerc || fail "Snip and PrtSc: to the clipboard only, Save on the notice, a click cancels, fresh each time"
command -v dbus-monitor >/dev/null && [ -f /usr/share/fennek/apps/power/main.qml ] && systemctl --global is-enabled -q fennek-power-feedback.service || fail "Shut down / Restart: dimmed screen with a spinner right away"
grep -q '^Bouncing=false' /etc/xdg/klaunchrc || fail "Starting an app: classic busy pointer, no bouncing icon"
python3 -c "import json; p=json.load(open('/etc/firefox/policies/policies.json'))['policies']; assert p['NoDefaultBookmarks'] and p['Homepage']['URL'] == 'about:home' and len(p['Bookmarks']) == 3 and 'uBlock0@raymondhill.net' in p['ExtensionSettings'] and p['Homepage']['StartPage'] == 'previous-session' and p['Preferences']['browser.newtabpage.activity-stream.widgets.enabled']['Value'] is False and p['Preferences']['browser.newtabpage.activity-stream.widgets.system.enabled']['Value'] is False and p['Preferences']['browser.tabs.tabMinWidth']['Value'] <= 60 and p['Preferences']['browser.newtabpage.activity-stream.widgets.system.weather.enabled']['Value'] is False and p['ExtensionSettings']['lexisubs@yourdomain.com']['default_area'] == 'navbar'" || fail "Firefox: no Fedora start page or bookmarks; restores your tabs; our bookmarks"
[ -f /usr/share/fennek/apps/winx/main.qml ] && grep -q '^X-KDE-Shortcuts=Meta+X' /usr/share/applications/fennek-winx.desktop && grep -q '^fennekwinxEnabled=true' /etc/xdg/kwinrc || fail "Super+X quick links"
grep -q 'Mode Constant' /usr/share/plasma/plasmoids/org.fennek.actioncenter/contents/ui/main.qml && ls /usr/lib64/qt6/plugins/plasma/kcms/systemsettings/kcm_nightlight.so >/dev/null || fail "Night light tile: on means on now; right-click opens its settings"
! grep -q 'org.kde.plasma.brightness' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js && grep -q 'setDarkMode' /usr/share/plasma/plasmoids/org.fennek.actioncenter/contents/ui/main.qml || fail "Brightness & Color: in the action center (with Dark mode), not the tray"
grep -q 'function resizeGroup' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'function fixGroupRows' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'function fitGroup' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'function fitTile' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml || fail "Start groups: fixed walls (never grow by themselves), corner grip to resize; the size follows the pointer the whole drag"
[ -f /usr/share/icons/Fennek/scalable/apps/fennek-get-leonote.svg ] && { ! command -v desktop-file-validate >/dev/null || desktop-file-validate /usr/share/applications/fennek-get-leonote.desktop; } || fail "Start: Leonote (get it)"
[ -x /usr/libexec/fennek-get-mpv ] && grep -q '^Exec=/usr/libexec/fennek-get-mpv' /usr/share/applications/fennek-get-mpv.desktop && grep -q 'fennek-get-mpv' /usr/share/fennek/apps/welcome/main.qml || fail "MPV Player (Oleks Edition): install on request"
[ -s /usr/share/locale/de/LC_MESSAGES/plasma_applet_com.github.zren.tiledmenu.mo ] && [ -s /usr/share/locale/uk/LC_MESSAGES/plasma_applet_com.github.zren.tiledmenu.mo -o ! -e /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/translate/uk.po ] || fail "Start's translations are compiled"
[ -x /usr/libexec/fennek-before-install ] && grep -q 'fennek-before-install' /usr/share/fennek/apps/live/main.qml || fail "the installer says first what would make shrinking fail"
grep -qx 'LLMNR=no' /usr/lib/systemd/resolved.conf.d/60-fennek.conf || fail "no LLMNR name lookups on the local network"
grep -q '^DefaultDependencies=no' /usr/lib/systemd/system/fedora-atomic-desktop-mandb-update.service.d/fennek-background.conf || fail "the manual page index doesn't hold up the start"
for e in /usr/share/applications/fennek-get-*.desktop; do i=$(sed -n 's/^Icon=//p' "$e" | head -1); [ -f "/usr/share/icons/Fennek/scalable/apps/$i.svg" ] || fail "Start's get-it entry has its own icon: ${e##*/}"; done
for e in fennek-get-steam fennek-get-heroic fennek-get-lutris fennek-get-libreoffice fennek-get-mpv fennek-get-leonote fennek-get-thunderbird fennek-get-gpu-screen-recorder; do grep -q "^Exec=/usr/libexec/fennek-get-app --hide $e" /usr/share/applications/$e.desktop || fail "$e: Don't recommend this"; done; grep -q 'fennek-get-app --sync' /usr/lib/systemd/user/fennek-new-apps.service || fail "get-it entries leave Start once the app is installed"
grep -q 'Support Fennek OS' /usr/share/fennek/apps/settings/main.qml && grep -q 'fennek-app settings support' /usr/share/fennek/apps/welcome/main.qml || fail "Support links in Settings and Welcome"
for v in Amber Sakura Royal Arcade Fennek-Classic Leonote-Night; do [ ! -e /usr/share/wallpapers/$v ] || fail "$v picture is in the system (it comes from FennekWallpapers)"; done
/usr/libexec/fennek-permissions --selftest >/dev/null && [ -s /usr/share/fennek/apps/permissions/Page.qml ] && grep -q '"page": "permissions"' /usr/share/fennek/apps/settings/catalog.js && grep -q 'exe-names' /usr/bin/fennek-run-exe || fail "App permissions page (store apps + classic programs)"
/usr/libexec/fennek-tasks --selftest >/dev/null && grep -q '^Exec=fennek-app taskmanager' /usr/share/applications/org.kde.plasma-systemmonitor.desktop && grep -q "\"Performance\", \"App history\", \"Startup\", \"Users\", \"Details\", \"Services\"" /usr/share/fennek/apps/taskmanager/main.qml && [ -f /usr/share/icons/Fennek/scalable/apps/fennek-taskmanager.svg ] && grep -q "^Exec=fennek-app taskmanager performance" /usr/share/applications/fennek-performance.desktop || fail "Task Manager: Fennek's, with the classic tabs; Performance opens on its tab"
grep -q 'freeze ' /usr/share/fennek/apps/taskmanager/main.qml && grep -q '"limit "' /usr/share/fennek/apps/taskmanager/main.qml && grep -q '"Block internet"' /usr/share/fennek/apps/taskmanager/main.qml || fail "Task Manager: Pause/Resume and Block internet"
grep -q '"Task Manager", "fennek-app taskmanager"' /usr/share/fennek/apps/security/main.qml && ! grep -q '"plasma-systemmonitor"' /usr/share/fennek/apps/security/main.qml || fail "Ctrl+Alt+Del > Task Manager opens Fennek's"
grep -q '^Exec=fennek-app settings kcm_about-distro' /usr/share/applications/fennek-about.desktop && grep -A1 'fennek-about.desktop' /etc/xdg/kglobalshortcutsrc | grep -q '^_launch=Meta+Pause' || fail "Super+Pause: About this computer"
command -v orca >/dev/null && grep -q "^Toggle Screen Reader On and Off=Meta+Ctrl+Return" /etc/xdg/kglobalshortcutsrc || fail "screen reader on Super+Ctrl+Enter"
[ -s /usr/share/fennek/apps/search/main.qml ] && grep -A1 "fennek-search.desktop" /etc/xdg/kglobalshortcutsrc | grep -q "^_launch=Meta+S" && /usr/libexec/fennek-search --selftest >/dev/null || fail "Search window (Super+S), with its index's full results"
grep -q '^minimizeallEnabled=true' /etc/xdg/kwinrc && grep -q '^MinimizeAll=Meta+M,' /etc/xdg/kglobalshortcutsrc && { [ -d /usr/share/kwin/scripts/minimizeall ] || [ -d /usr/share/kwin-wayland/scripts/minimizeall ]; } || fail "Super+M minimizes all windows"
/usr/libexec/fennek-boot-guard --selftest >/dev/null && [ -s /usr/share/fennek/boot-guard.cfg ] && grep -q 'fennek-boot-guard arm' /usr/lib/systemd/system/ostree-finalize-staged.service.d/fennek-boot-guard.conf \
&& [ -L /etc/systemd/system/multi-user.target.wants/fennek-boot-guard.service ] && [ -L /etc/systemd/system/multi-user.target.wants/fennek-boot-guard-vet.path ] && command -v grub2-editenv ostree >/dev/null \
&& grep -q 'custom.cfg' /usr/lib/bootupd/grub2-static/configs.d/41_custom.cfg || fail "boot guard: a new version that doesn't start undoes itself"
grep -q '^After=display-manager.service' /usr/lib/systemd/system/fennek-boot-guard.service && grep -q '^RequiresMountsFor=/boot' /usr/lib/systemd/system/fennek-boot-guard.service || fail "boot guard: watches the display manager in use, /boot stays until it has stopped"
grep -q '^KillMode=mixed' /usr/lib/systemd/system/fennek-boot-guard.service || fail "boot guard: a clean stop can write the tries back"
/usr/libexec/fennek-installed-apps --selftest >/dev/null && [ -s /usr/share/fennek/apps/installedapps/Page.qml ] && grep -q '"page": "installedapps"' /usr/share/fennek/apps/settings/catalog.js || fail "Installed apps page"
/usr/libexec/fennek-cleanup --selftest >/dev/null && [ -s /usr/share/fennek/apps/cleanup/Page.qml ] && grep -q '"page": "cleanup"' /usr/share/fennek/apps/settings/catalog.js || fail "Free up space (Recycle Bin, thumbnails, old caches, unused app parts)"
[ "$(wc -l < /usr/share/fennek/settings-index.tsv)" -ge 40 ] && grep -q '^X-Plasma-DBusRunner-Path=/settings' /usr/share/krunner/dbusplugins/fennek-settings.desktop && grep -q 'krunner_services,fennek-settings' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/config/main.xml || fail "Settings found from Start open in Fennek Settings"
[ -L /etc/systemd/user/graphical-session.target.wants/fennek-new-apps.path ] && grep -q '^PathChanged=/var/lib/flatpak/app' /usr/lib/systemd/user/fennek-new-apps.path || fail "new apps: a notice with what they may reach"
/usr/libexec/fennek-wallpaper-get --selftest >/dev/null && [ "$(python3 -c 'import json; print(len(json.load(open("/usr/share/fennek/wallpapers.json"))["wallpapers"]))')" -ge 9 ] && grep -q 'fennek-wallpaper-get' /usr/share/fennek/apps/personalize/Page.qml || fail "extra wallpapers on request (FennekWallpapers, pinned + checked)"
for t in amber sakura royal arcade; do [ -s /usr/share/plasma/look-and-feel/org.fennek.$t.desktop/contents/previews/preview.png ] && grep -q "^ColorScheme=Fennek" /usr/share/plasma/look-and-feel/org.fennek.$t.desktop/contents/defaults && grep -q "FennekClassicDark" /usr/share/plasma/look-and-feel/org.fennek.$t.desktop/contents/defaults || fail "colour theme $t (Settings > Themes)"; done
grep -q '"Id": "org.fennek.live"' /usr/share/plasma/wallpapers/org.fennek.live/metadata.json && grep -q 'OnBattery' /usr/share/plasma/wallpapers/org.fennek.live/contents/ui/main.qml || fail "Fennek Live wallpaper (moving petals / lights, still on battery)"
grep -q '^fennekshowdesktopEnabled=true' /etc/xdg/kwinrc && grep -q '^Fennek Show Desktop=Meta+D' /etc/xdg/kglobalshortcutsrc && grep -q '^Show Desktop=none' /etc/xdg/kglobalshortcutsrc && grep -q 'w.dialog' /usr/share/kwin-wayland/scripts/fennekshowdesktop/contents/code/main.js || fail "Super+D: classic show desktop, second press brings the windows back; dialogs go away too"
[ -s /usr/share/plasma/look-and-feel/org.fennek.amber.desktop/contents/previews/preview.png ] || fail "theme previews"
grep -q '^fennekarrowsEnabled=true' /etc/xdg/kwinrc && grep -q '^Fennek Maximize=Meta+Up' /etc/xdg/kglobalshortcutsrc && grep -q '^Window Quick Tile Top=none' /etc/xdg/kglobalshortcutsrc || fail "Super+Up maximizes, Super+Down restores or minimizes"
grep -q 'Meta+Ctrl+D' /usr/share/kwin-wayland/scripts/fennekarrows/contents/code/main.js && grep -q 'removeDesktop' /usr/share/kwin-wayland/scripts/fennekarrows/contents/code/main.js || fail "Super+Ctrl+D new virtual desktop, Super+Ctrl+F4 closes it"
python3 -c "import zipfile; [zipfile.ZipFile('/usr/share/templates/.source/'+f).testzip() for f in ('Word document.docx','Excel spreadsheet.xlsx','Presentation.odp')]" && [ -f /usr/share/templates/fennek-new-word.desktop ] || fail "New > Word document / Excel spreadsheet / Presentation"
grep -q 'function renameGroup' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'function headerClicked' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'function growGroupsFor' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'renameLoader' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileItem.qml || fail "Start groups: double-click the name to rename"
[ -f /usr/share/icons/Fennek/scalable/actions/fennek-dark-mode-symbolic.svg ] && grep -q "fennek-dark-mode-symbolic" /usr/share/plasma/plasmoids/org.fennek.actioncenter/contents/ui/main.qml || fail "Action center: Dark mode tile has its own icon"
grep -q 'function resizeTile' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileGrid.qml && grep -q 'SizeFDiagCursor' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileItem.qml && grep -q 'SizeHorCursor' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileItem.qml && grep -q 'SizeVerCursor' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileItem.qml && grep -q 'previewRow' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/TileItem.qml || fail "Start tiles: corner grip resizes; groups resize from the right wall, bottom wall or corner; folded groups show their apps as small tiles"
grep -q 'url = String(url)' /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/Utils.js || fail "Start tiles: a dropped app's address is read as text (Qt 6 hands over an object)"
{ rpm -q plasma-print-manager || rpm -q print-manager; } >/dev/null && rpm -q cups sane-backends >/dev/null || fail "printing (Printers page, print queue) and scanning"
grep -q "^DefaultProfile=Fennek.profile" /etc/xdg/konsolerc && grep -q "^ColorScheme=Fennek" /usr/share/konsole/Fennek.profile && [ -s /usr/share/konsole/Fennek.colorscheme ] || fail "Fennek terminal profile"
grep -q "Fennek OS: a personal greeting" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/lockscreen/LockScreenUi.qml && grep -q "Fennek" /etc/xdg/kscreenlockerrc || fail "lock screen greeting + wallpaper"
grep -q "^LastSeenVersion=99" /etc/xdg/plasma-welcomerc || fail "only one welcome window"
[ -s /usr/share/fennek/apps/welcome/main.qml ] && grep -q "^Exec=fennek-app welcome" /usr/share/applications/fennek-welcome.desktop && grep -q "app=welcome" /usr/libexec/fennek-first-login && grep -q "app=live" /usr/libexec/fennek-first-login || fail "welcome app"
grep -q "Fennek OS splash" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/splash/Splash.qml && grep -q "Fennek OS splash" /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/splash/Splash.qml || fail "Fennek splash"
udevadm verify --no-style /usr/lib/udev/rules.d/90-fennek-usb-writeback.rules >/dev/null && [ -x /usr/libexec/fennek-usb-writeback ] && grep -q '^ExecStart=/usr/libexec/fennek-usb-writeback' /usr/lib/systemd/system/fennek-usb-writeback.service || fail "USB drives: written without delay while attached"
grep -q "nmi_watchdog=0" /usr/lib/bootc/kargs.d/20-fennek-battery.toml && grep -q "^vm.dirty_writeback_centisecs = 1500" /usr/lib/sysctl.d/60-fennek-battery.conf || fail "battery kernel settings"
grep -q "^ColorScheme=FennekDark" /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults || fail "Fennek Dark colors"
grep -q "^theme=__aurorae__svg__FennekClassicDark" /usr/share/plasma/look-and-feel/org.fennek.dark.desktop/contents/defaults && grep -q "^theme=__aurorae__svg__FennekClassic$" /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/defaults && grep -q "fill=\"#202020\"" /usr/share/aurorae/themes/FennekClassicDark/decoration.svg && [ -s /usr/share/aurorae/themes/FennekClassicDark/FennekClassicDarkrc ] || fail "window frame follows light/dark theme"
! ls -d /usr/share/plasma/look-and-feel/org.fedoraproject.* 2>/dev/null || fail "Fedora global themes still listed"
for x in org.kde.breeze.desktop org.kde.breezedark.desktop org.kde.breezetwilight.desktop; do [ ! -e "/usr/share/plasma/look-and-feel/$x" ] || fail "KDE global theme still listed: $x"; done
# The log out screen: in the theme until Plasma 6.7, in the desktop's own package since 6.8.
for x in org.fennek.dark.desktop/contents/logout org.fennek.amber.desktop/contents/logout; do [ -d "/usr/share/plasma/look-and-feel/$x" ] || find /usr/share/plasma/shells/org.kde.plasma.desktop/contents -ipath '*logout*' | grep -q . || fail "Fennek's theme misses a screen: $x ($(ls /usr/share/plasma/look-and-feel/${x%%/*}/contents | xargs))"; done
[ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group PreviewSettings --key MaximumRemoteSize)" = 26214400 ] && grep -q '^PreviewsShown=true' /etc/skel/.local/share/dolphin/view_properties/remote/.directory || fail "Files: pictures on network shares"
grep -q '^BorderActivate=9' /etc/xdg/kwinrc || fail "no hot corner in the top left"
rpm -q ffmpegthumbs kdegraphics-thumbnailers >/dev/null && [ "$(kreadconfig6 --file /etc/xdg/kdeglobals --group PreviewSettings --key MaximumSize)" = 4294967296 ] || fail "Files: thumbnails for videos, PDFs and camera photos, big files too"
grep -aq fennekCollapsedGroups "$(ls /usr/lib64/libdolphinprivate.so.*.* | head -1)" || fail "Files: Fennek's patched view (no group lines, groups fold)"
python3 -c 'import sys; sys.exit("Files".encode("utf-16-le") not in open("/usr/bin/dolphin", "rb").read())' || fail "Files: its windows are titled Files, not Dolphin"
# Taskbar button right-click > App permissions: the task manager rebuilt with the item (its QML is compiled in, so the
# check is "not Fedora's file any more"), and the link it opens lands on Settings > App permissions.
t=/usr/lib64/qt6/plugins/plasma/applets/org.kde.plasma.taskmanager.so
{ rpm -Vf "$t" || true; } | grep -q "^..5.* $t$" && grep -q '^MimeType=x-scheme-handler/fennek-permissions;' /usr/share/applications/fennek-permissions-link.desktop \
&& grep -q 'arguments\[3\]' /usr/share/fennek/apps/permissions/Page.qml || fail "taskbar right-click: App permissions"
grep -q '^ShowToolTips=true' /etc/xdg/dolphinrc && grep -aq fennekHovered "$(ls /usr/lib64/libdolphinprivate.so.*.* | head -1)" && grep -q '^rating=false' /etc/xdg/baloofileinformationrc && grep -q '^version=13' /etc/xdg/baloofileinformationrc && /usr/libexec/fennek-files-info-panel --selftest >/dev/null || fail "Files: Information panel instead of hover pop-ups"
! grep -q '^HomeUrl=' /etc/xdg/dolphinrc && grep -aq 'fennekWatch' "$(ls /usr/lib64/libdolphinprivate.so.*.* | head -1)" && grep -q 'GroupedSorting=true' /etc/skel/.local/share/dolphin/view_properties/remote/.directory \
&& grep -q 'inode/x-fennek-drive' /usr/share/mime/types || fail "Home page (folders, frequent folders, drives) in Files"
[ -s /usr/share/wallpapers/Fennek/contents/images_dark/3840x2160.jpg ] && grep -q 'url(#fullmoon)' /usr/share/fennek/wallpaper-night.svg || fail "night version of the Fennek wallpaper (full moon)"
# Fennek's wallpaper first on the Background page: only it sits at the top; the stock ones are one folder down.
[ "$(ls /usr/share/wallpapers)" = "$(printf 'Fennek\nMore')" ] && [ -n "$(ls /usr/share/wallpapers/More)" ] && grep -q 'wallpapers/More/$n/' /usr/libexec/fennek-shortcuts-update || fail "Fennek's wallpaper first; stock ones one folder down: $(ls /usr/share/wallpapers | xargs)"
for k in desktop documents downloads music pictures videos; do [ -s /usr/share/icons/Fennek/scalable/places/fennek-folder-$k.svg ] && [ -e /usr/share/icons/Fennek-Dark/scalable/places/fennek-folder-$k.svg ] || fail "This PC folder icon $k"; done; [ -s /usr/share/icons/Fennek/scalable/devices/fennek-drive-used-90.svg ] && grep -q ff5a5a /usr/share/icons/Fennek/scalable/devices/fennek-drive-used-90.svg || fail "drive gauge red when nearly full"
grep -q '^MenuBar=Enabled' /etc/xdg/dolphinrc || fail "Files menu bar"
grep -q 'panel.floating = false' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || fail "taskbar layout"
grep -q '^LookAndFeelPackage=org.fennek.desktop' /usr/share/kde-settings/kde-profile/default/xdg/kdeglobals || fail "default lnf"
# What a fresh user actually gets: every kdeglobals in the system lookup must agree on Fennek's look.
for f in /etc/xdg/kdeglobals /usr/share/kde-settings/kde-profile/default/xdg/kdeglobals; do
v=$(grep -h '^LookAndFeelPackage=' "$f" 2>/dev/null | tail -1)
[ -z "$v" ] || [ "$v" = "LookAndFeelPackage=org.fennek.desktop" ] || fail "$f overrides the look: $v"
done
[ "$(XDG_CONFIG_DIRS=/etc/xdg:/usr/share/kde-settings/kde-profile/default/xdg HOME=$(mktemp -d) kreadconfig6 --group KDE --key LookAndFeelPackage)" = org.fennek.desktop ] \
|| fail "a new user wouldn't get the Fennek look"
[ -x /usr/bin/fennek-vt-check ] || fail "vt-check executable"
fennek-vt-check --selftest >/dev/null || fail "vt-check selftest"
grep -q '^Exec=fennek-vt-check %F' /usr/share/kio/servicemenus/fennek-virustotal.desktop || fail "vt service menu"
command -v snapper >/dev/null || fail "snapper"
[ -x /usr/libexec/fennek-home-snapshots ] || fail "snapshot setup script"
bash -n /usr/libexec/fennek-home-snapshots || fail "snapshot script syntax"
for u in fennek-home-snapshots.service snapper-timeline.timer snapper-cleanup.timer; do
systemctl is-enabled -q "$u" || fail "$u not enabled"
done
[ "$(stat -c %a /usr/lib/systemd/system/fennek-home-snapshots.service)" = 644 ] || fail "unit file perms"
grep -q 'wallpapers/Fennek/' /usr/lib/plasmalogin/defaults.conf || fail "login wallpaper"
wm=usr/share/plymouth/themes/spinner/watermark.png; img=$(ls /usr/lib/modules/*/initramfs.img)
[ "$(lsinitrd -f "$wm" "$img" | md5sum)" = "$(md5sum < "/$wm")" ] || fail "initramfs has Fennek OS splash"
{ rpm -V fedora-logos 2>/dev/null || true; } | grep -q "/$wm$" || fail "watermark still Fedora's"
/usr/libexec/fennek-download-guard --selftest >/dev/null || fail "download guard selftest"
fennek-usage --selftest >/dev/null && [ -L /etc/systemd/user/default.target.wants/fennek-usage.service ] || fail "usage history"
grep -q "^Exec=fennek-app settings apphistory" /usr/share/applications/fennek-apphistory.desktop && [ -s /usr/share/fennek/apps/apphistory/Page.qml ] || fail "App history window"
bash -n /usr/libexec/fennek-brightness && [ -x /usr/libexec/fennek-brightness ] || fail "brightness helper"
# Download guard: a sample matching a custom signature goes to quarantine, a clean file stays.
t=$(mktemp -d); mkdir -p "$t/db"
echo "Fennek OS fake malware sample" > "$t/evil.bin"; echo "harmless" > "$t/ok.txt"
sigtool --md5 "$t/evil.bin" > "$t/db/test.hdb"
HOME=$t XDG_DATA_HOME=$t/share FENNEK_CLAM_DB=$t/db /usr/libexec/fennek-download-guard "$t/evil.bin" "$t/ok.txt" >/dev/null
[ ! -e "$t/evil.bin" ] && compgen -G "$t/share/fennek/quarantine/*-evil.bin" >/dev/null || fail "threat not quarantined"
[ -e "$t/ok.txt" ] || fail "clean file was touched"
tr '\0' '\n' < "$(compgen -G "$t/share/fennek/quarantine/*-evil.bin.fennek-info")" | grep -q "$t/evil.bin" \
|| fail "quarantine doesn't record where the file came from"
rm -rf "$t"
# Watch mode: a file landing in Downloads gets scanned, even with a newline in its name.
t=$(mktemp -d); mkdir -p "$t/db" "$t/Downloads"
echo "Fennek fake malware sample" > "$t/sample"; sigtool --md5 "$t/sample" > "$t/db/test.hdb"
HOME=$t XDG_DATA_HOME=$t/share FENNEK_CLAM_DB=$t/db timeout 20 /usr/libexec/fennek-download-guard >/dev/null &
sleep 2; cp "$t/sample" "$t/Downloads/evil"$'\n'"name.bin"
for _ in $(seq 30); do compgen -G "$t/share/fennek/quarantine/*evil*" >/dev/null && break; sleep 0.5; done
compgen -G "$t/share/fennek/quarantine/*evil*" >/dev/null || fail "watch mode didn't quarantine"
kill %1 2>/dev/null; wait 2>/dev/null; rm -rf "$t"
systemctl is-enabled -q clamav-freshclam-once.timer || fail "freshclam timer"
[ -L /etc/systemd/user/default.target.wants/fennek-download-guard.service ] || fail "download guard not enabled"
if x86; then
grep -q "Fennek OS patch: no network" /usr/lib/python3*/site-packages/umu/umu_runtime.py || fail "umu no-telemetry patch"
python3 -c "import umu.umu_runtime" 2>/dev/null || fail "patched umu imports"
fi
grep -q "Europe/Simferopol" /usr/share/timezonefiles/timezones.json && fail "time zone map: Crimea drawn apart from Ukraine"
[ "$(firewall-offline-cmd --get-default-zone 2>/dev/null)" = FennekOS ] || fail "default firewall zone"
[ -z "$(firewall-offline-cmd --zone=FennekOS --list-ports 2>/dev/null)" ] || fail "FennekOS zone opens ports"
[ "$(firewall-offline-cmd --zone=FennekOS-Public --list-services 2>/dev/null)" = dhcpv6-client ] || fail "public zone"
[ "$(firewall-offline-cmd --zone=FennekOS-Sharing --list-ports 2>/dev/null)" = 445/tcp ] || fail "sharing zone"
[ "$(testparm -s --parameter-name='usershare allow guests' 2>/dev/null)" = No ] && [ "$(testparm -s --parameter-name='usershare max shares' 2>/dev/null)" -gt 0 ] && ! grep -q '^\[homes\]' /etc/samba/smb.conf && systemctl is-enabled -q fennek-sharing-zone.path || fail "folder sharing: password always, nothing shared by itself"
command -v opensnitchd >/dev/null || fail "opensnitchd"
! systemctl is-enabled -q opensnitch.service || fail "opensnitch must start only when something is blocked"
fennek-firewall --selftest >/dev/null || fail "firewall selftest"
/usr/libexec/fennek-first-login-places --selftest >/dev/null || fail "classic Files sidebar"
/usr/libexec/fennek-first-login-keyboard --selftest >/dev/null || fail "language switcher labels"
[ "$(kreadconfig6 --file /etc/xdg/klipperrc --group General --key KeepClipboardContents)" = false ] && [ "$(kreadconfig6 --file /etc/xdg/klipperrc --group General --key SaveImages)" = true ] && grep -q 'model.type === 4' /usr/share/plasma/plasmoids/org.fennek.actioncenter/contents/ui/main.qml && grep -q 'Meta+;' /etc/xdg/kglobalshortcutsrc || fail "clipboard: pictures in the history, nothing kept after signing out; Super+; emoji"
grep -q '/etc/xdg/autostart' /usr/libexec/fennek-first-login && /usr/libexec/fennek-tasks --selftest >/dev/null || fail "Task Manager: classic programs by name, what starts at sign-in can be switched off"
/usr/libexec/fennek-taskbar --selftest >/dev/null && [ -s /usr/share/fennek/apps/taskbar/Page.qml ] && grep -q '"page": "taskbar"' /usr/share/fennek/apps/settings/catalog.js || fail "Settings: Taskbar page"
/usr/libexec/fennek-send-to --selftest >/dev/null && command -v kioclient >/dev/null && grep -q 'imgurplugin' /etc/xdg/purposerc && grep -q '^tagsfileitemaction=false' /etc/xdg/kservicemenurc && [ "$(kreadconfig6 --file /etc/xdg/dolphinrc --group ContextMenu --key ShowCopyMoveMenu)" = true ] || fail "right-click menu: Send to, Copy To, no one-click public upload"
[ -L /etc/systemd/user/graphical-session.target.wants/fennek-taskbars.service ] && grep -q 'fennek-taskbars.service' /usr/share/kwin-wayland/scripts/fennekarrows/contents/code/main.js && [ -s /usr/share/fennek/taskbars.js ] || fail "a taskbar on every display"
grep -q 'noLatin' /usr/share/plasma/packages/org.kde.plasmasetup.keyboard/contents/ui/main.qml && grep -q 'capsLock.locked' /usr/share/sddm/themes/fennek/Main.qml && ! grep -q 'keyboard\.capsLock' /usr/share/sddm/themes/fennek/Main.qml || fail "keyboard: a Latin layout stays in the wizard; the sign-in shows Caps Lock and the language"
/usr/libexec/fennek-pin-quick-access --selftest >/dev/null && /usr/libexec/fennek-pin-start --selftest >/dev/null && grep -q "^Exec=/usr/libexec/fennek-pin-start" /usr/share/kio/servicemenus/fennek-pin-quick-access.desktop && grep -q "^X-KDE-Submenu=Pin to" /usr/share/kio/servicemenus/fennek-pin-quick-access.desktop || fail "Pin to: Start and Quick access"
h=$(mktemp -d); touch "$h/a b" "$h/c"
/usr/libexec/fennek-hide "$h/a b" "$h/a b" "file://$h/c" && [ "$(cat "$h/.hidden")" = "$(printf 'a b\nc')" ] \
&& /usr/libexec/fennek-hide --unhide "$h/a b" && [ "$(cat "$h/.hidden")" = c ] && /usr/libexec/fennek-hide --unhide "$h/c" \
&& [ ! -e "$h/.hidden" ] && grep -q "^Exec=/usr/libexec/fennek-hide --unhide" /usr/share/kio/servicemenus/fennek-hide.desktop \
|| fail "Hide / Unhide keeps names and places"
rm -rf "$h"
h=$(mktemp -d); mkdir -p "$h/Desktop" "$h/cfg"; touch "$h/a.txt"
HOME=$h XDG_CONFIG_HOME=$h/cfg /usr/libexec/fennek-shortcut-desktop "$h/a.txt" "file://$h/a.txt" \
&& [ "$(readlink "$h/Desktop/a.txt")" = "$h/a.txt" ] && [ -L "$h/Desktop/a (2).txt" ] \
&& grep -q "^Exec=/usr/libexec/fennek-shortcut-desktop" /usr/share/kio/servicemenus/fennek-pin-quick-access.desktop \
|| fail "Pin to > Desktop makes a shortcut, never overwrites one"
rm -rf "$h"
grep -q "^Exec=/usr/libexec/fennek-change-icon --reset" /usr/share/kio/servicemenus/fennek-change-icon.desktop \
&& cmp -s /usr/share/icons/Fennek/scalable/places/folder-documents.svg /usr/share/icons/Fennek/scalable/places/fennek-folder-documents.svg \
|| fail "Change icon on folders and shortcuts; main folders look like on This PC"
[ -s /usr/share/fennek/sandbox-seccomp.bpf ] && grep -q 'with_seccomp' /usr/bin/fennek-run-exe || fail ".exe sandbox: system-call filter present and used"
/usr/libexec/fennek-previous-versions-helper --selftest >/dev/null && command -v kdialog numfmt >/dev/null && grep -q "exec.path\">/usr/libexec/fennek-previous-versions-helper" /usr/share/polkit-1/actions/org.fennek.previous-versions.policy && grep -q "inode/directory" /usr/share/kio/servicemenus/fennek-previous-versions.desktop || fail "Restore previous versions (files and whole folders)"
grep -q '^Exec=fennek-firewall block %f' /usr/share/kio/servicemenus/fennek-firewall.desktop || fail "firewall menu"
[ -f /usr/share/plasma/plasmoids/com.github.zren.tiledmenu/metadata.json ] || fail "classic start menu (Tiled Menu)"
# Start tiles find their app whichever way Plasma writes its ids (without this they have no icon and don't open).
u=/usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui; [ "$(grep -c "bareId(favoriteId) == bareId(url)" $u/KickerAppModel.qml)" = 2 ] && [ "$(grep -c "url = Utils.parseDropUrl(url)" $u/TileGrid.qml)" -ge 3 ] || fail "Start tiles: app ids"
grep -q 'com.github.zren.tiledmenu' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || fail "start menu in taskbar"
[ "$(systemctl is-enabled rpm-ostree-countme.timer 2>&1)" = masked ] || fail "countme telemetry timer"
! grep -q '^countme=1' /etc/yum.repos.d/*.repo || fail "countme in repos"
! systemctl is-enabled -q dnf-makecache.timer 2>/dev/null || fail "dnf-makecache timer (needless daily download)"
! systemctl is-enabled -q unbound-anchor.timer 2>/dev/null || fail "unbound-anchor timer"
! systemctl is-enabled -q fwupd-refresh.timer 2>/dev/null && ! systemctl is-enabled -q smb.service 2>/dev/null && ! systemctl is-enabled -q nmb.service 2>/dev/null && ! systemctl is-enabled -q wsdd.service 2>/dev/null || fail "firmware list check and folder sharing stay off until asked for (PRIVACY.md)"
rpm -q firefox >/dev/null && grep -q '"DisableTelemetry": true' /etc/firefox/policies/policies.json || fail "Firefox + privacy policy"
grep -q '^Name=Files$' /usr/share/applications/org.kde.dolphin.desktop || fail "Files name"
grep -q '^Name=Task Manager$' /usr/share/applications/org.kde.plasma-systemmonitor.desktop || fail "Task Manager name"
grep -q '^X-KDE-Shortcuts=.*Ctrl+Shift+Esc' /usr/share/applications/org.kde.plasma-systemmonitor.desktop || fail "Ctrl+Shift+Esc"
grep -q '^X-KDE-Shortcuts=Meta+R' /usr/share/applications/fennek-run.desktop && grep -q "org.fennek.Search" /usr/share/fennek/apps/run/main.qml && /usr/libexec/fennek-run-command --selftest >/dev/null && ! grep -q 'Meta+R' /usr/share/applications/org.kde.krunner.desktop || fail "Win+R: Launch (commands, then files on this PC)"
grep -q '^X-KDE-Shortcuts=.*Meta+E' /usr/share/kglobalaccel/org.kde.dolphin.desktop || fail "Super+E opens File Explorer"
grep -q '^X-KDE-Shortcuts=Meta+Shift+S,' /usr/share/applications/org.kde.spectacle.desktop || fail "Super+Shift+S snips a region"
[ "$(kreadconfig6 --file /etc/xdg/kglobalshortcutsrc --group kwin --key Overview | cut -f1)" = Meta+Tab ] || fail "Super+Tab task view"
grep -q 'actioncenter").globalShortcut = "Meta+A"' /usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || fail "Super+A action center"
grep -q '^ViewMode=1' /etc/skel/.local/share/dolphin/view_properties/global/.directory || fail "Files details view"
q=$(ls -d /usr/lib*/qt6/qml/org/kde/desktop | head -1)
grep -q "wheelEnabled: false // Fennek" "$q/ComboBox.qml" && grep -q "wheelEnabled: false // Fennek" "$q/SpinBox.qml" \
&& grep -q "wheel.accepted = false; return; // Fennek" "$q/Slider.qml" && ! grep -q "^prefer " "$q/qmldir" || fail "mouse-wheel comfort patch (and the patched files are the ones used)"
for f in "$q/ComboBox.qml" "$q/SpinBox.qml" "$q/Slider.qml" /etc/zshrc $(x86 && ls /usr/lib/python3*/site-packages/umu/umu_runtime.py); do
[ "$(tr -d '\000' < "$f" | wc -c)" = "$(wc -c < "$f")" ] || fail "$f has zero bytes (broken in-place edit)"
done
id fsearch >/dev/null 2>&1 || useradd -m fsearch >/dev/null 2>&1
su fsearch -c "/usr/libexec/fennek-search --selftest" >/dev/null || fail "search selftest"
[ -L /etc/systemd/user/graphical-session.target.wants/fennek-search.service ] || fail "search service not enabled"
grep -q '^X-Plasma-DBusRunner-Service=org.fennek.Search' /usr/share/krunner/dbusplugins/fennek-search.desktop || fail "search runner"
for f in /usr/bin/fennek-* /usr/libexec/fennek-* /usr/lib/systemd/system/fennek-* /usr/share/plasma/plasmoids/com.github.zren.tiledmenu; do
[ "$(stat -c %u:%g "$f")" = 0:0 ] || fail "$f not owned by root"
done
python3 -c "
import json; d = json.load(open('/etc/containers/policy.json'))['transports']['docker']['ghcr.io/olexify/fennekos'][0]
assert json.load(open('/etc/containers/policy.json'))['transports']['docker']['ghcr.io/olexify/fennekos-nvidia'][0]['keyPath'] == d['keyPath']
assert d['type'] == 'sigstoreSigned' and d['keyPath'] == '/etc/pki/containers/fennek-cosign.pub'
" 2>/dev/null || fail "updates must require Fennek's signature"
grep -q 'BEGIN PUBLIC KEY' /etc/pki/containers/fennek-cosign.pub || fail "Fennek public key"
command -v mangohud >/dev/null || fail "MangoHud (in-game overlay)"
# Editions: the one CI asked for (FENNEK_EDITION) is the one built; the NVIDIA edition carries NVIDIA's driver.
[ "$(cat /usr/share/fennek/edition)" = "${FENNEK_EDITION:-}" ] || fail "edition is '$(cat /usr/share/fennek/edition)', expected '${FENNEK_EDITION:-}'"
if [ "${FENNEK_EDITION:-}" = nvidia ]; then
find /usr/lib/modules -name 'nvidia.ko*' | grep -q . || fail "NVIDIA edition: no NVIDIA kernel module"
command -v nvidia-settings >/dev/null && grep -qx 'Name=NVIDIA Settings' /usr/share/applications/*nvidia-settings*.desktop || fail "NVIDIA edition: NVIDIA Settings in Start"
# GPU Control: its service off until someone asks; Secure Boot's one key, nothing else the helper can be told
grep -qx 'Name=GPU Control' /usr/share/applications/io.github.ilya_zlobintsev.LACT.desktop && grep -q 'fennek-gpu-control' /usr/share/applications/io.github.ilya_zlobintsev.LACT.desktop \
&& command -v lact >/dev/null && ! systemctl is-enabled -q lactd.service && [ -f /etc/pki/akmods/certs/akmods-ublue.der ] || fail "NVIDIA edition: GPU Control, Secure Boot key"
find /usr/lib/modules -name 'v4l2loopback.ko*' | grep -q . && grep -q 'card_label="NV Broadcast Camera"' /etc/modprobe.d/fennek-vcam.conf && python3 -c "import gi; gi.require_version('Gtk', '4.0'); gi.require_version('Adw', '1')" && command -v pw-loopback >/dev/null && /usr/libexec/fennek-get-nvbroadcast --selftest >/dev/null && [ -f /usr/share/applications/fennek-get-nvbroadcast.desktop ] || fail "NVIDIA edition: what NV Broadcast needs from the system (virtual camera module and settings, GTK 4, PipeWire tools)"
fi
grep -q '"rhgb", "quiet"' /usr/lib/bootc/kargs.d/10-fennek-splash.toml || fail "boot splash kernel args"
python3 -c "import json; p=json.load(open('/etc/firefox/policies/policies.json'))['policies']; assert p['DisableTelemetry'] and p['DisableFirefoxStudies']" \
|| fail "Firefox telemetry policy"
# Privacy audit (informational, printed in CI logs): everything that could connect out. Keep PRIVACY.md in sync.
id fquar >/dev/null 2>&1 || useradd -m fquar >/dev/null 2>&1
su fquar -c "fennek-quarantine --selftest" >/dev/null || fail "quarantine selftest"
command -v efibootmgr >/dev/null || fail "efibootmgr (Restart to Windows)"
/usr/libexec/fennek-reboot-windows-helper --selftest >/dev/null || fail "reboot-windows selftest"
systemctl is-enabled -q fennek-detect-windows.service || fail "windows detection not enabled"
# Windows' own drives: read at the start of the computer by a sandboxed scan, never when a drive is plugged in; one hibernated drive makes them all read-only; no password for admins.
grep -q 'rd.live.image' /usr/libexec/fennek-windows-drives && grep -q 'adjust-system-clock' /usr/libexec/fennek-detect-windows && /usr/libexec/fennek-windows-scan --selftest >/dev/null && /usr/libexec/fennek-windows-drives --selftest >/dev/null || fail "Windows drives selftests"
for c in ntfsls ntfscat udisksctl /usr/bin/udevadm; do command -v "$c" >/dev/null || fail "Windows drives: $c"; done
systemctl is-enabled -q fennek-windows-scan.service && [ -L /etc/systemd/user/graphical-session.target.wants/fennek-windows-drives.service ] && grep -q 'type == "ntfs"' /usr/share/polkit-1/rules.d/60-fennek-windows-drive.rules || fail "Windows drives in Files"
grep -qxF 'ntfs_defaults=uid=$UID,gid=$GID,windows_names,norecover' /etc/udisks2/mount_options.conf || fail "Windows drives: a log Windows left unclean stays as it is"
for l in 'Before=systemd-user-sessions.service' 'TimeoutStopSec=2' 'ExecStartPre=-+/usr/bin/udevadm settle --timeout=30' 'ExecStart=-/usr/libexec/fennek-windows-scan --scan' 'ExecStartPost=+/usr/libexec/fennek-windows-scan --apply' 'DynamicUser=yes' 'RuntimeDirectory=fennek-windows-scan' 'DevicePolicy=closed' 'DeviceAllow=block-sd r' 'DeviceAllow=block-blkext r' 'DeviceAllow=block-virtblk r' 'DeviceAllow=block-mmc r' 'CapabilityBoundingSet=' 'NoNewPrivileges=yes' 'ProtectSystem=strict' 'ProtectHome=yes' 'PrivateTmp=yes' 'PrivateNetwork=yes' 'RestrictAddressFamilies=AF_UNIX' 'MemoryDenyWriteExecute=yes' 'SystemCallFilter=@system-service'; do grep -qxF "$l" /usr/lib/systemd/system/fennek-windows-scan.service || fail "Windows drive scan: no line $l"; done
v=$(systemd-analyze verify /usr/lib/systemd/system/fennek-windows-scan.service 2>&1 || true); ! grep 'fennek-windows-scan.service:[0-9]' <<<"$v" || fail "Windows drive scan: systemd doesn't take these lines"
[ ! -e /usr/libexec/fennek-ntfs-probe ] && [ ! -e /usr/lib/udev/rules.d/90-fennek-windows-drive.rules ] && ! grep -qrE 'fennek-(ntfs|windows)' /usr/lib/udev/rules.d || fail "something still reads a drive when it's plugged in"
grep -q 'set-local-rtc 1' /usr/libexec/fennek-detect-windows || fail "clock in step with Windows"
[ -L /etc/systemd/user/graphical-session.target.wants/fennek-exe-entries.service ] && grep -q 'fennek-run-exe --sync' /usr/lib/systemd/user/fennek-exe-entries.service || fail "installed programs in Start"
grep -q '^DndBehavior=MoveIfSameDevice' /etc/xdg/kdeglobals && grep -q 'Fennek Next Language' /usr/share/kwin*/scripts/fennekarrows/contents/code/main.js || fail "dragging files without a question, Alt+Shift"
[ -x /usr/lib/NetworkManager/dispatcher.d/90-fennek-everyday-apps ] || fail "everyday apps start when the computer gets online"
grep -q 'name="edit_find" shortcut="Ctrl+F; F3"' /usr/share/kxmlgui5/dolphin/dolphinui.rc || fail "F3 searches in Files"
python3 -c 'import glob; assert any("fennekTerm".encode("utf-16-le") in open(f, "rb").read() for f in glob.glob("/usr/bin/dolphin") + glob.glob("/usr/lib64/libdolphin*"))' || fail "Files searches names by plain words"
[ -x /usr/libexec/fennek-encryption ] && [ -x /usr/libexec/fennek-encryption-helper ] && [ -s /usr/share/polkit-1/actions/org.fennek.encryption.policy ] && grep -q '/usr/libexec/fennek-encryption' /usr/share/fennek/apps/settings/catalog.js || fail "Device encryption page"
[ -x /usr/lib/NetworkManager/dispatcher.d/90-fennek-sharing ] || fail "the computer lists itself only on networks you share on"
/usr/libexec/fennek-graphics-check --selftest >/dev/null && /usr/libexec/fennek-graphics-check --json | python3 -c 'import json,sys; d=json.load(sys.stdin); assert set(d) == {"edition", "nvidia", "secure_boot"}' \
&& grep -q 'exec.path">/usr/libexec/fennek-secure-boot-helper' /usr/share/polkit-1/actions/org.fennek.secure-boot.policy && grep -q '<allow_active>auth_admin</allow_active>' /usr/share/polkit-1/actions/org.fennek.secure-boot.policy \
&& command -v mokutil >/dev/null && [ -L /etc/systemd/user/graphical-session.target.wants/fennek-graphics-check.service ] || fail "graphics card check, Secure Boot step"
grep -q 'exec.path">/usr/libexec/fennek-reboot-windows-helper' /usr/share/polkit-1/actions/org.fennek.reboot-windows.policy \
|| fail "reboot-windows polkit action"
# Sign-in screen: SDDM is THE display manager, uses the Fennek theme, and the theme loads without QML errors
# (a broken greeter means nobody can log in).
[ "$(readlink -f /etc/systemd/system/display-manager.service)" = /usr/lib/systemd/system/sddm.service ] || fail "display manager isn't SDDM"
grep -q '^Current=fennek' /etc/sddm.conf.d/10-fennek.conf || fail "SDDM theme not selected"
greeter=$(command -v sddm-greeter-qt6 || command -v sddm-greeter)
# The test-mode greeter runs until stopped: timeout (exit 124) is expected; only QML errors count.
out=$(QT_QPA_PLATFORM=offscreen timeout 15 "$greeter" --test-mode --theme /usr/share/sddm/themes/fennek 2>&1 || true)
! grep -iE "qml.*(error|is not a type|not installed|unavailable|ReferenceError|TypeError)|file:///usr/share/sddm/themes/fennek/.*:[0-9]+" <<<"$out" \
|| { echo "$out" | grep -iE "qml|fennek" | head; fail "SDDM theme has QML errors"; }
# Start menu default tiles: every tile must point to an installed app (no blank tiles).
python3 - <<'PY' || fail "Start menu tile points to a missing app"
import base64, json, os, re
js = open("/usr/share/plasma/look-and-feel/org.fennek.desktop/contents/layouts/org.kde.plasma.desktop-layout.js").read()
tiles = json.loads(base64.b64decode(re.search(r'writeConfig\("tileModel", "([^"]+)"\)', js).group(1)))
missing = [t["url"] for t in tiles if "url" in t and not os.path.exists("/usr/share/applications/" + t["url"])]
assert not missing, missing
assert sum(1 for t in tiles if t.get("tileType") == "group") >= 2
PY
# Fennek Settings: every page it links to exists (no dead buttons); Super+I belongs to it alone.
# Fennek's own apps run as normal windows (system look, own taskbar icon) through fennek-app.
rc=0; fennek-app ../etc 2>/dev/null || rc=$?; [ $rc -eq 2 ] && ! command -v g++ >/dev/null || fail "fennek-app launcher (rejects paths; no compiler left)"
rc=0; fennek-kcm ../x 2>/dev/null || rc=$?; [ $rc -eq 2 ] || fail "fennek-kcm rejects anything but a page id"
rc=0; QT_QPA_PLATFORM=offscreen HOME=$(mktemp -d) timeout 8 fennek-kcm kcm_mouse >/tmp/kcm.log 2>&1 || rc=$?
[ $rc -eq 124 ] || { tail -5 /tmp/kcm.log; fail "Settings page in its own window without Apply (fennek-kcm, exit $rc)"; }
rc=0; QT_FORCE_STDERR_LOGGING=1 QT_QPA_PLATFORM=offscreen HOME=$(mktemp -d) timeout 10 fennek-app settings kcm_mouse >/tmp/kcmview.log 2>&1 || rc=$?
[ $rc -eq 124 ] && ! grep -q "can't be shown inside" /tmp/kcmview.log || { tail -5 /tmp/kcmview.log; fail "Settings shows its pages inside (KcmView, exit $rc)"; }
for pg in updates firewall quarantine backup apphistory support; do # Fennek's own pages open inside Settings too
rc=0; QT_FORCE_STDERR_LOGGING=1 QT_QPA_PLATFORM=offscreen HOME=$(mktemp -d) timeout 10 fennek-app settings $pg >/tmp/setpage.log 2>&1 || rc=$?
[ $rc -eq 124 ] && ! grep -q 'Page.qml.*\(Error\|error\|Cannot\|is not\)' /tmp/setpage.log || { tail -5 /tmp/setpage.log; fail "$pg opens inside Settings (exit $rc)"; }
done
grep -q '^X-KDE-Shortcuts=Print$' /usr/share/applications/fennek-printscreen.desktop && ! grep -A30 '^\[Desktop Entry\]' /usr/share/applications/org.kde.spectacle.desktop | grep -m1 '^X-KDE-Shortcuts=' | cut -d= -f2 | tr , '\n' | grep -qx 'Print' && [ -x /usr/libexec/fennek-printscreen ] || fail "PrtSc copies the screen, Alt+PrtSc the window, Super+PrtSc saves"
! grep -l '^X-KDE-Shortcuts=.*Meta+R\(,\|$\)' /usr/share/applications/*.desktop | grep -v fennek-run.desktop | grep -q . || fail "Super+R is the Run box only"
grep -q '^_launch=Ctrl+Alt+Del' /etc/xdg/kglobalshortcutsrc && grep -q '^Log Out=none,Ctrl+Alt+Del' /etc/xdg/kglobalshortcutsrc && grep -q 'fennek-security' /usr/share/kwin-wayland/scripts/fennekwinx/contents/code/main.js || fail "Ctrl+Alt+Del: security options screen"
t=$(mktemp -d); HOME=$t /usr/libexec/fennek-text set 13 Inter && [ "$(HOME=$t /usr/libexec/fennek-text get)" = "13 Inter" ] && ! HOME=$t /usr/libexec/fennek-text set 13 'Inter;rm' 2>/dev/null || fail "Personalize: text size and typeface (fennek-text)"; rm -rf "$t"
grep -q 'Inter,11,' /etc/xdg/kdeglobals && grep -q '"page": "personalize"' /usr/share/fennek/apps/settings/catalog.js || fail "Text 11 points by default; Personalize in Settings"
for a in settings firewall welcome apphistory backup live updates quarantine winx power run security personalize permissions taskmanager cleanup installedapps; do # starts off-screen and keeps running = the QML loads
rc=0; QT_FORCE_STDERR_LOGGING=1 QT_QPA_PLATFORM=offscreen HOME=$(mktemp -d) timeout 8 fennek-app "$a" >/tmp/app-$a.log 2>&1 || rc=$?
[ $rc -eq 124 ] || { tail -5 /tmp/app-$a.log; fail "app $a does not start (exit $rc)"; }
done
python3 - <<'PY' || fail "Fennek Settings links to something that isn't installed"
import glob, json, os, re, shutil
src = open("/usr/share/fennek/apps/settings/catalog.js").read()
cats = json.loads(src[src.index("["):])
kcms = {os.path.basename(p)[:-3] for p in glob.glob("/usr/lib64/qt6/plugins/plasma/kcms/**/*.so", recursive=True)}
bad = []
for c in cats:
for i in c["items"]:
if "kcm" in i and i["kcm"] not in kcms and not i.get("optional"): bad.append(i["kcm"])
if "cmd" in i and not shutil.which(i["cmd"].split()[0]): bad.append(i["cmd"])
if i.get("icon", "").startswith("fennek-") and not glob.glob("/usr/share/icons/Fennek/scalable/*/" + i["icon"] + ".svg"): bad.append(i["icon"])
if "page" in i and not os.path.exists(f"/usr/share/fennek/apps/{i['page']}/Page.qml"): bad.append(i["page"])
assert not bad, bad
opt = [i["kcm"] for c in cats for i in c["items"] if i.get("optional")]
print("Settings optional pages present:", sorted(k for k in opt if k in kcms), "missing:", sorted(k for k in opt if k not in kcms))
PY
python3 - <<'PY' || fail "QML: a top-level property or handler is set twice (the widget would not load)"
import glob, re, collections
bad = []
for f in glob.glob("/usr/share/plasma/plasmoids/org.fennek.*/contents/ui/main.qml") + glob.glob("/usr/share/fennek/apps/*/*.qml"):
keys = collections.Counter(m.group(1) for m in re.finditer(r"^ ((?:on[A-Z]\w*|[a-z]\w*(?:\.\w+)*)):(?!:)", open(f).read(), re.M)
if not m.group(1).startswith(("property", "readonly", "function", "signal", "required")))
bad += [f"{f}: {k}" for k, n in keys.items() if n > 1]
assert not bad, bad
PY
python3 - <<'PY' || fail "classic commands / language list open a Settings page that isn't installed"
import glob, os, re
kcms = {os.path.basename(p)[:-3] for p in glob.glob("/usr/lib64/qt6/plugins/plasma/kcms/**/*.so", recursive=True)}
src = open("/usr/libexec/fennek-classic-command").read() + open("/usr/share/plasma/plasmoids/org.fennek.keyboard/contents/ui/main.qml").read()
used = set(re.findall(r'\b(kcm_[a-z_]+)', src))
assert len(used) >= 7 and used <= kcms, sorted(used - kcms)
PY
python3 - <<'PY' || fail "Start right-click menu runs something that isn't installed"
import glob, os, re, shutil
src = open("/usr/share/plasma/plasmoids/com.github.zren.tiledmenu/contents/ui/main.qml").read()
cmds = re.findall(r'run\("([^"]+)"\)', src)
kcms = {os.path.basename(p)[:-3] for p in glob.glob("/usr/lib64/qt6/plugins/plasma/kcms/**/*.so", recursive=True)}
bad = [c for c in cmds if not shutil.which(c.split()[0]) or (c.startswith("systemsettings ") and c.split()[1] not in kcms)]
assert len(cmds) >= 10 and not bad, (len(cmds), bad)
PY
grep -q '^X-KDE-Shortcuts=Meta+I' /usr/share/applications/fennek-settings.desktop || fail "Super+I for Settings"
! grep -hs '^X-KDE-Shortcuts=.*Meta+I' /usr/share/kglobalaccel/systemsettings.desktop /usr/share/applications/systemsettings.desktop \
|| fail "Super+I still on KDE System Settings too"
# Battery defaults: power-saver on battery, balanced on AC (needs a power-profiles provider, e.g. tuned-ppd).
[ "$(kreadconfig6 --file /etc/xdg/powerdevilrc --group Battery --group Performance --key PowerProfile)" = power-saver ] \
|| fail "power-saver profile on battery"
[ "$(kreadconfig6 --file /etc/xdg/powerdevilrc --group AC --group Performance --key PowerProfile)" = balanced ] || fail "balanced on AC"
{ rpm -q tuned-ppd >/dev/null || rpm -q power-profiles-daemon >/dev/null; } || fail "no power-profiles provider"
[ "$(kreadconfig6 --file /etc/xdg/powerdevilrc --group BatteryManagement --key BatteryCriticalAction)" = 1 ] || fail "sleep on critical battery"
[ "$(kreadconfig6 --file /etc/xdg/powerdevilrc --group AC --group SuspendAndShutdown --key PowerButtonAction)" = 1 ] && grep -q 'PowerButtonAction 8' /usr/libexec/fennek-shortcuts-update || fail "power button: a laptop sleeps, a desktop shuts down"
grep -q 'invokeDefaultAction' /usr/share/plasma/plasmoids/org.fennek.actioncenter/contents/ui/main.qml && grep -q 'invokeAction' /usr/share/plasma/plasmoids/org.fennek.actioncenter/contents/ui/main.qml || fail "action center: a click on a notification and its buttons"
ls /usr/lib64/qt6/qml/org/kde/plasma/clock/qmldir >/dev/null && grep -q 'import org.kde.plasma.clock' /usr/share/plasma/plasmoids/org.fennek.clock/contents/ui/main.qml || fail "taskbar clock: follows the real time after sleep"
echo "--- privacy audit ---"
echo "enabled timers: $(systemctl list-unit-files --type=timer --state=enabled --no-legend 2>/dev/null | awk '{print $1}' | tr '\n' ' ')"
echo "user timers: $(ls /etc/systemd/user/timers.target.wants/ 2>/dev/null | tr '\n' ' ')"
echo "NM connectivity: $(grep -rh '^uri=' /usr/lib/NetworkManager/conf.d/ /etc/NetworkManager/conf.d/ 2>/dev/null | tr '\n' ' ')"
echo "services: $(systemctl list-unit-files --type=service --state=enabled --no-legend 2>/dev/null | awk '{print $1}' | tr '\n' ' ')"
echo "user services: $(systemctl --global list-unit-files --type=service --state=enabled --no-legend 2>/dev/null | awk '{print $1}' | tr '\n' ' ')"
echo "start menu apps: $(grep -L -E '^(NoDisplay|Hidden)=true' /usr/share/applications/*.desktop | xargs -n1 basename | tr '\n' ' ')"
echo "firefox: $(rpm -q firefox 2>/dev/null || echo 'not installed (rpm)')"
echo "notification popup files: $(ls /usr/share/plasma/plasmoids/org.kde.plasma.notifications/contents/ui 2>&1 | tr '\n' ' ' | cut -c1-400)"
echo "printing/scanning: $(rpm -q cups print-manager sane-backends ipp-usb avahi cups-filters 2>&1 | tr '\n' ' ')"
# For the action center's Clipboard tab: what the clipboard module offers.
cb=$(dirname "$(find /usr/lib64/qt6/qml/org/kde/plasma/private -name qmldir -path '*clipboard*' | head -1)" 2>/dev/null)
echo "clipboard module: $cb $(cat "$cb/qmldir" 2>/dev/null | tr '\n' ' ')"
grep -hoE 'exports: \[[^]]*\]|"[A-Za-z]+Role"|Q_ENUM|name: "[A-Z][A-Za-z]*Role[s]?"' "$cb"/*.qmltypes 2>/dev/null | sort -u | tr '\n' ' ' | cut -c1-2000 || true; echo
echo "clipboard applet: $(find /usr/share/plasma/plasmoids /usr/lib64/qt6/plugins/plasma/applets -iname '*clipboard*' 2>/dev/null | tr '\n' ' ')"
grep -rhoE 'Private\.[A-Za-z]+|import org\.kde\.plasma\.private\.clipboard[^\n]*' /usr/share/plasma/plasmoids/org.kde.plasma.clipboard/ 2>/dev/null | sort -u | tr '\n' ' ' || true; echo
echo "--- end audit ---"
echo "all checks passed"