freedesktop_file_parser/
structs.rs

1//! This module provides types for parsing and representing Linux `.desktop` files according to the
2//! [freedesktop.org Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/).
3//! Desktop entries are used to describe applications, shortcuts, and directories in desktop environments.
4
5use std::fmt::Display;
6use std::{collections::HashMap, path::PathBuf, str::FromStr};
7use thiserror::Error;
8
9/// A string that can have different values based on the system locale.
10/// Used for internationalization of desktop entries.
11#[derive(Debug, Clone, Default)]
12pub struct LocaleString {
13    /// The default value when no locale-specific variant is available
14    pub default: String,
15    /// Map of locale codes to translated strings
16    pub variants: HashMap<String, String>,
17}
18
19impl LocaleString {
20    /// Get the variant of the locale string, returns the default value if not found
21    pub fn get_variant(&self, locale: &str) -> &str {
22        match self.variants.get(locale) {
23            Some(v) => v,
24            None => &self.default,
25        }
26    }
27}
28
29/// A list of strings that can vary based on the system locale.
30/// Used for internationalized lists like keywords.
31#[derive(Debug, Clone, Default)]
32pub struct LocaleStringList {
33    /// The default list when no locale-specific variant is available
34    pub default: Vec<String>,
35    /// Map of locale codes to translated string lists
36    pub variants: HashMap<String, Vec<String>>,
37}
38
39impl LocaleStringList {
40    /// Get the variant of the locale string, returns the default value if not found
41    pub fn get_variant(&self, locale: &str) -> &[String] {
42        match self.variants.get(locale) {
43            Some(v) => v,
44            None => &self.default,
45        }
46    }
47}
48
49/// Represents an icon specification that can be either a file path
50/// or an icon name from the system theme.
51#[derive(Debug, Clone, Default)]
52pub struct IconString {
53    /// The icon specification string
54    pub content: String,
55}
56
57impl IconString {
58    /// Attempts to find the icon's full path on the system.
59    ///
60    /// First checks if the content is a direct path to an existing file.
61    /// If not, looks up the icon name in the freedesktop icon theme system.
62    ///
63    /// # Returns
64    /// - `Some(PathBuf)` if the icon is found either as a file or in the icon theme
65    /// - `None` if the icon cannot be found
66    pub fn get_icon_path(&self) -> Option<PathBuf> {
67        if std::fs::read(&self.content).is_ok() {
68            Some(self.content.clone().into())
69        } else {
70            freedesktop_icons::lookup(&self.content)
71                .with_size(48)
72                .with_scale(1)
73                .find()
74        }
75    }
76}
77
78/// Fields specific to Application type desktop entries.
79/// These fields are only valid when the entry type is Application.
80#[derive(Debug, Clone, Default)]
81pub struct ApplicationFields {
82    /// Path to an executable file on disk used to determine if the program is actually installed. If the path is not an absolute path, the file is looked up in the $PATH environment variable. If the file is not present or if it is not executable, the entry may be ignored (not be used in menus, for example).
83    pub try_exec: Option<String>,
84    /// Program to execute, possibly with arguments. See the Exec key for details on how this key works. The Exec key is required if DBusActivatable is not set to true. Even if DBusActivatable is true, Exec should be specified for compatibility with implementations that do not understand DBusActivatable.
85    pub exec: Option<String>,
86    /// If entry is of type Application, the working directory to run the program in.
87    pub path: Option<String>,
88    /// Whether the program runs in a terminal window.
89    pub terminal: Option<bool>,
90    /// Identifiers for application actions. This can be used to tell the application to make a specific action, different from the default behavior. The Application actions section describes how actions work.
91    pub actions: Option<Vec<String>>,
92    /// The MIME type(s) supported by this application.
93    pub mime_type: Option<Vec<String>>,
94    /// Categories in which the entry should be shown in a menu (for possible values see the Desktop Menu Specification).
95    pub categories: Option<Vec<String>>,
96    /// A list of interfaces that this application implements. By default, a desktop file implements no interfaces. See Interfaces for more information on how this works.
97    pub implements: Option<Vec<String>>,
98    /// A list of strings which may be used in addition to other metadata to describe this entry. This can be useful e.g. to facilitate searching through entries. The values are not meant for display, and should not be redundant with the values of Name or GenericName.
99    pub keywords: Option<LocaleStringList>,
100    /// If true, it is KNOWN that the application will send a "remove" message when started with the DESKTOP_STARTUP_ID environment variable set. If false, it is KNOWN that the application does not work with startup notification at all (does not shown any window, breaks even when using StartupWMClass, etc.). If absent, a reasonable handling is up to implementations (assuming false, using StartupWMClass, etc.). (See the [Startup Notification Protocol Specification](https://www.freedesktop.org/wiki/Specifications/startup-notification-spec/) for more details).
101    pub startup_notify: Option<bool>,
102    /// If specified, it is known that the application will map at least one window with the given string as its WM class or WM name hint (see the [Startup Notification Protocol Specification](https://www.freedesktop.org/wiki/Specifications/startup-notification-spec/) for more details).
103    pub startup_wm_class: Option<String>,
104    /// If true, the application prefers to be run on a more powerful discrete GPU if available, which we describe as “a GPU other than the default one” in this spec to avoid the need to define what a discrete GPU is and in which cases it might be considered more powerful than the default GPU. This key is only a hint and support might not be present depending on the implementation.
105    pub prefers_non_default_gpu: Option<bool>,
106    /// If true, the application has a single main window, and does not support having an additional one opened. This key is used to signal to the implementation to avoid offering a UI to launch another window of the app. This key is only a hint and support might not be present depending on the implementation.
107    pub single_main_window: Option<bool>,
108}
109
110/// Fields specific to Link type desktop entries.
111/// These fields are only valid when the entry type is Link.
112#[derive(Debug, Clone, Default)]
113pub struct LinkFields {
114    /// The URL that this desktop entry points to
115    pub url: String,
116}
117
118/// The type of desktop entry, which determines its behavior and required fields.
119#[derive(Debug, Clone, Default)]
120// Clippy suggests using Box<ApplicationFields> for Application instead
121// but this would break compatibility, so we disable the warning.
122#[allow(clippy::large_enum_variant)]
123pub enum EntryType {
124    /// An application that can be launched
125    Application(ApplicationFields),
126    /// A URL shortcut
127    Link(LinkFields),
128    /// A directory entry, typically used in menus
129    Directory,
130    /// An unknown or unsupported type
131    #[default]
132    Unknown,
133}
134
135impl FromStr for EntryType {
136    type Err = ();
137
138    /// Converts a string to an EntryType.
139    /// Never fails as unknown types become EntryType::Unknown.
140    fn from_str(s: &str) -> Result<Self, Self::Err> {
141        Ok(Self::from(s))
142    }
143}
144
145impl From<&str> for EntryType {
146    /// Converts a string to an EntryType.
147    /// Recognizes "Application", "Link", and "Directory".
148    /// Any other value becomes EntryType::Unknown.
149    fn from(value: &str) -> Self {
150        match value {
151            "Application" => Self::Application(ApplicationFields::default()),
152            "Link" => Self::Link(LinkFields::default()),
153            "Directory" => Self::Directory,
154            _ => Self::Unknown,
155        }
156    }
157}
158
159impl Display for EntryType {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        let str = match self {
162            Self::Application(_) => "Application",
163            Self::Link(_) => "Link",
164            Self::Directory => "Directory",
165            Self::Unknown => "Unknown",
166        };
167        write!(f, "{str}")
168    }
169}
170
171/// Represents a complete desktop entry, containing all the standard fields
172/// defined in the freedesktop.org specification.
173#[derive(Debug, Clone, Default)]
174pub struct DesktopEntry {
175    /// This specification defines 3 types of desktop entries: Application (type 1), Link (type 2) and Directory (type 3). To allow the addition of new types in the future, implementations should ignore desktop entries with an unknown type.
176    pub entry_type: EntryType, // required
177    /// Version of the Desktop Entry Specification that the desktop entry conforms with. Entries that confirm with this version of the specification should use 1.5. Note that the version field is not required to be present.
178    pub version: Option<String>,
179    /// Specific name of the application, for example "Mozilla".
180    pub name: LocaleString, // required
181    /// Generic name of the application, for example "Web Browser".
182    pub generic_name: Option<LocaleString>,
183    /// NoDisplay means "this application exists, but don't display it in the menus". This can be useful to e.g. associate this application with MIME types, so that it gets launched from a file manager (or other apps), without having a menu entry for it (there are tons of good reasons for this, including e.g. the netscape -remote, or kfmclient openURL kind of stuff).
184    pub no_display: Option<bool>,
185    /// Tooltip for the entry, for example "View sites on the Internet". The value should not be redundant with the values of Name and GenericName.
186    pub comment: Option<LocaleString>,
187    /// Icon to display in file manager, menus, etc. If the name is an absolute path, the given file will be used. If the name is not an absolute path, the algorithm described in the Icon Theme Specification will be used to locate the icon.
188    pub icon: Option<IconString>,
189    /// Hidden should have been called Deleted. It means the user deleted (at their level) something that was present (at an upper level, e.g. in the system dirs). It's strictly equivalent to the .desktop file not existing at all, as far as that user is concerned. This can also be used to "uninstall" existing files (e.g. due to a renaming) - by letting make install install a file with Hidden=true in it.
190    pub hidden: Option<bool>,
191    /// A list of strings identifying the desktop environments that should display/not display a given desktop entry.
192    /// By default, a desktop file should be shown, unless an OnlyShowIn key is present, in which case, the default is for the file not to be shown.
193    /// If $XDG_CURRENT_DESKTOP is set then it contains a colon-separated list of strings. In order, each string is considered. If a matching entry is found in OnlyShowIn then the desktop file is shown. If an entry is found in NotShowIn then the desktop file is not shown. If none of the strings match then the default action is taken (as above).
194    /// $XDG_CURRENT_DESKTOP should have been set by the login manager, according to the value of the DesktopNames found in the session file. The entry in the session file has multiple values separated in the usual way: with a semicolon.
195    /// The same desktop name may not appear in both OnlyShowIn and NotShowIn of a group.
196    pub only_show_in: Option<Vec<String>>,
197    /// A list of strings identifying the desktop environments that should display/not display a given desktop entry.
198    /// By default, a desktop file should be shown, unless an OnlyShowIn key is present, in which case, the default is for the file not to be shown.
199    /// If $XDG_CURRENT_DESKTOP is set then it contains a colon-separated list of strings. In order, each string is considered. If a matching entry is found in OnlyShowIn then the desktop file is shown. If an entry is found in NotShowIn then the desktop file is not shown. If none of the strings match then the default action is taken (as above).
200    /// $XDG_CURRENT_DESKTOP should have been set by the login manager, according to the value of the DesktopNames found in the session file. The entry in the session file has multiple values separated in the usual way: with a semicolon.
201    /// The same desktop name may not appear in both OnlyShowIn and NotShowIn of a group.
202    pub not_show_in: Option<Vec<String>>,
203    /// A boolean value specifying if D-Bus activation is supported for this application. If this key is missing, the default value is false. If the value is true then implementations should ignore the Exec key and send a D-Bus message to launch the application. See D-Bus Activation for more information on how this works. Applications should still include Exec= lines in their desktop files for compatibility with implementations that do not understand the DBusActivatable key.
204    pub dbus_activatable: Option<bool>,
205}
206
207/// Represents an application action, which defines an alternative way
208/// to launch an application with different parameters.
209///
210/// Actions are defined in the desktop file and allow applications to expose
211/// multiple entry points, such as "New Window" or "Private Browsing".
212#[derive(Default, Clone, Debug)]
213pub struct DesktopAction {
214    /// The name of the action, which can be localized
215    pub name: LocaleString,
216    /// The command to execute when this action is triggered
217    pub exec: Option<String>,
218    /// Optional icon specific to this action
219    pub icon: Option<IconString>,
220}
221
222/// Represents a complete desktop file including the main entry
223/// and all its associated actions.
224#[derive(Default, Clone, Debug)]
225pub struct DesktopFile {
226    /// The main desktop entry
227    pub entry: DesktopEntry,
228    /// Map of action identifiers to their definitions
229    pub actions: HashMap<String, DesktopAction>,
230}
231
232#[derive(Debug, Clone, Error)]
233pub enum ParseError {
234    #[error("Parse Error: Unacceptable character {ch:?} at line {row:?} column {col:?}, message: {msg:?}")]
235    UnacceptableCharacter {
236        ch: String,
237        row: usize,
238        col: usize,
239        msg: String,
240    },
241    #[error("Parse Error: Syntax error at line {row:?} column {col:?}, message: {msg:?}")]
242    Syntax { msg: String, row: usize, col: usize },
243    #[error("Parse Error: Repetitive entry at line {row:?} column {col:?}, message: {msg:?}. There should be only one entry on top of the file")]
244    RepetitiveEntry { msg: String, row: usize, col: usize },
245    #[error("Parse Error: Format error at line {row:?} column {col:?}, message: {msg:?}. The first heaedr should only be about an entry")]
246    FormatError { msg: String, row: usize, col: usize },
247    #[error("Parse Error: Internal error at line {row:?} column {col:?}, message: {msg:?}")]
248    InternalError { msg: String, row: usize, col: usize },
249    #[error("Parse Error: Repetitive declaration of key {key:?} and of entry or action at line {row:?} column {col:?}")]
250    RepetitiveKey { key: String, row: usize, col: usize },
251    #[error("Parse Error: Key Error, message: {msg:?}")]
252    KeyError { msg: String },
253}