quadraui 0.5.0

Cross-platform Rust UI toolkit: write one AppLogic and run it unmodified on TUI, GTK4 (Linux), macOS, and Windows backends
Documentation
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
//! Shared, backend-neutral desktop-interaction plumbing.
//!
//! Everything in this module is genuinely windowing-generic: it has no
//! dependency on `gtk4`, `objc2`, `windows`, or any other toolkit crate,
//! and none of the *code* here needs a toolkit feature to compile — the
//! whole point of extracting it is that a brand-new backend gets it for
//! free. Each piece used to be implemented once inside `gtk/` only; a
//! macOS or Windows backend would otherwise have had to reinvent it from
//! scratch:
//!
//! - [`WindowDragArm`] — the arm/threshold/commit state machine behind
//!   a CSD titlebar drag-to-move (or edge-resize) gesture. Extracted
//!   from `gtk/backend.rs`'s `armed_window_drag` field +
//!   `gtk/run.rs`'s motion-controller threshold check. **Not**
//!   adopted by `win::run`/`win::backend`: `win::run` creates its
//!   window with `WS_OVERLAPPEDWINDOW` — the standard native title bar
//!   and non-client resize border, not a client-side-decorated one —
//!   so title-bar drag-to-move and edge-resize are already handled
//!   entirely inside Windows' own non-client (`WM_NC*`) message
//!   handling; there is no quadraui-drawn titlebar/border for an app to
//!   press-and-drag on, so there is nothing for this arm/threshold/commit
//!   sequencing to gate.
//! - [`ModalPumpDepth`] / [`ModalPumpGuard`] — the re-entrancy guard
//!   around a nested native modal loop (a GTK async-dialog
//!   `MainContext::iteration` pump, an AppKit `runModal` /
//!   `performWindowDragWithEvent:`, a Win32 `IFileOpenDialog::Show`).
//!   Extracted from `gtk/services.rs`'s `pumping` field + `PumpGuard`.
//!   Adopted by `win::run`'s `wndproc` → `app.handle`/`app.render`
//!   dispatch via `win::run`'s own `guarded_call` — the actual
//!   `RefCell` double-borrow guard lives there since it has no toolkit
//!   dependency of its own either; see that function's docs.
//! - [`SmokeConfig`] + [`smoke_size_ok`] / [`smoke_clipboard_round_trip_ok`]
//!   — the headless smoke-mode predicates behind `QUADRAUI_*_SMOKE_MS` /
//!   `_SMOKE_PASTE`. Extracted from `gtk/run.rs`, renamed backend-neutral
//!   (no more `Gtk` in the name) and parameterised over the env-var
//!   names and size floor so a future backend can reuse the mechanism
//!   under its own env vars and default window size. Adopted by
//!   `win::run`'s `QUADRAUI_WIN_SMOKE_MS`/`_SMOKE_PASTE` one-shot
//!   `WM_TIMER` check alongside GTK's `QUADRAUI_GTK_SMOKE_MS`/
//!   `_SMOKE_PASTE`.
//! - [`ALL_RESIZE_EDGES`] / [`all_pointer_shapes`] — an enum-walk
//!   scaffold for a [`PointerShape`] → native-cursor lookup table.
//!   Every backend still owns its own table (cursor *names*/objects are
//!   inherently native), but this gives it a canonical, exhaustively-
//!   maintained list of inputs to build and test that table against
//!   instead of hand-duplicating the variant list per backend. Adopted by
//!   `win::backend`'s `PointerShape` → `IDC_*` cursor-resource mapping,
//!   driving `SetCursor`/`WM_SETCURSOR`.
//! - [`is_paste_keypress`] — the clipboard-paste keypress predicate.
//!   Extracted from `gtk::run`'s private `is_paste_keypress` and
//!   generalised to also cover macOS's Cmd-V, which used to be an inline
//!   `match` guard in `macos::run::dispatch_event` instead of a named,
//!   independently-testable predicate. See `docs/decisions/DECISIONS.md`
//!   D-011 for the shift-tolerance contract this settles once for every
//!   adopter instead of each backend picking its own, and D-011 §4
//!   specifically for why the predicate takes a [`PasteModifier`]
//!   parameter naming *which* platform modifier is native to the calling
//!   backend, rather than accepting either `ctrl` or `cmd`
//!   interchangeably. Adopted by `win::run::dispatch_event` to give
//!   Win-GUI its first `UiEvent::ClipboardPaste` at all.
//!
//! ## What stays backend-specific
//!
//! The actual native calls — `gdk4::Toplevel::begin_move`,
//! `NSWindow::performWindowDragWithEvent`, `DwmExtendFrameIntoClientArea`
//! — are deliberately **not** here. [`WindowDragArm::commit_if_past_threshold`]
//! hands back the caller's own press payload `P` once the gesture should
//! commit; what the caller does with it (the native "begin move" call)
//! stays in `gtk/backend.rs` / `macos/backend.rs` / a future
//! `win/backend.rs`. Same story for [`ModalPumpGuard`] (wraps *a*
//! nested-loop call, doesn't make one) and the pointer-shape scaffold
//! (lists the variants, doesn't map them to a native cursor).
//!
//! ## Why every item below is `#[cfg]`-gated despite having no toolkit
//! dependency
//!
//! Mirrors `crate::runtime`'s precedent exactly (see its module doc):
//! the workspace-wide `RUSTFLAGS: "-D warnings"` turns an
//! unused-`pub(crate)`-item warning into a hard build failure the moment
//! a feature combination compiles this module without any backend around
//! to call into it (e.g. `--features tui`, which has no window chrome
//! at all). Each item is gated on the feature(s) of the backend(s) that
//! actually call it today, not left permanently `#[allow(dead_code)]` —
//! adding a new adopter (e.g. a `win` backend wiring up `WindowDragArm`)
//! is a one-line `cfg` addition, not a license to leave the guard off.

// ─────────────────────────────────────────────────────────────────────
// WindowDragArm
// ─────────────────────────────────────────────────────────────────────

/// Arm/threshold/commit state machine for a deferred native window-drag
/// (or, by the same shape, edge-resize) gesture, generic over `P` — the
/// backend-native press payload (GTK: device + button + x + y +
/// timestamp; a future AppKit/Win32 backend has its own shape). This
/// struct only owns the arm/discard/threshold/commit *sequencing*; it
/// never interprets `P` itself.
///
/// ## Why "arm now, commit later past a threshold" instead of acting on
/// the raw press immediately
///
/// Calling a native "begin move" synchronously on the very first press
/// — before it's known whether a second press is coming — starts an
/// interactive move grab that swallows the second press, so a
/// double-click on a CSD titlebar never reaches the app as
/// `UiEvent::DoubleClick`. Deferring the actual native call until the
/// pointer has moved past a threshold distance since the arming press
/// avoids this; it's exactly what native `gtk4::WindowHandle` does too,
/// deferring its own move-start to `GestureDrag`'s threshold-gated
/// `drag-begin` signal rather than the raw press (see
/// `gtk::backend::GtkBackend::armed_window_drag`'s doc for the full
/// rationale this generalises).
///
/// Not every backend needs the threshold gating (AppKit's
/// `performWindowDragWithEvent:` already disambiguates a drag from a
/// double-click internally, so `macos::backend::MacBackend` uses
/// [`Self::arm`] / [`Self::take`] without ever calling
/// [`Self::commit_if_past_threshold`]) — the type doesn't force either
/// usage.
#[cfg(any(feature = "gtk", all(feature = "macos", target_os = "macos")))]
#[derive(Debug)]
pub(crate) struct WindowDragArm<P> {
    /// `(press, origin_x, origin_y)` — `None` when nothing is armed.
    armed: Option<(P, f64, f64)>,
}

#[cfg(any(feature = "gtk", all(feature = "macos", target_os = "macos")))]
impl<P> WindowDragArm<P> {
    /// No request armed.
    pub(crate) const fn new() -> Self {
        Self { armed: None }
    }

    /// Arm a deferred request with `press` and the screen-space origin
    /// `(origin_x, origin_y)` of the press that armed it. Overwrites any
    /// previously-armed (and never committed) request.
    pub(crate) fn arm(&mut self, press: P, origin_x: f64, origin_y: f64) {
        self.armed = Some((press, origin_x, origin_y));
    }

    /// Whether a request is currently armed. Test-only today: no
    /// production caller needs it (both `try_commit_window_drag`-style
    /// callers and [`Self::commit_if_past_threshold`] itself work
    /// without ever asking "is something armed?" directly), but it's
    /// handy for asserting a backend's own no-window/no-press guard
    /// state without exposing the private `armed` field.
    #[cfg(test)]
    pub(crate) fn is_armed(&self) -> bool {
        self.armed.is_some()
    }

    /// Screen-space origin of the press that armed the current request,
    /// if any. Test-only today: production callers only ever need
    /// [`Self::commit_if_past_threshold`], which does its own origin
    /// math internally.
    #[cfg(test)]
    pub(crate) fn origin(&self) -> Option<(f64, f64)> {
        self.armed.as_ref().map(|(_, x, y)| (*x, *y))
    }

    /// Discard an armed-but-uncommitted request without starting the
    /// gesture. Call when the button goes up before the pointer ever
    /// moved past the threshold — the press was a plain click (or the
    /// first half of a double-click), and leaving the state armed would
    /// let a later, unrelated hover-motion event accidentally commit it.
    ///
    /// Gated on `gtk`-or-`test` for the same `-D warnings` reason the
    /// module doc gives: `macos::backend` uses [`Self::arm`] /
    /// [`Self::take`] only (AppKit's `performWindowDragWithEvent:` needs
    /// no threshold gating), so on a `--features macos` build with no
    /// `gtk` around this method has no production caller and an ungated
    /// `pub(crate) fn` would be a hard `dead_code` build failure. A
    /// future backend that adopts the threshold path adds its feature
    /// here.
    #[cfg(any(feature = "gtk", test))]
    pub(crate) fn discard(&mut self) {
        self.armed = None;
    }

    /// Unconditionally take the armed press, if any, discarding origin
    /// tracking. For gestures with no competing double-click ambiguity
    /// to protect against (an edge-resize; or a backend, like AppKit,
    /// whose native call already disambiguates drag-vs-click itself) —
    /// see the type's doc comment.
    pub(crate) fn take(&mut self) -> Option<P> {
        self.armed.take().map(|(p, _, _)| p)
    }

    /// If a request is armed and the live pointer position
    /// `(current_x, current_y)` has moved at least `threshold_px` from
    /// the arming origin, take and return the press so the caller can
    /// start the native gesture. Returns `None` — leaving the request
    /// armed — if nothing is armed yet, or if it's armed but still
    /// under threshold.
    ///
    /// Gated on `gtk`-or-`test` for the same reason as [`Self::discard`]
    /// — see that method's note.
    #[cfg(any(feature = "gtk", test))]
    pub(crate) fn commit_if_past_threshold(
        &mut self,
        current_x: f64,
        current_y: f64,
        threshold_px: f64,
    ) -> Option<P> {
        let (_, origin_x, origin_y) = self.armed.as_ref()?;
        let dx = current_x - origin_x;
        let dy = current_y - origin_y;
        if (dx * dx + dy * dy).sqrt() >= threshold_px {
            self.take()
        } else {
            None
        }
    }
}

#[cfg(any(feature = "gtk", all(feature = "macos", target_os = "macos")))]
impl<P> Default for WindowDragArm<P> {
    fn default() -> Self {
        Self::new()
    }
}

// ─────────────────────────────────────────────────────────────────────
// ModalPumpDepth / ModalPumpGuard
// ─────────────────────────────────────────────────────────────────────

