ignis_applications/lib.rs
1use pyo3::prelude::*;
2
3/// Access desktop application entries defined according to the [XDG Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry/latest).
4///
5/// ### Example
6///
7/// ```python
8/// from ignis_applications import ApplicationService
9///
10/// service = ApplicationService()
11///
12/// # display names of all applications
13/// for i in service.apps:
14/// print(i.name)
15///
16/// # Fuzzy search by name
17/// firefox = service.search_by_name("firfx")
18///
19/// # Launch application
20/// firefox.launch()
21///
22/// # See actions
23///
24/// for action in firefox.actions:
25/// print(action.name)
26///
27/// # Launch action
28/// # action.launch()
29/// ```
30#[pymodule]
31mod ignis_applications {
32 use pyo3::prelude::*;
33
34 use applications::{
35 ActionHandle, ApplicationService as RApplicationService, DesktopAppHandle, Error as RError,
36 };
37
38 use pyo3::exceptions::{PyOSError, PyValueError};
39
40 fn to_py_err(e: RError) -> PyErr {
41 let msg = e.to_string();
42
43 match e {
44 RError::NotifyError(_) => PyOSError::new_err(msg),
45 RError::IOError(_) => PyOSError::new_err(msg),
46 RError::ExecEmpty => PyValueError::new_err(msg),
47 }
48 }
49
50 /// A desktop application action.
51 #[pyclass]
52 struct Action {
53 inner: ActionHandle,
54 }
55
56 #[pymethods]
57 impl Action {
58 /// Launches the action.
59 fn launch(&self) -> PyResult<()> {
60 self.inner.launch().map_err(to_py_err)
61 }
62
63 /// The name of the action.
64 ///
65 /// For example: `Launch in new window`.
66 #[getter]
67 fn name(&self) -> String {
68 self.inner.name()
69 }
70
71 /// The localized name of the action.
72 #[getter]
73 fn name_locale(&self) -> String {
74 self.inner.name_locale()
75 }
76
77 /// The icon of the action.
78 #[getter]
79 fn icon(&self) -> Option<String> {
80 self.inner.icon()
81 }
82
83 /// The localized icon of the action.
84 #[getter]
85 fn icon_locale(&self) -> Option<String> {
86 self.inner.icon_locale()
87 }
88
89 /// The exec string of the action.
90 #[getter]
91 fn exec(&self) -> Option<String> {
92 self.inner.exec()
93 }
94 }
95
96 /// A desktop application.
97 #[pyclass]
98 struct DesktopApp {
99 inner: DesktopAppHandle,
100 }
101
102 #[pymethods]
103 impl DesktopApp {
104 /// Launches the application based on the [`exec`][exec] string.
105 ///
106 /// Starts a default terminal window if [`terminal`][terminal] is `true`.
107 ///
108 /// The launched child process is detached from this process.
109 fn launch(&self) -> PyResult<()> {
110 self.inner.launch().map_err(to_py_err)
111 }
112
113 /// The unique ID of the application.
114 #[getter]
115 pub fn app_id(&self) -> String {
116 self.inner.app_id()
117 }
118
119 /// The name of the application.
120 ///
121 /// For example: `firefox`.
122 #[getter]
123 fn name(&self) -> String {
124 self.inner.name()
125 }
126
127 /// The localized name of the application.
128 #[getter]
129 pub fn name_locale(&self) -> String {
130 self.inner.name_locale()
131 }
132
133 /// The generic name of the application.
134 ///
135 /// For example: `Web browser`.
136 #[getter]
137 pub fn generic_name(&self) -> Option<String> {
138 self.inner.generic_name()
139 }
140
141 /// The localized generic name of the application.
142 #[getter]
143 pub fn generic_name_locale(&self) -> Option<String> {
144 self.inner.generic_name_locale()
145 }
146
147 /// The icon of the application.
148 ///
149 /// It's either the name of the icon or the absolute path.
150 #[getter]
151 pub fn icon(&self) -> Option<String> {
152 self.inner.icon()
153 }
154
155 /// The localized icon of the application.
156 #[getter]
157 pub fn icon_locale(&self) -> Option<String> {
158 self.inner.icon_locale()
159 }
160
161 /// A list of keywords describing the application.
162 #[getter]
163 pub fn keywords(&self) -> Vec<String> {
164 self.inner.keywords()
165 }
166
167 /// A list of localized keywords describing the application.
168 #[getter]
169 pub fn keywords_locale(&self) -> Vec<String> {
170 self.inner.keywords_locale()
171 }
172
173 /// The string containing the program to execute, possibly with arguments.
174 #[getter]
175 pub fn exec(&self) -> Option<String> {
176 self.inner.exec()
177 }
178
179 /// Whether the program should run in a terminal window.
180 #[getter]
181 pub fn terminal(&self) -> bool {
182 self.inner.terminal()
183 }
184
185 /// A list of application actions. Can be empty.
186 #[getter]
187 pub fn actions(&self) -> Vec<Action> {
188 self.inner
189 .actions()
190 .into_iter()
191 .map(|handle| Action { inner: handle })
192 .collect()
193 }
194 }
195
196 /// A service to access desktop applications.
197 /// It loads all application entries from `XDG_DATA_DIRS` and gets the system locale.
198 #[pyclass]
199 struct ApplicationService {
200 inner: RApplicationService,
201 }
202
203 #[pymethods]
204 impl ApplicationService {
205 #[new]
206 fn new() -> Self {
207 Self {
208 inner: RApplicationService::new(),
209 }
210 }
211
212 /// Starts watching for changes in application entries. Re-initializes apps if a change occurs.
213 fn watch(&self) -> PyResult<()> {
214 self.inner.watch().map_err(to_py_err)?;
215 Ok(())
216 }
217
218 /// A list of applications.
219 #[getter]
220 fn apps(&self) -> Vec<DesktopApp> {
221 self.inner
222 .apps()
223 .into_iter()
224 .map(|handle| DesktopApp { inner: handle })
225 .collect()
226 }
227
228 /// An application by its ID, or `None` if it is not found.
229 fn get_app_by_id(&self, app_id: &str) -> Option<DesktopApp> {
230 Some(DesktopApp {
231 inner: self.inner.app_by_id(app_id)?,
232 })
233 }
234
235 /// Fuzzily search through the application entries by provided application name.
236 fn search_by_name(&self, query: &str) -> Vec<DesktopApp> {
237 self.inner
238 .search_by_name(query)
239 .into_iter()
240 .map(|handle| DesktopApp { inner: handle })
241 .collect()
242 }
243
244 /// Invoke a callback when application list changes.
245 ///
246 /// ## Example
247 ///
248 /// ```python
249 /// from ignis_applications import ApplicationService
250 ///
251 /// service = ApplicationService()
252 /// service.watch()
253 ///
254 /// # You can try to install/remove some program on your system
255 /// # and "refreshed" will be printed
256 /// service.on_apps_refreshed(lambda: print("refreshed!"))
257 /// ```
258 fn on_apps_refreshed(&self, callback: Py<PyAny>) {
259 self.inner.on_apps_refreshed.connect(move |_| {
260 Python::attach(|py| {
261 if let Err(e) = callback.call0(py) {
262 e.print(py)
263 }
264 });
265 });
266 }
267 }
268}