detect_desktop_environment/lib.rs
1#![deny(missing_docs)]
2//! Desktop environment detection
3//!
4//! This crate implements automatic detection for the current desktop environment.
5//!
6//! See [`DesktopEnvironment`] for supported desktop environments.
7//!
8//! The environment can be detected using [`DesktopEnvironment::detect`]:
9//!
10//! ```rust
11//! use detect_desktop_environment::DesktopEnvironment;
12//!
13//! match DesktopEnvironment::detect() {
14//! Some(de) => println!("detected desktop environment: {de:?}"),
15//! None => println!("failed to detect desktop environment"),
16//! }
17//! ```
18
19use core::fmt;
20
21/// Desktop environments supported by `detect-desktop-environment`.
22///
23/// This enum provides a best-effort implementation of [`fmt::Display`]. It
24/// prints the name of the desktop environment in English. The exact string
25/// is *NOT* guaranteed to be stable: it may be updated as part of a minor
26/// release.
27// If adding new environment, please keep them sorted alphabetically and use `PascalCase`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[non_exhaustive]
30pub enum DesktopEnvironment {
31 /// Cinnamon, the default desktop environment for Linux Mint.
32 ///
33 /// - <https://en.wikipedia.org/wiki/Cinnamon_(desktop_environment)>
34 Cinnamon,
35 /// COSMIC, the legacy GNOME-based desktop environment for Linux Pop!_OS.
36 ///
37 /// Note: This corresponds to the classic COSMIC based on GNOME. For the new
38 /// [COSMIC Epoch](https://github.com/pop-os/cosmic-epoch) desktop
39 /// environment built in Rust, use [`DesktopEnvironment::CosmicEpoch`].
40 ///
41 /// - <https://github.com/pop-os/cosmic>
42 Cosmic,
43 /// COSMIC Epoch
44 ///
45 /// Note: This corresponds to the new COSMIC desktop environment
46 /// built by System76 in Rust for Linux Pop!_OS.
47 ///
48 /// - <https://github.com/pop-os/cosmic-epoch>
49 CosmicEpoch,
50 /// Deepin desktop environment
51 ///
52 /// - <https://www.deepin.org/index/en>
53 Dde,
54 /// EDE Desktop
55 ///
56 /// - <https://edeproject.org/>
57 Ede,
58 /// Endless OS desktop
59 ///
60 /// - <https://www.endlessos.org/os>
61 Endless,
62 /// Enlightenment desktop environment.
63 ///
64 /// - <https://en.wikipedia.org/wiki/Enlightenment_(software)>
65 Enlightenment,
66 /// Gnome, the default environment for many major Linux distributions.
67 ///
68 /// - <https://en.wikipedia.org/wiki/GNOME>
69 Gnome,
70 /// Hyprland tiling window manager
71 ///
72 /// - <https://hyprland.org/>
73 Hyprland,
74 /// KDE Plasma, the Kool Desktop Environment.
75 ///
76 /// - <https://kde.org/plasma-desktop/>
77 Kde,
78 /// LXDE
79 ///
80 /// - <https://www.lxde.org/>
81 Lxde,
82 /// LXQt
83 ///
84 /// - <https://lxqt-project.org/>
85 Lxqt,
86 /// MacOs, the environment for Apple's OS
87 MacOs,
88 /// MATE
89 ///
90 /// - <https://mate-desktop.org/>
91 Mate,
92 /// Legacy menu systems
93 ///
94 /// Listed in [Freedesktop Desktop Environments](https://specifications.freedesktop.org/menu-spec/latest/apb.html).
95 // Please send a PR if you have more details or better ideas about how to handle this value.
96 Old,
97 /// Elementary OS Desktop Environment
98 ///
99 /// - <https://elementary.io/>
100 Pantheon,
101 /// Razor-qt
102 ///
103 /// Discontinued Desktop Environment, this is an ancestor of LXQt.
104 ///
105 ///
106 /// - <https://github.com/Razor-qt/razor-qt>
107 Razor,
108 /// ROX Desktop
109 ///
110 /// - <https://rox.sourceforge.net/desktop/>
111 Rox,
112 /// Sway tiling window manager
113 ///
114 /// - <https://swaywm.org/>
115 Sway,
116 /// TrinityDesktopEnvironment
117 ///
118 /// - <https://www.trinitydesktop.org/>
119 Tde,
120 /// Unity, the legacy desktop environment for Ubuntu
121 ///
122 /// - <https://en.wikipedia.org/wiki/Unity_%28user_interface%29>
123 Unity,
124 /// Windows, the environments for Microsoft's OS
125 Windows,
126 /// Xfce
127 ///
128 /// - <https://xfce.org/>
129 Xfce,
130}
131
132impl DesktopEnvironment {
133 /// Detect the current desktop environment
134 ///
135 /// If the current desktop environment can't be detected, `None` is returned.
136 pub fn detect() -> Option<Self> {
137 Self::detect_impl()
138 }
139
140 /// Test if the desktop environment is based on the GTK framework
141 ///
142 /// See <https://en.wikipedia.org/wiki/Category:Desktop_environments_based_on_GTK>
143 ///
144 /// ```
145 /// use detect_desktop_environment::DesktopEnvironment;
146 ///
147 /// // All matching desktop environments:
148 /// assert!(DesktopEnvironment::Cinnamon.gtk());
149 /// assert!(DesktopEnvironment::Cosmic.gtk());
150 /// assert!(DesktopEnvironment::Gnome.gtk());
151 /// assert!(DesktopEnvironment::Lxde.gtk());
152 /// assert!(DesktopEnvironment::Mate.gtk());
153 /// assert!(DesktopEnvironment::Unity.gtk());
154 /// assert!(DesktopEnvironment::Xfce.gtk());
155 /// assert!(DesktopEnvironment::Pantheon.gtk());
156 /// assert!(DesktopEnvironment::Dde.gtk());
157 ///
158 /// // Non-GTK examples
159 /// assert!(!DesktopEnvironment::Kde.gtk());
160 /// assert!(!DesktopEnvironment::Windows.gtk());
161 /// ```
162 pub const fn gtk(self) -> bool {
163 use DesktopEnvironment::*;
164 matches!(
165 self,
166 Cinnamon | Cosmic | Dde | Gnome | Lxde | Mate | Pantheon | Unity | Xfce
167 )
168 }
169
170 /// Test if the desktop environment is based on the Qt framework
171 ///
172 /// ```
173 /// use detect_desktop_environment::DesktopEnvironment;
174 ///
175 /// // All matching desktop environments:
176 /// assert!(DesktopEnvironment::Kde.qt());
177 /// assert!(DesktopEnvironment::Lxqt.qt());
178 /// assert!(DesktopEnvironment::Razor.qt());
179 /// assert!(DesktopEnvironment::Tde.qt());
180 ///
181 /// // Non-Qt examples
182 /// assert!(!DesktopEnvironment::Gnome.qt());
183 /// assert!(!DesktopEnvironment::Windows.qt());
184 /// ```
185 pub const fn qt(self) -> bool {
186 use DesktopEnvironment::*;
187 matches!(self, Kde | Lxqt | Razor | Tde)
188 }
189
190 #[cfg(target_os = "macos")]
191 fn detect_impl() -> Option<Self> {
192 Some(DesktopEnvironment::MacOs)
193 }
194
195 #[cfg(target_os = "windows")]
196 fn detect_impl() -> Option<Self> {
197 Some(DesktopEnvironment::Windows)
198 }
199
200 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
201 fn detect_impl() -> Option<Self> {
202 std::env::var("XDG_CURRENT_DESKTOP")
203 .ok()
204 .as_deref()
205 .and_then(Self::from_xdg_current_desktop)
206 }
207
208 /// Parse the desktop environment from the name registered with Freedesktop.org
209 ///
210 /// See <https://specifications.freedesktop.org/menu-spec/latest/apb.html>
211 ///
212 /// Returns `None` if the desktop is not registered.
213 ///
214 /// This function is strictly restricted to the DEs registered with Freedesktop, for a more
215 /// complete list use [`DesktopEnvironment::from_xdg_name`]. Note that the check follows the
216 /// spec and is case-sensitive.
217 ///
218 /// ```
219 /// use detect_desktop_environment::DesktopEnvironment;
220 ///
221 /// assert_eq!(Some(DesktopEnvironment::Kde), DesktopEnvironment::from_freedesktop("KDE"));
222 /// assert_eq!(None, DesktopEnvironment::from_freedesktop("kde")); // must be uppercase
223 /// assert_eq!(None, DesktopEnvironment::from_freedesktop("SWAY")); // not registered
224 /// assert_eq!(None, DesktopEnvironment::from_freedesktop("unknown_de"));
225 /// ```
226 pub fn from_freedesktop(name: &str) -> Option<Self> {
227 // the patterns in the match below are ordered to match the order in the freedesktop table
228 match name {
229 "COSMIC" => Some(DesktopEnvironment::CosmicEpoch),
230 "GNOME" => Some(DesktopEnvironment::Gnome),
231 "GNOME-Classic" => Some(DesktopEnvironment::Gnome),
232 "GNOME-Flashback" => Some(DesktopEnvironment::Gnome),
233 "KDE" => Some(DesktopEnvironment::Kde),
234 "LXDE" => Some(DesktopEnvironment::Lxde),
235 "LXQt" => Some(DesktopEnvironment::Lxqt),
236 "MATE" => Some(DesktopEnvironment::Mate),
237 "Razor" => Some(DesktopEnvironment::Razor),
238 "ROX" => Some(DesktopEnvironment::Rox),
239 "TDE" => Some(DesktopEnvironment::Tde),
240 "Unity" => Some(DesktopEnvironment::Unity),
241 "XFCE" => Some(DesktopEnvironment::Xfce),
242 "EDE" => Some(DesktopEnvironment::Ede),
243 "Cinnamon" => Some(DesktopEnvironment::Cinnamon),
244 "Pantheon" => Some(DesktopEnvironment::Pantheon),
245 "DDE" => Some(DesktopEnvironment::Dde),
246 "Endless" => Some(DesktopEnvironment::Endless),
247 "Old" => Some(DesktopEnvironment::Old),
248 _ => None,
249 }
250 }
251
252 /// Parse the XDG desktop environment name
253 ///
254 /// This is an extended variant of [`DesktopEnvironment::from_freedesktop`]. It supports all
255 /// registered Freedesktop names, as well as some extra unregistered names. This is the
256 /// recommended method to parse names from the list in the env var `XDG_CURRENT_DESKTOP`.
257 ///
258 /// Returns `None` if the name is unknown.
259 ///
260 /// ```
261 /// use detect_desktop_environment::DesktopEnvironment;
262 ///
263 /// assert_eq!(Some(DesktopEnvironment::Kde), DesktopEnvironment::from_xdg_name("KDE")); // freedesktop DE
264 /// assert_eq!(None, DesktopEnvironment::from_xdg_name("kde")); // must be uppercase
265 /// assert_eq!(Some(DesktopEnvironment::Sway), DesktopEnvironment::from_xdg_name("SWAY")); // not registered
266 /// assert_eq!(None, DesktopEnvironment::from_xdg_name("unknown_de"));
267 /// ```
268 pub fn from_xdg_name(name: &str) -> Option<Self> {
269 if let Some(de) = Self::from_freedesktop(name) {
270 return Some(de);
271 }
272
273 // keep the patterns sorted alphabetically
274 match name {
275 "ENLIGHTENMENT" => Some(DesktopEnvironment::Enlightenment),
276 "Hyprland" => Some(DesktopEnvironment::Hyprland),
277 "SWAY" | "sway" => Some(DesktopEnvironment::Sway),
278 "X-Cinnamon" => Some(DesktopEnvironment::Cinnamon),
279 _ => None,
280 }
281 }
282
283 /// Retrieve the desktop environment from the format used by `XDG_CURRENT_DESKTOP`.
284 ///
285 /// `XDG_CURRENT_DESKTOP` is a colon separated list of information about the current desktop
286 /// environment.
287 /// See: <https://specifications.freedesktop.org/mime-apps-spec/1.0.1/ar01s02.html>
288 ///
289 /// Returns `None` if the resolution fails.
290 /// Duplicate entries are allowed as long as they correspond to same Desktop Environment.
291 pub fn from_xdg_current_desktop(xdg_current_desktop: &str) -> Option<Self> {
292 let mut resolved: Option<DesktopEnvironment> = None;
293
294 for part in xdg_current_desktop.split(':') {
295 let de = match Self::from_xdg_name(part) {
296 Some(de) => de,
297 None => {
298 // We ignore parsing errors as we don't really control which values are possible.
299 // Some of the entries don't even represent a DE (e.g. `ubuntu:GNOME`, where `ubuntu` is
300 // a distro, not a DE)
301 // If you want more control over this, open an issue to discuss it.
302 continue;
303 }
304 };
305 match resolved {
306 None => {
307 // first successfully parsed DE, store it but keep iterating to check for conflicts
308 resolved = Some(de)
309 }
310 Some(prev) => {
311 // a DE was already parsed previously, duplicates are allowed but a conflict causes
312 // immediate rejection with `None`.
313 // If you want more control over this, open an issue to discuss it.
314 if de != prev {
315 return None;
316 }
317 }
318 }
319 }
320
321 resolved
322 }
323}
324
325impl fmt::Display for DesktopEnvironment {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 match self {
328 Self::Cinnamon => write!(f, "Cinnamon"),
329 Self::Cosmic => write!(f, "COSMIC"),
330 Self::CosmicEpoch => write!(f, "COSMIC Epoch"),
331 Self::Dde => write!(f, "Deepin"),
332 Self::Ede => write!(f, "EDE"),
333 Self::Endless => write!(f, "Endless"),
334 Self::Enlightenment => write!(f, "Enlightenment"),
335 Self::Gnome => write!(f, "GNOME"),
336 Self::Hyprland => write!(f, "Hyprland"),
337 Self::Kde => write!(f, "KDE Plasma"),
338 Self::Lxde => write!(f, "LXDE"),
339 Self::Lxqt => write!(f, "LXQt"),
340 Self::MacOs => write!(f, "macOS"),
341 Self::Mate => write!(f, "MATE"),
342 Self::Old => write!(f, "Old"),
343 Self::Pantheon => write!(f, "Pantheon"),
344 Self::Razor => write!(f, "Razor"),
345 Self::Rox => write!(f, "ROX"),
346 Self::Sway => write!(f, "Sway"),
347 Self::Tde => write!(f, "TDE"),
348 Self::Unity => write!(f, "Unity"),
349 Self::Windows => write!(f, "Windows"),
350 Self::Xfce => write!(f, "Xfce"),
351 }
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn linux_tests() {
361 // Cases without colon
362 assert_eq!(
363 DesktopEnvironment::from_xdg_current_desktop("Cinnamon"),
364 Some(DesktopEnvironment::Cinnamon)
365 );
366 assert_eq!(
367 DesktopEnvironment::from_xdg_current_desktop("ENLIGHTENMENT"),
368 Some(DesktopEnvironment::Enlightenment)
369 );
370 assert_eq!(
371 DesktopEnvironment::from_xdg_current_desktop("GNOME"),
372 Some(DesktopEnvironment::Gnome)
373 );
374 assert_eq!(
375 DesktopEnvironment::from_xdg_current_desktop("KDE"),
376 Some(DesktopEnvironment::Kde)
377 );
378 assert_eq!(
379 DesktopEnvironment::from_xdg_current_desktop("LXDE"),
380 Some(DesktopEnvironment::Lxde)
381 );
382 assert_eq!(
383 DesktopEnvironment::from_xdg_current_desktop("LXQt"),
384 Some(DesktopEnvironment::Lxqt)
385 );
386 assert_eq!(
387 DesktopEnvironment::from_xdg_current_desktop("MATE"),
388 Some(DesktopEnvironment::Mate)
389 );
390 assert_eq!(
391 DesktopEnvironment::from_xdg_current_desktop("Unity"),
392 Some(DesktopEnvironment::Unity)
393 );
394 assert_eq!(
395 DesktopEnvironment::from_xdg_current_desktop("X-Cinnamon"),
396 Some(DesktopEnvironment::Cinnamon)
397 );
398 assert_eq!(
399 DesktopEnvironment::from_xdg_current_desktop("XFCE"),
400 Some(DesktopEnvironment::Xfce)
401 );
402 assert_eq!(
403 DesktopEnvironment::from_xdg_current_desktop("TDE"),
404 Some(DesktopEnvironment::Tde)
405 );
406 assert_eq!(
407 DesktopEnvironment::from_xdg_current_desktop("DDE"),
408 Some(DesktopEnvironment::Dde)
409 );
410 assert_eq!(
411 DesktopEnvironment::from_xdg_current_desktop("Pantheon"),
412 Some(DesktopEnvironment::Pantheon)
413 );
414 assert_eq!(
415 DesktopEnvironment::from_xdg_current_desktop("SWAY"),
416 Some(DesktopEnvironment::Sway)
417 );
418 assert_eq!(
419 DesktopEnvironment::from_xdg_current_desktop("Hyprland"),
420 Some(DesktopEnvironment::Hyprland)
421 );
422 assert_eq!(
423 DesktopEnvironment::from_xdg_current_desktop("COSMIC"),
424 Some(DesktopEnvironment::CosmicEpoch)
425 );
426
427 // Colon splitting
428 assert_eq!(
429 DesktopEnvironment::from_xdg_current_desktop("ubuntu:GNOME"),
430 Some(DesktopEnvironment::Gnome)
431 );
432 assert_eq!(
433 DesktopEnvironment::from_xdg_current_desktop("ubuntu:KDE"),
434 Some(DesktopEnvironment::Kde)
435 );
436 assert_eq!(
437 DesktopEnvironment::from_xdg_current_desktop("pop:GNOME"),
438 Some(DesktopEnvironment::Gnome)
439 );
440
441 // Mixed messages
442 assert_eq!(DesktopEnvironment::from_xdg_current_desktop("KDE:GNOME"), None);
443 assert_eq!(DesktopEnvironment::from_xdg_current_desktop("ubuntu:KDE:GNOME"), None);
444
445 // Strange cases
446 assert_eq!(
447 DesktopEnvironment::from_xdg_current_desktop("GNOME:GNOME"),
448 Some(DesktopEnvironment::Gnome)
449 );
450
451 // Empty string
452 assert_eq!(DesktopEnvironment::from_xdg_current_desktop(""), None);
453
454 // Unknown Desktop Environment
455 assert_eq!(DesktopEnvironment::from_xdg_current_desktop("foo"), None);
456 }
457}