Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:48:19.870Z 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

155 lines · 5.9 KB · rust
1//! System notifications over the XDG notifications D-Bus interface, via
2//! `notify-rust`.
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::Rc;
7
8use futures::StreamExt as _;
9use futures::channel::mpsc;
10use gpui::{
11    ForegroundExecutor, SharedString, SystemNotification, SystemNotificationResponse, Task,
12};
13
14/// The XDG action key invoked when the user activates the notification body
15/// rather than a specific action button.
16const DEFAULT_ACTION: &str = "default";
17
18type ResponseCallback = Rc<RefCell<Option<Box<dyn FnMut(SystemNotificationResponse)>>>>;
19
20pub(crate) struct SystemNotificationState {
21    response_sender: mpsc::UnboundedSender<SystemNotificationResponse>,
22    response_receiver: Option<mpsc::UnboundedReceiver<SystemNotificationResponse>>,
23    callback: ResponseCallback,
24    _response_task: Option<Task<()>>,
25}
26
27impl SystemNotificationState {
28    pub(crate) fn new() -> Self {
29        let (response_sender, response_receiver) = mpsc::unbounded();
30        Self {
31            response_sender,
32            response_receiver: Some(response_receiver),
33            callback: Rc::new(RefCell::new(None)),
34            _response_task: None,
35        }
36    }
37
38    pub(crate) fn show(&self, app_name: Option<&str>, notification: SystemNotification) {
39        let mut builder = notify_rust::Notification::new();
40        if let Some(app_name) = app_name {
41            builder.appname(app_name);
42        }
43        builder
44            .summary(&notification.title)
45            .body(&notification.body)
46            .action(DEFAULT_ACTION, DEFAULT_ACTION);
47        let mut action_ids = HashMap::new();
48        for (index, action) in notification.actions.iter().enumerate() {
49            let transport_id = format!("gpui-action-{index}");
50            builder.action(&transport_id, &action.label);
51            action_ids.insert(transport_id, action.id.clone());
52        }
53        let built = builder.finalize();
54
55        let sender = self.response_sender.clone();
56        let tag = notification.tag.clone();
57        // `show` connects to the session bus and `wait_for_action` blocks
58        // until the notification closes, so both run off the UI thread.
59        let spawn_result = std::thread::Builder::new()
60            .name("system-notification".to_string())
61            .spawn(move || match built.show() {
62                Ok(handle) => handle.wait_for_action(|action| {
63                    let action_id = match action {
64                        // Sent by `notify-rust` when the notification closes
65                        // without being activated.
66                        "__closed" => return,
67                        transport_id => {
68                            let Some(action_id) = response_action_id(transport_id, &action_ids)
69                            else {
70                                log::warn!(
71                                    "system notification returned unknown action {transport_id:?}"
72                                );
73                                return;
74                            };
75                            action_id
76                        }
77                    };
78                    sender
79                        .unbounded_send(SystemNotificationResponse { tag, action_id })
80                        .ok();
81                }),
82                Err(error) => log::warn!("failed to show system notification: {error}"),
83            });
84        if let Err(error) = spawn_result {
85            log::warn!("failed to spawn system notification thread: {error}");
86        }
87    }
88
89    pub(crate) fn dismiss(&self, _tag: &str) {
90        // The XDG notifications protocol only allows closing via the
91        // server-assigned id on a live handle, which `wait_for_action`
92        // consumes; stale notifications simply age out of the shade.
93    }
94
95    pub(crate) fn on_response(
96        &mut self,
97        executor: &ForegroundExecutor,
98        callback: Box<dyn FnMut(SystemNotificationResponse)>,
99    ) {
100        *self.callback.borrow_mut() = Some(callback);
101
102        // Responses arrive from per-notification threads; this task hands
103        // them to the registered callback on the main thread.
104        if let Some(mut receiver) = self.response_receiver.take() {
105            let callback = self.callback.clone();
106            self._response_task = Some(executor.spawn(async move {
107                while let Some(response) = receiver.next().await {
108                    // Take the callback out for the call: it may re-enter the
109                    // platform or replace itself.
110                    let taken = callback.borrow_mut().take();
111                    if let Some(mut taken) = taken {
112                        taken(response);
113                        callback.borrow_mut().get_or_insert(taken);
114                    }
115                }
116            }));
117        }
118    }
119}
120
121fn response_action_id(
122    transport_id: &str,
123    action_ids: &HashMap<String, SharedString>,
124) -> Option<Option<SharedString>> {
125    if transport_id == DEFAULT_ACTION {
126        Some(None)
127    } else {
128        action_ids.get(transport_id).cloned().map(Some)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn caller_action_ids_do_not_collide_with_transport_action_ids() {
138        let action_ids = HashMap::from([
139            ("gpui-action-0".to_string(), SharedString::from("default")),
140            ("gpui-action-1".to_string(), SharedString::from("__closed")),
141        ]);
142
143        assert_eq!(response_action_id(DEFAULT_ACTION, &action_ids), Some(None));
144        assert_eq!(
145            response_action_id("gpui-action-0", &action_ids),
146            Some(Some("default".into()))
147        );
148        assert_eq!(
149            response_action_id("gpui-action-1", &action_ids),
150            Some(Some("__closed".into()))
151        );
152        assert_eq!(response_action_id("unknown", &action_ids), None);
153    }
154}
155
Served at tenant.openagents/omega Member data and write actions are omitted.