/// Shared, cloneable re-entrancy depth counter for a nested native
/// modal-loop pump (GTK's `MainContext::iteration(true)` wait for an
/// async `gtk4::FileDialog`; AppKit's `runModal` / any call that
/// internally pumps the run loop, e.g. `performWindowDragWithEvent:`;
/// Win32's `IFileOpenDialog::Show`). `> 0` while such a pump is in
/// flight (possibly several deep, if one nested loop somehow triggers
/// another) — see [`ModalPumpGuard`].
///
/// Extracted from `gtk::services::GtkPlatformServices`'s `pumping` field.
/// The hazard it guards against: a nested pump lets *any* other
/// pending event-loop source run too — including the runner's own idle
/// timer and every input event controller, all of which may also
/// mutably borrow the shared `Rc<RefCell<Backend>>` that the code
/// driving the pump *already* holds borrowed. Left unguarded, that
/// second borrow panics with "already borrowed", and because it
/// typically happens inside a non-unwindable native callback frame, the
/// panic aborts the whole process instead of propagating. Every runner
/// closure that might re-enter should clone this handle and check
/// [`Self::is_pumping`] before touching the backend.
///
/// `#[cfg(any(feature = "gtk", all(feature = "win", any(target_os =
/// "windows", test))))]`, not `any(gtk, macos)`: GTK's async
/// `FileDialog` pump and `win::run`'s `wndproc` → `dispatch`/`WM_PAINT`
/// borrows are the adopters today. Unlike `win/backend.rs`'s WinAPI-calling
/// methods, `guarded_call` is pure `RefCell`/`Rc<Cell<>>` logic with no
/// toolkit dependency of its own, so it (and its unit test reproducing
/// the double-borrow hazard) compile and run under plain `--features win`
/// on *any* host — matching this module's own "no toolkit feature needed"
/// design — but only in a `cfg(test)` build: `win::run::guarded_call` has
/// no *production* caller unless `win32::wndproc` (its Windows-only
/// adopter) also compiles, so the `test` alternative exists purely to
/// keep a genuine consumer around for a plain, non-test
/// `cargo check --features win` on a non-Windows host — see
/// `win::run::guarded_call`'s own gate for the matching half of this.
/// `macos::backend`'s `performWindowDragWithEvent:` is listed above as a
/// *candidate* nested pump, but nothing in `macos/` guards on this
/// counter yet — and per the module doc, a `pub(crate)` item with no
/// caller under some compiled feature set is a hard `-D warnings` build
/// failure, not a warning. A macOS adopter widens this gate in the same
/// commit that adds the call.
#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
#[derive(Debug, Default, Clone)]
pub(crate) struct ModalPumpDepth(std::rc::Rc<std::cell::Cell<u32>>);

#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
impl ModalPumpDepth {
    /// A fresh counter at depth 0 (no pump in flight).
    pub(crate) fn new() -> Self {
        Self(std::rc::Rc::new(std::cell::Cell::new(0)))
    }

    /// Current nesting depth.
    pub(crate) fn get(&self) -> u32 {
        self.0.get()
    }

    /// `true` while at least one nested pump is in flight. Runner
    /// closures that would otherwise call `backend.borrow_mut()` check
    /// this first and no-op instead.
    pub(crate) fn is_pumping(&self) -> bool {
        self.get() > 0
    }
}

/// RAII guard that increments a [`ModalPumpDepth`] for its lifetime and
/// decrements it on drop (including on an early return or panic-unwind
/// out of the pump), so nested pumps stay guarded until the *outermost*
/// one finishes. Construct one right before starting a nested native
/// modal loop; hold it for the loop's duration.
///
/// See [`ModalPumpDepth`]'s gate note for this type's identical gate.
#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
pub(crate) struct ModalPumpGuard<'a> {
    depth: &'a ModalPumpDepth,
}

#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
impl<'a> ModalPumpGuard<'a> {
    pub(crate) fn new(depth: &'a ModalPumpDepth) -> Self {
        depth.0.set(depth.0.get() + 1);
        Self { depth }
    }
}

#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
impl Drop for ModalPumpGuard<'_> {
    fn drop(&mut self) {
        self.depth.0.set(self.depth.0.get() - 1);
    }
}

// ─────────────────────────────────────────────────────────────────────
// move_to_trash
// ─────────────────────────────────────────────────────────────────────

/// Move `path` to the platform trash/recycle bin via the cross-platform
/// `trash` crate — the one implementation shared by every
/// `PlatformServices::move_to_trash` override
/// (`tui`/`gtk`/`macos`/`win::services`), TUI included. Unlike this
/// module's other genuinely backend-neutral helpers, this one *does*
/// wrap a native call — but the same one on every platform: `trash`
/// itself dispatches internally to `NSFileManager
/// -trashItemAtURL:resultingItemURL:error:` on macOS, the
/// freedesktop.org trash spec on Linux, and
/// `SHFileOperationW(FOF_ALLOWUNDO)` on Windows, none of which need a
/// live desktop/window-server session — only a filesystem — so there is
/// no backend-specific variation left to hand-write. See
/// [`crate::backend::PlatformServices::move_to_trash`]'s doc for why
/// that means TUI gets this exact function too, instead of the
/// `Err(BackendError::Unsupported)` degrade TUI's remaining
/// desktop-shell-flavoured methods (native dialogs, notably) still fall
/// back to — [`reveal_in_file_manager`] below is the other
/// one that has since earned a real, best-effort implementation instead
/// of that placeholder.
///
/// `#[cfg]`-gated on every adopting feature (mirroring this module's own
/// "why every item is gated" note) — `win`'s production call site is
/// itself nested inside `win::services`'s own `#[cfg(target_os =
/// "windows")]` arm (see `WinPlatformServices::move_to_trash`), so the
/// `win` alternative here needs the identical `any(target_os =
/// "windows", test)` qualifier [`ModalPumpDepth`]'s gate note explains:
/// without it, `cargo check --features win` on a non-Windows host would
/// compile this function with no caller at all and trip `-D warnings`'
/// dead-code lint. The `test` half of that `any(..)` is what keeps this a
/// real, unit-testable consumer under `cargo test --features win` on any
/// host, matching that same precedent.
#[cfg(any(
    feature = "tui",
    feature = "gtk",
    all(feature = "macos", target_os = "macos"),
    all(feature = "win", any(target_os = "windows", test))
))]
pub(crate) fn move_to_trash(path: &std::path::Path) -> crate::backend::ServiceResult<()> {
    trash::delete(path).map_err(|e| crate::backend::BackendError::PlatformFailure {
        context: format!("trash::delete: {e}"),
    })
}

#[cfg(all(
    test,
    any(
        feature = "tui",
        feature = "gtk",
        all(feature = "macos", target_os = "macos"),
        feature = "win"
    )
))]
mod move_to_trash_tests {
    use super::*;

    /// Real round trip against this host's actual trash/recycle bin: a
    /// freshly-written temp file must be gone from its original path
    /// (moved, not copied) once [`move_to_trash`] reports success. Skips
    /// gracefully (rather than failing) when the underlying OS call
    /// itself fails — same posture as this crate's OS-clipboard tests on
    /// a headless display: on macOS, `trash`'s implementation goes
    /// through a Finder AppleEvent, which needs a live desktop session
    /// (and, on a *first* run, an Automation permission grant) that a
    /// sandboxed/headless dev box or CI container genuinely may not have,
    /// even though the *filesystem* `move_to_trash` itself only ever
    /// touches is always present.
    #[allow(clippy::print_stderr)]
    #[test]
    fn move_to_trash_removes_the_file_from_its_original_path() {
        let path = std::env::temp_dir().join(format!(
            "quadraui-956-move-to-trash-test-{}.txt",
            std::process::id()
        ));
        std::fs::write(&path, b"quadraui#956").expect("write temp file");
        assert!(path.exists(), "temp file must exist before trashing it");

        if let Err(e) = move_to_trash(&path) {
            eprintln!(
                "skipping: move_to_trash failed in this environment ({e:?}) — \
                 likely no live desktop/trash session"
            );
            let _ = std::fs::remove_file(&path);
            return;
        }

        assert!(
            !path.exists(),
            "the original path must no longer exist once move_to_trash succeeds"
        );
    }

    /// A path that never existed is a real, reportable failure — not
    /// silently `Ok(())` — matching every other `PlatformServices` method
    /// in this crate that surfaces the native call's actual outcome
    /// rather than papering over it.
    #[test]
    fn move_to_trash_on_a_nonexistent_path_reports_a_failure() {
        let path = std::env::temp_dir().join(format!(
            "quadraui-956-does-not-exist-{}-{}.txt",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or_default()
        ));
        assert!(!path.exists());
        assert!(move_to_trash(&path).is_err());
    }
}

// ─────────────────────────────────────────────────────────────────────
// wide_nul_terminated
// ─────────────────────────────────────────────────────────────────────

/// UTF-16, NUL-terminated — the framing every wide (`W`-suffixed) Win32
/// API expects (`ShellExecuteW`'s `operation`/`file`, `CF_UNICODETEXT`'s
/// clipboard payload, `NOTIFYICONDATAW`'s fixed-size fields, a context
/// menu item's label). The single implementation shared by
/// `win::services`, `win::tray`, `win::backend`, and this module's own
/// [`windows_shell_execute_open`] — before this, each of the first three
/// modules carried its own private copy of this exact two-line
/// conversion, and [`windows_shell_execute_open`] needed a fourth.
///
/// `#[cfg_attr(not(target_os = "windows"), allow(dead_code))]`: every
/// real caller only exists under `target_os = "windows"` (the WinAPI
/// call sites this feeds, and this module's own Windows-only opener), so
/// on any other host this is unreachable dead code the moment it
/// compiles at all under `--features win` alone.
#[cfg(any(feature = "tui", feature = "win"))]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn wide_nul_terminated(text: &str) -> Vec<u16> {
    text.encode_utf16().chain(std::iter::once(0)).collect()
}

#[cfg(all(test, any(feature = "tui", feature = "win")))]
mod wide_nul_terminated_tests {
    use super::*;

    #[test]
    fn encodes_utf16_and_appends_a_nul() {
        assert_eq!(
            wide_nul_terminated("ab"),
            vec!['a' as u16, 'b' as u16, 0u16]
        );
        assert_eq!(wide_nul_terminated(""), vec![0u16]);
    }
}

// ─────────────────────────────────────────────────────────────────────
// open_with_default
// ─────────────────────────────────────────────────────────────────────

/// Open `target` (a URL string or a filesystem path, carried as `&OsStr`
/// so a non-UTF-8 path need not be lossily converted before the call) —
/// the single opener implementation shared by `tui::services`, `macos::services`,
/// and `win::services`.
///
/// Unix: spawns `open` (macOS) / `xdg-open` (other Unix), detached
/// (stdout/stderr nulled so a slow or chatty opener can't wedge the
/// caller's own streams — matters most for `tui`, which shares stdout
/// with the terminal UI) and never through a shell — `execve`, so
/// `target` is never re-parsed for shell metacharacters. Windows:
/// `ShellExecuteW(NULL, "open", target, NULL, NULL, SW_SHOWNORMAL)`
/// through a **minimal, hand-written FFI declaration**, not the
/// `windows` crate — `windows` is `optional`, pulled in only by the
/// `win` feature's `dep:windows`, so it is not a dependency of a bare
/// `--features tui` build, and this must compile there too. Never goes
/// through `cmd.exe`: the `cmd /c start` approach is a CWE-78
/// command-injection hole, since `cmd.exe` re-parses its own command-line
/// text for `&`/`|`/`^`/`%` regardless of how the argument was quoted for
/// `CreateProcess`.
#[cfg(any(
    feature = "tui",
    all(feature = "macos", target_os = "macos"),
    all(feature = "win", target_os = "windows")
))]
pub(crate) fn open_with_default(target: &std::ffi::OsStr) -> crate::backend::ServiceResult<()> {
    if try_open_with_default(target) {
        Ok(())
    } else {
        Err(crate::backend::BackendError::PlatformFailure {
            context: format!("open_with_default: no opener launched for {target:?}"),
        })
    }
}

/// The "did the platform opener genuinely launch" step [`open_with_default`]
/// wraps into a [`crate::backend::ServiceResult`], exposed separately
/// (`pub(crate)`, not folded into `open_with_default` itself) because
/// `tui::services`'s `open_url_via` needs the raw bool to chain its own
/// OSC 8 hyperlink fallback when this returns `false` —
/// `open_with_default`'s `Err` shape has nowhere to carry that extra
/// step.
#[cfg(any(
    feature = "tui",
    all(feature = "macos", target_os = "macos"),
    all(feature = "win", target_os = "windows")
))]
pub(crate) fn try_open_with_default(target: &std::ffi::OsStr) -> bool {
    #[cfg(windows)]
    {
        windows_shell_execute_open(target)
    }
    #[cfg(not(windows))]
    {
        unix_open_with_default_command(target).spawn().is_ok()
    }
}

