1// SPDX-License-Identifier: Apache-2.0
2
3//! X11 activation handling.
4//!
5//! X11 has a "startup notification" specification similar to Wayland's, see this URL:
6//! <https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt>
7
8use super::{atoms::*, VoidCookie, X11Error, XConnection};
9
10use std::ffi::CString;
11use std::fmt::Write;
12
13use x11rb::protocol::xproto::{self, ConnectionExt as _};
14
15impl XConnection {
16 /// "Request" a new activation token from the server.
17 pub(crate) fn request_activation_token(&self, window_title: &str) -> Result<String, X11Error> {
18 // The specification recommends the format "hostname+pid+"_TIME"+current time"
19 let uname = rustix::system::uname();
20 let pid = rustix::process::getpid();
21 let time = self.timestamp();
22
23 let activation_token = format!(
24 "{}{}_TIME{}",
25 uname.nodename().to_str().unwrap_or("winit"),
26 pid.as_raw_nonzero(),
27 time
28 );
29
30 // Set up the new startup notification.
31 let notification = {
32 let mut buffer = Vec::new();
33 buffer.extend_from_slice(b"new: ID=");
34 quote_string(&activation_token, &mut buffer);
35 buffer.extend_from_slice(b" NAME=");
36 quote_string(window_title, &mut buffer);
37 buffer.extend_from_slice(b" SCREEN=");
38 push_display(&mut buffer, &self.default_screen_index());
39
40 CString::new(buffer)
41 .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))?
42 .into_bytes_with_nul()
43 };
44 self.send_message(&notification)?;
45
46 Ok(activation_token)
47 }
48
49 /// Finish launching a window with the given startup ID.
50 pub(crate) fn remove_activation_token(
51 &self,
52 window: xproto::Window,
53 startup_id: &str,
54 ) -> Result<(), X11Error> {
55 let atoms = self.atoms();
56
57 // Set the _NET_STARTUP_ID property on the window.
58 self.xcb_connection()
59 .change_property(
60 xproto::PropMode::REPLACE,
61 window,
62 atoms[_NET_STARTUP_ID],
63 xproto::AtomEnum::STRING,
64 8,
65 startup_id.len().try_into().unwrap(),
66 startup_id.as_bytes(),
67 )?
68 .check()?;
69
70 // Send the message indicating that the startup is over.
71 let message = {
72 const MESSAGE_ROOT: &str = "remove: ID=";
73
74 let mut buffer = Vec::with_capacity(
75 MESSAGE_ROOT
76 .len()
77 .checked_add(startup_id.len())
78 .and_then(|x| x.checked_add(1))
79 .unwrap(),
80 );
81 buffer.extend_from_slice(MESSAGE_ROOT.as_bytes());
82 quote_string(startup_id, &mut buffer);
83 CString::new(buffer)
84 .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))?
85 .into_bytes_with_nul()
86 };
87
88 self.send_message(&message)
89 }
90
91 /// Send a startup notification message to the window manager.
92 fn send_message(&self, message: &[u8]) -> Result<(), X11Error> {
93 let atoms = self.atoms();
94
95 // Create a new window to send the message over.
96 let screen = self.default_root();
97 let window = xproto::WindowWrapper::create_window(
98 self.xcb_connection(),
99 screen.root_depth,
100 screen.root,
101 -100,
102 -100,
103 1,
104 1,
105 0,
106 xproto::WindowClass::INPUT_OUTPUT,
107 screen.root_visual,
108 &xproto::CreateWindowAux::new()
109 .override_redirect(1)
110 .event_mask(
111 xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::PROPERTY_CHANGE,
112 ),
113 )?;
114
115 // Serialize the messages in 20-byte chunks.
116 let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN];
117 message
118 .chunks(20)
119 .map(|chunk| {
120 let mut buffer = [0u8; 20];
121 buffer[..chunk.len()].copy_from_slice(chunk);
122 let event =
123 xproto::ClientMessageEvent::new(8, window.window(), message_type, buffer);
124
125 // Set the message type to the continuation atom for the next chunk.
126 message_type = atoms[_NET_STARTUP_INFO];
127
128 event
129 })
130 .try_for_each(|event| {
131 // Send each event in order.
132 self.xcb_connection()
133 .send_event(
134 false,
135 screen.root,
136 xproto::EventMask::PROPERTY_CHANGE,
137 event,
138 )
139 .map(VoidCookie::ignore_error)
140 })?;
141
142 Ok(())
143 }
144}
145
146/// Quote a literal string as per the startup notification specification.
147fn quote_string(s: &str, target: &mut Vec<u8>) {
148 let total_len: usize = s.len().checked_add(3).expect(msg:"quote string overflow");
149 target.reserve(additional:total_len);
150
151 // Add the opening quote.
152 target.push(b'"');
153
154 // Iterate over the string split by literal quotes.
155 s.as_bytes().split(|&b: u8| b == b'"').for_each(|part: &[u8]| {
156 // Add the part.
157 target.extend_from_slice(part);
158
159 // Escape the quote.
160 target.push(b'\\');
161 target.push(b'"');
162 });
163
164 // Un-escape the last quote.
165 target.remove(index:target.len() - 2);
166}
167
168/// Push a `Display` implementation to the buffer.
169fn push_display(buffer: &mut Vec<u8>, display: &impl std::fmt::Display) {
170 struct Writer<'a> {
171 buffer: &'a mut Vec<u8>,
172 }
173
174 impl<'a> std::fmt::Write for Writer<'a> {
175 fn write_str(&mut self, s: &str) -> std::fmt::Result {
176 self.buffer.extend_from_slice(s.as_bytes());
177 Ok(())
178 }
179 }
180
181 write!(Writer { buffer }, "{}", display).unwrap();
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn properly_escapes_x11_messages() {
190 let assert_eq = |input: &str, output: &[u8]| {
191 let mut buf = vec![];
192 quote_string(input, &mut buf);
193 assert_eq!(buf, output);
194 };
195
196 assert_eq("", b"\"\"");
197 assert_eq("foo", b"\"foo\"");
198 assert_eq("foo\"bar", b"\"foo\\\"bar\"");
199 }
200}
201