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#[derive(Debug, Clone)]
9pub struct TrayMenu {
10 pub id: u32,
12 pub submenus: Vec<MenuItem>,
14}
15
16#[derive(Debug, Clone, Deserialize, Default)]
19pub struct MenuItem {
20 pub id: i32,
22
23 pub menu_type: MenuType,
25 pub label: Option<String>,
33 pub enabled: bool,
35 pub visible: bool,
37 pub icon_name: Option<String>,
39 pub icon_data: Option<Vec<u8>>,
41 pub shortcut: Option<Vec<Vec<String>>>,
51 pub toggle_type: ToggleType,
55 pub toggle_state: ToggleState,
64 pub children_display: Option<String>,
67 pub disposition: Disposition,
71 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 pub label: Option<Option<String>>,
92 pub enabled: Option<bool>,
94 pub visible: Option<bool>,
96 pub icon_name: Option<Option<String>>,
98 pub icon_data: Option<Option<Vec<u8>>>,
100 pub toggle_state: Option<ToggleState>,
109 pub disposition: Option<Disposition>,
113}
114
115#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
116pub enum MenuType {
117 Separator,
119 #[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 Checkmark,
137 Radio,
140 #[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#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Default)]
157pub enum ToggleState {
158 #[default]
160 On,
161 Off,
163 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 #[default]
181 Normal,
182 Informative,
184 Warning,
186 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 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 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}