Skip to repository content155 lines · 5.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:48:19.870Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
system_notifications.rs
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(¬ification.title)
45 .body(¬ification.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