/// macOS opener command, not yet spawned — factored out so a test can
/// assert on the program name and arguments a real call would spawn
/// without actually launching anything. See the Linux/BSD overload of
/// this same function (below) for the shared shape and doc.
#[cfg(all(target_os = "macos", any(feature = "tui", feature = "macos")))]
pub(crate) fn unix_open_with_default_command(target: &std::ffi::OsStr) -> std::process::Command {
    let mut cmd = std::process::Command::new("open");
    cmd.arg(target)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null());
    cmd
}

/// Linux/BSD opener command. See the macOS overload of this same
/// function (above) for the shared doc.
#[cfg(all(unix, not(target_os = "macos"), feature = "tui"))]
pub(crate) fn unix_open_with_default_command(target: &std::ffi::OsStr) -> std::process::Command {
    let mut cmd = std::process::Command::new("xdg-open");
    cmd.arg(target)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null());
    cmd
}

/// Windows opener: `ShellExecuteW`, called through a minimal hand-written
/// FFI declaration rather than the `windows` crate — see
/// [`open_with_default`]'s doc for why. Returns whether `ShellExecuteW`
/// reports success, per [`shell_execute_reports_success`]'s `> 32` rule.
#[cfg(all(windows, any(feature = "tui", feature = "win")))]
fn windows_shell_execute_open(target: &std::ffi::OsStr) -> bool {
    shell_execute_reports_success(windows_shell_execute_open_code(target))
}

/// `ShellExecuteW`'s documented success test, split out from the FFI call
/// itself ([`windows_shell_execute_open_code`]) so it is unit-testable on
/// *every* host — the FFI call is not.
///
/// Per `ShellExecuteW`'s own docs the return value is "greater than 32 if
/// successful, or an error value that is less than or equal to 32
/// otherwise" (e.g. `SE_ERR_FNF` = 2, `SE_ERR_NOASSOC` = 31). `32` itself
/// is the boundary and is a *failure* (`SE_ERR_DDEFAIL`), which is the
/// off-by-one this predicate exists to pin down.
///
/// Deliberately **not** `cfg(windows)`-gated: it is pure arithmetic with
/// no WinAPI in it, exactly like `win::msg`'s `WM_SIZE` unpacking, so
/// `ci.yml`'s ubuntu-leg `cargo test -p quadraui --features win` executes
/// its tests too. Only [`windows_shell_execute_open_code`] — the part
/// that genuinely calls into `shell32` — stays Windows-only.
///
/// `#[cfg_attr(not(target_os = "windows"), allow(dead_code))]` for the
/// same reason [`wide_nul_terminated`] carries one: its only non-test
/// caller is the Windows-only [`windows_shell_execute_open`].
#[cfg(any(feature = "tui", feature = "win"))]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn shell_execute_reports_success(code: isize) -> bool {
    code > 32
}

#[cfg(all(test, any(feature = "tui", feature = "win")))]
mod shell_execute_reports_success_tests {
    use super::*;

    /// [`shell_execute_reports_success`] is `ShellExecuteW`'s documented
    /// "greater than 32" rule, and the boundary is the whole reason it's a
    /// named function rather than an inline comparison: `32` itself
    /// (`SE_ERR_DDEFAIL`) is a *failure*, so a `>=` here would silently
    /// turn one real failure code into a reported success. Pure
    /// arithmetic, no WinAPI — so this runs on every host, including
    /// `ci.yml`'s ubuntu-leg `cargo test -p quadraui --features win`,
    /// where nothing else in the Windows opener compiles at all.
    #[test]
    fn success_threshold_is_strictly_above_32() {
        const SE_ERR_FNF: isize = 2;
        const SE_ERR_NOASSOC: isize = 31;
        const SE_ERR_DDEFAIL: isize = 32;

        assert!(!shell_execute_reports_success(0));
        assert!(!shell_execute_reports_success(SE_ERR_FNF));
        assert!(!shell_execute_reports_success(SE_ERR_NOASSOC));
        assert!(!shell_execute_reports_success(SE_ERR_DDEFAIL));
        assert!(shell_execute_reports_success(33));
        assert!(shell_execute_reports_success(42));
    }
}

/// The raw `ShellExecuteW(NULL, "open", target, NULL, NULL,
/// SW_SHOWNORMAL)` return value. Split from
/// [`windows_shell_execute_open`] so a test can assert on the actual
/// documented `SE_ERR_*` code a given target produces on a real host;
/// callers outside this module want the `bool`.
#[cfg(all(windows, any(feature = "tui", feature = "win")))]
fn windows_shell_execute_open_code(target: &std::ffi::OsStr) -> isize {
    // SAFETY: `ShellExecuteW` is a well-known, stable Win32 API. Both
    // wide-string buffers passed below are nul-terminated and kept alive
    // (as local `Vec<u16>`s) for the duration of the call; the remaining
    // arguments are the documented "no window handle / no extra
    // parameters / no explicit working directory" null pointers.
    //
    // `#[link(name = "shell32")]`: `ShellExecuteW` lives in
    // `shell32.dll`/`shell32.lib` — unlike the `kernel32`/`user32`
    // imports the MSVC CRT startup pulls in implicitly, this one needs an
    // explicit link directive since nothing else in a bare `tui`-feature
    // build references `shell32` at all.
    #[link(name = "shell32")]
    unsafe extern "system" {
        fn ShellExecuteW(
            hwnd: *mut core::ffi::c_void,
            operation: *const u16,
            file: *const u16,
            parameters: *const u16,
            directory: *const u16,
            show_cmd: i32,
        ) -> isize;
    }
    const SW_SHOWNORMAL: i32 = 1;

    let operation = wide_nul_terminated("open");
    let file = wide_nul_terminated(&target.to_string_lossy());
    unsafe {
        ShellExecuteW(
            std::ptr::null_mut(),
            operation.as_ptr(),
            file.as_ptr(),
            std::ptr::null(),
            std::ptr::null(),
            SW_SHOWNORMAL,
        )
    }
}

/// Process-wide `$PATH` is shared, mutable global state — every test
/// below (and `macos::services`'s `open_url_result` failure test) that
/// temporarily redirects it to prove a real "opener missing" failure must
/// serialize against every *other* such test in the same process, or a
/// concurrently-running test can observe (or clobber) the override. One
/// crate-wide lock rather than a per-module copy so tests under different
/// feature combinations can't race each other.
///
/// **`unix`-gated, deliberately** — every test that takes this lock is a
/// `$PATH`-override test, and the `$PATH`-stub technique only applies to
/// [`unix_open_with_default_command`]'s arm: Windows' opener
/// ([`windows_shell_execute_open`]) is a direct `ShellExecuteW` FFI call
/// with no `$PATH`-resolved binary to intercept, so no Windows test has
/// any reason to hold this. Without the `unix` here, a
/// `cargo test --features tui` on a Windows host compiles a lock with
/// zero call sites — `dead_code`, which `ci.yml`'s workflow-wide
/// `RUSTFLAGS: -D warnings` promotes to a hard error on the
/// `windows-latest` leg of the `tui` job alone, while every Linux and
/// macOS build of the identical source stays green.
#[cfg(all(
    test,
    unix,
    any(feature = "tui", all(feature = "macos", target_os = "macos"))
))]
pub(crate) static PATH_OVERRIDE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

#[cfg(all(
    test,
    any(
        feature = "tui",
        all(feature = "macos", target_os = "macos"),
        all(feature = "win", target_os = "windows")
    )
))]
mod open_with_default_tests {
    use super::*;

