Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:52:14.138Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

system_notifications.rs

199 lines · 7.6 KB · rust
1//! System notifications as Windows toast notifications.
2
3use std::cell::RefCell;
4use std::collections::{HashMap, hash_map::DefaultHasher};
5use std::hash::{Hash as _, Hasher as _};
6use std::rc::Rc;
7
8use futures::StreamExt as _;
9use futures::channel::mpsc;
10use gpui::{
11    ForegroundExecutor, SharedString, SystemNotification, SystemNotificationResponse, Task,
12};
13use windows::Data::Xml::Dom::XmlDocument;
14use windows::Foundation::TypedEventHandler;
15use windows::UI::Notifications::{
16    ToastActivatedEventArgs, ToastNotification, ToastNotificationManager, ToastNotifier,
17};
18use windows::core::{IInspectable, Interface as _, h};
19
20type ResponseCallback = Rc<RefCell<Option<Box<dyn FnMut(SystemNotificationResponse)>>>>;
21
22pub(crate) struct SystemNotificationState {
23    notifier: Option<ToastNotifier>,
24    active_toasts: HashMap<SharedString, ToastNotification>,
25    response_sender: mpsc::UnboundedSender<SystemNotificationResponse>,
26    response_receiver: Option<mpsc::UnboundedReceiver<SystemNotificationResponse>>,
27    callback: ResponseCallback,
28    _response_task: Option<Task<()>>,
29}
30
31impl SystemNotificationState {
32    pub(crate) fn new() -> Self {
33        let (response_sender, response_receiver) = mpsc::unbounded();
34        Self {
35            notifier: None,
36            active_toasts: HashMap::new(),
37            response_sender,
38            response_receiver: Some(response_receiver),
39            callback: Rc::new(RefCell::new(None)),
40            _response_task: None,
41        }
42    }
43
44    pub(crate) fn show(
45        &mut self,
46        has_package_identity: bool,
47        app_identity: Option<(&str, &str)>,
48        notification: SystemNotification,
49    ) -> windows::core::Result<()> {
50        let Some(notifier) = self.notifier(has_package_identity, app_identity)? else {
51            return Ok(());
52        };
53
54        let document = toast_document(&notification)?;
55        let toast = ToastNotification::CreateToastNotification(&document)?;
56        // Windows caps toast tags at 64 characters (post-Creators Update),
57        // so hash the arbitrary GPUI tag down to a fixed-width value.
58        let tag = {
59            let mut hasher = DefaultHasher::new();
60            notification.tag.hash(&mut hasher);
61            format!("{:016x}", hasher.finish())
62        };
63        toast.SetTag(&tag.into())?;
64
65        let sender = self.response_sender.clone();
66        let response_tag = notification.tag.clone();
67        toast.Activated(&TypedEventHandler::<ToastNotification, IInspectable>::new(
68            move |_sender, arguments| {
69                let action_id = arguments
70                    .as_ref()
71                    .and_then(|arguments| arguments.cast::<ToastActivatedEventArgs>().ok())
72                    .and_then(|arguments| arguments.Arguments().ok())
73                    .filter(|arguments| !arguments.is_empty())
74                    .map(|arguments| SharedString::from(arguments.to_string()));
75                sender
76                    .unbounded_send(SystemNotificationResponse {
77                        tag: response_tag.clone(),
78                        action_id,
79                    })
80                    .ok();
81                Ok(())
82            },
83        ))?;
84
85        if let Some(previous) = self.active_toasts.remove(&notification.tag) {
86            notifier.Hide(&previous)?;
87        }
88        notifier.Show(&toast)?;
89        self.active_toasts.insert(notification.tag, toast);
90        Ok(())
91    }
92
93    pub(crate) fn dismiss(&mut self, tag: &str) {
94        let Some(toast) = self.active_toasts.remove(tag) else {
95            return;
96        };
97        let Some(notifier) = &self.notifier else {
98            return;
99        };
100        if let Err(error) = notifier.Hide(&toast) {
101            log::warn!("failed to dismiss system notification: {error}");
102        }
103    }
104
105    pub(crate) fn on_response(
106        &mut self,
107        executor: &ForegroundExecutor,
108        callback: Box<dyn FnMut(SystemNotificationResponse)>,
109    ) {
110        *self.callback.borrow_mut() = Some(callback);
111
112        if let Some(mut receiver) = self.response_receiver.take() {
113            let callback = self.callback.clone();
114            self._response_task = Some(executor.spawn(async move {
115                while let Some(response) = receiver.next().await {
116                    // Take the callback out for the call: it may re-enter the
117                    // platform (e.g. to dismiss the notification it was told
118                    // about) or replace itself.
119                    let taken = callback.borrow_mut().take();
120                    if let Some(mut taken) = taken {
121                        taken(response);
122                        callback.borrow_mut().get_or_insert(taken);
123                    }
124                }
125            }));
126        }
127    }
128
129    fn notifier(
130        &mut self,
131        has_package_identity: bool,
132        app_identity: Option<(&str, &str)>,
133    ) -> windows::core::Result<Option<ToastNotifier>> {
134        if let Some(notifier) = &self.notifier {
135            return Ok(Some(notifier.clone()));
136        }
137
138        let notifier = if has_package_identity {
139            ToastNotificationManager::CreateToastNotifier()?
140        } else {
141            let Some((app_identifier, app_name)) = app_identity else {
142                log::warn!(
143                    "cannot show a system notification without an app identity; \
144                     call `App::set_app_identity` during startup"
145                );
146                return Ok(None);
147            };
148            register_app_user_model_id(app_identifier, app_name);
149            ToastNotificationManager::CreateToastNotifierWithId(&app_identifier.into())?
150        };
151
152        self.notifier = Some(notifier.clone());
153        Ok(Some(notifier))
154    }
155}
156
157fn toast_document(notification: &SystemNotification) -> windows::core::Result<XmlDocument> {
158    let document = XmlDocument::new()?;
159    let toast = document.CreateElement(h!("toast"))?;
160    document.AppendChild(&toast)?;
161
162    let visual = document.CreateElement(h!("visual"))?;
163    toast.AppendChild(&visual)?;
164    let binding = document.CreateElement(h!("binding"))?;
165    binding.SetAttribute(h!("template"), h!("ToastGeneric"))?;
166    visual.AppendChild(&binding)?;
167    for text in [&notification.title, &notification.body] {
168        let element = document.CreateElement(h!("text"))?;
169        element.SetInnerText(&text.as_ref().into())?;
170        binding.AppendChild(&element)?;
171    }
172
173    if !notification.actions.is_empty() {
174        let actions = document.CreateElement(h!("actions"))?;
175        toast.AppendChild(&actions)?;
176        for action in &notification.actions {
177            let action_element = document.CreateElement(h!("action"))?;
178            action_element.SetAttribute(h!("content"), &action.label.as_ref().into())?;
179            action_element.SetAttribute(h!("arguments"), &action.id.as_ref().into())?;
180            actions.AppendChild(&action_element)?;
181        }
182    }
183
184    let audio = document.CreateElement(h!("audio"))?;
185    audio.SetAttribute(h!("silent"), h!("true"))?;
186    toast.AppendChild(&audio)?;
187    Ok(document)
188}
189
190/// Registers the app's AUMID so toasts display correctly for an unpackaged app without a Start Menu shortcut.
191fn register_app_user_model_id(app_identifier: &str, app_name: &str) {
192    let result = windows_registry::CURRENT_USER
193        .create(format!(r"Software\Classes\AppUserModelId\{app_identifier}"))
194        .and_then(|key| key.set_string("DisplayName", app_name));
195    if let Err(error) = result {
196        log::warn!("failed to register AppUserModelID; notifications may not display: {error}");
197    }
198}
199
Served at tenant.openagents/omega Member data and write actions are omitted.