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#[derive(Deserialize, Serialize, Debug, Clone)]
10pub struct TrayMenu {
11 pub id: u32,
13 pub submenus: Vec<MenuItem>,
15}
16
17#[derive(Clone, Deserialize, Serialize, 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, Serialize, Default)]
106pub struct MenuDiff {
107 pub id: i32,
110 pub update: MenuItemUpdate,
112 pub remove: Vec<String>,
115}
116
117#[derive(Clone, Deserialize, Serialize, Default)]
118pub struct MenuItemUpdate {
119 pub label: Option<Option<String>>,
127 pub enabled: Option<bool>,
129 pub visible: Option<bool>,
131 pub icon_name: Option<Option<String>>,
133 pub icon_data: Option<Option<Vec<u8>>>,
135 pub toggle_state: Option<ToggleState>,
144 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 Separator,
176 #[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 Checkmark,
194 Radio,
197 #[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#[derive(Debug, Deserialize, Serialize, Copy, Clone, Eq, PartialEq, Default)]
214pub enum ToggleState {
215 #[default]
217 On,
218 Off,
220 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 #[default]
238 Normal,
239 Informative,
241 Warning,
243 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 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 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}