    #[cfg(target_os = "macos")]
    #[test]
    fn unix_open_with_default_command_uses_macos_open() {
        let cmd = unix_open_with_default_command(std::ffi::OsStr::new("https://example.com/1087"));
        assert_eq!(cmd.get_program(), "open");
        assert_eq!(
            cmd.get_args().collect::<Vec<_>>(),
            ["https://example.com/1087"]
        );
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn unix_open_with_default_command_uses_xdg_open() {
        let cmd = unix_open_with_default_command(std::ffi::OsStr::new("https://example.com/1087"));
        assert_eq!(cmd.get_program(), "xdg-open");
        assert_eq!(
            cmd.get_args().collect::<Vec<_>>(),
            ["https://example.com/1087"]
        );
    }

    /// A target that can never resolve, chosen so the real
    /// `ShellExecuteW` call fails *without* handing anything to the shell:
    /// an absolute path under a directory that does not exist, with an
    /// extension no handler is registered for. Verified on a real Windows
    /// host to return `SE_ERR_FNF` (2) immediately, with no UI.
    ///
    /// Deliberately **not** an unregistered URI scheme. That was this
    /// module's first attempt (`quadraui-1087-nonexistent-scheme://x`) and
    /// it is simply wrong on modern Windows: `ShellExecuteW` hands an
    /// unknown scheme off to the shell's "how do you want to open this?"
    /// / Store-lookup flow and returns **42 — success** — before any of
    /// that resolves, so the call reports success for a launch that never
    /// happens. It red-lined `ci.yml`'s "Test (win feature, real Windows)"
    /// step, which is the only leg that executes these `cfg(windows)`
    /// arms at all. Don't reintroduce it.
    #[cfg(windows)]
    fn unopenable_windows_target() -> std::ffi::OsString {
        std::ffi::OsString::from(format!(
            "C:\\quadraui-1087-no-such-directory-{}\\x.quadraui1087nohandler",
            std::process::id()
        ))
    }

    /// The real FFI call's real verdict on a target that cannot open:
    /// `SE_ERR_FNF`, which [`shell_execute_reports_success`] then reports
    /// as `false`. This is the Windows half of the "stop lying about
    /// the launch" bar — the `$PATH`-stub tests below cover the Unix arm,
    /// which has a spawned binary to intercept and Windows does not.
    #[cfg(windows)]
    #[test]
    fn windows_shell_execute_open_reports_a_real_failure_code() {
        const SE_ERR_FNF: isize = 2;
        let target = unopenable_windows_target();
        assert_eq!(windows_shell_execute_open_code(&target), SE_ERR_FNF);
        assert!(!windows_shell_execute_open(&target));
    }

    /// `open_with_default`'s `ServiceResult` wrapping on Windows — the
    /// same bar `open_with_default_reports_err_when_the_opener_is_missing`
    /// sets on Unix, reached through a real `ShellExecuteW` call instead
    /// of a `$PATH` stub.
    #[cfg(windows)]
    #[test]
    fn open_with_default_reports_err_when_windows_cannot_open_the_target() {
        let target = unopenable_windows_target();
        assert_eq!(
            open_with_default(&target),
            Err(crate::backend::BackendError::PlatformFailure {
                context: format!("open_with_default: no opener launched for {target:?}"),
            })
        );
    }

    /// [`windows_shell_execute_open`] never builds a `std::process::Command`
    /// (that's the whole point — no `cmd.exe`/shell in the loop at all),
    /// so there is no command to assert on beyond invoking `ShellExecuteW`
    /// for real. What this pins down: the exact metacharacters (`&`, `|`,
    /// `^`, `%`) that made the original `cmd /c start` approach
    /// exploitable are carried through inertly — a
    /// bare FFI call has no command-line text for those bytes to land in,
    /// so they stay part of the *filename* and the call fails with
    /// `SE_ERR_FNF` rather than executing `calc.exe`.
    ///
    /// The target is an unopenable path (see
    /// [`unopenable_windows_target`]) rather than the `https://` URL this
    /// test first used: a real URL launches the host's actual browser on
    /// every CI run and on every developer's desktop, which is both a
    /// side effect a unit test has no business having and a way to wedge
    /// the run behind a shell dialog.
    #[cfg(windows)]
    #[test]
    fn windows_shell_execute_open_treats_shell_metacharacters_as_inert_text() {
        const SE_ERR_FNF: isize = 2;
        let target = std::ffi::OsString::from(format!(
            "C:\\quadraui-1087-no-such-directory-{}\\q=foo&run=bar|calc.exe^%1.quadraui1087nohandler",
            std::process::id()
        ));
        assert_eq!(windows_shell_execute_open_code(&target), SE_ERR_FNF);
    }

    /// Acceptance bar: `try_open_with_default`/`open_with_default`
    /// through a real delegation — the same `$PATH`-override stub
    /// technique generalised from a previous test. Safe to do without
    /// launching a real browser only because `$PATH` is temporarily
    /// redirected (guarded by [`PATH_OVERRIDE_TEST_LOCK`], and always
    /// restored via a drop guard even on panic) to a directory containing
    /// a stub executable under the exact name
    /// [`unix_open_with_default_command`] looks up on this platform
    /// (`open` on macOS, `xdg-open` elsewhere on Unix) that exits `0`
    /// immediately.
    ///
    /// **Unix only** — Windows' opener ([`windows_shell_execute_open`])
    /// is a direct `ShellExecuteW` FFI call with no `$PATH`-resolved
    /// binary to intercept this way; see
    /// `windows_shell_execute_open_does_not_panic_on_shell_metacharacters`
    /// above for that platform's own coverage of the real function, and
    /// `win::services::open_url_result_reports_failure_for_an_unregistered_scheme`
    /// for its `ServiceResult`-level failure coverage.
    #[cfg(unix)]
    #[test]
    fn try_open_with_default_returns_true_through_a_real_delegation() {
        let _guard = PATH_OVERRIDE_TEST_LOCK
            .lock()
            .unwrap_or_else(|p| p.into_inner());

        #[cfg(target_os = "macos")]
        const OPENER_NAME: &str = "open";
        #[cfg(all(unix, not(target_os = "macos")))]
        const OPENER_NAME: &str = "xdg-open";

        let tmp = tempfile::tempdir().expect("tempdir");
        let stub_path = tmp.path().join(OPENER_NAME);
        std::fs::write(&stub_path, b"#!/bin/sh\nexit 0\n").expect("write stub opener");
        {
            use std::os::unix::fs::PermissionsExt;
            std::fs::set_permissions(&stub_path, std::fs::Permissions::from_mode(0o755))
                .expect("chmod +x stub opener");
        }
        let _restore = RestorePath::capture();
        std::env::set_var("PATH", tmp.path());

        assert!(try_open_with_default(std::ffi::OsStr::new(
            "https://example.com/1087"
        )));
    }

    /// The other half of the acceptance bar: with `$PATH` redirected
    /// to a directory containing *no* opener binary at all, the real
    /// spawn genuinely fails (`ENOENT`) and `try_open_with_default`
    /// honestly reports `false`. See `open_with_default_reports_err_when_the_opener_is_missing`
    /// below for the `ServiceResult`-level half of this — `open_with_default`
    /// itself is also compiled for a bare `--features tui` build (it backs
    /// `TuiPlatformServices::open_path`), even though `open_url_result`
    /// still calls `try_open_with_default` directly there so it can chain
    /// its own OSC 8 fallback.
    #[cfg(unix)]
    #[test]
    fn try_open_with_default_returns_false_when_the_opener_is_missing() {
        let _guard = PATH_OVERRIDE_TEST_LOCK
            .lock()
            .unwrap_or_else(|p| p.into_inner());
        let _restore = RestorePath::capture();
        let empty_dir = tempfile::tempdir().expect("tempdir");
        std::env::set_var("PATH", empty_dir.path());

        assert!(!try_open_with_default(std::ffi::OsStr::new(
            "https://example.com/1087"
        )));
    }

    /// `open_with_default`'s own `ServiceResult` wrapping, via the `$PATH`
    /// stub technique — Unix only: this technique needs a `$PATH`-resolved
    /// binary to intercept, which only applies to `open_with_default`'s
    /// Unix arm. Runs under either `macos` or bare `tui`. `win::services`'s
    /// own `ServiceResult`-level coverage uses a real `ShellExecuteW` call
    /// instead, since Windows' opener has no `$PATH`-resolved binary to
    /// intercept this way at all.
    #[cfg(any(
        all(feature = "macos", target_os = "macos"),
        all(feature = "tui", unix)
    ))]
    #[test]
    fn open_with_default_reports_err_when_the_opener_is_missing() {
        let _guard = PATH_OVERRIDE_TEST_LOCK
            .lock()
            .unwrap_or_else(|p| p.into_inner());
        let _restore = RestorePath::capture();
        let empty_dir = tempfile::tempdir().expect("tempdir");
        std::env::set_var("PATH", empty_dir.path());

        assert_eq!(
            open_with_default(std::ffi::OsStr::new("https://example.com/1087")),
            Err(crate::backend::BackendError::PlatformFailure {
                context: "open_with_default: no opener launched for \"https://example.com/1087\""
                    .to_string(),
            })
        );
    }

    /// RAII guard restoring the process-wide `$PATH` this test module
    /// temporarily overrides, even on an early return or panic-unwind —
    /// same "always restore, drop-guard" posture
    /// [`crate::desktop::ModalPumpGuard`] and this crate's other
    /// global-state test helpers use.
    #[cfg(unix)]
    struct RestorePath(Option<std::ffi::OsString>);

    #[cfg(unix)]
    impl RestorePath {
        fn capture() -> Self {
            Self(std::env::var_os("PATH"))
        }
    }

    #[cfg(unix)]
    impl Drop for RestorePath {
        fn drop(&mut self) {
            match self.0.take() {
                Some(path) => std::env::set_var("PATH", path),
                None => std::env::remove_var("PATH"),
            }
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// reveal_in_file_manager
// ─────────────────────────────────────────────────────────────────────

/// The TUI degrade for `PlatformServices::reveal_in_file_manager`. A
/// terminal session on a desktop can reveal a file in its file manager
/// exactly as readily as it can launch a browser, so `Unsupported` was a
/// placeholder, not a structural fact.
///
/// Tries, per platform:
/// - **Windows**: [`windows_explorer_select`] —
///   `ShellExecuteW(NULL, "open", "explorer.exe", "/select,\"<path>\"",
///   NULL, SW_SHOWNORMAL)`, the same minimal hand-written FFI declaration
///   [`windows_shell_execute_open_code`] uses, pointed at `explorer.exe`
///   with an explicit `/select,` switch instead of handing `path`
///   straight to the registered "open" handler.
/// - **macOS**: [`macos_reveal_in_file_manager_command`] — `open -R
///   <path>`, the CLI front end to the same
///   `NSWorkspace::activateFileViewerSelectingURLs` call
///   `macos::services::MacPlatformServices::reveal_in_file_manager` makes
///   directly; TUI has no AppKit to link against, so this reaches for the
///   command-line door into the identical native call instead.
/// - **Linux/BSD**: [`linux_show_items_via_dbus`] —
///   `org.freedesktop.FileManager1.ShowItems` over the session D-Bus, the
///   same standard method
///   `gtk::services::GtkPlatformServices::reveal_in_file_manager` drives
///   through a linked `gio::DBusConnection::call_sync`; TUI has no `gio`
///   (a `gtk`-feature-only dependency), so this drives the identical
///   well-known method through the `dbus-send` CLI tool instead — the
///   same "reach for the CLI door onto a native facility instead of
///   linking a client library" trade [`write_clipboard_via_native_tool`]
///   already makes for the system clipboard. Falls back to
///   [`try_open_with_default`] on `path`'s *parent directory* when that
///   call fails (no `FileManager1` listener — a headless container, a
///   minimal window manager with no file manager running): not a perfect
///   "reveal, selected" experience, but a real file-manager window opened
///   at the right place, on a desktop session that plainly has one,
///   instead of a blanket `Unsupported`.
///
/// Best-effort throughout, matching every other `ServiceResult`-returning
/// TUI degrade in this crate: a successful spawn/D-Bus call reports
/// `Ok(())` even though the launched file manager may itself fail to open
/// a window with no way for this call to observe it.
#[cfg(feature = "tui")]
pub(crate) fn reveal_in_file_manager(path: &std::path::Path) -> crate::backend::ServiceResult<()> {
    if try_reveal_in_file_manager(path) {
        Ok(())
    } else {
        Err(crate::backend::BackendError::Unsupported)
    }
}

/// The raw "did anything launch" half of [`reveal_in_file_manager`],
/// split out (mirroring [`try_open_with_default`]'s own split from
/// [`open_with_default`]) purely so tests can exercise the per-platform
/// dispatch without wrapping every assertion in a `ServiceResult`.
#[cfg(feature = "tui")]
fn try_reveal_in_file_manager(path: &std::path::Path) -> bool {
    #[cfg(windows)]
    {
        windows_explorer_select(path)
    }
    #[cfg(target_os = "macos")]
    {
        macos_reveal_in_file_manager_command(path).spawn().is_ok()
    }
    #[cfg(all(unix, not(target_os = "macos")))]
    {
        if linux_show_items_via_dbus(path) {
            return true;
        }
        match path.parent() {
            Some(parent) => try_open_with_default(parent.as_os_str()),
            None => false,
        }
    }
}

/// macOS opener command, not yet spawned — `open -R <path>` ("Reveal in
/// Finder" from the command line). Factored out so a test can assert on
/// the program name and arguments a real call would spawn without
/// actually launching anything, the same shape
/// [`unix_open_with_default_command`] uses.
#[cfg(all(target_os = "macos", feature = "tui"))]
fn macos_reveal_in_file_manager_command(path: &std::path::Path) -> std::process::Command {
    let mut cmd = std::process::Command::new("open");
    cmd.arg("-R")
        .arg(path)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null());
    cmd
}

/// Windows opener: `ShellExecuteW`, pointed at `explorer.exe` with an
/// explicit `/select,"<path>"` parameter — see [`reveal_in_file_manager`]'s
/// doc for why. Returns whether `ShellExecuteW` reports success, per
/// [`shell_execute_reports_success`]'s `> 32` rule, the same predicate
/// [`windows_shell_execute_open`] already uses.
#[cfg(all(windows, feature = "tui"))]
fn windows_explorer_select(path: &std::path::Path) -> bool {
    shell_execute_reports_success(windows_explorer_select_code(path))
}

/// The raw `ShellExecuteW(NULL, "open", "explorer.exe",
/// "/select,\"<path>\"", NULL, SW_SHOWNORMAL)` return value. Split from
/// [`windows_explorer_select`] so a test can assert on the actual
/// documented `SE_ERR_*` code a given path produces on a real host —
/// same split [`windows_shell_execute_open_code`] has from
/// [`windows_shell_execute_open`]. `path.display()` is safe to embed
/// unquoted-of-inner-quotes here: Windows' filesystem itself forbids `"`
/// in a path, so there is no in-band way for `path` to break out of the
/// `/select,"..."` quoting this builds. That covers embedded quotes, but
/// not the classic MSVCRT argv-splitting edge case where a path's literal
/// string ends in a backslash immediately before the closing quote (an
/// odd run of trailing backslashes there escapes the quote instead of
/// closing the string) — not exploitable for injection (nothing follows
/// for an escaped quote to expose) and not reachable through
/// `path.display()` for an ordinary file/directory path (which doesn't
/// carry a trailing separator), so [`windows_explorer_select_parameters`]
/// strips one defensively rather than leaving it as a latent trap.
#[cfg(all(windows, feature = "tui"))]
fn windows_explorer_select_code(path: &std::path::Path) -> isize {
    // SAFETY: see `windows_shell_execute_open_code`'s identical SAFETY
    // note — both wide-string buffers below are nul-terminated and kept
    // alive for the duration of the call.
    #[link(name = "shell32")]
    unsafe extern "system" {
        fn ShellExecuteW(
            hwnd: *mut core::ffi::c_void,
            operation: *const u16,
            file: *const u16,
            parameters: *const u16,
            directory: *const u16,
            show_cmd: i32,
        ) -> isize;
    }
    const SW_SHOWNORMAL: i32 = 1;

    let operation = wide_nul_terminated("open");
    let file = wide_nul_terminated("explorer.exe");
    let parameters = wide_nul_terminated(&windows_explorer_select_parameters(path));
    unsafe {
        ShellExecuteW(
            std::ptr::null_mut(),
            operation.as_ptr(),
            file.as_ptr(),
            parameters.as_ptr(),
            std::ptr::null(),
            SW_SHOWNORMAL,
        )
    }
}

/// Build the `/select,"<path>"` parameter string
/// [`windows_explorer_select_code`] hands `explorer.exe`, split out (pure
/// string formatting, no WinAPI) so it is unit-testable on every host —
/// the FFI call itself is not. Mirrors
/// [`shell_execute_reports_success`]'s "no WinAPI, so it runs everywhere"
/// posture: unconditionally compiled under `feature = "tui"` rather than
/// `cfg(windows)`-gated, so `ci.yml`'s ubuntu-leg `cargo test --features
/// tui` exercises it too.
///
/// `#[cfg_attr(not(windows), allow(dead_code))]` for the same reason
/// [`wide_nul_terminated`] carries one: its only non-test caller
/// ([`windows_explorer_select_code`]) is Windows-only.
#[cfg(feature = "tui")]
#[cfg_attr(not(windows), allow(dead_code))]
fn windows_explorer_select_parameters(path: &std::path::Path) -> String {
    // Strip a trailing separator before quoting: an odd run of
    // backslashes immediately before the closing `"` escapes that quote
    // rather than closing the string under MSVCRT-style argv splitting
    // (see this function's doc comment above its caller,
    // `windows_explorer_select_code`, for the full reasoning). Ordinary
    // file/directory paths never carry one, so this is a defensive
    // no-op in practice, not a behavior change for real callers.
    let mut rendered = path.display().to_string();
    while rendered.len() > 1 && (rendered.ends_with('\\') || rendered.ends_with('/')) {
        rendered.pop();
    }
    format!("/select,\"{rendered}\"")
}

/// Build the `dbus-send` invocation for
/// `org.freedesktop.FileManager1.ShowItems` — not yet
/// spawned, so a test can assert on the constructed command without a
/// live D-Bus session. Drives the same standard method
/// `gtk::services::GtkPlatformServices::reveal_in_file_manager` reaches
/// through a linked `gio::DBusConnection::call_sync`, at the CLI-tool
/// level instead — see [`reveal_in_file_manager`]'s doc for why TUI can't
/// link `gio` directly.
#[cfg(all(unix, not(target_os = "macos"), feature = "tui"))]
fn linux_show_items_via_dbus_command(path: &std::path::Path) -> std::process::Command {
    let uri = path_to_file_uri(path);
    let mut cmd = std::process::Command::new("dbus-send");
    cmd.args([
        "--session",
        "--dest=org.freedesktop.FileManager1",
        "--type=method_call",
        "/org/freedesktop/FileManager1",
        "org.freedesktop.FileManager1.ShowItems",
    ])
    .arg(format!("array:string:{uri}"))
    .arg("string:")
    .stdout(std::process::Stdio::null())
    .stderr(std::process::Stdio::null());
    cmd
}

/// Run [`linux_show_items_via_dbus_command`], reporting whether
/// `dbus-send` itself exited successfully — `dbus-send` reports a
/// non-zero exit for a D-Bus-level failure (no session bus, no
/// `FileManager1` listener) exactly the way a failed native
/// `gio::DBusConnection::call_sync` would, so an exit-status check is
/// enough; there is no reply payload this call needs to read back.
#[cfg(all(unix, not(target_os = "macos"), feature = "tui"))]
fn linux_show_items_via_dbus(path: &std::path::Path) -> bool {
    linux_show_items_via_dbus_command(path)
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Percent-encode `path` into a `file://` URI: every byte outside RFC
/// 3986's unreserved set (`ALPHA` / `DIGIT` / `-._~`) plus the path
/// separator `/` is escaped as `%XX`. Good enough for
/// `ShowItems`'s `as` (array-of-strings) argument — not a full RFC 3986
/// implementation — but in particular escapes `,` (which would otherwise
/// be misread as a `dbus-send` `array:string:` element separator) and
/// whitespace, both of which a real filesystem path can easily contain.
/// [`gtk::services::GtkPlatformServices::reveal_in_file_manager`] gets
/// the same encoding for free from `gio::File::for_path(path).uri()`;
/// TUI has no `gio` to call, so this is the minimal hand-rolled
/// equivalent.
#[cfg(all(unix, not(target_os = "macos"), feature = "tui"))]
fn path_to_file_uri(path: &std::path::Path) -> String {
    use std::os::unix::ffi::OsStrExt;

    let mut out = String::from("file://");
    for byte in path.as_os_str().as_bytes() {
        let b = *byte;
        let is_unreserved =
            b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/');
        if is_unreserved {
            out.push(b as char);
        } else {
            out.push_str(&format!("%{b:02X}"));
        }
    }
    out
}

#[cfg(all(test, feature = "tui"))]
mod reveal_in_file_manager_tests {
    use super::*;

    #[cfg(target_os = "macos")]
    #[test]
    fn macos_reveal_in_file_manager_command_uses_open_dash_r() {
        let cmd = macos_reveal_in_file_manager_command(std::path::Path::new("/tmp/quadraui1092"));
        assert_eq!(cmd.get_program(), "open");
        assert_eq!(
            cmd.get_args().collect::<Vec<_>>(),
            ["-R", "/tmp/quadraui1092"]
        );
    }

    /// Pure string formatting, no WinAPI — runs on every host, including
    /// `ci.yml`'s ubuntu-leg `cargo test --features tui`, same posture
    /// [`shell_execute_reports_success_tests`]'s own doc explains.
    /// `Path::new(r"C:\a b\c.txt")` renders identically via `.display()`
    /// on Unix (one opaque path component, backslashes preserved as
    /// literal bytes) and on Windows (a real backslash-separated path),
    /// so the expected output is the same on either host.
    #[test]
    fn windows_explorer_select_parameters_quotes_the_path() {
        assert_eq!(
            windows_explorer_select_parameters(std::path::Path::new(r"C:\a b\c.txt")),
            r#"/select,"C:\a b\c.txt""#
        );
    }

    #[test]
    fn windows_explorer_select_parameters_strips_a_trailing_separator() {
        // A path ending in a backslash right before the closing quote
        // would otherwise escape that quote under MSVCRT-style argv
        // splitting instead of closing the string.
        assert_eq!(
            windows_explorer_select_parameters(std::path::Path::new(r"C:\dir\")),
            r#"/select,"C:\dir""#
        );
        assert_eq!(
            windows_explorer_select_parameters(std::path::Path::new(r"C:\a b\c.txt")),
            r#"/select,"C:\a b\c.txt""#
        );
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn linux_show_items_via_dbus_command_targets_file_manager1() {
        let cmd = linux_show_items_via_dbus_command(std::path::Path::new("/tmp/quadraui 1092"));
        assert_eq!(cmd.get_program(), "dbus-send");
        assert_eq!(
            cmd.get_args().collect::<Vec<_>>(),
            [
                "--session",
                "--dest=org.freedesktop.FileManager1",
                "--type=method_call",
                "/org/freedesktop/FileManager1",
                "org.freedesktop.FileManager1.ShowItems",
                "array:string:file:///tmp/quadraui%201092",
                "string:",
            ]
        );
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn path_to_file_uri_percent_encodes_spaces_and_commas() {
        assert_eq!(
            path_to_file_uri(std::path::Path::new("/tmp/a,b c/d.txt")),
            "file:///tmp/a%2Cb%20c/d.txt"
        );
    }

    /// Acceptance bar: when neither the D-Bus leg nor the
    /// `xdg-open`-the-parent fallback can reach anything (`$PATH`
    /// redirected to an empty directory), `reveal_in_file_manager` honestly
    /// reports `Unsupported` rather than a false `Ok(())`. Doesn't exercise
    /// the D-Bus leg for real (that needs a live session bus this test
    /// environment may not have) — only pins the "both legs failed" outcome
    /// via the `$PATH` stub, which is the fallback this crate can actually
    /// control in a test.
    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn reveal_in_file_manager_reports_unsupported_when_every_leg_fails() {
        let _guard = PATH_OVERRIDE_TEST_LOCK
            .lock()
            .unwrap_or_else(|p| p.into_inner());
        struct RestorePath(Option<std::ffi::OsString>);
        impl Drop for RestorePath {
            fn drop(&mut self) {
                match self.0.take() {
                    Some(path) => std::env::set_var("PATH", path),
                    None => std::env::remove_var("PATH"),
                }
            }
        }
        let _restore = RestorePath(std::env::var_os("PATH"));
        let empty_dir = tempfile::tempdir().expect("tempdir");
        std::env::set_var("PATH", empty_dir.path());

        let target = std::env::temp_dir().join("quadraui-1092-no-such-file.txt");
        // `dbus-send` itself is very unlikely to be on an empty `$PATH`
        // (it's the binary this leg spawns), so this exercises the "not
        // even `dbus-send` present" corner of the D-Bus leg alongside the
        // `xdg-open`-the-parent fallback's own missing-opener leg.
        assert_eq!(
            reveal_in_file_manager(&target),
            Err(crate::backend::BackendError::Unsupported)
        );
    }
}

/// Escape `"` and `\` so a string can be embedded inside an AppleScript
/// double-quoted string literal. Shared by [`display_notification_script`]
/// below — `macos::services::MacPlatformServices::send_notification`'s
/// own private helper until issue #1092 gave `tui::services` a second
/// `osascript` call site (the TUI degrade's macOS notification fallback
/// leg) that needed byte-for-byte the same escaping.
#[cfg(all(target_os = "macos", any(feature = "macos", feature = "tui")))]
pub(crate) fn applescript_escape(s: &str) -> String {
    s.replace('\\', "\\\\").replace('"', "\\\"")
}

/// Build the `osascript -e` argument an AppleScript `display
/// notification` call spawns. `display notification` needs an explicit
/// `sound name` clause to make *any* sound — omitting it entirely is
/// silent, not the system default. So `silent: false` (the default — a
/// caller that never calls `Notification::with_silent` gets a
/// notification that actually makes a sound, matching ordinary
/// desktop-notification expectations) adds `sound name ""`, which plays
/// the system default notification sound.
#[cfg(all(target_os = "macos", any(feature = "macos", feature = "tui")))]
pub(crate) fn display_notification_script(title: &str, body: &str, silent: bool) -> String {
    let mut script = format!(
        "display notification \"{body}\" with title \"{title}\"",
        body = applescript_escape(body),
        title = applescript_escape(title),
    );
    if !silent {
        script.push_str(" sound name \"\"");
    }
    script
}

/// Build the `osascript -e <script>` command
/// `tui::services::TuiPlatformServices::send_notification`'s macOS
/// fallback leg spawns — not yet spawned, so a test can
/// assert on the constructed argv (in particular, that `title`/`body`
/// reach `osascript` via [`display_notification_script`] unmodified)
/// without a live display session. Split out for the same reason
/// [`macos_reveal_in_file_manager_command`] and
/// [`linux_show_items_via_dbus_command`] are: every other command this
/// issue builds is unit-testable this way, and the notification fallback
/// legs shouldn't be the one exception.
#[cfg(all(target_os = "macos", feature = "tui"))]
pub(crate) fn tui_macos_notify_command(
    title: &str,
    body: &str,
    silent: bool,
) -> std::process::Command {
    let script = display_notification_script(title, body, silent);
    let mut cmd = std::process::Command::new("osascript");
    cmd.arg("-e").arg(script);
    cmd
}

/// Build the `notify-send <title> <body>` command
/// `tui::services::TuiPlatformServices::send_notification`'s Linux/BSD
/// fallback leg spawns — not yet spawned, so a test can
/// assert `title` and `body` arrive as two separate argv entries (not
/// shell-concatenated, so a body containing spaces can't be mistaken for
/// extra `notify-send` flags) without a live notification-daemon
/// session.
#[cfg(all(unix, not(target_os = "macos"), feature = "tui"))]
pub(crate) fn tui_notify_send_command(title: &str, body: &str) -> std::process::Command {
    let mut cmd = std::process::Command::new("notify-send");
    cmd.arg(title).arg(body);
    cmd
}

/// `unix`-gated as a whole, not per-test: both command builders this
/// module covers ([`tui_macos_notify_command`],
/// [`tui_notify_send_command`]) are themselves `cfg(unix)` — Windows has
/// no native command-line notifier leg at all (OSC 9 is natively
/// supported by Windows Terminal, so `tui::services`'s escape leg covers
/// it alone; see that module's "Notifications" doc). A
/// `#[cfg(all(test, feature = "tui"))]` module with only `cfg(unix)` and
/// `cfg(target_os = "macos")` tests inside compiles to an *empty* module
/// on Windows, whose `use super::*` is then an `unused_imports` warning —
/// and `ci.yml`'s windows-latest "Test (tui feature)" step runs under
/// `RUSTFLAGS: -D warnings`, so that warning is a hard build failure
/// there (and only there: the "Build (tui feature)" step above it sets no
/// such flag, which is why this surfaced as a test-step-only red).
/// Windows-side coverage of `send_notification` lives in
/// `tui::services`'s `notification_osc_tests` instead, which is exactly
/// the whole method on that platform.
#[cfg(all(test, unix, feature = "tui"))]
mod tui_send_notification_command_tests {
    use super::*;

    #[cfg(target_os = "macos")]
    #[test]
    fn tui_macos_notify_command_uses_osascript_with_the_display_notification_script() {
        let cmd = tui_macos_notify_command("Build failed", "3 errors", false);
        assert_eq!(cmd.get_program(), "osascript");
        let args: Vec<_> = cmd.get_args().collect();
        assert_eq!(args[0], "-e");
        assert_eq!(
            args[1],
            display_notification_script("Build failed", "3 errors", false).as_str()
        );
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn tui_notify_send_command_passes_title_and_body_as_separate_args() {
        let cmd = tui_notify_send_command("Build failed", "3 errors");
        assert_eq!(cmd.get_program(), "notify-send");
        assert_eq!(
            cmd.get_args().collect::<Vec<_>>(),
            ["Build failed", "3 errors"]
        );
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn tui_notify_send_command_keeps_body_out_of_argv0() {
        // A body containing something that looks like a flag must stay
        // its own argv entry, never get shell-concatenated with the
        // title into something `notify-send` could misparse.
        let cmd = tui_notify_send_command("t", "--urgency=critical");
        assert_eq!(
            cmd.get_args().collect::<Vec<_>>(),
            ["t", "--urgency=critical"]
        );
    }
}

#[cfg(all(test, target_os = "macos", any(feature = "macos", feature = "tui")))]
mod display_notification_script_tests {
    use super::*;

    #[test]
    fn applescript_escape_handles_quotes_and_backslashes() {
        // Empty + pass-through.
        assert_eq!(applescript_escape(""), "");
        assert_eq!(applescript_escape("plain text"), "plain text");
        // Single-character escapes.
        assert_eq!(applescript_escape("a\"b"), "a\\\"b");
        assert_eq!(applescript_escape("c\\d"), "c\\\\d");
        // Combined. Backslash MUST be escaped first so the subsequent
        // quote-escape's added backslashes aren't re-escaped.
        assert_eq!(applescript_escape("e\"f\\g"), "e\\\"f\\\\g");
        // Order check: a backslash followed by a quote in input should
        // produce `\\\"` (escaped slash + escaped quote), not `\\\\\"`
        // (double-escaped slash + quote).
        assert_eq!(applescript_escape("\\\""), "\\\\\\\"");
    }

    #[test]
    fn display_notification_script_not_silent_adds_default_sound_clause() {
        let script = display_notification_script("t", "b", false);
        assert_eq!(
            script,
            "display notification \"b\" with title \"t\" sound name \"\""
        );
    }

    #[test]
    fn display_notification_script_silent_omits_sound_clause() {
        let script = display_notification_script("t", "b", true);
        assert_eq!(script, "display notification \"b\" with title \"t\"");
        assert!(!script.contains("sound"));
    }

    #[test]
    fn display_notification_script_escapes_title_and_body() {
        let script = display_notification_script("t\"itle", "b\\ody", true);
        assert_eq!(
            script,
            "display notification \"b\\\\ody\" with title \"t\\\"itle\""
        );
    }
}

// ─────────────────────────────────────────────────────────────────────
// map_arboard_error
// ─────────────────────────────────────────────────────────────────────

/// Map an `arboard::Error` from a named native call into a
/// [`crate::backend::BackendError::PlatformFailure`]. `arboard::Error`
/// has no "unsupported" variant of its own — every arm this maps
/// (including `ContentNotAvailable`, e.g. "clipboard has no image right
/// now") is a real outcome of a call the calling backend *does*
/// implement, so `PlatformFailure` — not
/// [`crate::backend::BackendError::Unsupported`] — is the honest
/// mapping; see `BackendError::Unsupported`'s own doc for why that
/// variant is reserved for "this backend has no implementation" instead.
#[cfg(any(feature = "gtk", all(feature = "macos", target_os = "macos")))]
pub(crate) fn map_arboard_error(call: &str, err: arboard::Error) -> crate::backend::BackendError {
    crate::backend::BackendError::PlatformFailure {
        context: format!("{call}: {err}"),
    }
}

#[cfg(all(
    test,
    any(feature = "gtk", all(feature = "macos", target_os = "macos"))
))]
mod map_arboard_error_tests {
    use super::*;

    #[test]
    fn wraps_the_call_name_and_error_into_a_platform_failure() {
        let err = map_arboard_error("arboard::get_image", arboard::Error::ContentNotAvailable);
        match err {
            crate::backend::BackendError::PlatformFailure { context } => {
                assert!(context.contains("arboard::get_image"));
                assert!(context.contains(&arboard::Error::ContentNotAvailable.to_string()));
            }
            other => panic!("expected PlatformFailure, got {other:?}"),
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// Headless smoke-mode config + predicates
// ─────────────────────────────────────────────────────────────────────

/// Headless smoke-mode config. `None` unless the `ms`-suffixed env var
/// [`Self::from_env`] is asked to read is set — see `gtk::run`'s module
/// doc's "Headless smoke mode" section for the full motivating rationale
/// (a live-window class of bug a display-free driver test structurally
/// can't catch).
///
/// `#[cfg(any(feature = "gtk", all(feature = "win", any(target_os =
/// "windows", test))))]` — same reasoning as [`ModalPumpDepth`]'s gate
/// note: `win::run`'s real smoke lane only exists once
/// `target_os = "windows"` also compiles `mod win32`, but the `test`
/// alternative keeps this type (and its pure predicates below) a real,
/// unit-testable consumer under a plain `cargo test --features win` on
/// any host.
#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SmokeConfig {
    /// Delay after the window is presented before the one-shot check
    /// fires and the window is closed.
    pub(crate) after_ms: u64,
    /// Optional text to round-trip through the real OS clipboard and
    /// replay as a synthetic paste.
    pub(crate) paste_text: Option<String>,
}

#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
impl SmokeConfig {
    /// Reads the smoke-mode env vars once. `ms_var` is the env var that
    /// enables smoke mode (e.g. `QUADRAUI_GTK_SMOKE_MS`) — returns
    /// `None` (the default — zero behavioral change) unless it's set and
    /// parses as a `u64`. `paste_var` (e.g. `QUADRAUI_GTK_SMOKE_PASTE`)
    /// is optional; if set, its value round-trips through the paste
    /// path. Parameterised by name (rather than hardcoding the `GTK`
    /// infix) so a future backend can reuse this under its own env-var
    /// names without colliding with GTK's.
    pub(crate) fn from_env(ms_var: &str, paste_var: &str) -> Option<Self> {
        let after_ms = std::env::var(ms_var).ok()?.parse().ok()?;
        let paste_text = std::env::var(paste_var).ok();
        Some(Self {
            after_ms,
            paste_text,
        })
    }
}

/// Is `width`x`height` a plausible, non-broken window/surface
/// allocation? The direct regression check for a tiny/wrapped-window bug
/// class. Pure and display-free. Each backend supplies its own floor
/// (`min_width`/`min_height`) — its own default window size and how far
/// below it is still "plausible" — this function only owns the comparison.
#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
pub(crate) fn smoke_size_ok(width: i32, height: i32, min_width: i32, min_height: i32) -> bool {
    width >= min_width && height >= min_height
}

/// Did the OS clipboard round-trip `written` back byte-for-byte? Pure
/// comparison, factored out so the pass/fail rule is unit-testable
/// without a real clipboard.
#[cfg(any(
    feature = "gtk",
    all(feature = "win", any(target_os = "windows", test))
))]
pub(crate) fn smoke_clipboard_round_trip_ok(written: &str, read_back: Option<&str>) -> bool {
    read_back == Some(written)
}

// ─────────────────────────────────────────────────────────────────────
// Paste-keypress predicate
// ─────────────────────────────────────────────────────────────────────

/// Which single platform modifier a backend's clipboard-paste chord
/// treats as native — Ctrl on Linux/GTK and Windows, Cmd (⌘) on macOS.
/// [`crate::Modifiers::cmd`] is the crate-wide abstraction for "this
/// platform's OS-level modifier key" — but that shared abstraction means
/// `modifiers.cmd` can be held on *any* backend (Super on Linux, the
/// Windows key on Win32), not only on macOS. [`is_paste_keypress`] takes
/// this enum so each backend tells the predicate which one of `ctrl`/`cmd`
/// is *its* paste modifier, instead of the predicate accepting either
/// interchangeably — see D-011 in `docs/decisions/DECISIONS.md` for the
/// regression this fixes (plain Super+V used to trigger paste on GTK, and
/// plain Ctrl+V used to trigger paste on macOS, neither of which is that
/// platform's actual convention).
///
/// `#[allow(dead_code)]`: each variant is constructed in production code
/// by exactly one backend (`Ctrl` by TUI/GTK/Windows via
/// [`crate::runtime::preprocess_event`], `Cmd` by macOS via the same
/// function's `PreprocessBackend::paste_modifier` override), and this
/// module compiles under `any(tui, gtk, macos, win)` — the same
/// single-backend feature-gate shape `win::run::dispatch_event`'s own
/// `#[allow(dead_code)]` documents. Building with only one of those
/// features active (e.g. the `ubuntu-latest` `--features win` leg, which
/// has no `tui`/`gtk`/`macos`) so only one variant is ever constructed
/// outside `#[cfg(test)]` would otherwise trip `-D warnings`' dead-code
/// lint on the other variant — despite both being genuinely constructed
/// on the CI legs where their respective backend feature is active, and
/// both being exercised unconditionally by `is_paste_keypress_tests`
/// below, which is what actually proves out D-011 §4's contract for
/// every backend on any host.
#[cfg(any(
    feature = "tui",
    feature = "gtk",
    all(feature = "macos", target_os = "macos"),
    feature = "win"
))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub(crate) enum PasteModifier {
    /// Ctrl-V / Ctrl-Shift-V — TUI, GTK (Linux), and Win32.
    Ctrl,
    /// Cmd-V / Cmd-Shift-V (⌘V / ⌘⇧V) — macOS.
    Cmd,
}

/// Is `key`+`modifiers` this platform's clipboard-paste shortcut?
///
/// One predicate for every desktop backend; `native` names which
/// platform modifier the *calling* backend treats as "paste" — GTK and
/// `win` both pass [`PasteModifier::Ctrl`], macOS passes
/// [`PasteModifier::Cmd`]. Only that modifier is accepted: the *other*
/// one must be explicitly absent, not merely ignored, so e.g. plain
/// Super+V (GTK, where `modifiers.cmd` is Super) and plain Ctrl+V
/// (macOS) are both rejected exactly as they were before this predicate
/// was lifted out of `gtk::run`/inlined in `macos::run` — see D-011 §4
/// in `docs/decisions/DECISIONS.md` for why "either modifier alone" was wrong.
/// `alt` is never a valid paste chord on any backend.
///
/// `shift` is deliberately **not** checked either way — Ctrl-Shift-V and
/// (by the same reasoning, extended here) Cmd-Shift-V both trigger paste
/// too. This extends the shift-tolerant contract to every adopter —
/// including macOS, which used to require `shift: false` in its inline
/// Cmd-V match guard before this predicate replaced it. See D-011 in
/// `docs/decisions/DECISIONS.md` for why this settles the contract once
/// instead of letting each new adopter silently pick its own.
///
/// Case-insensitive on the letter (`'v'` or `'V'`) on every backend.
#[cfg(any(
    feature = "tui",
    feature = "gtk",
    all(feature = "macos", target_os = "macos"),
    feature = "win"
))]
pub(crate) fn is_paste_keypress(
    key: &crate::Key,
    modifiers: &crate::Modifiers,
    native: PasteModifier,
) -> bool {
    let (native_held, other_held) = match native {
        PasteModifier::Ctrl => (modifiers.ctrl, modifiers.cmd),
        PasteModifier::Cmd => (modifiers.cmd, modifiers.ctrl),
    };
    matches!(key, crate::Key::Char('v') | crate::Key::Char('V'))
        && native_held
        && !other_held
        && !modifiers.alt
}

