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}