/ de-omarchy / bin / omarchy-upgrade-to-quattro
swadhinbiswas Fix installer: batch repo pkgs + stdin-safe AUR prompts, create /etc/environment.d, retarget all runtime fallbacks to /usr/share/de-omarchy · 8/22/2026
3eba9d4
2371 lines · 91.7 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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
#!/bin/bash

# omarchy:summary=Upgrade a legacy Omarchy install to the package-backed Omarchy quattro layout.
# omarchy:args=[--yes] [--reboot] [--dev] [--channel stable|rc|edge] [--user USER]
# omarchy:examples=omarchy upgrade to quattro | omarchy upgrade to quattro --dev
# omarchy:requires-sudo=true

set -euo pipefail

usage() {
  cat <<'USAGE'
Usage: omarchy-upgrade-to-quattro [--yes] [--reboot] [--dev] [--channel stable|rc|edge] [--user USER]

Upgrade any pre-quattro Omarchy install to the package-backed Omarchy quattro layout.
This script is intentionally self-contained so it can be shipped as a command
in Omarchy 3.8.x or run directly with curl | bash on older systems.

Options:
  -y, --yes                 Do not prompt before upgrading
  --reboot                  Reboot automatically after a successful upgrade
  --dev                     Use dev packages [omarchy-dev / omarchy-settings-dev] (uses edge channel)
  --channel stable|rc|edge  Override the default stable Omarchy package channel
  --user USER               User account to refresh after package install
  -h, --help                Show this help
USAGE
}

log() {
  printf '\033[32m==>\033[0m %s\n' "$*"
}

warn() {
  printf '\033[33mWarning:\033[0m %s\n' "$*" >&2
}

fail() {
  printf '\033[31mError:\033[0m %s\n' "$*" >&2
  exit 1
}

normalize_channel() {
  case "${1:-}" in
    stable|master|main) echo stable ;;
    rc) echo rc ;;
    edge|dev) echo edge ;;
    *) return 1 ;;
  esac
}

valid_channel() {
  normalize_channel "$1" >/dev/null 2>&1
}

# The channel the legacy install is already on, read the way
# omarchy-version-channel reads it. Order matters: mirror.omarchy.org is a
# substring of both of the others.
detect_installed_channel() {
  [[ -f /etc/pacman.d/mirrorlist ]] || return 1

  if grep -q 'https://stable-mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
    echo stable
  elif grep -q 'https://rc-mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
    echo rc
  elif grep -q 'https://mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
    echo edge
  else
    return 1
  fi
}

channel_override="${OMARCHY_UPGRADE_CHANNEL:-}"
channel_override_cli=0
target_user="${OMARCHY_INSTALL_USER:-}"
yes=0
auto_reboot=0
boot_cmdline_unsafe=0
use_dev_packages=${OMARCHY_UPGRADE_DEV:-0}

while (($#)); do
  case "$1" in
    -y|--yes)
      yes=1
      shift
      ;;
    --reboot)
      auto_reboot=1
      shift
      ;;
    --dev)
      use_dev_packages=1
      shift
      ;;
    --channel)
      channel_override="${2:-}"
      channel_override_cli=1
      shift 2
      ;;
    --user)
      target_user="${2:-}"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      fail "Unknown option: $1"
      ;;
  esac
done

case "$use_dev_packages" in
  1|true|yes|on) use_dev_packages=1 ;;
  0|false|no|off|"") use_dev_packages=0 ;;
  *) fail "Invalid OMARCHY_UPGRADE_DEV value '$use_dev_packages'. Use 1/0, true/false, or pass --dev." ;;
esac

# Land on the Quattro equivalent of wherever the machine already is, unless the
# caller said otherwise. Stable machines get the production packages off the
# stable channel. rc and edge machines stay on their own mirror, but take the
# dev packages, because that is where Quattro lives until it ships.
if [[ -z $channel_override ]] && (( ! use_dev_packages )); then
  case "$(detect_installed_channel || true)" in
    stable) channel_override=stable ;;
    rc) channel_override=rc use_dev_packages=1 ;;
    edge) channel_override=edge use_dev_packages=1 ;;
  esac
fi

if (( use_dev_packages )); then
  # The dev packages are only published to the edge package repo, which the rc
  # and edge channels both point at; only stable is incompatible. rc keeps its
  # own Arch mirror either way.
  if [[ -n $channel_override ]]; then
    if ! valid_channel "$channel_override"; then
      fail "Invalid channel '$channel_override'. Use stable, rc, or edge."
    fi
    if [[ $(normalize_channel "$channel_override") == stable ]]; then
      fail "--dev needs the edge package repo; use --channel rc or --channel edge."
    fi
  else
    channel_override=edge
  fi
fi

if { [[ -n $channel_override ]] || (( channel_override_cli )); } && ! valid_channel "$channel_override"; then
  fail "Invalid channel '$channel_override'. Use stable, rc, or edge."
fi

if ! command -v pacman >/dev/null 2>&1; then
  fail "This upgrade is only supported on Arch/Omarchy systems with pacman."
fi

if ! command -v sudo >/dev/null 2>&1 && (( EUID != 0 )); then
  fail "sudo is required when not running as root."
fi

if [[ -z $target_user ]]; then
  if (( EUID == 0 )); then
    target_user="${SUDO_USER:-}"
  else
    target_user="${USER:-$(id -un)}"
  fi
fi

if [[ -z $target_user || $target_user == root ]]; then
  fail "Could not determine the non-root Omarchy user. Re-run with --user USER."
fi

if ! getent passwd "$target_user" >/dev/null; then
  fail "User '$target_user' does not exist."
fi

target_home=$(getent passwd "$target_user" | cut -d: -f6)
[[ -n $target_home && -d $target_home ]] || fail "Home directory for '$target_user' was not found."
target_uid=$(id -u "$target_user")
target_runtime_dir="/run/user/$target_uid"
package_path="/usr/share/de-omarchy/bin:/usr/local/bin:/usr/bin:/bin:$target_home/.local/bin"

as_root() {
  if (( EUID == 0 )); then
    "$@"
  else
    sudo "$@"
  fi
}

run_as_user() {
  if (( EUID == 0 )); then
    if command -v sudo >/dev/null 2>&1; then
      sudo -u "$target_user" -H "$@"
    else
      runuser -u "$target_user" -- "$@"
    fi
  else
    "$@"
  fi
}

run_as_user_session() {
  run_as_user env \
    HOME="$target_home" \
    USER="$target_user" \
    LOGNAME="$target_user" \
    XDG_RUNTIME_DIR="$target_runtime_dir" \
    DBUS_SESSION_BUS_ADDRESS="unix:path=$target_runtime_dir/bus" \
    XDG_CURRENT_DESKTOP=Hyprland \
    XDG_SESSION_DESKTOP=Hyprland \
    DESKTOP_SESSION=hyprland \
    "$@"
}

# Run an Omarchy command as the user with the package-backed paths, without
# assuming a live session. Extra NAME=VALUE args extend the environment.
run_as_user_omarchy() {
  run_as_user env \
    HOME="$target_home" \
    USER="$target_user" \
    LOGNAME="$target_user" \
    OMARCHY_PATH=/usr/share/de-omarchy \
    PATH="$package_path" \
    "$@"
}

package_installed_exact() {
  local pkg="$1"
  pacman -Qq "$pkg" 2>/dev/null | grep -Fxq "$pkg"
}