// ─────────────────────────────────────────────────────────────────────
// PointerShape enum-walk scaffold
// ─────────────────────────────────────────────────────────────────────

/// Every [`crate::backend::ResizeEdge`] variant, in a stable order.
/// Backends building a `ResizeEdge` → native-surface-edge or →
/// cursor-name table (and the tests that exercise it exhaustively)
/// iterate this instead of hand-listing all 8 arms — see e.g.
/// `gtk::backend::resize_edge_to_surface_edge_maps_every_variant`.
/// `#[cfg(test)]`: every consumer today is a mapping-table *test* (a
/// production `set_cursor`/`begin_window_resize` match handles one
/// `ResizeEdge` at a time and never needs the full list) — see the
/// module-doc note on why every item here is gated.
#[cfg(all(
    test,
    any(
        feature = "gtk",
        all(feature = "macos", target_os = "macos"),
        feature = "win"
    )
))]
pub(crate) const ALL_RESIZE_EDGES: [crate::backend::ResizeEdge; 8] = [
    crate::backend::ResizeEdge::North,
    crate::backend::ResizeEdge::South,
    crate::backend::ResizeEdge::East,
    crate::backend::ResizeEdge::West,
    crate::backend::ResizeEdge::NorthEast,
    crate::backend::ResizeEdge::NorthWest,
    crate::backend::ResizeEdge::SouthEast,
    crate::backend::ResizeEdge::SouthWest,
];

