Skip to main content

system_tray/
menu.rs

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