system_tray/
menu.rs

1use crate::dbus::dbus_menu_proxy::{MenuLayout, PropertiesUpdate, UpdatedProps};
2use crate::error::{Error, Result};
3use serde::Deserialize;
4use std::collections::HashMap;
5use zbus::zvariant::{Array, OwnedValue, Structure, Value};
6
7/// A menu that should be displayed when clicking corresponding tray icon
8#[derive(Debug, Clone)]
9pub struct TrayMenu {
10    /// The unique identifier of the menu
11    pub id: u32,
12    /// A recursive list of submenus
13    pub submenus: Vec<MenuItem>,
14}
15
16/// List of properties taken from:
17/// <https://github.com/AyatanaIndicators/libdbusmenu/blob/4d03141aea4e2ad0f04ab73cf1d4f4bcc4a19f6c/libdbusmenu-glib/dbus-menu.xml#L75>
18#[derive(Debug, Clone, Deserialize, Default)]
19pub struct MenuItem {
20    /// Unique numeric id
21    pub id: i32,
22
23    /// Either a standard menu item or a separator [`MenuType`]
24    pub menu_type: MenuType,
25    /// Text of the item, except that:
26    ///  - two consecutive underscore characters "__" are displayed as a
27    ///    single underscore,
28    ///  - any remaining underscore characters are not displayed at all,
29    ///  - the first of those remaining underscore characters (unless it is
30    ///    the last character in the string) indicates that the following
31    ///    character is the access key.
32    pub label: Option<String>,
33    /// Whether the item can be activated or not.
34    pub enabled: bool,
35    /// True if the item is visible in the menu.
36    pub visible: bool,
37    /// Icon name of the item, following the freedesktop.org icon spec.
38    pub icon_name: Option<String>,
39    /// PNG data of the icon.
40    pub icon_data: Option<Vec<u8>>,
41    /// The shortcut of the item. Each array represents the key press
42    /// in the list of keypresses. Each list of strings contains a list of
43    /// modifiers and then the key that is used. The modifier strings
44    /// allowed are: "Control", "Alt", "Shift" and "Super".
45    ///
46    /// - A simple shortcut like Ctrl+S is represented as:
47    ///   [["Control", "S"]]
48    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
49    ///   [["Control", "Q"], ["Alt", "X"]]
50    pub shortcut: Option<Vec<Vec<String>>>,
51    /// How the menuitem feels the information it's displaying to the
52    /// user should be presented.
53    /// See [`ToggleType`].
54    pub toggle_type: ToggleType,
55    /// Describe the current state of a "togglable" item.
56    /// See [`ToggleState`].
57    ///
58    /// # Note:
59    /// The implementation does not itself handle ensuring that only one
60    /// item in a radio group is set to "on", or that a group does not have
61    /// "on" and "indeterminate" items simultaneously; maintaining this
62    /// policy is up to the toolkit wrappers.
63    pub toggle_state: ToggleState,
64    /// If the menu item has children this property should be set to
65    /// "submenu".
66    pub children_display: Option<String>,
67    /// How the menuitem feels the information it's displaying to the
68    /// user should be presented.
69    /// See [`Disposition`]
70    pub disposition: Disposition,
71    /// Nested submenu items belonging to this item.
72    pub submenu: Vec<MenuItem>,
73}
74
75#[derive(Debug, Clone, Deserialize, Default)]
76pub struct MenuDiff {
77    pub id: i32,
78    pub update: MenuItemUpdate,
79    pub remove: Vec<String>,
80}
81
82#[derive(Debug, Clone, Deserialize, Default)]
83pub struct MenuItemUpdate {
84    /// Text of the item, except that:
85    ///  - two consecutive underscore characters "__" are displayed as a
86    ///    single underscore,
87    ///  - any remaining underscore characters are not displayed at all,
88    ///  - the first of those remaining underscore characters (unless it is
89    ///    the last character in the string) indicates that the following
90    ///    character is the access key.
91    pub label: Option<Option<String>>,
92    /// Whether the item can be activated or not.
93    pub enabled: Option<bool>,
94    /// True if the item is visible in the menu.
95    pub visible: Option<bool>,
96    /// Icon name of the item, following the freedesktop.org icon spec.
97    pub icon_name: Option<Option<String>>,
98    /// PNG data of the icon.
99    pub icon_data: Option<Option<Vec<u8>>>,
100    /// Describe the current state of a "togglable" item.
101    /// See [`ToggleState`].
102    ///
103    /// # Note:
104    /// The implementation does not itself handle ensuring that only one
105    /// item in a radio group is set to "on", or that a group does not have
106    /// "on" and "indeterminate" items simultaneously; maintaining this
107    /// policy is up to the toolkit wrappers.
108    pub toggle_state: Option<ToggleState>,
109    /// How the menuitem feels the information it's displaying to the
110    /// user should be presented.
111    /// See [`Disposition`]
112    pub disposition: Option<Disposition>,
113}
114
115#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
116pub enum MenuType {
117    ///  a separator
118    Separator,
119    /// an item which can be clicked to trigger an action or show another menu
120    #[default]
121    Standard,
122}
123
124impl From<&str> for MenuType {
125    fn from(value: &str) -> Self {
126        match value {
127            "separator" => Self::Separator,
128            _ => Self::default(),
129        }
130    }
131}
132
133#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
134pub enum ToggleType {
135    /// Item is an independent togglable item
136    Checkmark,
137    /// Item is part of a group where only one item can be
138    /// toggled at a time
139    Radio,
140    /// Item cannot be toggled
141    #[default]
142    CannotBeToggled,
143}
144
145impl From<&str> for ToggleType {
146    fn from(value: &str) -> Self {
147        match value {
148            "checkmark" => Self::Checkmark,
149            "radio" => Self::Radio,
150            _ => Self::default(),
151        }
152    }
153}
154
155/// Describe the current state of a "togglable" item.
156#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
157pub enum ToggleState {
158    /// This item is toggled
159    #[default]
160    On,
161    /// Item is not toggled
162    Off,
163    /// Item is not toggalble
164    Indeterminate,
165}
166
167impl From<i32> for ToggleState {
168    fn from(value: i32) -> Self {
169        match value {
170            0 => Self::Off,
171            1 => Self::On,
172            _ => Self::Indeterminate,
173        }
174    }
175}
176
177#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
178pub enum Disposition {
179    /// a standard menu item
180    #[default]
181    Normal,
182    /// providing additional information to the user
183    Informative,
184    ///  looking at potentially harmful results
185    Warning,
186    /// something bad could potentially happen
187    Alert,
188}
189
190impl From<&str> for Disposition {
191    fn from(value: &str) -> Self {
192        match value {
193            "informative" => Self::Informative,
194            "warning" => Self::Warning,
195            "alert" => Self::Alert,
196            _ => Self::default(),
197        }
198    }
199}
200
201impl TryFrom<MenuLayout> for TrayMenu {
202    type Error = Error;
203
204    fn try_from(value: MenuLayout) -> Result<Self> {
205        let submenus = value
206            .fields
207            .submenus
208            .iter()
209            .map(MenuItem::try_from)
210            .collect::<std::result::Result<_, _>>()?;
211
212        Ok(Self {
213            id: value.id,
214            submenus,
215        })
216    }
217}
218
219impl TryFrom<&OwnedValue> for MenuItem {
220    type Error = Error;
221
222    fn try_from(value: &OwnedValue) -> Result<Self> {
223        let structure = value.downcast_ref::<&Structure>()?;
224
225        let mut fields = structure.fields().iter();
226
227        // defaults for enabled/visible are true
228        // and setting here avoids having to provide a full `Default` impl
229        let mut menu = MenuItem {
230            enabled: true,
231            visible: true,
232            ..Default::default()
233        };
234
235        if let Some(Value::I32(id)) = fields.next() {
236            menu.id = *id;
237        }
238
239        if let Some(Value::Dict(dict)) = fields.next() {
240            menu.children_display = dict
241                .get::<&str, &str>(&"children-display")?
242                .map(str::to_string);
243
244            // see: https://github.com/gnustep/libs-dbuskit/blob/4dc9b56216e46e0e385b976b0605b965509ebbbd/Bundles/DBusMenu/com.canonical.dbusmenu.xml#L76
245            menu.label = dict
246                .get::<&str, &str>(&"label")?
247                .map(|label| label.replace('_', ""));
248
249            if let Some(enabled) = dict.get::<&str, bool>(&"enabled")? {
250                menu.enabled = enabled;
251            }
252
253            if let Some(visible) = dict.get::<&str, bool>(&"visible")? {
254                menu.visible = visible;
255            }
256
257            menu.icon_name = dict.get::<&str, &str>(&"icon-name")?.map(str::to_string);
258
259            if let Some(array) = dict.get::<&str, &Array>(&"icon-data")? {
260                menu.icon_data = Some(get_icon_data(array)?);
261            }
262
263            if let Some(disposition) = dict
264                .get::<&str, &str>(&"disposition")
265                .ok()
266                .flatten()
267                .map(Disposition::from)
268            {
269                menu.disposition = disposition;
270            }
271
272            menu.toggle_state = dict
273                .get::<&str, i32>(&"toggle-state")
274                .ok()
275                .flatten()
276                .map(ToggleState::from)
277                .unwrap_or_default();
278
279            menu.toggle_type = dict
280                .get::<&str, &str>(&"toggle-type")
281                .ok()
282                .flatten()
283                .map(ToggleType::from)
284                .unwrap_or_default();
285
286            menu.menu_type = dict
287                .get::<&str, &str>(&"type")
288                .ok()
289                .flatten()
290                .map(MenuType::from)
291                .unwrap_or_default();
292        };
293
294        if let Some(Value::Array(array)) = fields.next() {
295            let mut submenu = vec![];
296            for value in array.iter() {
297                let value = OwnedValue::try_from(value)?;
298                let menu = MenuItem::try_from(&value)?;
299                submenu.push(menu);
300            }
301
302            menu.submenu = submenu;
303        }
304
305        Ok(menu)
306    }
307}
308
309impl TryFrom<PropertiesUpdate<'_>> for Vec<MenuDiff> {
310    type Error = Error;
311
312    fn try_from(value: PropertiesUpdate<'_>) -> Result<Self> {
313        let mut res = HashMap::new();
314
315        for updated in value.updated {
316            let id = updated.id;
317            let update = MenuDiff {
318                id,
319                update: updated.try_into()?,
320                ..Default::default()
321            };
322
323            res.insert(id, update);
324        }
325
326        for removed in value.removed {
327            let update = res.entry(removed.id).or_insert_with(|| MenuDiff {
328                id: removed.id,
329                ..Default::default()
330            });
331
332            update.remove = removed.fields.iter().map(ToString::to_string).collect();
333        }
334
335        Ok(res.into_values().collect())
336    }
337}
338
339impl TryFrom<UpdatedProps<'_>> for MenuItemUpdate {
340    type Error = Error;
341
342    fn try_from(value: UpdatedProps) -> Result<Self> {
343        let dict = value.fields;
344
345        let icon_data = if let Some(arr) = dict
346            .get("icon-data")
347            .map(Value::downcast_ref::<&Array>)
348            .transpose()?
349        {
350            Some(Some(get_icon_data(arr)?))
351        } else {
352            None
353        };
354
355        Ok(Self {
356            label: dict
357                .get("label")
358                .map(|v| v.downcast_ref::<&str>().map(ToString::to_string).ok()),
359
360            enabled: dict
361                .get("enabled")
362                .and_then(|v| Value::downcast_ref::<bool>(v).ok()),
363
364            visible: dict
365                .get("visible")
366                .and_then(|v| Value::downcast_ref::<bool>(v).ok()),
367
368            icon_name: dict
369                .get("icon-name")
370                .map(|v| v.downcast_ref::<&str>().map(ToString::to_string).ok()),
371
372            icon_data,
373
374            toggle_state: dict
375                .get("toggle-state")
376                .and_then(|v| Value::downcast_ref::<i32>(v).ok())
377                .map(ToggleState::from),
378
379            disposition: dict
380                .get("disposition")
381                .and_then(|v| Value::downcast_ref::<&str>(v).ok())
382                .map(Disposition::from),
383        })
384    }
385}
386
387fn get_icon_data(array: &Array) -> Result<Vec<u8>> {
388    array
389        .iter()
390        .map(|v| v.downcast_ref::<u8>().map_err(Into::into))
391        .collect::<Result<Vec<_>>>()
392}