/// Every [`crate::backend::PointerShape`] variant, in a stable order —
/// [`crate::backend::PointerShape::Default`] plus one
/// [`crate::backend::PointerShape::Resize`] per [`ALL_RESIZE_EDGES`]
/// edge (9 total). Each backend still owns its own `PointerShape` →
/// native cursor table (cursor names/objects are inherently native —
/// GTK's are CSS cursor-name strings, AppKit's are `NSCursor` objects,
/// and AppKit in particular has no public diagonal-resize cursor, so
/// backends may legitimately map more than one `PointerShape` to the
/// same native cursor) — this is the enum-walk *scaffold* that was
/// missing: a canonical list backends' own mapping tests iterate
/// against instead of hand-duplicating the variant list per backend.
#[cfg(all(
    test,
    any(
        feature = "gtk",
        all(feature = "macos", target_os = "macos"),
        feature = "win"
    )
))]
pub(crate) fn all_pointer_shapes() -> [crate::backend::PointerShape; 9] {
    use crate::backend::PointerShape;
    [
        PointerShape::Default,
        PointerShape::Resize(ALL_RESIZE_EDGES[0]),
        PointerShape::Resize(ALL_RESIZE_EDGES[1]),
        PointerShape::Resize(ALL_RESIZE_EDGES[2]),
        PointerShape::Resize(ALL_RESIZE_EDGES[3]),
        PointerShape::Resize(ALL_RESIZE_EDGES[4]),
        PointerShape::Resize(ALL_RESIZE_EDGES[5]),
        PointerShape::Resize(ALL_RESIZE_EDGES[6]),
        PointerShape::Resize(ALL_RESIZE_EDGES[7]),
    ]
}

