pixelactions_core/display.rs
1//! Which display server this session is actually running.
2//!
3//! On macOS and Windows the windowing system is a compile-time fact. On
4//! Linux it is not: the same binary on the same machine faces X11 or
5//! Wayland depending on which session the user logged into, and the two
6//! need different injection paths entirely.
7//!
8//! Getting this wrong is not a crash, it is worse. Injecting through
9//! `XWayland` on a Wayland session reaches X clients only, so the pointer
10//! travels over native windows that never receive the events — a run that
11//! clicks through *some* windows and not others, reporting success either
12//! way. So the answer is decided once, here, from the environment, and a
13//! session we cannot name is refused rather than assumed.
14//!
15//! The environment is passed in rather than read here: this crate is
16//! platform-free, and a decision that takes its inputs as arguments is a
17//! decision that can be tested against every session shape without
18//! needing that session.
19
20use serde::Serialize;
21
22/// The display server a Linux session is running.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "lowercase")]
25pub enum Server {
26 Wayland,
27 X11,
28 /// Nothing in the environment identifies a session — the usual case
29 /// being a plain TTY, a container, or a cron job with no desktop.
30 Unknown,
31}
32
33impl Server {
34 pub fn name(self) -> &'static str {
35 match self {
36 Self::Wayland => "wayland",
37 Self::X11 => "x11",
38 Self::Unknown => "unknown",
39 }
40 }
41}
42
43/// Decide from the three variables that describe a Linux session.
44///
45/// `XDG_SESSION_TYPE` is the authority when it says something meaningful,
46/// because it is what the login session itself declares. The socket
47/// variables are the fallback, and `WAYLAND_DISPLAY` is checked first on
48/// purpose: a Wayland session almost always *also* sets `DISPLAY`, for
49/// `XWayland`. Trusting `DISPLAY` first would misread nearly every modern
50/// desktop as X11 — which is exactly the half-working failure above.
51pub fn detect(
52 session_type: Option<&str>,
53 wayland_display: Option<&str>,
54 x_display: Option<&str>,
55) -> Server {
56 let declared = session_type.map(str::trim).unwrap_or_default();
57 match declared.to_ascii_lowercase().as_str() {
58 "wayland" => return Server::Wayland,
59 "x11" => return Server::X11,
60 _ => {}
61 }
62 if present(wayland_display) {
63 return Server::Wayland;
64 }
65 if present(x_display) {
66 return Server::X11;
67 }
68 Server::Unknown
69}
70
71/// A variable set to the empty string is not set, as far as a socket name
72/// is concerned. Shells produce this often enough to matter.
73fn present(value: Option<&str>) -> bool {
74 value.is_some_and(|value| !value.trim().is_empty())
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn a_declared_session_type_wins() {
83 assert_eq!(detect(Some("wayland"), None, None), Server::Wayland);
84 assert_eq!(detect(Some("x11"), None, None), Server::X11);
85 // Even when the other variable disagrees, which is the normal
86 // case on a Wayland desktop running XWayland.
87 assert_eq!(
88 detect(Some("wayland"), Some("wayland-0"), Some(":0")),
89 Server::Wayland
90 );
91 assert_eq!(detect(Some("x11"), None, Some(":0")), Server::X11);
92 }
93
94 #[test]
95 fn case_and_whitespace_in_the_declaration_are_tolerated() {
96 assert_eq!(detect(Some("Wayland"), None, None), Server::Wayland);
97 assert_eq!(detect(Some(" X11 "), None, None), Server::X11);
98 }
99
100 /// The variable exists but says something else — `tty`, on a console
101 /// login. The sockets decide from there.
102 #[test]
103 fn an_unhelpful_declaration_falls_through_to_the_sockets() {
104 assert_eq!(
105 detect(Some("tty"), Some("wayland-0"), None),
106 Server::Wayland
107 );
108 assert_eq!(detect(Some("tty"), None, Some(":0")), Server::X11);
109 assert_eq!(detect(Some("tty"), None, None), Server::Unknown);
110 }
111
112 /// The case this ordering exists for: both sockets set, which is what
113 /// every GNOME and KDE Wayland session looks like.
114 #[test]
115 fn wayland_wins_over_a_leftover_x_display() {
116 assert_eq!(detect(None, Some("wayland-0"), Some(":0")), Server::Wayland);
117 }
118
119 #[test]
120 fn nothing_set_is_unknown_rather_than_a_guess() {
121 assert_eq!(detect(None, None, None), Server::Unknown);
122 }
123
124 #[test]
125 fn an_empty_variable_is_not_a_session() {
126 assert_eq!(detect(Some(""), Some(""), Some("")), Server::Unknown);
127 assert_eq!(detect(None, Some(" "), Some(":0")), Server::X11);
128 }
129
130 #[test]
131 fn every_server_has_a_name() {
132 assert_eq!(Server::Wayland.name(), "wayland");
133 assert_eq!(Server::X11.name(), "x11");
134 assert_eq!(Server::Unknown.name(), "unknown");
135 }
136}