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 | |
8 | use super::{atoms::*, VoidCookie, X11Error, XConnection}; |
9 | |
10 | use std::ffi::CString; |
11 | use std::fmt::Write; |
12 | |
13 | use x11rb::protocol::xproto::{self, ConnectionExt as _}; |
14 | |
15 | impl 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(¬ification)?; |
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. |
147 | fn 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. |
169 | fn 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)] |
185 | mod 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 | |