Skip to main content

notify_rust/
hints.rs

1#![cfg_attr(rustfmt, rustfmt_skip)]
2
3#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))]
4use zbus::zvariant;
5
6#[cfg(all(unix, not(target_os = "macos")))]
7pub(crate) mod message;
8
9#[cfg(all(feature = "images_no_default_features", any(feature = "dbus", feature = "zbus"), unix, not(target_os = "macos")))]
10use crate::image::Image;
11
12#[cfg(all(feature = "images_no_default_features", feature = "zbus", unix, not(target_os = "macos")))]
13use crate::image::image_spec_str;
14use crate::Urgency;
15
16#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] use crate::notification::Notification;
17#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] use std::collections::HashMap;
18
19mod constants;
20
21#[cfg(all(unix, not(target_os = "macos")))]
22#[derive(Eq, PartialEq, Hash, Clone, Debug)]
23pub(crate) enum CustomHintType {
24    Int,
25    String,
26}
27
28/// Hints allow you to pass extra information to the notification server.
29///
30/// Many of these are standardized by:
31///
32/// * <https://specifications.freedesktop.org/notification-spec/latest/hints.html>
33///
34/// Which hints are actually implemented depends strongly on the notification server you talk to.
35/// Usually [`get_capabilities()`](`crate::get_capabilities`) gives some clues, but the standard usually mentions much more
36/// than is actually available.
37///
38/// Pass these to [`Notification::hint`].
39#[derive(Eq, PartialEq, Hash, Clone, Debug)]
40pub enum Hint {
41    /// If true, server may interpret action identifiers as named icons and display those.
42    ActionIcons(bool),
43
44    /// Check out:
45    ///
46    /// * <https://specifications.freedesktop.org/notification-spec/latest/hints.html>
47    Category(String),
48
49    /// Name of the `DesktopEntry` representing the calling application.
50    /// In case of `"firefox.desktop"` use `"firefox"`. May be used to retrieve the correct icon.
51    DesktopEntry(String),
52
53    /// Image as raw data
54    #[cfg(all(feature = "images_no_default_features", unix, not(target_os = "macos")))]
55    ImageData(Image),
56
57    /// Display the image at this path.
58    ImagePath(String),
59
60    /// This does not work on all servers, however timeout=0 will do the job
61    Resident(bool),
62
63    /// Play the sound at this path.
64    SoundFile(String),
65
66    /// A themeable named sound from the freedesktop.org [sound naming specification](http://0pointer.de/public/sound-naming-spec.html) to play when the notification pops up.
67    /// Similar to icon-name, but for sounds. An example would be `"message-new-instant"`.
68    SoundName(String),
69
70    /// Suppress the notification sound.
71    SuppressSound(bool),
72
73    /// When set, the server will treat the notification as transient and bypass the server's persistence capability, if it exists.
74    Transient(bool),
75
76    /// Lets the notification point to a certain 'x' position on the screen.
77    /// Requires `Y`.
78    X(i32),
79
80    /// Lets the notification point to a certain 'y' position on the screen.
81    /// Requires `X`.
82    Y(i32),
83
84    /// Pass a [`Urgency`](crate::Urgency), either `Low`, `Normal`, or `Critical`.
85    Urgency(Urgency),
86
87    /// If you want to pass something entirely different.
88    Custom(String, String),
89
90    /// A custom numerical (integer) hint
91    CustomInt(String, i32),
92
93    /// Only used by this `NotificationServer` implementation.
94    Invalid // TODO find a better solution to this
95}
96
97impl Hint {
98    /// Get the `bool` representation of this hint.
99    pub fn as_bool(&self) -> Option<bool> {
100        match *self {
101            | Hint::ActionIcons(inner)
102            | Hint::Resident(inner)
103            | Hint::SuppressSound(inner)
104            | Hint::Transient(inner) => Some(inner),
105            _ => None
106        }
107    }
108
109    /// Get the `i32` representation of this hint.
110    pub fn as_i32(&self) -> Option<i32> {
111        match *self {
112            Hint::X(inner) | Hint::Y(inner) => Some(inner),
113            _ => None
114        }
115    }
116
117    /// Get the `&str` representation of this hint.
118    pub fn as_str(&self) -> Option<&str> {
119        match *self {
120            Hint::DesktopEntry(ref inner) |
121            Hint::ImagePath(ref inner)    |
122            Hint::SoundFile(ref inner)    |
123            Hint::SoundName(ref inner)    => Some(inner),
124            _ => None
125        }
126    }
127
128    /// Convenience function for converting a name and value into a hint.
129    pub fn from_key_val(name: &str, value: &str) -> Result<Hint, String> {
130        match (name,value){
131            (constants::ACTION_ICONS,val)    => val.parse::<bool>().map(Hint::ActionIcons).map_err(|e|e.to_string()),
132            (constants::CATEGORY, val)       => Ok(Hint::Category(val.to_owned())),
133            (constants::DESKTOP_ENTRY, val)  => Ok(Hint::DesktopEntry(val.to_owned())),
134            (constants::IMAGE_PATH, val)     => Ok(Hint::ImagePath(val.to_owned())),
135            (constants::RESIDENT, val)       => val.parse::<bool>().map(Hint::Resident).map_err(|e|e.to_string()),
136            (constants::SOUND_FILE, val)     => Ok(Hint::SoundFile(val.to_owned())),
137            (constants::SOUND_NAME, val)     => Ok(Hint::SoundName(val.to_owned())),
138            (constants::SUPPRESS_SOUND, val) => val.parse::<bool>().map(Hint::SuppressSound).map_err(|e|e.to_string()),
139            (constants::TRANSIENT, val)      => val.parse::<bool>().map(Hint::Transient).map_err(|e|e.to_string()),
140            (constants::X, val)              => val.parse::<i32>().map(Hint::X).map_err(|e|e.to_string()),
141            (constants::Y, val)              => val.parse::<i32>().map(Hint::Y).map_err(|e|e.to_string()),
142            _                                => Err(String::from("unknown name"))
143        }
144    }
145}
146
147#[cfg(all(unix, not(target_os = "macos")))]
148impl Hint {}
149
150#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))]
151#[test]
152fn test_hints_to_map() {
153
154    // custom value should only be there once if the names are identical
155
156    let n1 = Notification::new()
157        .hint(Hint::Custom("foo".into(), "bar1".into()))
158        .hint(Hint::Custom("foo".into(), "bar2".into()))
159        .hint(Hint::Custom("f00".into(), "bar3".into()))
160        .finalize();
161
162     assert_eq!(hints_to_map(&n1), maplit::hashmap!{
163         "foo" => zvariant::Value::Str("bar2".into()),
164         "f00" => zvariant::Value::Str("bar3".into())
165     });
166}
167
168#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))]
169pub(crate) fn hints_to_map(notification: &Notification) -> HashMap::<&str, zvariant::Value<'_>> {
170    notification
171        .get_hints()
172        .map(Into::into)
173        .collect()
174}
175
176#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))]
177impl<'a> From<&'a Hint> for (&'a str, zvariant::Value<'a>) {
178    fn from(val: &'a Hint) -> Self {
179        use self::constants::*;
180        match val {
181            Hint::ActionIcons(value)       => (ACTION_ICONS   , zvariant::Value::Bool(*value)), // bool
182            Hint::Category(value)          => (CATEGORY       , zvariant::Value::Str(value.as_str().into())),
183            Hint::DesktopEntry(value)      => (DESKTOP_ENTRY  , zvariant::Value::Str(value.as_str().into())),
184
185            #[cfg(all(feature = "zbus", feature = "images_no_default_features", unix, not(target_os = "macos")))]
186            //Hint::ImageData(image)         => (image_spec(*crate::SPEC_VERSION).as_str(), ImagePayload::from(*image).into()),
187            Hint::ImageData(image)         => (
188                image_spec_str(*crate::SPEC_VERSION),
189                zvariant::Value::Structure(
190                    image.to_tuple().into()
191                )
192            ),
193
194
195            Hint::ImagePath(value)         => (IMAGE_PATH     , zvariant::Value::Str(value.as_str().into())),
196            Hint::Resident(value)          => (RESIDENT       , zvariant::Value::Bool(*value)), // bool
197            Hint::SoundFile(value)         => (SOUND_FILE     , zvariant::Value::Str(value.as_str().into())),
198            Hint::SoundName(value)         => (SOUND_NAME     , zvariant::Value::Str(value.as_str().into())),
199            Hint::SuppressSound(value)     => (SUPPRESS_SOUND , zvariant::Value::Bool(*value)),
200            Hint::Transient(value)         => (TRANSIENT      , zvariant::Value::Bool(*value)),
201            Hint::X(value)                 => (X              , zvariant::Value::I32(*value)),
202            Hint::Y(value)                 => (Y              , zvariant::Value::I32(*value)),
203            Hint::Urgency(value)           => (URGENCY        , zvariant::Value::U8(*value as u8)),
204            Hint::Custom(key, val)         => (key.as_str()   , zvariant::Value::Str(val.as_str().into())),
205            Hint::CustomInt(key, val)      => (key.as_str()   , zvariant::Value::I32(*val)),
206            Hint::Invalid                  => (INVALID        , zvariant::Value::Str(INVALID.into()))
207        }
208    }
209}
210
211
212#[cfg(all(feature = "dbus", unix, not(target_os = "macos")))]
213impl<'a, A: dbus::arg::RefArg> From<(&'a String, &'a A)> for Hint {
214    fn from(pair: (&String, &A)) -> Self {
215
216        let (key, variant) = pair;
217        match (key.as_ref(), variant.as_u64(), variant.as_i64(), variant.as_str().map(String::from)) {
218
219            (constants::ACTION_ICONS,   Some(1),  _,       _          ) => Hint::ActionIcons(true),
220            (constants::ACTION_ICONS,   _,        _,       _          ) => Hint::ActionIcons(false),
221            (constants::URGENCY,        level,    _,       _          ) => Hint::Urgency(level.into()),
222            (constants::CATEGORY,       _,        _,       Some(name) ) => Hint::Category(name),
223
224            (constants::DESKTOP_ENTRY,  _,        _,       Some(entry)) => Hint::DesktopEntry(entry),
225            (constants::IMAGE_PATH,     _,        _,       Some(path) ) => Hint::ImagePath(path),
226            (constants::RESIDENT,       Some(1),  _,       _          ) => Hint::Resident(true),
227            (constants::RESIDENT,       _,        _,       _          ) => Hint::Resident(false),
228
229            (constants::SOUND_FILE,     _,        _,       Some(path) ) => Hint::SoundFile(path),
230            (constants::SOUND_NAME,     _,        _,       Some(name) ) => Hint::SoundName(name),
231            (constants::SUPPRESS_SOUND, Some(1),  _,       _          ) => Hint::SuppressSound(true),
232            (constants::SUPPRESS_SOUND, _,        _,       _          ) => Hint::SuppressSound(false),
233            (constants::TRANSIENT,      Some(1),  _,       _          ) => Hint::Transient(true),
234            (constants::TRANSIENT,      _,        _,       _          ) => Hint::Transient(false),
235            (constants::X,              _,        Some(x), _          ) => Hint::X(x as i32),
236            (constants::Y,              _,        Some(y), _          ) => Hint::Y(y as i32),
237
238            other => {
239                eprintln!("Invalid Hint {:#?} ", other);
240                Hint::Invalid
241            }
242        }
243    }
244}