// ─────────────────────────────────────────────────────────────────────
// Caught-panic reporting dedup
// ─────────────────────────────────────────────────────────────────────
//
// `macos::run`'s `drawRect:`/responder-method bodies and `win::run`'s
// `wndproc` both hand a Rust callback directly to a C ABI that cannot
// unwind — an uncaught panic there escalates straight to `abort()`
// instead of staying a recoverable panic. Both bracket every such body —
// or, for `win::run`, the shared `dispatch_event`/`render_frame` helpers
// every message ultimately funnels through — in
// `std::panic::catch_unwind(AssertUnwindSafe(..))` and report a caught
// panic through [`report_caught_panic_once`] rather than calling
// `crate::diagnostics::emit` directly, so a rasteriser gap re-entered on
// every redraw is reported once per process, not once per frame.
#[cfg(any(feature = "win", all(feature = "macos", target_os = "macos")))]
pub(crate) fn report_caught_panic_once(site: &str, payload: &(dyn std::any::Any + Send)) {
    static SEEN: std::sync::OnceLock<std::sync::Mutex<std::collections::HashSet<String>>> =
        std::sync::OnceLock::new();
    let mut seen = SEEN
        .get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    // Dedup key is `site` alone, not `site` + message: the goal is "don't
    // spam the same reachable panic on every repaint," not "distinguish
    // every possible payload string" — a `todo!()` at a fixed call site
    // always carries the same message anyway.
    if !seen.insert(site.to_string()) {
        return;
    }
    drop(seen);

    let message = panic_payload_message(payload);
    crate::diagnostics::emit(format!(
        "quadraui: panic caught at {site} — frame/event skipped, process kept alive ({message})"
    ));
}

/// Best-effort text for a `catch_unwind` payload. `Any` gives us nothing
/// beyond a downcast; the two shapes `panic!`/`todo!`/`unwrap()` actually
/// produce are a `&'static str` literal or a `String` from a `format!`ed
/// message, so those are the only two cases worth naming.
#[cfg(any(feature = "win", all(feature = "macos", target_os = "macos")))]
fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
    if let Some(s) = payload.downcast_ref::<&str>() {
        (*s).to_string()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        "<non-string panic payload>".to_string()
    }
}

#[cfg(all(
    test,
    any(feature = "win", all(feature = "macos", target_os = "macos"))
))]
mod panic_report_tests {
    use super::*;
    use std::sync::{Arc, Mutex};

    /// Distinct `site` per test — the dedup registry is a process-global
    /// static shared by every test in this binary, so a colliding label
    /// between tests (or a re-run of the same test) would make the
    /// second call a silent no-op and the assertion below flaky.
    fn unique_site(tag: &str) -> String {
        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
        let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        format!("desktop::tests::{tag}#{n}")
    }

    /// Installs a recording sink and returns it. Callers must hold
    /// [`crate::diagnostics::test_guard`] for the duration — the sink
    /// slot is a process-global shared with `diagnostics.rs`'s own tests.
    fn install_recording_sink() -> Arc<Mutex<Vec<String>>> {
        let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
        let received_in_sink = Arc::clone(&received);
        crate::diagnostics::set_sink(move |msg: &str| {
            received_in_sink.lock().unwrap().push(msg.to_string());
        });
        received
    }

    #[test]
    fn first_report_at_a_site_reaches_the_sink() {
        let _g = crate::diagnostics::test_guard();
        let site = unique_site("first_report");
        let received = install_recording_sink();
        let payload: Box<dyn std::any::Any + Send> = Box::new("boom");
        report_caught_panic_once(&site, payload.as_ref());
        crate::diagnostics::clear_sink();
        assert_eq!(received.lock().unwrap().len(), 1);
        assert!(received.lock().unwrap()[0].contains(&site));
        assert!(received.lock().unwrap()[0].contains("boom"));
    }

    #[test]
    fn repeated_reports_at_the_same_site_are_reported_once() {
        let _g = crate::diagnostics::test_guard();
        let site = unique_site("repeated_reports");
        let received = install_recording_sink();
        for _ in 0..5 {
            let payload: Box<dyn std::any::Any + Send> = Box::new("same panic every frame");
            report_caught_panic_once(&site, payload.as_ref());
        }
        crate::diagnostics::clear_sink();
        assert_eq!(
            received.lock().unwrap().len(),
            1,
            "five panics at the same site must produce exactly one report, \
             not one per frame"
        );
    }

    #[test]
    fn distinct_sites_each_report_independently() {
        let _g = crate::diagnostics::test_guard();
        let site_a = unique_site("distinct_a");
        let site_b = unique_site("distinct_b");
        let received = install_recording_sink();
        let payload_a: Box<dyn std::any::Any + Send> = Box::new("a");
        let payload_b: Box<dyn std::any::Any + Send> = Box::new("b");
        report_caught_panic_once(&site_a, payload_a.as_ref());
        report_caught_panic_once(&site_b, payload_b.as_ref());
        crate::diagnostics::clear_sink();
        assert_eq!(received.lock().unwrap().len(), 2);
    }

    #[test]
    fn string_payload_message_is_included() {
        let _g = crate::diagnostics::test_guard();
        let site = unique_site("string_payload");
        let received = install_recording_sink();
        let payload: Box<dyn std::any::Any + Send> = Box::new(format!("formatted {}", 42));
        report_caught_panic_once(&site, payload.as_ref());
        crate::diagnostics::clear_sink();
        assert!(received.lock().unwrap()[0].contains("formatted 42"));
    }

    #[test]
    fn non_string_payload_does_not_panic_the_reporter() {
        let _g = crate::diagnostics::test_guard();
        let site = unique_site("non_string_payload");
        let received = install_recording_sink();
        let payload: Box<dyn std::any::Any + Send> = Box::new(42i32);
        report_caught_panic_once(&site, payload.as_ref());
        crate::diagnostics::clear_sink();
        assert_eq!(received.lock().unwrap().len(), 1);
    }
}

#[cfg(all(
    test,
    any(feature = "gtk", all(feature = "macos", target_os = "macos"))
))]
mod window_drag_arm_tests {
    use super::*;

    #[test]
    fn new_is_unarmed() {
        let arm: WindowDragArm<i32> = WindowDragArm::new();
        assert!(!arm.is_armed());
        assert!(arm.origin().is_none());
    }

    #[test]
    fn default_is_unarmed() {
        let arm: WindowDragArm<i32> = WindowDragArm::default();
        assert!(!arm.is_armed());
    }

    #[test]
    fn arm_records_press_and_origin() {
        let mut arm = WindowDragArm::new();
        arm.arm(42, 10.0, 20.0);
        assert!(arm.is_armed());
        assert_eq!(arm.origin(), Some((10.0, 20.0)));
    }

    #[test]
    fn arm_overwrites_a_previous_unconsumed_request() {
        let mut arm = WindowDragArm::new();
        arm.arm(1, 0.0, 0.0);
        arm.arm(2, 5.0, 5.0);
        assert_eq!(arm.origin(), Some((5.0, 5.0)));
        assert_eq!(arm.take(), Some(2));
    }

    #[test]
    fn discard_clears_state() {
        let mut arm = WindowDragArm::new();
        arm.arm(1, 0.0, 0.0);
        arm.discard();
        assert!(!arm.is_armed());
        assert!(arm.origin().is_none());
    }

    #[test]
    fn take_consumes_and_clears() {
        let mut arm = WindowDragArm::new();
        arm.arm("press", 0.0, 0.0);
        assert_eq!(arm.take(), Some("press"));
        assert!(!arm.is_armed());
        assert_eq!(arm.take(), None);
    }

    #[test]
    fn commit_if_past_threshold_none_when_unarmed() {
        let mut arm: WindowDragArm<i32> = WindowDragArm::new();
        assert_eq!(arm.commit_if_past_threshold(100.0, 100.0, 8.0), None);
    }

    #[test]
    fn commit_if_past_threshold_none_under_threshold_and_stays_armed() {
        let mut arm = WindowDragArm::new();
        arm.arm("press", 0.0, 0.0);
        // 3-4-5 triangle: distance 5, under an 8px threshold.
        assert_eq!(arm.commit_if_past_threshold(3.0, 4.0, 8.0), None);
        assert!(arm.is_armed(), "still under threshold: must stay armed");
    }

    #[test]
    fn commit_if_past_threshold_takes_at_exactly_the_threshold() {
        let mut arm = WindowDragArm::new();
        arm.arm("press", 0.0, 0.0);
        assert_eq!(arm.commit_if_past_threshold(8.0, 0.0, 8.0), Some("press"));
        assert!(!arm.is_armed());
    }

