linux_windowing.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  2. pub enum Backend {
  3. Auto,
  4. Wayland,
  5. X11,
  6. }
  7. #[derive(Debug, Clone, PartialEq, Eq)]
  8. pub struct BackendDecision {
  9. pub backend: Backend,
  10. pub note: String,
  11. }
  12. #[derive(Debug, Clone, Default)]
  13. pub struct SessionEnv {
  14. pub wayland_display: bool,
  15. pub xdg_session_type: Option<String>,
  16. pub display: bool,
  17. pub xdg_current_desktop: Option<String>,
  18. pub xdg_session_desktop: Option<String>,
  19. pub desktop_session: Option<String>,
  20. pub oc_allow_wayland: Option<String>,
  21. pub oc_force_x11: Option<String>,
  22. pub oc_force_wayland: Option<String>,
  23. pub oc_linux_decorations: Option<String>,
  24. pub oc_force_decorations: Option<String>,
  25. pub oc_no_decorations: Option<String>,
  26. pub i3_sock: bool,
  27. }
  28. impl SessionEnv {
  29. pub fn capture() -> Self {
  30. Self {
  31. wayland_display: std::env::var_os("WAYLAND_DISPLAY").is_some(),
  32. xdg_session_type: std::env::var("XDG_SESSION_TYPE").ok(),
  33. display: std::env::var_os("DISPLAY").is_some(),
  34. xdg_current_desktop: std::env::var("XDG_CURRENT_DESKTOP").ok(),
  35. xdg_session_desktop: std::env::var("XDG_SESSION_DESKTOP").ok(),
  36. desktop_session: std::env::var("DESKTOP_SESSION").ok(),
  37. oc_allow_wayland: std::env::var("OC_ALLOW_WAYLAND").ok(),
  38. oc_force_x11: std::env::var("OC_FORCE_X11").ok(),
  39. oc_force_wayland: std::env::var("OC_FORCE_WAYLAND").ok(),
  40. oc_linux_decorations: std::env::var("OC_LINUX_DECORATIONS").ok(),
  41. oc_force_decorations: std::env::var("OC_FORCE_DECORATIONS").ok(),
  42. oc_no_decorations: std::env::var("OC_NO_DECORATIONS").ok(),
  43. i3_sock: std::env::var_os("I3SOCK").is_some(),
  44. }
  45. }
  46. }
  47. pub fn select_backend(env: &SessionEnv, prefer_wayland: bool) -> Option<BackendDecision> {
  48. if is_truthy(env.oc_force_x11.as_deref()) {
  49. return Some(BackendDecision {
  50. backend: Backend::X11,
  51. note: "Forcing X11 due to OC_FORCE_X11=1".into(),
  52. });
  53. }
  54. if is_truthy(env.oc_force_wayland.as_deref()) {
  55. return Some(BackendDecision {
  56. backend: Backend::Wayland,
  57. note: "Forcing native Wayland due to OC_FORCE_WAYLAND=1".into(),
  58. });
  59. }
  60. if !is_wayland_session(env) {
  61. return None;
  62. }
  63. if prefer_wayland {
  64. return Some(BackendDecision {
  65. backend: Backend::Wayland,
  66. note: "Wayland session detected; forcing native Wayland from settings".into(),
  67. });
  68. }
  69. if is_truthy(env.oc_allow_wayland.as_deref()) {
  70. return Some(BackendDecision {
  71. backend: Backend::Wayland,
  72. note: "Wayland session detected; forcing native Wayland due to OC_ALLOW_WAYLAND=1"
  73. .into(),
  74. });
  75. }
  76. Some(BackendDecision {
  77. backend: Backend::Auto,
  78. note: "Wayland session detected; using native Wayland first with X11 fallback (auto backend). Set OC_FORCE_X11=1 to force X11."
  79. .into(),
  80. })
  81. }
  82. pub fn use_decorations(env: &SessionEnv) -> bool {
  83. if let Some(mode) = decoration_override(env.oc_linux_decorations.as_deref()) {
  84. return match mode {
  85. DecorationOverride::Native => true,
  86. DecorationOverride::None => false,
  87. DecorationOverride::Auto => default_use_decorations(env),
  88. };
  89. }
  90. if is_truthy(env.oc_force_decorations.as_deref()) {
  91. return true;
  92. }
  93. if is_truthy(env.oc_no_decorations.as_deref()) {
  94. return false;
  95. }
  96. default_use_decorations(env)
  97. }
  98. fn default_use_decorations(env: &SessionEnv) -> bool {
  99. if is_known_tiling_session(env) {
  100. return false;
  101. }
  102. if !is_wayland_session(env) {
  103. return true;
  104. }
  105. is_full_desktop_session(env)
  106. }
  107. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  108. enum DecorationOverride {
  109. Auto,
  110. Native,
  111. None,
  112. }
  113. fn decoration_override(value: Option<&str>) -> Option<DecorationOverride> {
  114. let value = value?.trim().to_ascii_lowercase();
  115. if matches!(value.as_str(), "auto") {
  116. return Some(DecorationOverride::Auto);
  117. }
  118. if matches!(
  119. value.as_str(),
  120. "native" | "server" | "de" | "wayland" | "on" | "true" | "1"
  121. ) {
  122. return Some(DecorationOverride::Native);
  123. }
  124. if matches!(
  125. value.as_str(),
  126. "none" | "off" | "false" | "0" | "client" | "csd"
  127. ) {
  128. return Some(DecorationOverride::None);
  129. }
  130. None
  131. }
  132. fn is_truthy(value: Option<&str>) -> bool {
  133. matches!(
  134. value.map(|v| v.trim().to_ascii_lowercase()),
  135. Some(v) if matches!(v.as_str(), "1" | "true" | "yes" | "on")
  136. )
  137. }
  138. fn is_wayland_session(env: &SessionEnv) -> bool {
  139. env.wayland_display
  140. || matches!(
  141. env.xdg_session_type.as_deref(),
  142. Some(value) if value.eq_ignore_ascii_case("wayland")
  143. )
  144. }
  145. fn is_full_desktop_session(env: &SessionEnv) -> bool {
  146. desktop_tokens(env).any(|value| {
  147. matches!(
  148. value.as_str(),
  149. "gnome"
  150. | "kde"
  151. | "plasma"
  152. | "xfce"
  153. | "xfce4"
  154. | "x-cinnamon"
  155. | "cinnamon"
  156. | "mate"
  157. | "lxqt"
  158. | "budgie"
  159. | "pantheon"
  160. | "deepin"
  161. | "unity"
  162. | "cosmic"
  163. )
  164. })
  165. }
  166. fn is_known_tiling_session(env: &SessionEnv) -> bool {
  167. if env.i3_sock {
  168. return true;
  169. }
  170. desktop_tokens(env).any(|value| {
  171. matches!(
  172. value.as_str(),
  173. "niri"
  174. | "sway"
  175. | "swayfx"
  176. | "hyprland"
  177. | "river"
  178. | "i3"
  179. | "i3wm"
  180. | "bspwm"
  181. | "dwm"
  182. | "qtile"
  183. | "xmonad"
  184. | "leftwm"
  185. | "dwl"
  186. | "awesome"
  187. | "herbstluftwm"
  188. | "spectrwm"
  189. | "worm"
  190. | "i3-gnome"
  191. )
  192. })
  193. }
  194. fn desktop_tokens<'a>(env: &'a SessionEnv) -> impl Iterator<Item = String> + 'a {
  195. [
  196. env.xdg_current_desktop.as_deref(),
  197. env.xdg_session_desktop.as_deref(),
  198. env.desktop_session.as_deref(),
  199. ]
  200. .into_iter()
  201. .flatten()
  202. .flat_map(|desktop| desktop.split(':'))
  203. .map(|value| value.trim().to_ascii_lowercase())
  204. }
  205. #[cfg(test)]
  206. mod tests {
  207. use super::*;
  208. #[test]
  209. fn prefers_wayland_first_on_wayland_session() {
  210. let env = SessionEnv {
  211. wayland_display: true,
  212. display: true,
  213. ..Default::default()
  214. };
  215. let decision = select_backend(&env, false).expect("missing decision");
  216. assert_eq!(decision.backend, Backend::Auto);
  217. }
  218. #[test]
  219. fn force_x11_override_wins() {
  220. let env = SessionEnv {
  221. wayland_display: true,
  222. display: true,
  223. oc_force_x11: Some("1".into()),
  224. oc_allow_wayland: Some("1".into()),
  225. oc_force_wayland: Some("1".into()),
  226. ..Default::default()
  227. };
  228. let decision = select_backend(&env, true).expect("missing decision");
  229. assert_eq!(decision.backend, Backend::X11);
  230. }
  231. #[test]
  232. fn prefer_wayland_forces_wayland_backend() {
  233. let env = SessionEnv {
  234. wayland_display: true,
  235. display: true,
  236. ..Default::default()
  237. };
  238. let decision = select_backend(&env, true).expect("missing decision");
  239. assert_eq!(decision.backend, Backend::Wayland);
  240. }
  241. #[test]
  242. fn force_wayland_override_works_outside_wayland_session() {
  243. let env = SessionEnv {
  244. display: true,
  245. oc_force_wayland: Some("1".into()),
  246. ..Default::default()
  247. };
  248. let decision = select_backend(&env, false).expect("missing decision");
  249. assert_eq!(decision.backend, Backend::Wayland);
  250. }
  251. #[test]
  252. fn allow_wayland_forces_wayland_backend() {
  253. let env = SessionEnv {
  254. wayland_display: true,
  255. display: true,
  256. oc_allow_wayland: Some("1".into()),
  257. ..Default::default()
  258. };
  259. let decision = select_backend(&env, false).expect("missing decision");
  260. assert_eq!(decision.backend, Backend::Wayland);
  261. }
  262. #[test]
  263. fn xdg_session_type_wayland_is_detected() {
  264. let env = SessionEnv {
  265. xdg_session_type: Some("wayland".into()),
  266. ..Default::default()
  267. };
  268. let decision = select_backend(&env, false).expect("missing decision");
  269. assert_eq!(decision.backend, Backend::Auto);
  270. }
  271. #[test]
  272. fn returns_none_when_not_wayland_and_no_overrides() {
  273. let env = SessionEnv {
  274. display: true,
  275. xdg_current_desktop: Some("GNOME".into()),
  276. ..Default::default()
  277. };
  278. assert!(select_backend(&env, false).is_none());
  279. }
  280. #[test]
  281. fn prefer_wayland_setting_does_not_override_x11_session() {
  282. let env = SessionEnv {
  283. display: true,
  284. xdg_current_desktop: Some("GNOME".into()),
  285. ..Default::default()
  286. };
  287. assert!(select_backend(&env, true).is_none());
  288. }
  289. #[test]
  290. fn disables_decorations_on_niri() {
  291. let env = SessionEnv {
  292. xdg_current_desktop: Some("niri".into()),
  293. wayland_display: true,
  294. ..Default::default()
  295. };
  296. assert!(!use_decorations(&env));
  297. }
  298. #[test]
  299. fn keeps_decorations_on_gnome() {
  300. let env = SessionEnv {
  301. xdg_current_desktop: Some("GNOME".into()),
  302. wayland_display: true,
  303. ..Default::default()
  304. };
  305. assert!(use_decorations(&env));
  306. }
  307. #[test]
  308. fn disables_decorations_when_session_desktop_is_tiling() {
  309. let env = SessionEnv {
  310. xdg_session_desktop: Some("Hyprland".into()),
  311. wayland_display: true,
  312. ..Default::default()
  313. };
  314. assert!(!use_decorations(&env));
  315. }
  316. #[test]
  317. fn disables_decorations_for_unknown_wayland_session() {
  318. let env = SessionEnv {
  319. xdg_current_desktop: Some("labwc".into()),
  320. wayland_display: true,
  321. ..Default::default()
  322. };
  323. assert!(!use_decorations(&env));
  324. }
  325. #[test]
  326. fn disables_decorations_for_dwm_on_x11() {
  327. let env = SessionEnv {
  328. xdg_current_desktop: Some("dwm".into()),
  329. display: true,
  330. ..Default::default()
  331. };
  332. assert!(!use_decorations(&env));
  333. }
  334. #[test]
  335. fn disables_decorations_for_i3_on_x11() {
  336. let env = SessionEnv {
  337. xdg_current_desktop: Some("i3".into()),
  338. display: true,
  339. ..Default::default()
  340. };
  341. assert!(!use_decorations(&env));
  342. }
  343. #[test]
  344. fn disables_decorations_for_i3sock_without_xdg_tokens() {
  345. let env = SessionEnv {
  346. display: true,
  347. i3_sock: true,
  348. ..Default::default()
  349. };
  350. assert!(!use_decorations(&env));
  351. }
  352. #[test]
  353. fn keeps_decorations_for_gnome_on_x11() {
  354. let env = SessionEnv {
  355. xdg_current_desktop: Some("GNOME".into()),
  356. display: true,
  357. ..Default::default()
  358. };
  359. assert!(use_decorations(&env));
  360. }
  361. #[test]
  362. fn no_decorations_override_wins() {
  363. let env = SessionEnv {
  364. xdg_current_desktop: Some("GNOME".into()),
  365. oc_no_decorations: Some("1".into()),
  366. ..Default::default()
  367. };
  368. assert!(!use_decorations(&env));
  369. }
  370. #[test]
  371. fn linux_decorations_native_override_wins() {
  372. let env = SessionEnv {
  373. xdg_current_desktop: Some("niri".into()),
  374. wayland_display: true,
  375. oc_linux_decorations: Some("native".into()),
  376. ..Default::default()
  377. };
  378. assert!(use_decorations(&env));
  379. }
  380. #[test]
  381. fn linux_decorations_none_override_wins() {
  382. let env = SessionEnv {
  383. xdg_current_desktop: Some("GNOME".into()),
  384. wayland_display: true,
  385. oc_linux_decorations: Some("none".into()),
  386. ..Default::default()
  387. };
  388. assert!(!use_decorations(&env));
  389. }
  390. #[test]
  391. fn linux_decorations_auto_uses_default_policy() {
  392. let env = SessionEnv {
  393. xdg_current_desktop: Some("sway".into()),
  394. wayland_display: true,
  395. oc_linux_decorations: Some("auto".into()),
  396. ..Default::default()
  397. };
  398. assert!(!use_decorations(&env));
  399. }
  400. #[test]
  401. fn linux_decorations_override_beats_legacy_overrides() {
  402. let env = SessionEnv {
  403. xdg_current_desktop: Some("GNOME".into()),
  404. wayland_display: true,
  405. oc_linux_decorations: Some("none".into()),
  406. oc_force_decorations: Some("1".into()),
  407. ..Default::default()
  408. };
  409. assert!(!use_decorations(&env));
  410. }
  411. }