Skip to main content

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}