    #[test]
    fn commit_if_past_threshold_takes_past_threshold() {
        let mut arm = WindowDragArm::new();
        arm.arm("press", 10.0, 10.0);
        // 6-8-10 triangle: distance 10 from origin (10, 10) to (16, 18).
        assert_eq!(arm.commit_if_past_threshold(16.0, 18.0, 8.0), Some("press"));
        assert!(!arm.is_armed());
    }
}

#[cfg(all(test, any(feature = "gtk", feature = "win")))]
mod modal_pump_tests {
    use super::*;

    #[test]
    fn new_starts_at_zero() {
        let depth = ModalPumpDepth::new();
        assert_eq!(depth.get(), 0);
        assert!(!depth.is_pumping());
    }

    #[test]
    fn default_starts_at_zero() {
        let depth = ModalPumpDepth::default();
        assert_eq!(depth.get(), 0);
    }

    #[test]
    fn guard_increments_while_held_and_decrements_on_drop() {
        let depth = ModalPumpDepth::new();
        {
            let _guard = ModalPumpGuard::new(&depth);
            assert_eq!(depth.get(), 1);
            assert!(depth.is_pumping());
        }
        assert_eq!(depth.get(), 0);
        assert!(!depth.is_pumping());
    }

    #[test]
    fn nested_guards_stay_pumping_until_the_outermost_drops() {
        let depth = ModalPumpDepth::new();
        let outer = ModalPumpGuard::new(&depth);
        assert_eq!(depth.get(), 1);
        {
            let _inner = ModalPumpGuard::new(&depth);
            assert_eq!(depth.get(), 2);
        }
        assert_eq!(depth.get(), 1, "inner drop must not clear the outer guard");
        assert!(depth.is_pumping());
        drop(outer);
        assert_eq!(depth.get(), 0);
    }

    #[test]
    fn cloned_handle_observes_the_same_counter() {
        let depth = ModalPumpDepth::new();
        let handle = depth.clone();
        let _guard = ModalPumpGuard::new(&depth);
        assert_eq!(
            handle.get(),
            1,
            "clone must share the underlying Rc<Cell<>>"
        );
    }
}

#[cfg(all(test, any(feature = "gtk", feature = "win")))]
mod smoke_config_tests {
    use super::*;
    use std::env;
    use std::sync::Mutex;

    // `env::set_var`/`remove_var` are process-global; serialise this
    // module's tests against each other so they don't race (matches the
    // pattern `gtk::run`'s own smoke tests didn't need, since those only
    // tested the pure predicates — `from_env` itself is new coverage
    // here). Shared by both the GTK and `win` adopters — the env
    // var names below are test-local sentinels, not either backend's real
    // `QUADRAUI_*_SMOKE_MS` names, so there's no cross-backend collision
    // risk running both features' tests in the same process.
    static ENV_LOCK: Mutex<()> = Mutex::new(());

    #[test]
    fn from_env_none_when_ms_var_unset() {
        let _lock = ENV_LOCK.lock().unwrap();
        let ms_var = "QUADRAUI_DESKTOP_TEST_SMOKE_MS_UNSET";
        let paste_var = "QUADRAUI_DESKTOP_TEST_SMOKE_PASTE_UNSET";
        env::remove_var(ms_var);
        env::remove_var(paste_var);
        assert_eq!(SmokeConfig::from_env(ms_var, paste_var), None);
    }

    #[test]
    fn from_env_none_when_ms_var_unparseable() {
        let _lock = ENV_LOCK.lock().unwrap();
        let ms_var = "QUADRAUI_DESKTOP_TEST_SMOKE_MS_BAD";
        env::set_var(ms_var, "not-a-number");
        assert_eq!(
            SmokeConfig::from_env(ms_var, "QUADRAUI_DESKTOP_TEST_UNSET"),
            None
        );
        env::remove_var(ms_var);
    }

    #[test]
    fn from_env_reads_ms_and_optional_paste() {
        let _lock = ENV_LOCK.lock().unwrap();
        let ms_var = "QUADRAUI_DESKTOP_TEST_SMOKE_MS_OK";
        let paste_var = "QUADRAUI_DESKTOP_TEST_SMOKE_PASTE_OK";
        env::set_var(ms_var, "250");
        env::set_var(paste_var, "hello");
        assert_eq!(
            SmokeConfig::from_env(ms_var, paste_var),
            Some(SmokeConfig {
                after_ms: 250,
                paste_text: Some("hello".to_string()),
            })
        );
        env::remove_var(ms_var);
        env::remove_var(paste_var);
    }

    #[test]
    fn from_env_paste_none_when_paste_var_unset() {
        let _lock = ENV_LOCK.lock().unwrap();
        let ms_var = "QUADRAUI_DESKTOP_TEST_SMOKE_MS_NOPASTE";
        let paste_var = "QUADRAUI_DESKTOP_TEST_SMOKE_PASTE_NOPASTE_UNSET";
        env::remove_var(paste_var);
        env::set_var(ms_var, "10");
        assert_eq!(
            SmokeConfig::from_env(ms_var, paste_var),
            Some(SmokeConfig {
                after_ms: 10,
                paste_text: None,
            })
        );
        env::remove_var(ms_var);
    }

    #[test]
    fn smoke_size_ok_accepts_at_or_above_the_floor() {
        assert!(smoke_size_ok(200, 150, 200, 150));
        assert!(smoke_size_ok(800, 600, 200, 150));
    }

    #[test]
    fn smoke_size_ok_rejects_below_the_floor() {
        assert!(!smoke_size_ok(199, 150, 200, 150));
        assert!(!smoke_size_ok(200, 149, 200, 150));
        // Content wrapped into an ~8px-wide column.
        assert!(!smoke_size_ok(8, 600, 200, 150));
    }

    #[test]
    fn clipboard_round_trip_ok_when_read_back_matches() {
        assert!(smoke_clipboard_round_trip_ok(
            "quadraui smoke",
            Some("quadraui smoke")
        ));
    }

    #[test]
    fn clipboard_round_trip_rejects_a_missing_or_mismatched_read() {
        assert!(!smoke_clipboard_round_trip_ok("quadraui smoke", None));
        assert!(!smoke_clipboard_round_trip_ok(
            "quadraui smoke",
            Some("something else")
        ));
    }
}

#[cfg(all(
    test,
    any(
        feature = "tui",
        feature = "gtk",
        all(feature = "macos", target_os = "macos"),
        feature = "win"
    )
))]
mod is_paste_keypress_tests {
    //! Coverage for [`is_paste_keypress`] — the single predicate
    //! `tui::run`, `gtk::run`, `macos::run`, and `win::run`'s
    //! `dispatch_event`s all now call (via
    //! `crate::runtime::preprocess_event`), each backend supplying its
    //! own [`PasteModifier`]. Pure/display-
    //! free: no live backend needed to exercise every branch of the
    //! contract D-011 (`docs/decisions/DECISIONS.md`) records.
    use super::*;
    use crate::{Key, Modifiers};

    fn mods(ctrl: bool, shift: bool, alt: bool, cmd: bool) -> Modifiers {
        Modifiers {
            ctrl,
            shift,
            alt,
            cmd,
        }
    }

    #[test]
    fn plain_ctrl_v_is_a_paste_keypress_for_ctrl_native_backends() {
        // GTK (Linux) and Win32.
        assert!(is_paste_keypress(
            &Key::Char('v'),
            &mods(true, false, false, false),
            PasteModifier::Ctrl
        ));
        assert!(is_paste_keypress(
            &Key::Char('V'),
            &mods(true, false, false, false),
            PasteModifier::Ctrl
        ));
    }

    #[test]
    fn plain_cmd_v_is_a_paste_keypress_for_cmd_native_backends() {
        // macOS — `cmd` is the crate-wide abstraction for the platform
        // OS-modifier key (⌘ on macOS).
        assert!(is_paste_keypress(
            &Key::Char('v'),
            &mods(false, false, false, true),
            PasteModifier::Cmd
        ));
    }

    #[test]
    fn plain_cmd_v_is_not_a_paste_keypress_for_ctrl_native_backends() {
        // D-011 §4 regression check: on GTK, `modifiers.cmd` is Super
        // (SUPER_MASK/META_MASK — see `gtk::events`), and on `win` it's
        // the Windows key. Plain Super+V / Win+V must NOT trigger paste
        // just because *some* backend's native modifier is held — GTK's
        // pre-lift `is_paste_keypress` never accepted it, and adopting
        // the shared predicate must not silently start accepting it.
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(false, false, false, true),
            PasteModifier::Ctrl
        ));
    }

    #[test]
    fn plain_ctrl_v_is_not_a_paste_keypress_for_cmd_native_backends() {
        // D-011 §4 regression check: macOS's pre-lift inline Cmd-V guard
        // required `ctrl: false` exactly, so plain Ctrl-V never triggered
        // paste on macOS. The shared predicate must preserve that.
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(true, false, false, false),
            PasteModifier::Cmd
        ));
    }

    #[test]
    fn ctrl_shift_v_is_a_paste_keypress_for_ctrl_native_backends() {
        // Some terminal emulators reserve plain Ctrl-V for
        // a control byte and use Ctrl-Shift-V for paste instead.
        assert!(is_paste_keypress(
            &Key::Char('v'),
            &mods(true, true, false, false),
            PasteModifier::Ctrl
        ));
    }

    #[test]
    fn cmd_shift_v_is_a_paste_keypress_for_cmd_native_backends() {
        // D-011: shift tolerance: Cmd-Shift-V also triggers paste on macOS.
        assert!(is_paste_keypress(
            &Key::Char('v'),
            &mods(false, true, false, true),
            PasteModifier::Cmd
        ));
    }

    #[test]
    fn ctrl_alt_v_is_not_a_paste_keypress() {
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(true, false, true, false),
            PasteModifier::Ctrl
        ));
    }

    #[test]
    fn cmd_alt_v_is_not_a_paste_keypress() {
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(false, false, true, true),
            PasteModifier::Cmd
        ));
    }

    #[test]
    fn both_ctrl_and_cmd_is_not_a_paste_keypress() {
        // The other platform modifier must be explicitly absent, not
        // merely not-required — holding both is rejected regardless of
        // which one is native to the backend.
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(true, false, false, true),
            PasteModifier::Ctrl
        ));
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(true, false, false, true),
            PasteModifier::Cmd
        ));
    }

    #[test]
    fn shift_v_alone_is_not_a_paste_keypress() {
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &mods(false, true, false, false),
            PasteModifier::Ctrl
        ));
    }

    #[test]
    fn plain_v_is_not_a_paste_keypress() {
        assert!(!is_paste_keypress(
            &Key::Char('v'),
            &Modifiers::default(),
            PasteModifier::Ctrl
        ));
    }

    #[test]
    fn ctrl_c_is_not_a_paste_keypress() {
        assert!(!is_paste_keypress(
            &Key::Char('c'),
            &mods(true, false, false, false),
            PasteModifier::Ctrl
        ));
    }
}

#[cfg(all(
    test,
    any(
        feature = "gtk",
        all(feature = "macos", target_os = "macos"),
        feature = "win"
    )
))]
mod pointer_shape_scaffold_tests {
    use super::*;
    use crate::backend::{PointerShape, ResizeEdge};

    #[test]
    fn all_resize_edges_has_every_variant_exactly_once() {
        let mut seen: Vec<ResizeEdge> = ALL_RESIZE_EDGES.to_vec();
        seen.sort_by_key(|e| format!("{e:?}"));
        seen.dedup();
        assert_eq!(
            seen.len(),
            ALL_RESIZE_EDGES.len(),
            "ALL_RESIZE_EDGES must list every ResizeEdge variant exactly once"
        );
    }

    #[test]
    fn all_pointer_shapes_is_default_plus_one_resize_per_edge() {
        let shapes = all_pointer_shapes();
        assert_eq!(shapes.len(), ALL_RESIZE_EDGES.len() + 1);
        assert_eq!(shapes[0], PointerShape::Default);
        for (shape, edge) in shapes[1..].iter().zip(ALL_RESIZE_EDGES.iter()) {
            assert_eq!(*shape, PointerShape::Resize(*edge));
        }
    }
}