mark_packages_explicit() {
  local pkg installed_pkg
  local installed_packages=()

  for pkg in "$@"; do
    # pacman -Qq resolves installed providers too (for example nvim -> neovim),
    # while package_installed_exact intentionally does not.
    installed_pkg=$(pacman -Qq "$pkg" 2>/dev/null | head -n1 || true)
    [[ -n $installed_pkg ]] && installed_packages+=("$installed_pkg")
  done

  ((${#installed_packages[@]})) || return 0
  mapfile -t installed_packages < <(printf '%s\n' "${installed_packages[@]}" | sort -u)

  if ! as_root pacman -D --asexplicit "${installed_packages[@]}" >/dev/null; then
    warn "Could not mark installed default packages as explicit; retired-package cleanup may leave extra dependencies installed."
  fi
}

target_hyprland_pid() {
  pgrep -u "$target_uid" -x Hyprland 2>/dev/null | head -n1 || true
}

target_process_env() {
  local name="$1" pid value
  pid=$(target_hyprland_pid)
  [[ -n $pid && -r /proc/$pid/environ ]] || return 1
  value=$(tr '\0' '\n' <"/proc/$pid/environ" 2>/dev/null | awk -v name="$name" '
    BEGIN { prefix = name "=" }
    index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }
  ' || true)
  [[ -n $value ]] || return 1
  printf '%s\n' "$value"
}

valid_wayland_display() {
  [[ -n ${1:-} && -S $target_runtime_dir/$1 ]]
}

discover_wayland_display() {
  local candidate
  candidate="${WAYLAND_DISPLAY:-}"
  if valid_wayland_display "$candidate"; then
    printf '%s\n' "$candidate"
    return 0
  fi

  candidate=$(target_process_env WAYLAND_DISPLAY || true)
  if valid_wayland_display "$candidate"; then
    printf '%s\n' "$candidate"
    return 0
  fi

  local candidates=()
  mapfile -t candidates < <(find "$target_runtime_dir" -maxdepth 1 -type s -name 'wayland-*' -printf '%f\n' 2>/dev/null || true)
  (( ${#candidates[@]} == 1 )) || return 1
  printf '%s\n' "${candidates[0]}"
}

valid_hyprland_signature() {
  [[ -n ${1:-} ]] || return 1
  [[ -S $target_runtime_dir/hypr/$1/.socket.sock || -S $target_runtime_dir/hypr/$1/.socket2.sock ]]
}

discover_hyprland_signature() {
  local candidate
  candidate="${HYPRLAND_INSTANCE_SIGNATURE:-}"
  if valid_hyprland_signature "$candidate"; then
    printf '%s\n' "$candidate"
    return 0
  fi

  candidate=$(target_process_env HYPRLAND_INSTANCE_SIGNATURE || true)
  if valid_hyprland_signature "$candidate"; then
    printf '%s\n' "$candidate"
    return 0
  fi

  local candidates=()
  mapfile -t candidates < <(find "$target_runtime_dir/hypr" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null || true)
  (( ${#candidates[@]} == 1 )) || return 1
  printf '%s\n' "${candidates[0]}"
}

channel=$(normalize_channel "${channel_override:-stable}")

case "$channel" in
  stable)
    mirror_server='https://stable-mirror.omarchy.org/$repo/os/$arch'
    package_server='https://pkgs.omarchy.org/stable/$arch'
    ;;
  rc)
    mirror_server='https://rc-mirror.omarchy.org/$repo/os/$arch'
    package_server='https://pkgs.omarchy.org/edge/$arch'
    ;;
  edge)
    mirror_server='https://mirror.omarchy.org/$repo/os/$arch'
    package_server='https://pkgs.omarchy.org/edge/$arch'
    ;;
esac

if (( ! yes )); then
  printf '\033[2J\033[3J\033[H' >/dev/tty
  cat <<EOF
████████▄   ███    █▄     ▄████████     ███         ███        ▄████████  ▄██████▄
███    ███  ███    ███   ███    ███ ▀█████████▄ ▀█████████▄   ███    ███ ███    ███
███    ███  ███    ███   ███    ███    ▀███▀▀██    ▀███▀▀██   ███    ███ ███    ███
███    ███  ███    ███   ███    ███     ███   ▀     ███   ▀  ▄███▄▄▄▄██▀ ███    ███
███    ███  ███    ███ ▀███████████     ███         ███     ▀▀███▀▀▀▀▀   ███    ███
███    ███  ███    ███   ███    ███     ███         ███     ▀███████████ ███    ███
███  ▀ ███  ███    ███   ███    ███     ███         ███       ███    ███ ███    ███
 ▀██████▀▄█ ████████▀    ███    █▀     ▄████▀      ▄████▀     ███    ███  ▀██████▀
                                                              ███    ███

Upgrading Omarchy to Quattro is a one-way street!
You cannot downgrade from Quattro.
Make sure you have a backup.

EOF

  gum confirm "Continue with upgrade?" </dev/tty || exit 0
fi

sudo_keepalive_pid=""
hyprland_config_reload_suppressed=0
upgrade_started=0
upgrade_completed=0

cleanup_on_exit() {
  local exit_status=$?

  if (( hyprland_config_reload_suppressed )); then
    restore_hyprland_config_reload >/dev/null 2>&1 || true
  fi

  if [[ -n ${sudo_keepalive_pid:-} ]]; then
    kill "$sudo_keepalive_pid" 2>/dev/null || true
  fi

  # set -e aborts without a word, and the last thing on screen is a green
  # progress line, so a half-finished upgrade otherwise reads as a finished one.
  if (( exit_status != 0 && upgrade_started && ! upgrade_completed )); then
    printf '\n\033[31mUpgrade incomplete - do NOT reboot.\033[0m\n' >&2
    printf 'The system is part Omarchy 3 and part Omarchy quattro; rebooting now can leave it without a working network or desktop.\n' >&2
    printf 'Fix the error reported above and run this script again. Re-running is safe and resumes the remaining steps.\n' >&2
  fi

  exit "$exit_status"
}
trap cleanup_on_exit EXIT

if (( EUID != 0 )); then
  sudo -v
  while true; do sudo -n true; sleep 60; done 2>/dev/null &
  sudo_keepalive_pid=$!
fi

backup_suffix=$(date +%Y%m%d%H%M%S)

configure_pacman_channel() {
  log "Configuring Omarchy pacman repositories ($channel)"

  as_root install -d -m 0755 /etc/pacman.d
  if [[ -f /etc/pacman.d/mirrorlist ]]; then
    as_root cp -f /etc/pacman.d/mirrorlist "/etc/pacman.d/mirrorlist.omarchy-upgrade-to-quattro.$backup_suffix.bak"
  fi
  printf 'Server = %s\n' "$mirror_server" | as_root tee /etc/pacman.d/mirrorlist >/dev/null

  if [[ ! -f /etc/pacman.conf ]]; then
    fail "/etc/pacman.conf does not exist."
  fi

  as_root cp -f /etc/pacman.conf "/etc/pacman.conf.omarchy-upgrade-to-quattro.$backup_suffix.bak"

  # Replace the normal Omarchy package repository. Dev package mode uses the
  # edge channel and resolves omarchy-dev / omarchy-settings-dev from [omarchy].
  local tmp

  tmp=$(mktemp)
  awk -v server="$package_server" '
    BEGIN { in_omarchy = 0; wrote = 0 }
    /^[[:space:]]*\[omarchy\][[:space:]]*$/ {
      if (!wrote) {
        print "[omarchy]"
        print "SigLevel = Optional TrustAll"
        print "Server = " server
        wrote = 1
      }
      in_omarchy = 1
      next
    }
    /^[[:space:]]*\[/ { in_omarchy = 0 }
    !in_omarchy { print }
    END {
      if (!wrote) {
        print ""
        print "[omarchy]"
        print "SigLevel = Optional TrustAll"
        print "Server = " server
      }
    }
  ' /etc/pacman.conf >"$tmp"
  as_root install -m 0644 "$tmp" /etc/pacman.conf
  rm -f "$tmp"
}

install_keyrings() {
  log "Refreshing package databases and keyrings"

  # Pre-quattro installs cached omarchy packages downloaded from the legacy
  # repo. The quattro package server rebuilds some of them under the same
  # name and version with different bytes, so the cached copies fail the new
  # database's checksum and abort --noconfirm transactions as "corrupted".
  as_root find /var/cache/pacman/pkg -maxdepth 1 -name 'omarchy-*' -delete 2>/dev/null || true

  # -Syy forces the database download. configure_pacman_channel just repointed
  # the mirrors, and a plain -Sy keeps the legacy database whenever the new
  # server's copy isn't newer, leaving stale checksums that no amount of
  # re-downloading can satisfy.

  as_root pacman -Syy --noconfirm archlinux-keyring omarchy-keyring || {
    warn "Keyring package install failed; attempting to trust the Omarchy packaging key directly."
    as_root pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org || true
    as_root pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 || true
    as_root pacman -Syy --noconfirm archlinux-keyring omarchy-keyring
  }
}

remove_legacy_installer_package() {
  if pacman -Q omarchy-installer >/dev/null 2>&1; then
    log "Removing legacy omarchy-installer package"
    # The pre-quattro omarchy meta package may depend on omarchy-installer. Ignore
    # that edge temporarily; the next transaction installs the Omarchy quattro meta.
    as_root pacman -Rdd --noconfirm omarchy-installer
  fi
}

remove_legacy_limine_configs() {
  # Mirrors the old 1762446739 migration: old Limine installs sometimes left
  # alternate limine.conf files in paths Limine searches before /boot/limine.conf.
  # If any are still present, Limine may ignore the package-managed default config.
  local canonical_limine_config=/boot/limine.conf
  local legacy_limine_configs=(
    /boot/EFI/arch-limine/limine.conf
    /boot/EFI/limine/limine.conf
    /boot/EFI/BOOT/limine.conf
    /boot/limine/limine.conf
  )
  local legacy_limine_config
  local found_legacy_limine_configs=()

  for legacy_limine_config in "${legacy_limine_configs[@]}"; do
    if as_root test -f "$legacy_limine_config"; then
      found_legacy_limine_configs+=("$legacy_limine_config")
    fi
  done

  ((${#found_legacy_limine_configs[@]})) || return 0

  if ! as_root test -f "$canonical_limine_config"; then
    fail "Legacy Limine configs exist (${found_legacy_limine_configs[*]}) but $canonical_limine_config does not. Do not reboot until the bootloader config is repaired."
  fi

  log "Removing legacy alternate Limine configs"
  as_root rm -f "${found_legacy_limine_configs[@]}"
}

normalize_limine_config() {
  local machine_id legacy_uki

  as_root test -f /boot/limine.conf || return 0

  log "Normalizing Limine config"
  # Mirrors old 1777570652: normalize British/American spelling, the old color
  # index, and help colors that pre-quattro installs may never have migrated.
  as_root sed -i -E 's/^interface_branding_colou?r: 2$/interface_branding_color: 9ece6a/' /boot/limine.conf
  as_root sed -i 's/^interface_branding_colour: /interface_branding_color: /' /boot/limine.conf
  as_root sed -i -E '/^interface_help_colou?r(_bright)?:/d' /boot/limine.conf
  as_root sed -i '/^interface_branding_color:/a interface_help_color_bright: 9ece6a' /boot/limine.conf
  as_root sed -i '/^interface_branding_color:/a interface_help_color: 9ece6a' /boot/limine.conf

  # Pre-quattro custom-UKI installs used <machine-id>_linux.efi. Omarchy quattro uses the
  # stable omarchy_linux.efi path generated by limine-entry-tool.
  machine_id=$(as_root cat /etc/machine-id 2>/dev/null || true)
  legacy_uki="/boot/EFI/Linux/${machine_id}_linux.efi"
  if [[ -n $machine_id ]] && as_root test -f /boot/EFI/Linux/omarchy_linux.efi && as_root test -f "$legacy_uki"; then
    as_root rm -f "$legacy_uki"
  fi
}

preserve_kernel_cmdline_root() {
  local default_conf=/etc/default/limine
  local cmdline param key root_source root_stack root_uuid subvol uki
  local boot_params=()
  local missing=()
  local have_root=0 have_mount_mode=0 have_unlock=0

  command -v limine-mkinitcpio >/dev/null 2>&1 || return 0
  as_root test -f /boot/limine.conf || return 0

  # The omarchy-defaults.conf drop-in appends to KERNEL_CMDLINE[default] with
  # +=, which makes limine-entry-tool ignore /etc/kernel/cmdline and
  # /proc/cmdline. Fresh installs pin root= in /etc/default/limine via the ISO;
  # upgrades never created that file, so root= silently drops out and the next
  # boot lands in an emergency shell. Ask the tool for its effective default
  # cmdline — the key this function appends to — rather than parsing the config
  # layers ourselves.
  if as_root limine-entry-tool --get-cmdline default 2>/dev/null |
    grep -qE '(^|[[:space:]])root='; then
    return 0
  fi

  # Copy the boot-critical parameters of the running kernel verbatim, so a
  # PARTUUID, a LABEL or a bare device node all survive.
  cmdline=$(cat /proc/cmdline 2>/dev/null || true)
  for param in $cmdline; do
    key=${param%%=*}
    case $key in
      root)
        have_root=1
        boot_params+=("$param")
        ;;
      rw | ro)
        have_mount_mode=1
        boot_params+=("$param")
        ;;
      cryptdevice | cryptkey | rd.luks.name | rd.luks.uuid | rd.luks.options | rd.luks.key | rd.luks.crypttab | dm-mod.create)
        have_unlock=1
        boot_params+=("$param")
        ;;
      rootflags | rootfstype | rootwait | rootdelay | resume | resume_offset | rd.lvm.lv | rd.lvm.vg | rd.md.uuid | rd.dm.uuid)
        boot_params+=("$param")
        ;;
    esac
  done

  # Already-broken system (booted through the emergency shell): derive root=
  # from the mounted root instead. Refuse when the root sits on dm-crypt and
  # the cmdline carries no unlock parameters, since root= alone would still not
  # boot. lsblk -s walks the parents, where the crypt layer hides on
  # LVM-on-LUKS; --nofsroot keeps findmnt from appending a btrfs subvolume
  # lsblk cannot resolve; the capture keeps a SIGPIPE under pipefail from
  # reading as "no crypt layer".
  if ((!have_root)); then
    root_source=$(findmnt -no SOURCE --nofsroot / 2>/dev/null || true)
    root_stack=$(lsblk -nso TYPE "$root_source" 2>/dev/null || true)
    if grep -qx crypt <<<"$root_stack" && ((!have_unlock)); then
      warn "The root filesystem is on dm-crypt and the booted kernel cmdline carries no unlock parameters, so they cannot be recovered automatically. Add root= and the matching cryptdevice or rd.luks.* parameters to $default_conf by hand, then run limine-mkinitcpio."
      boot_cmdline_unsafe=1
      return 0
    fi

    root_uuid=$(findmnt -no UUID / 2>/dev/null || true)
    if [[ -z $root_uuid ]]; then
      warn "Could not determine the root filesystem UUID; the kernel cmdline was left untouched."
      boot_cmdline_unsafe=1
      return 0
    fi

    boot_params=("root=UUID=$root_uuid" "${boot_params[@]}")
    ((have_mount_mode)) || boot_params+=(rw)

    # Btrfs needs its subvolume too, or the initramfs mounts the wrong tree.
    subvol=$(findmnt -no FSROOT / 2>/dev/null || true)
    if [[ $(findmnt -no FSTYPE / 2>/dev/null) == "btrfs" && -n $subvol && $subvol != "/" ]]; then
      boot_params+=("rootflags=subvol=${subvol#/}")
    fi
  fi

  log "Preserving the kernel cmdline root parameters in $default_conf"
  as_root tee -a "$default_conf" >/dev/null <<EOF
# Written by omarchy-upgrade-to-quattro. The += drop-ins in
# /etc/limine-entry-tool.d/ stop limine-entry-tool from reading
# /etc/kernel/cmdline and /proc/cmdline, so the parameters identifying the
# root filesystem have to be stated explicitly.
KERNEL_CMDLINE[default]+=" ${boot_params[*]}"
EOF

  # Keep going on failure so the verification below still runs; the unsafe
  # flag blocks the reboot at the end of the upgrade.
  if ! as_root limine-mkinitcpio; then
    warn "limine-mkinitcpio failed; the boot entries were not regenerated."
    boot_cmdline_unsafe=1
  fi

  as_root grep -qE '(^|[[:space:]])root=' /boot/limine.conf || missing+=(/boot/limine.conf)

  # The cmdline that boots is the one embedded in the UKIs, so check those too.
  if command -v objcopy >/dev/null 2>&1; then
    while read -r uki; do
      [[ -n $uki ]] || continue
      as_root objcopy -O binary --only-section=.cmdline "$uki" /dev/stdout 2>/dev/null |
        tr -d '\0' | grep -E '(^|[[:space:]])root=' >/dev/null || missing+=("$uki")
    done < <(as_root find /boot/EFI/Linux -maxdepth 1 -name 'omarchy_linux*.efi' 2>/dev/null)
  fi

  if ((${#missing[@]})); then
    warn "root= is still missing from ${missing[*]}. Do not reboot until the kernel cmdline is repaired, or the system will drop to an emergency shell."
    boot_cmdline_unsafe=1
  fi
}

configure_snapper_policy() {
  local snapper_config_script=/usr/share/de-omarchy/install/config/snapper.sh

  if ! as_root test -f "$snapper_config_script"; then
    warn "$snapper_config_script is unavailable; Snapper snapshot retention was not normalized."
    return 0
  fi

  log "Configuring Omarchy snapshot retention"
  as_root env OMARCHY_PATH=/usr/share/de-omarchy bash -euo pipefail "$snapper_config_script"
}

configure_lock_authentication() {
  # The upgrade runs against whatever the channel currently ships, which can
  # still predate the rename of omarchy-setup-lock to omarchy-apply-lock.
  local apply_lock="" candidate
  for candidate in /usr/share/de-omarchy/bin/omarchy-{apply,setup}-lock; do
    if [[ -x $candidate ]]; then
      apply_lock=$candidate
      break
    fi
  done

  [[ -n $apply_lock ]] || fail "Neither omarchy-apply-lock nor omarchy-setup-lock is available under /usr/share/de-omarchy/bin; lock screen authentication could not be configured."

  log "Configuring lock screen authentication"
  as_root env \
    OMARCHY_INSTALL_USER="$target_user" \
    OMARCHY_PATH=/usr/share/de-omarchy \
    PATH="$package_path" \
    "$apply_lock"
}

create_pre_upgrade_snapshot() {
  # 127 means Snapper is deliberately absent. Any other failure already said
  # what went wrong, and a missing snapshot is not worth blocking the upgrade
  # over, but it must not pass for one either.
  omarchy-snapshot create || (($? == 127)) ||
    echo -e "\e[33mContinuing the upgrade without a snapshot.\e[0m" >&2
}

remove_conflicting_legacy_packages() {
  # Remove legacy packages that are known to conflict with Omarchy quattro defaults
  # before the main package transaction. Broader retired-package cleanup runs
  # after NetworkManager and the rest of the new stack are installed.
  local conflicting_packages=(
    asdcontrol-git
    lazydocker-bin
    localsend-bin
    omarchy-chromium
    omarchy-lazyvim
    openai-codex-bin
    ttf-jetbrains-mono
    ttf-jetbrains-mono-nerd
    wayfreeze
    wayfreeze-git
  )

  local pkg
  for pkg in "${conflicting_packages[@]}"; do
    if package_installed_exact "$pkg"; then
      # Some pre-quattro meta packages depend on these retired/default packages. We
      # intentionally break those old dependency edges before installing the
      # Omarchy quattro meta, otherwise pacman prompts to remove conflicts (for
      # example ttf-jetbrains-mono-nerd -> ttf-jetbrains-mono-nerd-basic) and
      # --noconfirm answers "no".
      as_root pacman -Rdd --noconfirm "$pkg" || warn "Could not remove conflicting legacy package '$pkg'; continuing."
    fi
  done
}

migrate_1password_beta_package() {
  if ! package_installed_exact 1password-beta && ! package_installed_exact 1password-beta-bin; then
    return 0
  fi

  log "Replacing 1Password Beta with stable 1Password"
  if ! as_root pacman -S --noconfirm --ask 4 1password; then
    warn "Could not replace 1Password Beta with stable 1Password; leaving the existing 1Password package installed."
  fi
}

install_omarchy_quattro_packages() {
  local omarchy_package=omarchy
  local settings_package=omarchy-settings
  if (( use_dev_packages )); then
    omarchy_package=omarchy-dev
    settings_package=omarchy-settings-dev
  fi

  local core_packages=(
    omarchy-keyring
    "$settings_package"
    omarchy-nvim
    "$omarchy_package"
    fakeroot
    pacman-contrib
  )
  local base_packages=()

  log "Upgrading system packages and installing Omarchy quattro packages"
  (( use_dev_packages )) && log "Using dev packages [omarchy-dev / omarchy-settings-dev]"
  # --noconfirm answers "no" to conflict-removal prompts. --ask 4 preselects
  # removing the conflicting package, which is required for legacy packages like
  # ttf-jetbrains-mono-nerd -> ttf-jetbrains-mono-nerd-basic.
  as_root env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --needed --noconfirm --ask 4 --overwrite='*' "${core_packages[@]}"
  mark_packages_explicit "${core_packages[@]}"

  if [[ -f /usr/share/de-omarchy/install/omarchy-base.packages ]]; then
    log "Installing Omarchy quattro default package set"
    mapfile -t base_packages < <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/de-omarchy/install/omarchy-base.packages)
    if (( ${#base_packages[@]} )); then
      as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${base_packages[@]}"
      mark_packages_explicit "${base_packages[@]}"
    fi
  else
    warn "/usr/share/de-omarchy/install/omarchy-base.packages was not found; skipping default package set install."
  fi

  # Archinstall installs the audio application during fresh ISO installs, so
  # legacy upgrades need to ensure the same PipeWire stack is present without
  # depending on Archinstall or Omarchy setup helpers.
  local audio_packages=(
    pipewire
    pipewire-alsa
    pipewire-jack
    pipewire-pulse
    gst-plugin-pipewire
    libpulse
    wireplumber
  )
  as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${audio_packages[@]}"
  mark_packages_explicit "${audio_packages[@]}"
}

install_hardware_transition_packages() {
  local hardware_packages=()

  if lspci | grep -iE '(Multimedia audio controller|Audio device).*Intel' >/dev/null; then
    hardware_packages+=(sof-firmware)
  fi

  if lspci | grep -iE '(VGA|Display).*Intel' >/dev/null; then
    hardware_packages+=(vulkan-intel)
  fi
  if lspci | grep -iE '(VGA|Display).*AMD' >/dev/null; then
    hardware_packages+=(vulkan-radeon)
  fi
  if lspci | grep -iE '(VGA|Display).*Apple' >/dev/null; then
    hardware_packages+=(vulkan-asahi)
  fi

  if (( ${#hardware_packages[@]} > 0 )); then
    log "Installing hardware support needed by the Omarchy quattro layout"
    as_root pacman -S --needed --noconfirm "${hardware_packages[@]}"
    mark_packages_explicit "${hardware_packages[@]}"
  fi
}

remove_retired_default_packages() {
  local retired_packages=(
    asdcontrol-git
    blueberry
    bluetui
    chaotic-keyring
    chaotic-mirrorlist
    claude-code
    dust
    elephant
    elephant-all
    elephant-bluetooth
    elephant-calc
    elephant-clipboard
    elephant-desktopapplications
    elephant-files
    elephant-menus
    elephant-providerlist
    elephant-runner
    elephant-symbols
    elephant-todo
    elephant-unicode
    elephant-websearch
    fcitx5-configtool
    gcc14
    hypridle
    hyprlock
    impala
    iwd
    lazydocker-bin
    lmstudio
    lmstudio-bin
    localsend-bin
    makima-bin
    mako
    omarchy-chromium
    omarchy-lazyvim
    omarchy-walker
    openai-codex
    openai-codex-bin
    opencode
    pavucontrol
    playerctl
    polkit-gnome
    qt5-remoteobjects
    satty
    swaybg
    swayosd
    walker-bin
    ttf-jetbrains-mono
    ttf-jetbrains-mono-nerd
    vlc
    waybar
    wayfreeze
    wayfreeze-git
    wf-recorder
    wiremix
    wl-screenrec
    yay-bin-debug
    zoom
  )

  log "Removing packages retired from the Omarchy quattro default install"
  local pkg
  local installed_retired=()
  for pkg in "${retired_packages[@]}"; do
    if package_installed_exact "$pkg"; then
      installed_retired+=("$pkg")
    fi
  done

  ((${#installed_retired[@]})) || return 0

  # Remove as one transaction so dependency anchors like waybar/playerctl and
  # omarchy-walker/elephant-* disappear together instead of failing one-by-one.
  # Preflight first so dependency failures do not print scary pacman errors in
  # the middle of an otherwise successful upgrade.
  if pacman -Rs --print "${installed_retired[@]}" >/dev/null 2>&1; then
    if as_root pacman -Rns --noconfirm --ask 4 "${installed_retired[@]}"; then
      return 0
    fi
    warn "Batch retired-package removal failed; retrying dependency-aware groups."
  else
    warn "Some retired packages are still required by non-retired packages; retrying dependency-aware groups."
  fi

  local group group_pkg required_by_line
  local group_packages=()
  local installed_group=()
  local fallback_groups=(
    "omarchy-walker walker elephant-all elephant elephant-bluetooth elephant-calc elephant-clipboard elephant-desktopapplications elephant-files elephant-menus elephant-providerlist elephant-runner elephant-symbols elephant-todo elephant-unicode elephant-websearch"
    "waybar playerctl"
    "impala iwd"
    "blueberry bluetui"
    "wiremix pavucontrol"
  )

  for group in "${fallback_groups[@]}"; do
    read -r -a group_packages <<<"$group"
    installed_group=()
    for group_pkg in "${group_packages[@]}"; do
      if package_installed_exact "$group_pkg"; then
        installed_group+=("$group_pkg")
      fi
    done
    ((${#installed_group[@]})) || continue

    if pacman -Rs --print "${installed_group[@]}" >/dev/null 2>&1; then
      as_root pacman -Rns --noconfirm --ask 4 "${installed_group[@]}" || warn "Could not remove retired package group: ${installed_group[*]}"
    else
      warn "Leaving retired package group installed because other packages still depend on it: ${installed_group[*]}"
    fi
  done

  for pkg in "${installed_retired[@]}"; do
    package_installed_exact "$pkg" || continue

    if pacman -Rs --print "$pkg" >/dev/null 2>&1; then
      as_root pacman -Rns --noconfirm --ask 4 "$pkg" || warn "Could not remove retired package '$pkg'; leaving it installed."
    else
      required_by_line=$(LC_ALL=C pacman -Qi "$pkg" 2>/dev/null | awk -F: '/^Required By/ { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit }')
      if [[ -n $required_by_line && $required_by_line != None ]]; then
        warn "Leaving retired package '$pkg' installed because it is still required by: $required_by_line"
      else
        warn "Leaving retired package '$pkg' installed because pacman reports remaining dependency constraints."
      fi
    fi
  done
}

cleanup_legacy_user_paths() {
  log "Cleaning legacy user command shims"
  run_as_user env TARGET_HOME="$target_home" BACKUP_SUFFIX="$backup_suffix" bash <<'USER_CLEANUP'
set -euo pipefail
local_bin="$TARGET_HOME/.local/bin"
legacy_root="$TARGET_HOME/.local/share/omarchy"
legacy_bin="$legacy_root/bin/"
if [[ -d $local_bin ]]; then
  shopt -s nullglob
  for link in "$local_bin"/omarchy*; do
    [[ -L $link ]] || continue
    target=$(readlink "$link" || true)
    case "$target" in
      "$legacy_bin"*|../share/omarchy/bin/*|*.local/share/omarchy/bin/*)
        rm -f "$link"
        ;;
    esac
  done
fi

legacy_hypr_config_active() {
  local hyprland_conf="$TARGET_HOME/.config/hypr/hyprland.conf"
  [[ -f $hyprland_conf ]] || return 1
  grep -q '\.local/share/omarchy/default/hypr' "$hyprland_conf"
}

populate_legacy_hypr_defaults() {
  local shim_hypr_dir="$1" backup="$2" tmp_dir archive_dir

  rm -rf "$shim_hypr_dir"
  mkdir -p "$shim_hypr_dir"

  # The backup is the checkout the running session is sourcing right now, so it
  # is both the correct content and already on disk. Only reach for the network
  # when there is no backup, which means the legacy root was already a symlink.
  if [[ -n $backup && -d $backup/default/hypr ]]; then
    cp -a "$backup/default/hypr/." "$shim_hypr_dir/"
  elif command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1; then
    tmp_dir=$(mktemp -d)
    archive_dir="$tmp_dir/archive"
    mkdir -p "$archive_dir"
    if curl -fsSL https://github.com/basecamp/omarchy/archive/refs/heads/master.tar.gz | tar -xz -C "$archive_dir" &&
      [[ -d $archive_dir/omarchy-master/default/hypr ]]; then
      cp -a "$archive_dir/omarchy-master/default/hypr/." "$shim_hypr_dir/"
    fi
    rm -rf "$tmp_dir"
  fi

  find "$shim_hypr_dir" -type f -name '*.conf' -print -quit | grep -q .
}

create_live_omarchy_overlay() {
  local backup="$1" compat_root shim_hypr_dir entry base cleanup_hook

  compat_root="$TARGET_HOME/.local/state/omarchy/upgrade-to-quattro-live-omarchy"
  shim_hypr_dir="$TARGET_HOME/.local/state/omarchy/upgrade-to-quattro-hypr-defaults"

  populate_legacy_hypr_defaults "$shim_hypr_dir" "$backup" || return 1

  rm -rf "$compat_root"
  mkdir -p "$compat_root/default"

  shopt -s dotglob nullglob
  for entry in /usr/share/de-omarchy/*; do
    [[ -e $entry ]] || continue
    base=$(basename "$entry")
    [[ $base == default ]] && continue
    ln -s "$entry" "$compat_root/$base"
  done
  for entry in /usr/share/de-omarchy/default/*; do
    [[ -e $entry ]] || continue
    base=$(basename "$entry")
    [[ $base == hypr ]] && continue
    ln -s "$entry" "$compat_root/default/$base"
  done
  ln -s "$shim_hypr_dir" "$compat_root/default/hypr"

  cleanup_hook="$TARGET_HOME/.config/omarchy/hooks/post-boot.d/cleanup-upgrade-to-quattro-live-shim"
  mkdir -p "$(dirname "$cleanup_hook")"
  cat >"$cleanup_hook" <<CLEANUP_HOOK
#!/bin/bash
set -euo pipefail
legacy_root="\$HOME/.local/share/omarchy"
compat_root="$compat_root"
shim_hypr_dir="$shim_hypr_dir"
if [[ -L \$legacy_root && \$(readlink "\$legacy_root") == "\$compat_root" ]]; then
  ln -sfn /usr/share/de-omarchy "\$legacy_root"
fi
rm -rf "\$compat_root" "\$shim_hypr_dir"
rm -f "\$0"
CLEANUP_HOOK
  chmod +x "$cleanup_hook"

  printf '%s\n' "$compat_root"
}

# Existing terminals still have OMARCHY_PATH=$HOME/.local/share/omarchy and
# often have $OMARCHY_PATH/bin early in PATH. Make that stale path resolve to
# the package-backed tree for the rest of the live session, while backing up
# the old git checkout for inspection/rollback. If the live Hyprland session is
# still using legacy ~/.config/hypr/*.conf files, keep those user files in place
# and provide a temporary default/hypr/*.conf shim so reloads do not explode.
if [[ -e /usr/share/de-omarchy ]]; then
  mkdir -p "$TARGET_HOME/.local/share"
  backup=""
  if [[ -L $legacy_root ]]; then
    rm -f "$legacy_root"
  elif [[ -e $legacy_root ]]; then
    backup="$legacy_root.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
    mv "$legacy_root" "$backup"
  fi

  if legacy_hypr_config_active; then
    if compat_root=$(create_live_omarchy_overlay "$backup"); then
      ln -sfn "$compat_root" "$legacy_root"
    else
      echo "Warning: Could not prepare legacy Hyprland conf shim; linking ~/.local/share/omarchy directly to /usr/share/de-omarchy." >&2
      ln -sfn /usr/share/de-omarchy "$legacy_root"
    fi
  else
    ln -sfn /usr/share/de-omarchy "$legacy_root"
  fi
fi
USER_CLEANUP
}

retired_user_units=(
  hyprpolkitagent.service
  mako.service
  omarchy-battery-monitor.service
  omarchy-battery-monitor.timer
  omarchy-shell.service
  swayosd-server.service
  elephant.service
  [email protected]
)

cleanup_retired_user_unit_files() {
  run_as_user env RETIRED_UNITS="${retired_user_units[*]}" bash <<'USER_UNITS'
set -euo pipefail
read -r -a units <<<"$RETIRED_UNITS"
shopt -s nullglob
for base in "$HOME/.config/systemd/user" "$HOME/.local/share/systemd/user"; do
  [[ -d $base ]] || continue
  for unit in "${units[@]}"; do
    rm -f "$base/$unit"
    for link in "$base"/*.wants/"$unit" "$base"/*.requires/"$unit"; do
      rm -f "$link"
    done
  done
done
USER_UNITS
}

cleanup_retired_services() {
  log "Disabling services retired by Omarchy quattro"

  # iwd is retired alongside the NetworkManager enable it depends on. Do not
  # stop systemd-networkd mid-upgrade; disabling is enough and avoids dropping a
  # running connection before the user can reboot into NetworkManager.
  local networkd_unit
  for networkd_unit in \
    systemd-networkd.service \
    systemd-networkd.socket \
    systemd-networkd-varlink.socket \
    systemd-networkd-varlink-metrics.socket \
    systemd-networkd-resolve-hook.socket; do
    as_root systemctl disable "$networkd_unit" >/dev/null 2>&1 || true
  done
  as_root systemctl disable systemd-networkd-wait-online.service >/dev/null 2>&1 || true
  as_root systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true

  cleanup_retired_user_unit_files
  if [[ -S $target_runtime_dir/bus ]]; then
    run_as_user_session systemctl --user disable "${retired_user_units[@]}" >/dev/null 2>&1 || true
    run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
  fi
}

# The Omarchy 3 session keeps its bar, launcher, and notifications until the
# reboot, which is the real cutover. Swapping the UI out underneath a running
# session buys nothing and only creates ways for the upgrade to look broken.

# Run a command inside the user's live Wayland session. Fails if no session
# can be discovered. Extra NAME=VALUE args extend the environment.
run_as_user_wayland_session() {
  local wayland_display hyprland_signature
  [[ -d $target_runtime_dir ]] || return 1

  wayland_display=$(discover_wayland_display) || return 1
  hyprland_signature=$(discover_hyprland_signature) || return 1

  run_as_user_session \
    WAYLAND_DISPLAY="$wayland_display" \
    HYPRLAND_INSTANCE_SIGNATURE="$hyprland_signature" \
    XDG_SESSION_TYPE=wayland \
    OMARCHY_PATH=/usr/share/de-omarchy \
    PATH="$package_path" \
    "$@"
}

run_hyprctl_session() {
  local hyprctl_bin
  hyprctl_bin=/usr/bin/hyprctl
  [[ -x $hyprctl_bin ]] || hyprctl_bin=$(command -v hyprctl || true)
  [[ -n $hyprctl_bin ]] || return 1

  run_as_user_wayland_session "$hyprctl_bin" "$@"
}

suppress_hyprland_config_reload() {
  # Say so up front when the session cannot be reached at all. Otherwise this
  # silently does nothing and the only evidence is the config error bar that
  # shows up minutes later.
  if ! run_as_user_wayland_session true >/dev/null 2>&1; then
    warn "Could not reach the live Hyprland session; it will report config errors during the swap, and the Omarchy shell will start after reboot."
    return 0
  fi

  log "Suppressing live Hyprland reloads during the config swap"
  set_hyprland_reload_state true
  hyprland_config_reload_suppressed=1
}

# Which API answers depends on the parser the running Hyprland was started
# with: the legacy .conf parser only takes keyword, and Quattro's Lua parser
# rejects it outright with "keyword can't work with non-legacy parsers". The
# session being upgraded is the legacy one, but the hyprctl binary is whichever
# the package transaction has installed by then, so ask both ways.
set_hyprland_reload_state() {
  local value="$1"

  run_hyprctl_session keyword misc:disable_autoreload "$value" >/dev/null 2>&1 || true
  run_hyprctl_session keyword debug:suppress_errors "$value" >/dev/null 2>&1 || true
  run_hyprctl_session eval \
    "hl.config({ misc = { disable_autoreload = $value }, debug = { suppress_errors = $value } })" \
    >/dev/null 2>&1 || true
}

restore_hyprland_config_reload() {
  (( hyprland_config_reload_suppressed )) || return 0

  set_hyprland_reload_state false

  # Backstop. An explicit hyprctl reload re-reads the config from disk, which
  # resets both keywords above before it reports what it found, so suppression
  # cannot survive one, and Hyprland keeps the resulting error bar up until a
  # later clean reload that this script deliberately never performs. Callers
  # that reload during the swap are supposed to check
  # OMARCHY_UPGRADE_TO_QUATTRO_LIVE, but one slipping through should not leave
  # an error bar on a finished upgrade. By now the shims have made the legacy
  # config resolve again, so clearing the overlay is safe.
  run_hyprctl_session seterror disable >/dev/null 2>&1 || true

  hyprland_config_reload_suppressed=0
}

ensure_sleep_lock_service() {
  [[ -S $target_runtime_dir/bus ]] || return 0

  log "Ensuring Omarchy sleep lock service is enabled"
  run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
  run_as_user_session systemctl --user reset-failed omarchy-sleep-lock.service >/dev/null 2>&1 || true
  if ! run_as_user_session systemctl --user enable --now omarchy-sleep-lock.service >/dev/null 2>&1; then
    warn "Could not start omarchy-sleep-lock.service; it should start after reboot."
  fi
}

run_post_upgrade_migrations() {
  PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0

  log "Running Omarchy migrations"
  if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
    warn "Could not run Omarchy migrations; the user may be prompted to run them after login."
  fi
}

run_final_system_package_upgrade() {
  log "Checking for remaining system package updates"
  as_root env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm --ask 4 \
    --overwrite '/etc/docker/daemon.json' \
    --overwrite '/etc/gnupg/dirmngr.conf' \
    --overwrite '/etc/mkinitcpio.conf.d/omarchy_hooks.conf' \
    --overwrite '/etc/mkinitcpio.conf.d/thunderbolt_module.conf' \
    --overwrite '/etc/modprobe.d/omarchy-usb-autosuspend.conf' \
    --overwrite '/etc/sddm.conf.d/10-theme.conf' \
    --overwrite '/etc/sddm.conf.d/10-wayland.conf' \
    --overwrite '/etc/sudoers.d/omarchy-asdcontrol' \
    --overwrite '/etc/sudoers.d/omarchy-passwd-tries' \
    --overwrite '/etc/sudoers.d/omarchy-tzupdate' \
    --overwrite '/etc/sysctl.d/90-omarchy-file-watchers.conf' \
    --overwrite '/etc/sysctl.d/99-omarchy-sysctl.conf' \
    --overwrite '/etc/systemd/logind.conf.d/10-ignore-power-button.conf' \
    --overwrite '/etc/systemd/resolved.conf.d/10-disable-multicast.conf' \
    --overwrite '/etc/systemd/resolved.conf.d/20-docker-dns.conf' \
    --overwrite '/etc/systemd/system.conf.d/10-faster-shutdown.conf' \
    --overwrite '/etc/systemd/system/[email protected]/10-faster-shutdown.conf' \
    --overwrite '/etc/systemd/system/docker.service.d/no-block-boot.conf' \
    --overwrite '/etc/systemd/system/plocate-updatedb.service.d/ac-only.conf' \
    --overwrite '/etc/systemd/system.conf.d/20-omarchy-nofile.conf' \
    --overwrite '/etc/systemd/user.conf.d/20-omarchy-nofile.conf' \
    --overwrite '/usr/lib/systemd/system-sleep/unmount-fuse' \
    --overwrite '/usr/share/plymouth/themes/omarchy/*' \
    --overwrite '/usr/share/sddm/hyprland.lua' \
    --overwrite '/usr/share/sddm/themes/omarchy/*'
}

run_post_upgrade_update_steps() {
  local update_output update_status

  if PATH="$package_path" command -v omarchy-update-aur-pkgs >/dev/null 2>&1; then
    if ! run_as_user_omarchy omarchy-update-aur-pkgs; then
      warn "Could not update AUR packages; running omarchy-update after reboot may still update AUR packages."
    fi
  fi

  if PATH="$package_path" command -v omarchy-update-mise >/dev/null 2>&1; then
    if ! run_as_user_omarchy omarchy-update-mise; then
      warn "Could not update mise tools; running omarchy-update after reboot may still update mise-managed tools."
    fi
  fi

  if PATH="$package_path" command -v omarchy-update-available >/dev/null 2>&1; then
    log "Refreshing update indicator state"
    update_status=0
    update_output=$(run_as_user_omarchy omarchy-update-available 2>&1) || update_status=$?

    if (( update_status == 0 )); then
      warn "Updates are still available after the upgrade; run omarchy-update after reboot:"
      printf '%s\n' "$update_output" >&2
    fi
  fi
}

enable_system_service() {
  local unit="$1"
  as_root systemctl enable "$unit" >/dev/null 2>&1 || warn "Could not enable $unit; it may not be installed on this system."
}

apply_firewall_defaults() {
  # Run the packaged config script rather than a copy of it, the same way
  # configure_snapper_policy and configure_lock_authentication already do. The
  # copy this replaces had drifted: it never installed the ufw-docker rules, so
  # upgraded machines came up without the Docker firewall protections a fresh
  # install gets.
  local firewall_script=/usr/share/de-omarchy/install/config/firewall.sh

  if ! command -v ufw >/dev/null 2>&1; then
    warn "ufw is not installed; skipping firewall defaults."
    return 0
  fi

  if ! as_root test -f "$firewall_script"; then
    warn "$firewall_script is unavailable; firewall defaults were not applied."
    return 0
  fi

  log "Applying Omarchy firewall defaults"
  as_root env OMARCHY_PATH=/usr/share/de-omarchy PATH="$package_path" \
    bash -euo pipefail "$firewall_script" ||
    warn "Could not apply firewall defaults; run 'sudo bash $firewall_script' after reboot."
}

root_filesystem_encrypted() {
  local root_source root_type

  root_source=$(findmnt -n -o SOURCE / 2>/dev/null || true)
  [[ $root_source == /dev/mapper/* ]] && return 0

  if [[ -n $root_source ]]; then
    root_type=$(lsblk -no TYPE "$root_source" 2>/dev/null | head -n1 || true)
    [[ $root_type == crypt ]] && return 0
  fi

  as_root bash -c '[[ -s /etc/crypttab ]] && grep -qvE "^[[:space:]]*(#|$)" /etc/crypttab' >/dev/null 2>&1
}

should_enable_sddm_autologin() {
  as_root test -f /etc/sddm.conf.d/autologin.conf && return 0
  as_root systemctl is-enabled omarchy-seamless-login.service >/dev/null 2>&1 && return 0
  root_filesystem_encrypted
}

apply_system_transition() {
  log "Applying Omarchy quattro system transition"
  [[ -d /usr/share/de-omarchy ]] || fail "/usr/share/de-omarchy was not installed."

  as_root install -d -m 0755 /usr/share/icons/Yaru/scalable/actions
  as_root ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-previous-symbolic.svg \
    /usr/share/icons/Yaru/scalable/actions/go-previous-symbolic.svg
  as_root ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-next-symbolic.svg \
    /usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
  as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true

  as_root install -d -m 0777 /etc/chromium/policies/managed
  as_root install -d -m 0755 /usr/lib/chromium
  printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
    as_root tee /usr/lib/chromium/initial_preferences >/dev/null

  if getent group docker >/dev/null; then
    as_root usermod -aG docker "$target_user"
  fi

  if [[ -f /usr/bin/powerprofilesctl ]]; then
    as_root sed -i '/env python3/ c\#!/bin/python3' /usr/bin/powerprofilesctl || true
  fi

  as_root install -d -m 0755 /etc/systemd/resolved.conf.d /etc/docker
  cat <<'EOF' | as_root tee /etc/systemd/resolved.conf.d/10-disable-multicast.conf >/dev/null
[Resolve]
LLMNR=no
MulticastDNS=no
EOF
  cat <<'EOF' | as_root tee /etc/systemd/resolved.conf.d/20-docker-dns.conf >/dev/null
[Resolve]
DNSStubListenerExtra=172.17.0.1
EOF
  cat <<'EOF' | as_root tee /etc/docker/daemon.json >/dev/null
{
    "log-driver": "json-file",
    "log-opts": { "max-size": "10m", "max-file": "5" },
    "dns": ["172.17.0.1"],
    "bip": "172.17.0.1/16"
}
EOF
  as_root ln -sfn ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

  # Fresh Omarchy socket-activates Docker. Disable the old always-on service if
  # an upgraded install had enabled it, but do not stop running containers mid-upgrade.
  as_root systemctl disable docker.service >/dev/null 2>&1 || true

  enable_system_service cups.service
  enable_system_service cups-browsed.service
  enable_system_service avahi-daemon.service
  enable_system_service linux-modules-cleanup.service
  enable_system_service docker.socket
  enable_system_service systemd-resolved.service
  # NetworkManager replaces iwd, and both halves of that swap belong in one
  # step: a machine that boots with both enabled has two daemons fighting over
  # the Wi-Fi adapter. iwd is disabled but never stopped, so the running
  # connection survives until the reboot.
  enable_system_service NetworkManager.service
  as_root systemctl disable iwd.service >/dev/null 2>&1 || true
  enable_system_service bluetooth.service
  enable_system_service power-profiles-daemon.service
  enable_system_service sddm.service

  apply_firewall_defaults

  log "Normalizing SDDM login configuration"
  # The omarchy package already installed 10-theme.conf and 10-wayland.conf.
  as_root install -d -m 0755 /etc/sddm.conf.d
  cat <<'EOF' | as_root tee /etc/sddm.conf.d/99-omarchy-login.conf >/dev/null
[Users]
RememberLastUser=true
RememberLastSession=true
EOF

  local autologin_user
  if should_enable_sddm_autologin; then
    if as_root test -f /etc/sddm.conf.d/autologin.conf; then
      autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
    fi
    [[ -n ${autologin_user:-} ]] || autologin_user="$target_user"
    cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null
[Autologin]
User=$autologin_user
Session=omarchy.desktop
EOF
  else
    as_root rm -f /etc/sddm.conf.d/autologin.conf
  fi

  as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm
  cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null
[Last]
Session=omarchy.desktop
User=$target_user
EOF
  as_root chown sddm:sddm /var/lib/sddm /var/lib/sddm/state.conf 2>/dev/null || true

  as_root rm -f /etc/systemd/system/[email protected]/autologin.conf
  as_root rm -f /etc/systemd/system/plymouth-quit.service.d/wait-for-graphical.conf
  as_root rm -f /etc/systemd/system/omarchy-seamless-login.service
  as_root rm -f /usr/local/bin/seamless-login
  as_root systemctl disable omarchy-seamless-login.service >/dev/null 2>&1 || true

  as_root systemctl disable makima.service >/dev/null 2>&1 || true
  as_root rm -rf /etc/systemd/system/makima.service.d
  as_root rm -f /etc/udev/rules.d/99-uinput.rules
  as_root systemctl disable dell-xps-haptic-touchpad.service >/dev/null 2>&1 || true
  as_root rm -f /etc/systemd/system/dell-xps-haptic-touchpad.service
  as_root rm -f /etc/systemd/system/multi-user.target.wants/dell-xps-haptic-touchpad.service
  as_root rm -rf /etc/systemd/system/dell-xps-haptic-touchpad.service.d
  as_root rm -f /etc/udev/rules.d/99-dell-xps-haptic-touchpad.rules
  as_root rm -f /etc/omarchy-dell-haptic-touchpad.env
  as_root rm -f /etc/pacman.d/hooks/walker-restart.hook
  as_root rm -f \
    /etc/systemd/system.conf.d/99-omarchy-nofile.conf \
    /etc/systemd/user.conf.d/99-omarchy-nofile.conf
  as_root systemctl daemon-reload >/dev/null 2>&1 || true

  if [[ -f /etc/pam.d/sddm ]]; then
    as_root sed -i '/-auth.*pam_gnome_keyring\.so/d' /etc/pam.d/sddm
    as_root sed -i '/-password.*pam_gnome_keyring\.so/d' /etc/pam.d/sddm
  fi

  if [[ -f /etc/pam.d/system-auth ]]; then
    as_root sed -i 's|^\(auth\s\+required\s\+pam_faillock.so\)\s\+preauth.*$|\1 preauth silent deny=10 unlock_time=120|' /etc/pam.d/system-auth
    as_root sed -i 's|^\(auth\s\+\[default=die\]\s\+pam_faillock.so\)\s\+authfail.*$|\1 authfail deny=10 unlock_time=120|' /etc/pam.d/system-auth
  fi
  if [[ -f /etc/pam.d/sddm-autologin ]]; then
    as_root sed -i '/pam_faillock\.so preauth/d' /etc/pam.d/sddm-autologin
    as_root sed -i '/pam_faillock\.so authsucc/d' /etc/pam.d/sddm-autologin
    as_root sed -i '/auth.*pam_permit\.so/a auth        required    pam_faillock.so authsucc' /etc/pam.d/sddm-autologin
  fi
}

apply_user_transition() {
  log "Applying Omarchy quattro user transition"
  [[ -d /usr/share/de-omarchy ]] || fail "/usr/share/de-omarchy was not installed."

  run_as_user env \
    TARGET_HOME="$target_home" \
    BACKUP_SUFFIX="$backup_suffix" \
    OMARCHY_UPGRADE_YES="$yes" \
    OMARCHY_USER_NAME="${OMARCHY_USER_NAME:-}" \
    OMARCHY_USER_EMAIL="${OMARCHY_USER_EMAIL:-}" \
    bash <<'USER_SETUP'
set -euo pipefail
root=/usr/share/de-omarchy
state_dir="$HOME/.local/state/omarchy"
mkdir -p "$state_dir" "$HOME/.config"

file_sha256() {
  sha256sum "$1" | awk '{print $1}'
}

known_config_default_hashes() {
  cat <<'DEFAULT_HASHES'
# action<TAB>relative ~/.config path<TAB>sha256
# refresh: hash-matched shipped defaults
refresh	alacritty/alacritty.toml	d43ab27e10ba1fd0505d50fe374b9bd02fa2e1d888fe2252c5208542fe5ddc8f
refresh	alacritty/alacritty.toml	4e627909552ceebc80f5839955a09a7f804c501fee9a1fe76e13d95ad6cf5030
refresh	alacritty/alacritty.toml	73dc34be744039613ef522aab84c27468b61ff13d7bc0ed2d57a83904e13f613
refresh	alacritty/alacritty.toml	e25fae44db040d97714cf4ca3d77e48f0ae32fed9e02e6b3db9ad4d705bdaed5
refresh	alacritty/alacritty.toml	999562ef35642910bec05c2444c3acdcd6e29bc60ec541529d9c69a20cad5c88
refresh	alacritty/alacritty.toml	8bf6dee79e634949617cd7acdd2756da52f7c3031b8eac98a7948f2fef0cf07e
refresh	alacritty/alacritty.toml	e4d201b6b9afd2f591d64f98eb50eb19247b8b839c2eaacfe2623628203c486f
refresh	autostart/org.fcitx.Fcitx5.desktop	a0260e7f35579a73f77e257e9e3795abd8b33119a17b931ca6016a07036e1670
refresh	btop/btop.conf	e6b86ddda073e06bf64cfbbde9652f9335171d8ca8c622e617478a3c696a6a3d
refresh	btop/btop.conf	7f4122f2ed5f4c95489950cce2c0b192e19ae6fa74b0666d25e0b10040b2cee8
refresh	btop/btop.conf	4e17dd28c43948af48a408ed9b0901c008e57dfe3b8fcf09649ec10f28746137
refresh	chromium/Default/Preferences	67aef8ae3a6e0ee1180057198f47f2ea412ea1f01051d77edce4c465fb83d887
refresh	chromium-flags.conf	3a8fe5b85bc42b27f5a0f36f3b674dc8e22a7d670a6aabbdc4f2d4057b97a1d4
refresh	chromium-flags.conf	3d52925a48ac3704f9427ecec70310d9c89be5442c41ece183122a9246837be4
refresh	chromium-flags.conf	4bc8710615d2151464e9bd6d1123fbfbb8f56dea4cf0568320bc7b50d24ad205
refresh	chromium-flags.conf	1df1a5282f9525bfbb97a697c8169c08c1adb2b5a4de5ae94d84df7b4a78f2fc
refresh	chromium-flags.conf	0408c524892830f5cf43e94c389b897cbc093ede312f49a4627bfabf692653c2

refresh	fcitx5/conf/clipboard.conf	2d53b40811ee7ceb71c7db0422a3ed733647f389379a57dbf2915d522dff4193
refresh	fcitx5/conf/xcb.conf	f5cf059182c4361fa7b16d5149f20a09eb1e0915f4463ba60dae626b86df7d9c
refresh	foot/foot.ini	52e30647172522459b2179af714c19b62a35fe11bc23c49839fda5dca3253e84
refresh	foot/foot.ini	ac16453a4b4ebc997ff6732e7e4448dc759c25587fdb8aa1d1a7eee92b518987
refresh	ghostty/config	ed589d69296f5aa083e4dba43fc3d40cfbe2f6d10b382f3d0dc2ff76d5ec8fe5
refresh	ghostty/config	2832c9c27cdcd735e913d538c3c61005fd90e909541bdcb746bb454851ae79ca
refresh	ghostty/config	8200bc34b7ced8170d512c619f0408ac7dd7d7b4e64021ac66393d8a8bf1d951
refresh	ghostty/config	006fbe4048de02fc040eeefd0974081c6f7df5dae419f8d542fc1adf08b0d6fb
refresh	ghostty/config	e55a409a490d4f80c19c9242a59db8e00e271e01feddb8fda6a5a7102d39af20
refresh	ghostty/config	b65e4903813d442a25540eef5a24313121c640a57114ee3d76d1ec7408d86b27
refresh	ghostty/config	4e6ccba3d0584832cedf22713842f8ce7eca383c2ea3bfe61286d2b8df73f3fe
refresh	git/config	3e1bdf77aa65b7bb412cc25c02bac4734e98c10e4a63815b98737bf817b6f682
refresh	hypr/hyprsunset.conf	3ac2e9fe716fb8c612f07ca608ba7eee491921c2386118aae54ff9c8121726f2
refresh	hypr/hyprsunset.conf	e6fa7d909a90b64df7f75e8842bc2541e75463a47bbc7213ec79a56364f563f2
refresh	hypr/hyprsunset.conf	afe595d9d31574bde0f3ab8f7d370d0710bc188d6e5643f73255ffa7a202b647
refresh	hypr/hyprsunset.conf	3042e4a923030239f4edf7b1943f41f4ed57107550177130c96f3b215b226e22
refresh	hypr/xdph.conf	637899cd3926c4b8c758d3cb812cc1e7ac7643aca83a8e5030208234d064c839
refresh	hypr/xdph.conf	2fded88a27b89f33cbc2b64a1258117df74b591f39a84c78ec3504a7d3b09136
refresh	hyprland-preview-share-picker/config.yaml	cb7f8768566ce41f8b09249b504ea8f6dbd6ed830637eef24c4e94e94486d113
refresh	hyprland-preview-share-picker/config.yaml	cf88364e497e4af2adaff9a0b7ca12227d29f831bc7e18846dd8f03bf4c618a8
refresh	imv/config	58fd37d4862eb484d50c2c0cc507836e685a776d8da8dd9f20bd894809c31cd6
refresh	imv/config	569b52b9614a78be67ce9f0a828ff1a7a73e4741d4751577ec6b247ab78e29ea
refresh	imv/config	832b19821b4c69f9381aea6902b1b80c5682c30091e6d4a59493c8edf827bf72
refresh	kitty/kitty.conf	69d6cbd2f8adcaec09407b5fb71ec7956cd510f6d64c36600ab7f7e7d3812cc6
refresh	kitty/kitty.conf	6c5fe293bb92b1ea5fd955afcec126597ae0a665395db1d38b9beb6461c587d1
refresh	kitty/kitty.conf	68779240358653a03ede95c4c872d94e3ab3742cf64a982b17e94f42aa2b74d4
refresh	kitty/kitty.conf	9599cfd1f1fbd7e14fe1ba48196137be3cf8bcfa95f6c24d15d8053d88f73f0a
refresh	kitty/kitty.conf	ea3bc5e353f7312d774362310f7c511584efd6076c163b4cad1a9cc05c0f80ab
refresh	kitty/kitty.conf	8984216fe0c0d006b969a7494dd4aad79ac78e9d15bd7a82ccbf5f10eac48fa6
refresh	lazygit/config.yml	e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
refresh	obsidian/user-flags.conf	6c00f4daeff2b018bda247151a8edee44115273f554cfda00909bc5e56a3ae85
refresh	omarchy/hooks/battery-low.d/play-warning-sound.sample	9623f5534cd52f76a51cfbb4e8a0699a550bf7f0f5f2ff54a1c78534d907fdf1
refresh	omarchy/hooks/font-set.d/show-font-notification.sample	d28090fd796b02fa799dcf7160e46ce9fc8d7a17b476ef87639d0b83a347dc37
refresh	omarchy/hooks/post-boot.d/weather.sample	fe37567a09046f1e4fcff9a9c74e4a036270cdf6da73dcc5cd3aa617f9a7c2ba
refresh	omarchy/hooks/post-update.d/show-update-notification.sample	7995adc93a0ac6cd1afcd7ed9ac6d0ff058751ce4df4594aa78855603a613815
refresh	omarchy/hooks/theme-set.d/show-theme-notification.sample	b9d6754d95057bdb2f37595fd2f40055872118cb2f442bdc7b0b900317bac332
refresh	omarchy/themed/alacritty.toml.tpl.sample	ea5889bf79e273aa9cf1e7a1b2916dae7751fc0dced2cd53af36a2ebeecb4f1e
refresh	omarchy/themed/alacritty.toml.tpl.sample	a05d015227a406470b571d9d8f372eb8042df94b32cf746e71fd2f513c9880ac
refresh	opencode/opencode.json	3806450805c8661a729c46aea325826e9ee7d158d650c9d750f5d0a763ec2c03
refresh	opencode/opencode.json	fb067af039252d48ef5b5c573a5e12c17357af4324b218aca5efa36f882846dd
refresh	starship.toml	b17c9b5f096fc125e97050e359171c6011d6b3c7d7e9ac28f630e75b4d4bb9db
refresh	tmux/tmux.conf	345abaa3f128529abbbe4da3a24c227170c9679fb0af1f3d87eb15959e183219
refresh	tmux/tmux.conf	93088311b8b67eb10ede3040b55e81193ff43fe16c5b2a6e3799300e00f39e3d
refresh	tmux/tmux.conf	02d7c3914b73b8fec139316b9e1e88a40764617c6f4f1b6a3c05ad9c4a68b944
refresh	tmux/tmux.conf	6633a3c59441087fdead8c30601e50c39fc7e74a66a32514296657332ff6cde9
refresh	tmux/tmux.conf	473abae53d3aa7da59be65cc6301a15f6cc878045ae3f4c5e180ee013a0a04a7
refresh	tmux/tmux.conf	bf881713284aa2cc6722a199e42d57c005ae5922ef4eb6305593ead09b13ca7c
refresh	xournalpp/settings.xml	47eafeeb3c246af1a5fb2cd1d87a748ce127d13410c351194c5d89bbaefb623d
refresh	xournalpp/settings.xml	1c1a9efbf1b6dc7813bf3440fd5bf8409fc283e8d0192ca297dc36a10e12c846
# retire: nothing is actually copied. The new package owns the file at the
# system path below (shipped by omarchy-settings), so once the user's
# hash-matched copy in ~/.config is removed, the system path takes effect
# via the usual XDG/fontconfig/systemd lookup order. User overrides
# (hash mismatch) are kept active as backups.
# Legacy ~/.config path -> package-owned destination (where the file now lives):
# environment.d/fcitx.conf -> /usr/lib/environment.d/10-omarchy-fcitx.conf
# fastfetch/config.jsonc -> /etc/fastfetch/config.jsonc
# fontconfig/fonts.conf -> /usr/share/fontconfig/conf.avail/50-omarchy.conf
# mimeapps.list -> /usr/share/applications/mimeapps.list
# omarchy.ttf -> /usr/share/fonts/omarchy/omarchy.ttf
# systemd/user/bt-agent.service -> /usr/lib/systemd/user/bt-agent.service
# systemd/user/omarchy-recover-internal-monitor.service -> /usr/lib/systemd/user/omarchy-recover-internal-monitor.service
# systemd/user/omarchy-sleep-lock.service -> /usr/lib/systemd/user/omarchy-sleep-lock.service
# uwsm/default -> /usr/share/de-omarchy/default/uwsm/default
# uwsm/env -> /usr/share/uwsm/env.d/10-omarchy
# xdg-terminals.list -> /usr/share/xdg-terminal-exec/hyprland-xdg-terminals.list
retire	environment.d/fcitx.conf	faacbd703a8f797013968b235fec6289a9c921d3ec7b7f8b4e1cd24392f68b9c
retire	environment.d/fcitx.conf	8b077b6ea2594dc1d4b57592a3bc62703df4959e86b93ea0fc488b6686309095
retire	fastfetch/config.jsonc	6adebf87d93902fb4bc68824a8e7c71e482846af23fea199e489c5496f791590
retire	fastfetch/config.jsonc	fe61d9f7cfac800adb7bd21f75f1bfcca266c609b6efa02a6e25303ac192e86e
retire	fastfetch/config.jsonc	62fa26fa5fb8e11d31c5ec9f3a53b9b6b1124b29134b19329a092878ff624581
retire	fastfetch/config.jsonc	abb336459df8dea8f84df1dd54a2f223adec558df3de39cce84ca83f88acc02f
retire	fastfetch/config.jsonc	f1c9a999a4f31a215b10474eeac8894ce6da220d680c30d2aab7b3c1b7a539a6
retire	fastfetch/config.jsonc	97aefdea2103d6c73e063bf0d11ad26da8fe60630305ac4eb2970cfc50cb0d91
retire	fastfetch/config.jsonc	22565532d139ab8c2ddb21dcfe56460dc9f2f8a712b8d29f125c1c8b29beca37
retire	fastfetch/config.jsonc	b65cb9ca36eed45929bfc35443762b14957d3847f6bed361a338224a87450e3a
retire	fastfetch/config.jsonc	3d153225131d3031b99b2e1a07e84d22cd0e51fcae1e701b08f6b6a558bc7a9c
retire	fastfetch/config.jsonc	404d0273c380cfdad4a037fa87202fc1d96be178c867e8a9d27b0a772f6e2fa1
retire	fastfetch/config.jsonc	4a98ff77ed46109d08ef7301249ea6823d1abba6c39c0871680feebf96620bf2
retire	fastfetch/config.jsonc	2cc59c562e51ed3a58a784a17414ad40699253d0a9522eb7ce01faf6d7cff7c2
retire	fastfetch/config.jsonc	07027778c12914c7077022cc835de4ac6c38af90aed539a871b13c7c89603e87
retire	fastfetch/config.jsonc	0bc8cb0a24ec07e786cccbad446fd80b72520e7effd9bff8cfa27b7361211f4b
retire	fastfetch/config.jsonc	c827c3a24f64c418d309244a8a967def0c522d824addf65d8ee0741afbc970bd
retire	fastfetch/config.jsonc	c909b43ba000887cd418d9eaf98c1c9c5ba938aa27f9246cf8fa5662946c4e87
retire	fastfetch/config.jsonc	b2e5757851f0703edb8bcfc4b51a00bf5e303c1df26b8c63e6b7df6be4ead4ed
retire	fastfetch/config.jsonc	1a0a085e7a80036f415148da21f772e99898de7e70768ec7b7613f8cc620700c
retire	fontconfig/fonts.conf	7db33b0f8673f71f42604255cad222194294cd401315fecde6b83151eecebe91
retire	fontconfig/fonts.conf	f35b5b4bd0d8d3926f5d9a49ca791fc66b00793786420508d4a4c41ae9ccbefc
retire	fontconfig/fonts.conf	6dec98b539388b95ecfdb3c7ab951001c712820eb0581002832cb7bfdde1a654
retire	fontconfig/fonts.conf	3545f6c5a8c1465df7a4e251b3c2047d4ba03654c86636794f64cb0d8ea8ef63
retire	fontconfig/fonts.conf	0f085b449f1cbe8eda3b59a6235bf2a3ddb6e982ab5d22fa642248916e63bafc
retire	mimeapps.list	3b574cef135b5deb7a8a0c7e17139037cf1fe155300e3809f60bed0e04120975
retire	omarchy.ttf	e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966
retire	systemd/user/bt-agent.service	0406b577a1225dc2a9f86638d3c346eb3635168576f04050be50ebcc0be6be12
retire	systemd/user/omarchy-recover-internal-monitor.service	b9b92cedc44cf3cb6216948629be55b53d16746e31896dc6469fd49ba55e82f4
retire	systemd/user/omarchy-recover-internal-monitor.service	e1483079b9f2aefcd43b4722c75a31643e5f3bb5a2eada5f52f1d7d201e8c289
retire	systemd/user/omarchy-sleep-lock.service	6870b232a6c0474b59187882e6d25ae771bba735098bcbedef8a2b73b97e2b6a
retire	systemd/user/omarchy-sleep-lock.service	bcd1a76cb5c63514922bc5e11af22ae480fc6d06a99863364e02bdf3c7bdceaf
retire	uwsm/default	2e89b03a3710b70cf754ea0c8db4388bb35a7519870036abff5ea8a584eafbc0
retire	uwsm/default	3622ba134d29b639a155424a5226215e213c8508144e98001ffe128a8967c9b5
retire	uwsm/default	73712c5a677532bd4afcee5800ae9b76e3d02a2130659a96a8b7fbe2fd2c3d53
retire	uwsm/default	56f968d461da1b8360bc21d5fb7d5375121b772dd06d9397ebcda55a9097c2a0
retire	uwsm/default	f7558795e1f6b89357a780ad4e1c63ffdd56e423f31c9a888738afe894e16623
retire	uwsm/env	60eae2629f2dbc70bb068de7bcb245032a429dd989f98b573eb315c0760b4ea5
retire	uwsm/env	daf70280edd80bae94660f8509d294df2aaa242d3428320bf6542341efff5a6d
retire	uwsm/env	61a4147404a2b6b920f6f414a8c9bcef69b8bcf2a1251e1ddd2fbc37fe45e04d
retire	uwsm/env	32130b0033e1d3229d89e6f73394e272eb3adad07314f740f8df9629c1f89beb
retire	uwsm/env	278df755a1679f2735742c61de9789614cdb2021fbbb4e802f056c0403d1a6b8
retire	uwsm/env	7be477d4e734d30ebc2caf9dbad6133dcb4aef38d64eb0979beab46f1c0e2464
retire	uwsm/env	0c2cd9c32bd8aea26c62bc18c4a287a29999d4b2ec2b3216b666fb204ab3c471
retire	uwsm/env	957dc0a95f8e909a378ce82967d7a55eb474e2fe5b2672212272e88530c4d4aa
retire	uwsm/env	425036ed11f004f59a5fca324305b13189a6aca4b4dd1582532f075d14413d93
retire	uwsm/env	9eb186609afce36a525e58e0b0858eb586869c5d067a7e5545bb5be606ab8a44
retire	xdg-terminals.list	7303fa8d1d74eda73cf5fb4c90183a8608c191bb80d7fb9d96ab60f9df06022e
retire	xdg-terminals.list	d035da571a6bfc7d626d4a88846ea86a8adb6f6662f1f8b4abe2330e53ceb71b
DEFAULT_HASHES
}

declare -A known_config_default_hashes_by_key=()
while IFS=$'	' read -r action rel hash; do
  [[ -n ${action:-} && $action != \#* ]] || continue
  known_config_default_hashes_by_key["$action:$rel:$hash"]=1
done < <(known_config_default_hashes)

default_hash_paths() {
  local action_filter="$1" action rel hash
  known_config_default_hashes | while IFS=$'	' read -r action rel hash; do
    [[ -n ${action:-} && $action != \#* ]] || continue
    [[ $action == "$action_filter" ]] || continue
    printf '%s\n' "$rel"
  done | sort -u
}

is_known_default_hash() {
  local action="$1" rel="$2" file="$3" hash
  [[ -f $file ]] || return 1
  hash=$(file_sha256 "$file")
  [[ -n ${known_config_default_hashes_by_key["$action:$rel:$hash"]+set} ]]
}

backup_config_file() {
  local file="$1"
  [[ -e $file || -L $file ]] || return 0
  cp -af "$file" "$file.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
}

mapfile -t retired_config_files < <(default_hash_paths retire)

# These are new Omarchy quattro config entry points. Older installs cannot have an
# Omarchy-shipped legacy copy to hash-match, so always install the quattro default.
always_copy_config_files=(
  hypr/.luarc.json
  hypr/autostart.lua
  hypr/bindings.lua
  hypr/hyprland.lua
  hypr/input.lua
  hypr/looknfeel.lua
  hypr/monitors.lua
  omarchy/extensions/omarchy-menu.jsonc
  omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
  omarchy/shell.json
)

migrate_uwsm_env_customizations() {
  local source="$1" dest="$HOME/.config/uwsm/env.d/99-omarchy-upgrade-env" tmp
  [[ -f $source ]] || return 0
  is_known_default_hash retire uwsm/env "$source" && return 0

  tmp=$(mktemp)
  awk '
    function omitted(reason) { print ": # omarchy-upgrade omitted legacy Omarchy " reason; next }

    /^[[:space:]]*export[[:space:]]+OMARCHY_PATH=\$HOME\/\.local\/share\/omarchy[[:space:]]*$/ { omitted("OMARCHY_PATH") }
    /^[[:space:]]*export[[:space:]]+OMARCHY_PATH="\$\{OMARCHY_PATH:-\/usr\/share\/omarchy\}"[[:space:]]*$/ { omitted("OMARCHY_PATH") }
    /^[[:space:]]*\[ -f \/etc\/omarchy\.conf \] && \. \/etc\/omarchy\.conf[[:space:]]*$/ { omitted("/etc/omarchy.conf source") }
    /^[[:space:]]*export[[:space:]]+PATH=\$OMARCHY_PATH\/bin:\$PATH:\$HOME\/\.local\/bin[[:space:]]*$/ { omitted("PATH setup") }
    /^[[:space:]]*export[[:space:]]+PATH="\$OMARCHY_PATH\/bin:\$PATH:\$HOME\/\.local\/bin"[[:space:]]*$/ { omitted("PATH setup") }
    /^[[:space:]]*source[[:space:]]+~\/\.config\/uwsm\/default[[:space:]]*$/ { omitted("uwsm/default source") }
    /^[[:space:]]*\[ -f "\$HOME\/\.config\/uwsm\/default" \] && \. "\$HOME\/\.config\/uwsm\/default"[[:space:]]*$/ { omitted("uwsm/default source") }
    /^[[:space:]]*omarchy-cmd-present[[:space:]]+mise && eval "\$\(mise activate bash --shims\)"[[:space:]]*$/ { omitted("mise activation") }

    { print }
  ' "$source" >"$tmp"

  if [[ -s $tmp ]]; then
    if ! sh -n "$tmp" >/dev/null 2>&1; then
      cp -f "$source" "$tmp"
    fi

    mkdir -p "$(dirname "$dest")"
    {
      echo '# Preserved custom ~/.config/uwsm/env content during Omarchy quattro upgrade.'
      echo '# Exact legacy Omarchy-managed setup lines were replaced with no-op markers where safe.'
      cat "$tmp"
    } >"$dest"
  fi

  rm -f "$tmp"
}

is_retired_config_file() {
  local rel="$1" retired
  [[ $rel == systemd/user/* ]] && return 0

  for retired in "${retired_config_files[@]}"; do
    [[ $rel == "$retired" ]] && return 0
  done

  return 1
}

copy_config_default() {
  local rel="$1" source="$root/config/$1" target="$HOME/.config/$1"
  [[ -e $source || -L $source ]] || return 0

  if [[ -e $target || -L $target ]]; then
    if [[ -f $source && -f $target ]] && cmp -s "$source" "$target"; then
      return 0
    fi
    backup_config_file "$target"
    rm -f "$target"
  fi

  mkdir -p "$(dirname "$target")"
  cp -P "$source" "$target"
}

copy_missing_config_defaults() {
  local source_dir="$1" target_dir="$2" source_path rel target_path
  [[ -d $source_dir ]] || return 0

  while IFS= read -r -d '' source_path; do
    rel="${source_path#$source_dir/}"
    is_retired_config_file "$rel" && continue

    target_path="$target_dir/$rel"
    [[ -e $target_path || -L $target_path ]] && continue

    mkdir -p "$(dirname "$target_path")"
    cp -P "$source_path" "$target_path"
  done < <(find "$source_dir" \( -type f -o -type l \) -print0)
}

refresh_known_config_defaults() {
  local rel file default

  while IFS= read -r rel; do
    file="$HOME/.config/$rel"
    default="$root/config/$rel"
    [[ -f $file && -f $default ]] || continue
    is_known_default_hash refresh "$rel" "$file" || continue
    copy_config_default "$rel"
  done < <(default_hash_paths refresh)
}

mark_removed_preinstalls_from_legacy_bindings() {
  local bindings_file="$HOME/.config/hypr/bindings.lua"
  local plain_bindings_sha="5d92d4008b04848256445da3cf5c10422a8d537baad499a4e63f60daecaefc6f"

  [[ -f $bindings_file ]] || return 0
  [[ $(file_sha256 "$bindings_file") == "$plain_bindings_sha" ]] || return 0

  mkdir -p "$state_dir"
  touch "$state_dir/preinstalls-removed"
}

copy_always_config_defaults() {
  local rel
  for rel in "${always_copy_config_files[@]}"; do
    copy_config_default "$rel"
  done
}

repair_chromium_copy_url_extension_flags() {
  local file
  local -a flag_files=(
    "$HOME/.config/chromium-flags.conf"
    "$HOME/.config/chrome-flags.conf"
    "$HOME/.config/google-chrome-flags.conf"
    "$HOME/.config/google-chrome-stable-flags.conf"
    "$HOME/.config/brave-flags.conf"
    "$HOME/.config/brave-browser-flags.conf"
    "$HOME/.config/brave-origin-beta-flags.conf"
    "$HOME/.config/microsoft-edge-flags.conf"
    "$HOME/.config/microsoft-edge-stable-flags.conf"
    "$HOME/.config/opera-flags.conf"
    "$HOME/.config/vivaldi-flags.conf"
    "$HOME/.config/helium-flags.conf"
  )

  for file in "${flag_files[@]}"; do
    [[ -f $file ]] || continue
    grep -qF '.local/share/omarchy/default/chromium/extensions/copy-url' "$file" || continue

    backup_config_file "$file"
    python3 - "$file" <<'CHROMIUM_FLAGS_PATCH_PY'
import sys
from pathlib import Path

path = Path(sys.argv[1])
new_path = "/usr/share/de-omarchy/default/chromium/extensions/copy-url"
legacy_paths = [
    "~/.local/share/omarchy/default/chromium/extensions/copy-url",
    "$HOME/.local/share/omarchy/default/chromium/extensions/copy-url",
    str(Path.home() / ".local/share/omarchy/default/chromium/extensions/copy-url"),
]
text = path.read_text()
for legacy_path in legacy_paths:
    text = text.replace(legacy_path, new_path)
path.write_text(text)
CHROMIUM_FLAGS_PATCH_PY
  done
}

repair_sleep_lock_unit_override() {
  local unit="$HOME/.config/systemd/user/omarchy-sleep-lock.service"
  local legacy_exec='ExecStart=%h/.local/share/omarchy/bin/omarchy-system-sleep-monitor'
  local current_exec='ExecStart=/usr/bin/omarchy-system-sleep-monitor'
  local tmp
  [[ -f $unit ]] || return 0

  if grep -qxF "$legacy_exec" "$unit"; then
    backup_config_file "$unit"
    tmp=$(mktemp)
    LEGACY_EXEC="$legacy_exec" CURRENT_EXEC="$current_exec" awk '
      $0 == ENVIRON["LEGACY_EXEC"] { print ENVIRON["CURRENT_EXEC"]; next }
      { print }
    ' "$unit" >"$tmp" && mv "$tmp" "$unit"
    rm -f "$tmp"
  fi
}

is_known_bashrc_default() {
  local file="$1" hash
  [[ -f $file ]] || return 1
  hash=$(file_sha256 "$file")

  case "$hash" in
    06ef4dcaa0530d7410513b65b82eea45f147d386a7689dcf1f5636fdad7561ff|\
    907d367c7589361788a2b0456003f6ef0a2e049548b77b9acf5b0f8cecc2ab0f|\
    7f46fdc7e3016ab2309d49607a9dddc907b06066cebcf23a63e0110f5d962d73|\
    322f495563435da1dc70e16968ee33f6a9d81ab361b79ebfd97814b83a89e011|\
    56009cd83e46e0715911877f173bf1dab966f3e22b6995c530937f2feb18b667|\
    1a8c98df85360a85acde31effa33c419ce9d27bc17e3a0d5900d4a6cb1743bcc)
      return 0
      ;;
  esac

  return 1
}

install_bash_startup() {
  local bashrc="$HOME/.bashrc" bash_profile="$HOME/.bash_profile"
  [[ -f $root/default/bashrc ]] || return 0

  backup_config_file "$bashrc"

  if [[ ! -e $bashrc && ! -L $bashrc ]] || is_known_bashrc_default "$bashrc"; then
    cp "$root/default/bashrc" "$bashrc"
  elif grep -qxF 'source ~/.local/share/omarchy/default/bash/rc' "$bashrc" || grep -qxF 'source "$OMARCHY_PATH/default/bash/rc"' "$bashrc"; then
    python3 - "$bashrc" <<'BASHRC_PATCH_PY'
import sys
from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text()
new_block = '''# /etc/omarchy.conf is written by omarchy-dev-link. When absent, force the
# package default instead of preserving a stale inherited dev-link value before
# we decide which rc file to source.
if [[ -f /etc/omarchy.conf ]]; then
  source /etc/omarchy.conf
  export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/de-omarchy}"
else
  export OMARCHY_PATH=/usr/share/de-omarchy
fi
source "$OMARCHY_PATH/default/bash/rc"
'''
text = text.replace('source ~/.local/share/omarchy/default/bash/rc\n', new_block)
text = text.replace(': "${OMARCHY_PATH:=/usr/share/de-omarchy}"\nsource "$OMARCHY_PATH/default/bash/rc"\n', new_block)
path.write_text(text)
BASHRC_PATCH_PY
  fi

  if [[ ! -e $bash_profile && ! -L $bash_profile ]]; then
    echo '[[ -f ~/.bashrc ]] && . ~/.bashrc' >"$bash_profile"
  fi
}

# Some former ~/.config defaults are now package-owned elsewhere so future
# updates can change defaults without rewriting user files. Back them up before
# cleanup, then keep only real user customizations active.
uwsm_env="$HOME/.config/uwsm/env"
if [[ -f $uwsm_env ]]; then
  cp -f "$uwsm_env" "$uwsm_env.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
  migrate_uwsm_env_customizations "$uwsm_env"
fi

for rel in "${retired_config_files[@]}"; do
  file="$HOME/.config/$rel"
  backup_config_file "$file"
done

# Existing user config may be customized. Refresh only files whose hash matches
# a default Omarchy has shipped before, fill in missing defaults for old installs,
# and always write the quattro-only entry points that older installs cannot have.
refresh_known_config_defaults
copy_missing_config_defaults "$root/config" "$HOME/.config"
mark_removed_preinstalls_from_legacy_bindings
copy_always_config_defaults
repair_chromium_copy_url_extension_flags

# Carry over user-added backgrounds from the legacy git checkout backup, then
# remove old symlinks that pointed ~/.config/omarchy/themes/* back into the
# pre-quattro checkout. Omarchy quattro reads stock themes from /usr/share/de-omarchy/themes.
legacy_omarchy_backup="$HOME/.local/share/omarchy.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
if [[ -d $legacy_omarchy_backup/themes && -d $legacy_omarchy_backup/.git ]]; then
  (
    cd "$legacy_omarchy_backup"
    git ls-files --others -- 'themes/*/backgrounds/*' 2>/dev/null | while IFS= read -r bg_file; do
      theme_name=${bg_file#themes/}
      theme_name=${theme_name%%/*}
      mkdir -p "$HOME/.config/omarchy/backgrounds/$theme_name"
      mv "$bg_file" "$HOME/.config/omarchy/backgrounds/$theme_name/"
    done
  )
fi
if [[ -d $HOME/.config/omarchy/themes ]]; then
  find "$HOME/.config/omarchy/themes" -mindepth 1 -maxdepth 1 -type l -delete
fi

# Retire legacy UI configs replaced by omarchy-shell. Hyprland .conf files are
# intentionally left in place for users to reference/port after the upgrade.
for rel in waybar swayosd mako walker; do
  file="$HOME/.config/$rel"
  if [[ -e $file || -L $file ]]; then
    backup="$file.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
    rm -rf "$backup"
    mv "$file" "$backup"
  fi
done

for rel in "${retired_config_files[@]}"; do
  file="$HOME/.config/$rel"
  backup="$file.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"

  # UWSM env was hardcoded to the legacy checkout on older installs. Custom
  # non-Omarchy lines were migrated to env.d above; never keep the active file.
  if [[ $rel == uwsm/env ]]; then
    rm -f "$file"
    continue
  fi

  if [[ -f $backup ]] && ! is_known_default_hash retire "$rel" "$backup"; then
    # Keep intentional user overrides active; package defaults remain lower
    # priority under /usr/lib or /usr/share.
    mkdir -p "$(dirname "$file")"
    cp -af "$backup" "$file"
  else
    rm -f "$file"
  fi
done
repair_sleep_lock_unit_override
install_bash_startup

mkdir -p "$HOME/.agents/skills" "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.pi/agent/skills"
if [[ -d $root/default/agents/skills/omarchy ]]; then
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.agents/skills/omarchy"
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.claude/skills/omarchy"
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.codex/skills/omarchy"
  ln -sfn "$root/default/agents/skills/omarchy" "$HOME/.pi/agent/skills/omarchy"
fi

mkdir -p "$HOME/.local/state/omarchy/toggles/hypr"
if [[ -f $root/default/hypr/toggles/flags.lua ]]; then
  cp "$root/default/hypr/toggles/flags.lua" "$HOME/.local/state/omarchy/toggles/hypr/"
fi
# Legacy Hyprland .conf entry points source ~/.local/state/omarchy/toggles/hypr/*.conf.
# Keep an empty match around for the live pre-reboot session; quattro Lua ignores it.
if [[ -f $HOME/.config/hypr/hyprland.conf ]]; then
  : >"$HOME/.local/state/omarchy/toggles/hypr/flags.conf"
fi

mkdir -p "$HOME/.local/share/nautilus-python/extensions"
for extension in localsend.py transcode.py; do
  if [[ -f $root/default/nautilus-python/extensions/$extension ]]; then
    cp "$root/default/nautilus-python/extensions/$extension" "$HOME/.local/share/nautilus-python/extensions/"
  fi
done

mkdir -p "$HOME/.config/omarchy/branding"
[[ -f $root/icon.txt ]] && cp "$root/icon.txt" "$HOME/.config/omarchy/branding/about.txt"
[[ -f $root/logo.txt ]] && cp "$root/logo.txt" "$HOME/.config/omarchy/branding/screensaver.txt"

mkdir -p "$HOME/Downloads" "$HOME/Pictures" "$HOME/Videos" "$HOME/Projects" "$HOME/.config/gtk-3.0"
if command -v xdg-user-dirs-update >/dev/null 2>&1; then
  xdg-user-dirs-update --set TEMPLATES "$HOME" || true
  xdg-user-dirs-update --set PUBLICSHARE "$HOME" || true
  xdg-user-dirs-update --set DESKTOP "$HOME" || true
fi
rmdir "$HOME/Templates" "$HOME/Public" "$HOME/Desktop" 2>/dev/null || true
touch "$HOME/.config/gtk-3.0/bookmarks"
for dir in Downloads Projects Pictures Videos; do
  bookmark="file://$HOME/$dir $dir"
  grep -qxF "$bookmark" "$HOME/.config/gtk-3.0/bookmarks" || echo "$bookmark" >>"$HOME/.config/gtk-3.0/bookmarks"
done

if [[ -n ${OMARCHY_USER_NAME//[[:space:]]/} ]]; then
  git config --global user.name "$OMARCHY_USER_NAME" || true
fi
if [[ -n ${OMARCHY_USER_EMAIL//[[:space:]]/} ]]; then
  git config --global user.email "$OMARCHY_USER_EMAIL" || true
fi
if [[ ! -f $HOME/.XCompose ]]; then
  cat >"$HOME/.XCompose" <<EOF
# Run omarchy-restart-xcompose to apply changes

# Include fast emoji access
include "/usr/share/de-omarchy/default/xcompose"

# Identification
<Multi_key> <space> <n> : "$OMARCHY_USER_NAME"
<Multi_key> <space> <e> : "$OMARCHY_USER_EMAIL"
EOF
fi

mkdir -p "$HOME/.config/omarchy/themes" "$HOME/.config/btop/themes"
legacy_current_dir="$HOME/.config/omarchy/current"
current_state_dir="$HOME/.local/state/omarchy/current"
if [[ -e $legacy_current_dir || -L $legacy_current_dir ]]; then
  mkdir -p "$HOME/.local/state/omarchy"
  if [[ ! -e $current_state_dir && ! -L $current_state_dir ]]; then
    mv "$legacy_current_dir" "$current_state_dir"
  else
    if [[ -d $legacy_current_dir && -d $current_state_dir ]]; then
      cp -an "$legacy_current_dir/." "$current_state_dir/" 2>/dev/null || true
    fi
    rm -rf "$legacy_current_dir"
  fi
fi
replace_legacy_current_path() {
  local file="$1"

  [[ -f $file ]] || return 0
  python3 - "$file" "$HOME" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
home = sys.argv[2]
text = path.read_text()
replacements = {
  f"{home}/.config/omarchy/current": f"{home}/.local/state/omarchy/current",
  "~/.config/omarchy/current": "~/.local/state/omarchy/current",
  "../omarchy/current": "../../.local/state/omarchy/current",
}
for old, new in replacements.items():
  text = text.replace(old, new)
path.write_text(text)
PY
}
for file in \
  "$HOME/.config/alacritty/alacritty.toml" \
  "$HOME/.config/foot/foot.ini" \
  "$HOME/.config/ghostty/config" \
  "$HOME/.config/hypr/hyprland.conf" \
  "$HOME/.config/hyprland-preview-share-picker/config.yaml" \
  "$HOME/.config/kitty/kitty.conf"; do
  replace_legacy_current_path "$file"
done
ln -snf "$HOME/.local/state/omarchy/current/theme/btop.theme" "$HOME/.config/btop/themes/current.theme"
if [[ ! -e $HOME/.local/state/omarchy/current/background && -d $HOME/.local/state/omarchy/current/theme/backgrounds ]]; then
  background=$(find "$HOME/.local/state/omarchy/current/theme/backgrounds" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) | sort | head -n1)
  [[ -n ${background:-} ]] && ln -snf "$background" "$HOME/.local/state/omarchy/current/background"
fi

mkdir -p "$HOME/.config/wireplumber/wireplumber.conf.d"
if [[ -f $root/default/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf ]]; then
  cp "$root/default/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf" "$HOME/.config/wireplumber/wireplumber.conf.d/"
fi

enable_user_unit() {
  local unit="$1" unit_path link_target target
  local -a targets=()

  if [[ -f $HOME/.config/systemd/user/$unit ]]; then
    unit_path="$HOME/.config/systemd/user/$unit"
    link_target="../$unit"
  elif [[ -f /usr/lib/systemd/user/$unit ]]; then
    unit_path="/usr/lib/systemd/user/$unit"
    link_target="$unit_path"
  elif [[ -f $root/default/systemd/user/$unit ]]; then
    unit_path="$root/default/systemd/user/$unit"
    link_target="$unit_path"
  else
    return 0
  fi

  mapfile -t targets < <(awk -F= '
    /^\[Install\]/ { in_install=1; next }
    /^\[/ { in_install=0 }
    in_install && $1 == "WantedBy" {
      n=split($2, values, /[[:space:]]+/)
      for (i = 1; i <= n; i++) if (values[i] != "") print values[i]
    }
  ' "$unit_path")
  ((${#targets[@]})) || targets=(default.target)
  for target in "${targets[@]}"; do
    mkdir -p "$HOME/.config/systemd/user/$target.wants"
    ln -sfn "$link_target" "$HOME/.config/systemd/user/$target.wants/$unit"
  done
}
enable_user_unit bt-agent.service
enable_user_unit omarchy-sleep-lock.service
enable_user_unit omarchy-recover-internal-monitor.service
mkdir -p "$HOME/.config/systemd/user/default.target.wants"
for unit in pipewire-pulse.service pipewire-pulse.socket; do
  if [[ -e /usr/lib/systemd/user/$unit ]]; then
    ln -sfn "/usr/lib/systemd/user/$unit" "$HOME/.config/systemd/user/default.target.wants/$unit"
  fi
done

keyring_dir="$HOME/.local/share/keyrings"
keyring_file="$keyring_dir/Default_keyring.keyring"
default_file="$keyring_dir/default"
mkdir -p "$keyring_dir"
if [[ ! -f $keyring_file ]]; then
  cat >"$keyring_file" <<EOF
[keyring]
display-name=Default keyring
ctime=$(date +%s)
mtime=0
lock-on-idle=false
lock-after=false
EOF
fi
if [[ ! -f $default_file ]]; then
  printf 'Default_keyring\n' >"$default_file"
fi
chmod 700 "$keyring_dir"
chmod 600 "$keyring_file"
chmod 644 "$default_file"

mkdir -p "$HOME/.local/share/applications"
rm -f "$HOME/.config/autostart/walker.desktop"
rm -rf "$HOME/.config/elephant" "$HOME/.config/makima" "$HOME/.config/wiremix"
rm -rf "$HOME/.config/systemd/user/[email protected]"
rm -f \
  "$HOME/.config/systemd/user/omarchy-battery-monitor.service" \
  "$HOME/.config/systemd/user/omarchy-battery-monitor.timer" \
  "$HOME/.config/systemd/user/omarchy-shell.service" \
  "$HOME/.config/systemd/user/swayosd-server.service" \
  "$HOME/.config/systemd/user/elephant.service"
rm -f \
  "$HOME/.local/state/omarchy/toggles/mako.ini" \
  "$HOME/.local/state/omarchy/toggles/waybar-off" \
  "$HOME/.local/state/omarchy/toggles/hyprlock.conf" \
  "$HOME/.local/state/omarchy/toggles/lock.conf" \
  "$HOME/.local/state/omarchy/toggles/hypr/rounded-corners.lua"

for desktop in \
  About.desktop \
  Activity.desktop \
  Alacritty.desktop \
  Cliamp.desktop \
  blueberry.desktop \
  nvim.desktop \
  omarchy.desktop \
  org.pulseaudio.pavucontrol.desktop \
  vlc.desktop \
  wiremix.desktop; do
  rm -f "$HOME/.local/share/applications/$desktop"
done

legacy_icons_dir="$HOME/.local/share/applications/icons"
legacy_icons_backup="$HOME/.local/share/applications/icons.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak"
if [[ -d $legacy_icons_dir ]]; then
  mkdir -p "$legacy_icons_backup"
  for icon in \
    Basecamp.png \
    Battle.net.png \
    ChatGPT.png \
    Cliamp.png \
    Discord.png \
    "Disk Usage.png" \
    Docker.png \
    GitHub.png \
    "Google Contacts.png" \
    "Google Maps.png" \
    "Google Messages.png" \
    "Google Photos.png" \
    HEY.png \
    imv.png \
    "Retro Gaming.png" \
    WhatsApp.png \
    windows.png \
    X.png \
    YouTube.png \
    Zoom.png; do
    if [[ -e $legacy_icons_dir/$icon ]]; then
      mv -f "$legacy_icons_dir/$icon" "$legacy_icons_backup/"
    fi
  done
  rmdir "$legacy_icons_dir" 2>/dev/null || true
  rmdir "$legacy_icons_backup" 2>/dev/null || true
fi

if [[ -d $root/applications ]]; then
  find "$root/applications" -maxdepth 1 -type f -name '*.desktop' -exec cp -f {} "$HOME/.local/share/applications/" \;
fi
if [[ -f $root/default/alacritty/Alacritty.desktop && ! -f $HOME/.local/share/applications/Alacritty.desktop ]]; then
  cp "$root/default/alacritty/Alacritty.desktop" "$HOME/.local/share/applications/"
fi
if command -v update-desktop-database >/dev/null 2>&1; then
  update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
fi
if command -v xdg-settings >/dev/null 2>&1; then
  env -u BROWSER xdg-settings set default-web-browser chromium.desktop || true
fi
if command -v xdg-mime >/dev/null 2>&1; then
  xdg-mime default HEY.desktop x-scheme-handler/mailto || true
fi

# This script is fetched from the branch and runs against whatever packaged
# tree the channel currently serves, so the packaged tree can be older than the
# script. The completion markers are written directly rather than through
# omarchy-done, because a command missing from an older build would abort the
# upgrade two thirds of the way through.
done_dir="$state_dir/done"
mkdir -p "$done_dir"
touch "$done_dir/first-run-user" "$done_dir/finalize-user"
rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done"
USER_SETUP
}

apply_user_hardware_transition() {
  if grep -qi "DX13260" /sys/class/dmi/id/product_name 2>/dev/null ||
    grep -qi "DX13260" /sys/class/dmi/id/product_family 2>/dev/null; then
    log "Adjusting text scaling for Dell XPS 13 DX13260"
    if ! run_as_user_session gsettings set org.gnome.desktop.interface text-scaling-factor 0.95; then
      warn "Could not adjust text scaling for Dell XPS 13 DX13260; continuing."
    fi
  fi
}

write_legacy_theme_hyprland_conf() {
  run_as_user env HOME="$target_home" bash <<'THEME_COMPAT'
set -euo pipefail
hyprland_conf="$HOME/.config/hypr/hyprland.conf"
[[ -f $hyprland_conf ]] || exit 0
# Legacy current-path references were already rewritten during the user
# transition; only the theme hyprland.conf shim is needed here.
grep -q 'current/theme/hyprland.conf' "$hyprland_conf" || exit 0

theme_dir="$HOME/.local/state/omarchy/current/theme"
colors="$theme_dir/colors.toml"
target="$theme_dir/hyprland.conf"
mkdir -p "$theme_dir"
accent="7aa2f7"
if [[ -f $colors ]]; then
  parsed=$(awk -F= '/^[[:space:]]*accent[[:space:]]*=/ { gsub(/[[:space:]"#]/, "", $2); print $2; exit }' "$colors")
  [[ -n ${parsed:-} ]] && accent="$parsed"
fi
{
  printf '$activeBorderColor = rgb(%s)\n\n' "$accent"
  cat <<'EOF'
general {
    col.active_border = $activeBorderColor
}

group {
    col.border_active = $activeBorderColor
}
EOF
} >"$target"
THEME_COMPAT
}

refresh_current_theme_after_upgrade() {
  local theme_name runtime_dir

  theme_name=$(run_as_user env HOME="$target_home" bash -c '
    set -euo pipefail
    theme_name_path="$HOME/.local/state/omarchy/current/theme.name"
    current_theme_path="$HOME/.local/state/omarchy/current/theme"
    legacy_theme_name_path="$HOME/.config/omarchy/current/theme.name"
    legacy_current_theme_path="$HOME/.config/omarchy/current/theme"

    if [[ -f $theme_name_path ]]; then
      read -r theme_name <"$theme_name_path" || true
      printf "%s" "${theme_name:-}"
    elif [[ -f $legacy_theme_name_path ]]; then
      read -r theme_name <"$legacy_theme_name_path" || true
      printf "%s" "${theme_name:-}"
    elif [[ -L $current_theme_path ]]; then
      basename -- "$(readlink "$current_theme_path")"
    elif [[ -L $legacy_current_theme_path ]]; then
      basename -- "$(readlink "$legacy_current_theme_path")"
    else
      printf ""
    fi
  ')
  theme_name=${theme_name//$'\r'/}
  theme_name=${theme_name//$'\n'/}
  [[ -n $theme_name ]] || theme_name="Tokyo Night"

  log "Refreshing current theme with Omarchy quattro templates ($theme_name)"

  runtime_dir="$target_runtime_dir"
  [[ -d $runtime_dir ]] || runtime_dir=/tmp

  if ! run_as_user_omarchy \
    XDG_RUNTIME_DIR="$runtime_dir" \
    DBUS_SESSION_BUS_ADDRESS="unix:path=$runtime_dir/bus" \
    XDG_CURRENT_DESKTOP=Hyprland \
    XDG_SESSION_DESKTOP=Hyprland \
    XDG_SESSION_TYPE=wayland \
    DESKTOP_SESSION=hyprland \
    OMARCHY_THEME_HEADLESS=1 \
    omarchy-theme-set "$theme_name"; then
    warn "Could not refresh current theme '$theme_name'. Run 'omarchy theme set \"$theme_name\"' after reboot if theme-dependent app config looks stale."
  fi

  write_legacy_theme_hyprland_conf

  # Headless theme refresh intentionally skips omarchy-theme-set's live post
  # hooks because one of them runs `hyprctl reload`. Still poke terminal
  # emulators so the active upgrade terminal picks up generated theme files.
  run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
}

# Everything below mutates the system, so a non-zero exit from here on leaves a
# half-upgraded machine that must not be rebooted.
upgrade_started=1

# Suppress live Hyprland reloads before anything touches the legacy config
# trees; the swap windows otherwise surface transient "source= globbing error"
# popups in the running session.
suppress_hyprland_config_reload
create_pre_upgrade_snapshot
configure_pacman_channel
install_keyrings
remove_legacy_installer_package
remove_legacy_limine_configs
remove_conflicting_legacy_packages
install_omarchy_quattro_packages
install_hardware_transition_packages
normalize_limine_config
preserve_kernel_cmdline_root
configure_snapper_policy
configure_lock_authentication
migrate_1password_beta_package
cleanup_legacy_user_paths
apply_system_transition
apply_user_transition
apply_user_hardware_transition
run_as_user_omarchy omarchy-refresh-applications ||
  warn "Could not refresh application launchers; run 'omarchy refresh applications' after reboot."
run_as_user_omarchy omarchy-bar defaults ||
  warn "Could not restore the default bar widgets; run 'omarchy bar defaults' after reboot."
cleanup_retired_services
ensure_sleep_lock_service
remove_retired_default_packages
run_post_upgrade_migrations
run_final_system_package_upgrade
run_post_upgrade_update_steps
refresh_current_theme_after_upgrade
# Do not force-reload Hyprland in the live upgraded session. The legacy
# default/hypr and theme hyprland.conf shims above exist specifically so the
# pre-reboot session can keep running safely; the reboot performs the real
# Omarchy quattro Lua-config cutover.
restore_hyprland_config_reload

upgrade_completed=1

cat <<EOF

WARNING: You must address any errors in the above before rebooting.

EOF

if (( boot_cmdline_unsafe )); then
  warn "Skipping reboot: root= could not be confirmed in the boot entries. Repair the kernel cmdline (see the warnings above) before rebooting, or the system will drop to an emergency shell."
elif (( auto_reboot )); then
  log "Rebooting because --reboot was passed"
  as_root systemctl reboot
elif gum confirm "Reboot to complete Quattro upgrade now?" </dev/tty; then
  as_root systemctl reboot
else
  echo "Not rebooting. Please reboot manually after confirming there were no errors."
fi