1use crate::dbus::dbus_menu_proxy::{MenuLayout, PropertiesUpdate, UpdatedProps};
2use crate::error::{Error, Result};
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::fmt::{Debug, Formatter};
6use zbus::zvariant::{Array, OwnedValue, Structure, Value};
7
8#[derive(Debug, Clone)]
10pub struct TrayMenu {
11 pub id: u32,
13 pub submenus: Vec<MenuItem>,
15}
16
17#[derive(Clone, Deserialize, Default)]
20pub struct MenuItem {
21 pub id: i32,
23
24 pub menu_type: MenuType,
26 pub label: Option<String>,
34 pub enabled: bool,
36 pub visible: bool,
38 pub icon_name: Option<String>,
40 pub icon_data: Option<Vec<u8>>,
42 pub shortcut: Option<Vec<Vec<String>>>,
52 pub toggle_type: ToggleType,
56 pub toggle_state: ToggleState,
65 pub children_display: Option<String>,
68 pub disposition: Disposition,
72 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#[derive(Debug, Clone, Deserialize, Default)]
105pub struct MenuDiff {
106 pub id: i32,
107 pub update: MenuItemUpdate,
108 pub remove: Vec<String>,
109}
110
111#[derive(Clone, Deserialize, Default)]
112pub struct MenuItemUpdate {
113 pub label: Option<Option<String>>,
121 pub enabled: Option<bool>,
123 pub visible: Option<bool>,
125 pub icon_name: Option<Option<String>>,
127 pub icon_data: Option<Option<Vec<u8>>>,
129 pub toggle_state: Option<ToggleState>,
138 pub disposition: Option<Disposition>,
142}
143
144impl Debug for MenuItemUpdate {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("MenuItemUpdate")
147 .field("label", &self.label)
148 .field("enabled", &self.enabled)
149 .field("visible", &self.visible)
150 .field("icon_name", &self.icon_name)
151 .field(
152 "icon_data",
153 &format!(
154 "<length: {:?}>",
155 self.icon_data.as_ref().map(|d| d
156 .as_ref()
157 .map_or("none".to_string(), |d| d.len().to_string()))
158 ),
159 )
160 .field("toggle_state", &self.toggle_state)
161 .field("disposition", &self.disposition)
162 .finish()
163 }
164}
165
166#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
167pub enum MenuType {
168 Separator,
170 #[default]
172 Standard,
173}
174
175impl From<&str> for MenuType {
176 fn from(value: &str) -> Self {
177 match value {
178 "separator" => Self::Separator,
179 _ => Self::default(),
180 }
181 }
182}
183
184#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
185pub enum ToggleType {
186 Checkmark,
188 Radio,
191 #[default]
193 CannotBeToggled,
194}
195
196impl From<&str> for ToggleType {
197 fn from(value: &str) -> Self {
198 match value {
199 "checkmark" => Self::Checkmark,
200 "radio" => Self::Radio,
201 _ => Self::default(),
202 }
203 }
204}
205
206#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
208pub enum ToggleState {
209 #[default]
211 On,
212 Off,
214 Indeterminate,
216}
217
218impl From<i32> for ToggleState {
219 fn from(value: i32) -> Self {
220 match value {
221 0 => Self::Off,
222 1 => Self::On,
223 _ => Self::Indeterminate,
224 }
225 }
226}
227
228#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
229pub enum Disposition {
230 #[default]
232 Normal,
233 Informative,
235 Warning,
237 Alert,
239}
240
241impl From<&str> for Disposition {
242 fn from(value: &str) -> Self {
243 match value {
244 "informative" => Self::Informative,
245 "warning" => Self::Warning,
246 "alert" => Self::Alert,
247 _ => Self::default(),
248 }
249 }
250}
251
252impl TryFrom<MenuLayout> for TrayMenu {
253 type Error = Error;
254
255 fn try_from(value: MenuLayout) -> Result<Self> {
256 let submenus = value
257 .fields
258 .submenus
259 .iter()
260 .map(MenuItem::try_from)
261 .collect::<std::result::Result<_, _>>()?;
262
263 Ok(Self {
264 id: value.id,
265 submenus,
266 })
267 }
268}
269
270impl TryFrom<&OwnedValue> for MenuItem {
271 type Error = Error;
272
273 fn try_from(value: &OwnedValue) -> Result<Self> {
274 let structure = value.downcast_ref::<&Structure>()?;
275
276 let mut fields = structure.fields().iter();
277
278 let mut menu = MenuItem {
281 enabled: true,
282 visible: true,
283 ..Default::default()
284 };
285
286 if let Some(Value::I32(id)) = fields.next() {
287 menu.id = *id;
288 }
289
290 if let Some(Value::Dict(dict)) = fields.next() {
291 menu.children_display = dict
292 .get::<&str, &str>(&"children-display")?
293 .map(str::to_string);
294
295 menu.label = dict
297 .get::<&str, &str>(&"label")?
298 .map(|label| label.replace('_', ""));
299
300 if let Some(enabled) = dict.get::<&str, bool>(&"enabled")? {
301 menu.enabled = enabled;
302 }
303
304 if let Some(visible) = dict.get::<&str, bool>(&"visible")? {
305 menu.visible = visible;
306 }
307
308 menu.icon_name = dict.get::<&str, &str>(&"icon-name")?.map(str::to_string);
309
310 if let Some(array) = dict.get::<&str, &Array>(&"icon-data")? {
311 menu.icon_data = Some(get_icon_data(array)?);
312 }
313
314 if let Some(disposition) = dict
315 .get::<&str, &str>(&"disposition")
316 .ok()
317 .flatten()
318 .map(Disposition::from)
319 {
320 menu.disposition = disposition;
321 }
322
323 menu.toggle_state = dict
324 .get::<&str, i32>(&"toggle-state")
325 .ok()
326 .flatten()
327 .map(ToggleState::from)
328 .unwrap_or_default();
329
330 menu.toggle_type = dict
331 .get::<&str, &str>(&"toggle-type")
332 .ok()
333 .flatten()
334 .map(ToggleType::from)
335 .unwrap_or_default();
336
337 menu.menu_type = dict
338 .get::<&str, &str>(&"type")
339 .ok()
340 .flatten()
341 .map(MenuType::from)
342 .unwrap_or_default();
343 }
344
345 if let Some(Value::Array(array)) = fields.next() {
346 let mut submenu = vec![];
347 for value in array.iter() {
348 let value = OwnedValue::try_from(value)?;
349 let menu = MenuItem::try_from(&value)?;
350 submenu.push(menu);
351 }
352
353 menu.submenu = submenu;
354 }
355
356 Ok(menu)
357 }
358}
359
360impl TryFrom<PropertiesUpdate<'_>> for Vec<MenuDiff> {
361 type Error = Error;
362
363 fn try_from(value: PropertiesUpdate<'_>) -> Result<Self> {
364 let mut res = HashMap::new();
365
366 for updated in value.updated {
367 let id = updated.id;
368 let update = MenuDiff {
369 id,
370 update: updated.try_into()?,
371 ..Default::default()
372 };
373
374 res.insert(id, update);
375 }
376
377 for removed in value.removed {
378 let update = res.entry(removed.id).or_insert_with(|| MenuDiff {
379 id: removed.id,
380 ..Default::default()
381 });
382
383 update.remove = removed.fields.iter().map(ToString::to_string).collect();
384 }
385
386 Ok(res.into_values().collect())
387 }
388}
389
390impl TryFrom<UpdatedProps<'_>> for MenuItemUpdate {
391 type Error = Error;
392
393 fn try_from(value: UpdatedProps) -> Result<Self> {
394 let dict = value.fields;
395
396 let icon_data = if let Some(arr) = dict
397 .get("icon-data")
398 .map(Value::downcast_ref::<&Array>)
399 .transpose()?
400 {
401 Some(Some(get_icon_data(arr)?))
402 } else {
403 None
404 };
405
406 Ok(Self {
407 label: dict
408 .get("label")
409 .map(|v| v.downcast_ref::<&str>().map(ToString::to_string).ok()),
410
411 enabled: dict
412 .get("enabled")
413 .and_then(|v| Value::downcast_ref::<bool>(v).ok()),
414
415 visible: dict
416 .get("visible")
417 .and_then(|v| Value::downcast_ref::<bool>(v).ok()),
418
419 icon_name: dict
420 .get("icon-name")
421 .map(|v| v.downcast_ref::<&str>().map(ToString::to_string).ok()),
422
423 icon_data,
424
425 toggle_state: dict
426 .get("toggle-state")
427 .and_then(|v| Value::downcast_ref::<i32>(v).ok())
428 .map(ToggleState::from),
429
430 disposition: dict
431 .get("disposition")
432 .and_then(|v| Value::downcast_ref::<&str>(v).ok())
433 .map(Disposition::from),
434 })
435 }
436}
437
438fn get_icon_data(array: &Array) -> Result<Vec<u8>> {
439 array
440 .iter()
441 .map(|v| v.downcast_ref::<u8>().map_err(Into::into))
442 .collect::<Result<Vec<_>>>()
443}