Skip to main content

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}