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