winit/platform_impl/linux/x11/
activation.rs

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::*;
9use super::{VoidCookie, X11Error, XConnection};
10
11use std::ffi::CString;
12use std::fmt::Write;
13
14use x11rb::protocol::xproto::{self, ConnectionExt as _};
15
16impl 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(&notification)?;
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.
141fn quote_string(s: &str, target: &mut Vec<u8>) {
142    let total_len = s.len().checked_add(3).expect("quote string overflow");
143    target.reserve(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| b == b'"').for_each(|part| {
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(target.len() - 2);
160}
161
162/// Push a `Display` implementation to the buffer.
163fn 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)]
179mod 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}