open_sesame/platform/wayland/
protocols.rs

1//! Wayland protocol handling for COSMIC desktop
2//!
3//! Uses:
4//! - ext_foreign_toplevel_list_v1: Window enumeration
5//! - zcosmic_toplevel_info_v1: Get cosmic handles
6//! - zcosmic_toplevel_manager_v1: Window activation
7
8use crate::core::window::{AppId, Window, WindowId};
9use crate::util::{Error, Result};
10use cosmic_client_toolkit::cosmic_protocols::toplevel_info::v1::client::{
11    zcosmic_toplevel_handle_v1::{self, ZcosmicToplevelHandleV1},
12    zcosmic_toplevel_info_v1::{self, ZcosmicToplevelInfoV1},
13};
14use cosmic_client_toolkit::cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1::ZcosmicToplevelManagerV1;
15use std::collections::HashMap;
16use std::os::unix::io::AsFd;
17use std::sync::OnceLock;
18use std::time::{Duration, Instant};
19use wayland_client::{
20    Connection, Dispatch, EventQueue, Proxy, QueueHandle,
21    globals::{GlobalList, GlobalListContents, registry_queue_init},
22    protocol::{wl_registry, wl_seat::WlSeat},
23};
24use wayland_protocols::ext::foreign_toplevel_list::v1::client::{
25    ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1},
26    ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
27};
28
29/// Get Wayland timeout from environment or use default
30fn wayland_timeout() -> Duration {
31    std::env::var("SESAME_WAYLAND_TIMEOUT_MS")
32        .ok()
33        .and_then(|s| s.parse::<u64>().ok())
34        .map(Duration::from_millis)
35        .unwrap_or(Duration::from_secs(2))
36}
37
38/// Timeout for Wayland roundtrip operations (default 2s, override with SESAME_WAYLAND_TIMEOUT_MS)
39fn get_wayland_timeout() -> Duration {
40    static TIMEOUT: OnceLock<Duration> = OnceLock::new();
41    *TIMEOUT.get_or_init(wayland_timeout)
42}
43
44/// Perform a Wayland roundtrip with timeout protection
45///
46/// Prevents indefinite blocking if the compositor hangs or deadlocks.
47fn roundtrip_with_timeout<D: 'static>(
48    conn: &Connection,
49    event_queue: &mut EventQueue<D>,
50    state: &mut D,
51) -> Result<()> {
52    use std::os::unix::io::AsRawFd;
53
54    let start = Instant::now();
55    let fd = conn.as_fd().as_raw_fd();
56    let timeout = get_wayland_timeout();
57
58    loop {
59        // Flush pending requests to server
60        conn.flush()
61            .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
62
63        // Dispatch pending events without blocking
64        event_queue
65            .dispatch_pending(state)
66            .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
67
68        // Check timeout expiration
69        let elapsed = start.elapsed();
70        if elapsed >= timeout {
71            return Err(Error::Other(format!(
72                "Wayland roundtrip timed out after {:?}",
73                elapsed
74            )));
75        }
76
77        // Calculate remaining time for poll
78        let remaining = timeout - elapsed;
79        let timeout_ms = remaining.as_millis().min(100) as i32;
80
81        // Poll for readability
82        let mut pollfd = libc::pollfd {
83            fd,
84            events: libc::POLLIN,
85            revents: 0,
86        };
87
88        let ret = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
89
90        if ret < 0 {
91            let err = std::io::Error::last_os_error();
92            if err.kind() == std::io::ErrorKind::Interrupted {
93                continue;
94            }
95            return Err(Error::WaylandConnection(Box::new(err)));
96        }
97
98        if ret > 0 && (pollfd.revents & libc::POLLIN) != 0 {
99            // Read available data from socket
100            if let Some(guard) = conn.prepare_read()
101                && let Err(e) = guard.read()
102            {
103                return Err(Error::WaylandConnection(Box::new(e)));
104            }
105
106            // Dispatch received events
107            event_queue
108                .dispatch_pending(state)
109                .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
110
111            // Final blocking roundtrip to ensure all server events are received
112            event_queue
113                .roundtrip(state)
114                .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
115            return Ok(());
116        }
117    }
118}
119
120/// Pending toplevel data being collected from events
121#[derive(Debug, Default)]
122struct PendingToplevel {
123    identifier: Option<String>,
124    app_id: Option<String>,
125    title: Option<String>,
126    is_activated: bool,
127}
128
129// ============================================================================
130// Window Enumeration
131// ============================================================================
132
133/// State for toplevel enumeration
134struct EnumerationState {
135    #[allow(dead_code)]
136    list: ExtForeignToplevelListV1,
137    info: ZcosmicToplevelInfoV1,
138    pending: HashMap<u32, PendingToplevel>,
139    cosmic_pending: HashMap<u32, u32>, // cosmic handle id -> foreign handle id
140    toplevels: Vec<(ExtForeignToplevelHandleV1, PendingToplevel)>,
141}
142
143impl EnumerationState {
144    fn bind(globals: &GlobalList, qh: &QueueHandle<Self>) -> Result<Self> {
145        let list = globals
146            .bind::<ExtForeignToplevelListV1, _, _>(qh, 1..=1, ())
147            .map_err(|_| Error::MissingProtocol {
148                protocol: "ext_foreign_toplevel_list_v1",
149            })?;
150
151        let info = globals
152            .bind::<ZcosmicToplevelInfoV1, _, _>(qh, 2..=3, ())
153            .map_err(|_| Error::MissingProtocol {
154                protocol: "zcosmic_toplevel_info_v1",
155            })?;
156
157        Ok(Self {
158            list,
159            info,
160            pending: HashMap::new(),
161            cosmic_pending: HashMap::new(),
162            toplevels: Vec::new(),
163        })
164    }
165}
166
167/// Enumerate all windows on the desktop
168pub fn enumerate_windows() -> Result<Vec<Window>> {
169    tracing::debug!("enumerate_windows: starting");
170    let conn = Connection::connect_to_env().map_err(|e| Error::WaylandConnection(Box::new(e)))?;
171
172    let (globals, mut event_queue) = registry_queue_init::<EnumerationState>(&conn)
173        .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
174    let qh = event_queue.handle();
175
176    let mut state = EnumerationState::bind(&globals, &qh)?;
177    tracing::debug!("enumerate_windows: bound to protocols");
178
179    // First roundtrip: receive toplevel events (with timeout protection)
180    roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
181    tracing::debug!(
182        "enumerate_windows: roundtrip 1 complete, {} toplevels found",
183        state.toplevels.len()
184    );
185
186    // Request cosmic handles for state information
187    for (handle, pending) in &state.toplevels {
188        let foreign_id = handle.id().protocol_id();
189        let cosmic_handle = state.info.get_cosmic_toplevel(handle, &qh, ());
190        let cosmic_id = cosmic_handle.id().protocol_id();
191        state.cosmic_pending.insert(cosmic_id, foreign_id);
192        tracing::debug!(
193            "enumerate_windows: requested cosmic handle for {} (foreign_id={}, cosmic_id={})",
194            pending.app_id.as_deref().unwrap_or("?"),
195            foreign_id,
196            cosmic_id
197        );
198    }
199
200    // Second roundtrip: receive cosmic state events (with timeout protection)
201    roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
202    tracing::debug!("enumerate_windows: roundtrip 2 complete (cosmic state events)");
203
204    // Protocol state validation: verify all cosmic handles were received
205    if state.cosmic_pending.len() != state.toplevels.len() {
206        tracing::warn!(
207            "Protocol state desync detected: requested {} cosmic handles but pending map has {} entries. Some window state may be incomplete.",
208            state.toplevels.len(),
209            state.cosmic_pending.len()
210        );
211    }
212
213    // Convert to Window structs with focused window positioned last
214    let mut windows: Vec<Window> = state
215        .toplevels
216        .into_iter()
217        .filter_map(|(_handle, pending)| {
218            let app_id = pending.app_id?;
219            if app_id.is_empty() {
220                return None;
221            }
222
223            tracing::info!(
224                "Window: {} - {} (is_activated: {})",
225                app_id,
226                pending.title.as_deref().unwrap_or("?"),
227                pending.is_activated
228            );
229
230            Some(Window::with_focus(
231                WindowId::new(pending.identifier.unwrap_or_default()),
232                AppId::new(app_id),
233                pending.title.unwrap_or_default(),
234                pending.is_activated,
235            ))
236        })
237        .collect();
238
239    tracing::info!(
240        "enumerate_windows: {} windows after filtering",
241        windows.len()
242    );
243
244    // Reorder windows with focused window at end for Alt+Tab behavior
245    // Index 0 becomes the previous window for quick Alt+Tab switching
246    if let Some(focused_idx) = windows.iter().position(|w| w.is_focused) {
247        tracing::info!(
248            "enumerate_windows: focused window at index {}, moving to end",
249            focused_idx
250        );
251        let focused = windows.remove(focused_idx);
252        windows.push(focused);
253    } else {
254        tracing::warn!("enumerate_windows: NO focused window detected - MRU order unavailable");
255    }
256
257    // Log final order
258    for (i, w) in windows.iter().enumerate() {
259        tracing::info!(
260            "  [{}] {} - {} (focused: {})",
261            i,
262            w.app_id,
263            w.title,
264            w.is_focused
265        );
266    }
267
268    Ok(windows)
269}
270
271// Dispatch implementations for EnumerationState
272
273impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EnumerationState {
274    fn event(
275        _state: &mut Self,
276        _proxy: &wl_registry::WlRegistry,
277        _event: wl_registry::Event,
278        _data: &GlobalListContents,
279        _conn: &Connection,
280        _qh: &QueueHandle<Self>,
281    ) {
282    }
283}
284
285impl Dispatch<ExtForeignToplevelListV1, ()> for EnumerationState {
286    fn event(
287        state: &mut Self,
288        _proxy: &ExtForeignToplevelListV1,
289        event: ext_foreign_toplevel_list_v1::Event,
290        _data: &(),
291        _conn: &Connection,
292        _qh: &QueueHandle<Self>,
293    ) {
294        if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event {
295            let id = toplevel.id().protocol_id();
296            state.pending.insert(id, PendingToplevel::default());
297        }
298    }
299
300    wayland_client::event_created_child!(EnumerationState, ExtForeignToplevelListV1, [
301        ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ())
302    ]);
303}
304
305impl Dispatch<ExtForeignToplevelHandleV1, ()> for EnumerationState {
306    fn event(
307        state: &mut Self,
308        proxy: &ExtForeignToplevelHandleV1,
309        event: ext_foreign_toplevel_handle_v1::Event,
310        _data: &(),
311        _conn: &Connection,
312        _qh: &QueueHandle<Self>,
313    ) {
314        let id = proxy.id().protocol_id();
315
316        match event {
317            ext_foreign_toplevel_handle_v1::Event::Identifier { identifier } => {
318                if let Some(pending) = state.pending.get_mut(&id) {
319                    pending.identifier = Some(identifier);
320                }
321            }
322            ext_foreign_toplevel_handle_v1::Event::Title { title } => {
323                if let Some(pending) = state.pending.get_mut(&id) {
324                    pending.title = Some(title);
325                }
326            }
327            ext_foreign_toplevel_handle_v1::Event::AppId { app_id } => {
328                if let Some(pending) = state.pending.get_mut(&id) {
329                    pending.app_id = Some(app_id);
330                }
331            }
332            ext_foreign_toplevel_handle_v1::Event::Done => {
333                if let Some(pending) = state.pending.remove(&id) {
334                    state.toplevels.push((proxy.clone(), pending));
335                }
336            }
337            ext_foreign_toplevel_handle_v1::Event::Closed => {
338                state.pending.remove(&id);
339            }
340            _ => {}
341        }
342    }
343}
344
345impl Dispatch<ZcosmicToplevelInfoV1, ()> for EnumerationState {
346    fn event(
347        _state: &mut Self,
348        _proxy: &ZcosmicToplevelInfoV1,
349        _event: zcosmic_toplevel_info_v1::Event,
350        _data: &(),
351        _conn: &Connection,
352        _qh: &QueueHandle<Self>,
353    ) {
354    }
355
356    wayland_client::event_created_child!(EnumerationState, ZcosmicToplevelInfoV1, [
357        zcosmic_toplevel_info_v1::EVT_TOPLEVEL_OPCODE => (ZcosmicToplevelHandleV1, ())
358    ]);
359}
360
361impl Dispatch<ZcosmicToplevelHandleV1, ()> for EnumerationState {
362    fn event(
363        state: &mut Self,
364        proxy: &ZcosmicToplevelHandleV1,
365        event: zcosmic_toplevel_handle_v1::Event,
366        _data: &(),
367        _conn: &Connection,
368        _qh: &QueueHandle<Self>,
369    ) {
370        let cosmic_id = proxy.id().protocol_id();
371
372        // Resolve cosmic handle to foreign handle for pending toplevel update
373        if let Some(&foreign_id) = state.cosmic_pending.get(&cosmic_id) {
374            match &event {
375                zcosmic_toplevel_handle_v1::Event::State { state: state_bytes } => {
376                    tracing::debug!(
377                        "Cosmic state event for cosmic_id={}, foreign_id={}, bytes={:?}",
378                        cosmic_id,
379                        foreign_id,
380                        state_bytes
381                    );
382
383                    // Verify proper 4-byte alignment (each state is a u32)
384                    if state_bytes.len() % 4 != 0 {
385                        tracing::warn!(
386                            "Malformed state data: {} bytes is not 4-byte aligned, skipping",
387                            state_bytes.len()
388                        );
389                        return;
390                    }
391
392                    // Extract state values from byte array
393                    for chunk in state_bytes.chunks_exact(4) {
394                        // SAFETY: chunks_exact(4) guarantees exactly 4 bytes per chunk,
395                        // and alignment was validated above
396                        let state_value =
397                            u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
398                        tracing::debug!("  State value: {}", state_value);
399                        // State::Activated = 2
400                        if state_value == 2 {
401                            tracing::debug!("  -> Window is ACTIVATED");
402                            // Locate pending toplevel by foreign_id
403                            if let Some((_, pending)) = state
404                                .toplevels
405                                .iter_mut()
406                                .find(|(h, _)| h.id().protocol_id() == foreign_id)
407                            {
408                                pending.is_activated = true;
409                            }
410                        }
411                    }
412                }
413                other => {
414                    tracing::debug!("Cosmic event: {:?}", other);
415                }
416            }
417        }
418    }
419}
420
421// ============================================================================
422// Window Activation
423// ============================================================================
424
425/// State for window activation
426struct ActivationState {
427    #[allow(dead_code)]
428    list: ExtForeignToplevelListV1,
429    info: ZcosmicToplevelInfoV1,
430    manager: ZcosmicToplevelManagerV1,
431    seat: WlSeat,
432    pending: HashMap<u32, PendingToplevel>,
433    toplevels: Vec<(ExtForeignToplevelHandleV1, String)>, // handle + identifier
434    target_identifier: String,
435    cosmic_handle: Option<ZcosmicToplevelHandleV1>,
436    activated: bool,
437}
438
439impl ActivationState {
440    fn bind(globals: &GlobalList, qh: &QueueHandle<Self>, target: String) -> Result<Self> {
441        let list = globals
442            .bind::<ExtForeignToplevelListV1, _, _>(qh, 1..=1, ())
443            .map_err(|_| Error::MissingProtocol {
444                protocol: "ext_foreign_toplevel_list_v1",
445            })?;
446
447        let info = globals
448            .bind::<ZcosmicToplevelInfoV1, _, _>(qh, 2..=3, ())
449            .map_err(|_| Error::MissingProtocol {
450                protocol: "zcosmic_toplevel_info_v1",
451            })?;
452
453        let manager = globals
454            .bind::<ZcosmicToplevelManagerV1, _, _>(qh, 1..=4, ())
455            .map_err(|_| Error::MissingProtocol {
456                protocol: "zcosmic_toplevel_manager_v1",
457            })?;
458
459        let seat =
460            globals
461                .bind::<WlSeat, _, _>(qh, 1..=9, ())
462                .map_err(|_| Error::MissingProtocol {
463                    protocol: "wl_seat",
464                })?;
465
466        Ok(Self {
467            list,
468            info,
469            manager,
470            seat,
471            pending: HashMap::new(),
472            toplevels: Vec::new(),
473            target_identifier: target,
474            cosmic_handle: None,
475            activated: false,
476        })
477    }
478
479    /// Request cosmic handle for the target window
480    fn request_cosmic_handle(&mut self, qh: &QueueHandle<Self>) -> bool {
481        let target = self
482            .toplevels
483            .iter()
484            .find(|(_, id)| *id == self.target_identifier);
485
486        if let Some((handle, _)) = target {
487            tracing::debug!("Requesting cosmic handle for target");
488            let cosmic_handle = self.info.get_cosmic_toplevel(handle, qh, ());
489            self.cosmic_handle = Some(cosmic_handle);
490            true
491        } else {
492            tracing::warn!("Target window not found: {}", self.target_identifier);
493            false
494        }
495    }
496
497    /// Activate the window
498    fn activate(&mut self) {
499        if self.activated {
500            return;
501        }
502
503        if let Some(cosmic_handle) = &self.cosmic_handle {
504            tracing::info!("Activating window");
505            self.manager.activate(cosmic_handle, &self.seat);
506            self.activated = true;
507        }
508    }
509}
510
511/// Activate a window by its identifier
512pub fn activate_window(id: &WindowId) -> Result<()> {
513    let identifier = id.as_str();
514    let conn = Connection::connect_to_env().map_err(|e| Error::WaylandConnection(Box::new(e)))?;
515    let (globals, mut event_queue) = registry_queue_init::<ActivationState>(&conn)
516        .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
517    let qh = event_queue.handle();
518
519    let mut state = ActivationState::bind(&globals, &qh, identifier.to_string())?;
520
521    // First roundtrip: retrieve all toplevels (with timeout protection)
522    roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
523
524    // Request cosmic handle for target window
525    if !state.request_cosmic_handle(&qh) {
526        return Err(Error::WindowNotFound {
527            identifier: identifier.to_string(),
528        });
529    }
530
531    // Second roundtrip: wait for cosmic handle (with timeout protection)
532    roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
533
534    // Activate target window
535    state.activate();
536
537    // Third roundtrip: ensure activation is processed (with timeout protection)
538    roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
539
540    if state.activated {
541        tracing::info!("Window activated successfully");
542        Ok(())
543    } else {
544        Err(Error::ActivationFailed(
545            "Failed to activate window".to_string(),
546        ))
547    }
548}
549
550// Dispatch implementations for ActivationState
551
552impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for ActivationState {
553    fn event(
554        _state: &mut Self,
555        _proxy: &wl_registry::WlRegistry,
556        _event: wl_registry::Event,
557        _data: &GlobalListContents,
558        _conn: &Connection,
559        _qh: &QueueHandle<Self>,
560    ) {
561    }
562}
563
564impl Dispatch<ExtForeignToplevelListV1, ()> for ActivationState {
565    fn event(
566        state: &mut Self,
567        _proxy: &ExtForeignToplevelListV1,
568        event: ext_foreign_toplevel_list_v1::Event,
569        _data: &(),
570        _conn: &Connection,
571        _qh: &QueueHandle<Self>,
572    ) {
573        if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event {
574            let id = toplevel.id().protocol_id();
575            state.pending.insert(id, PendingToplevel::default());
576        }
577    }
578
579    wayland_client::event_created_child!(ActivationState, ExtForeignToplevelListV1, [
580        ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ())
581    ]);
582}
583
584impl Dispatch<ExtForeignToplevelHandleV1, ()> for ActivationState {
585    fn event(
586        state: &mut Self,
587        proxy: &ExtForeignToplevelHandleV1,
588        event: ext_foreign_toplevel_handle_v1::Event,
589        _data: &(),
590        _conn: &Connection,
591        _qh: &QueueHandle<Self>,
592    ) {
593        let id = proxy.id().protocol_id();
594
595        match event {
596            ext_foreign_toplevel_handle_v1::Event::Identifier { identifier } => {
597                if let Some(pending) = state.pending.get_mut(&id) {
598                    pending.identifier = Some(identifier);
599                }
600            }
601            ext_foreign_toplevel_handle_v1::Event::AppId { app_id } => {
602                if let Some(pending) = state.pending.get_mut(&id) {
603                    pending.app_id = Some(app_id);
604                }
605            }
606            ext_foreign_toplevel_handle_v1::Event::Done => {
607                if let Some(pending) = state.pending.remove(&id)
608                    && let Some(identifier) = pending.identifier
609                {
610                    state.toplevels.push((proxy.clone(), identifier));
611                }
612            }
613            _ => {}
614        }
615    }
616}
617
618impl Dispatch<ZcosmicToplevelInfoV1, ()> for ActivationState {
619    fn event(
620        _state: &mut Self,
621        _proxy: &ZcosmicToplevelInfoV1,
622        _event: zcosmic_toplevel_info_v1::Event,
623        _data: &(),
624        _conn: &Connection,
625        _qh: &QueueHandle<Self>,
626    ) {
627    }
628}
629
630impl Dispatch<ZcosmicToplevelHandleV1, ()> for ActivationState {
631    fn event(
632        _state: &mut Self,
633        _proxy: &ZcosmicToplevelHandleV1,
634        _event: zcosmic_toplevel_handle_v1::Event,
635        _data: &(),
636        _conn: &Connection,
637        _qh: &QueueHandle<Self>,
638    ) {
639    }
640}
641
642impl Dispatch<ZcosmicToplevelManagerV1, ()> for ActivationState {
643    fn event(
644        _state: &mut Self,
645        _proxy: &ZcosmicToplevelManagerV1,
646        _event: cosmic_client_toolkit::cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1::Event,
647        _data: &(),
648        _conn: &Connection,
649        _qh: &QueueHandle<Self>,
650    ) {
651    }
652}
653
654impl Dispatch<WlSeat, ()> for ActivationState {
655    fn event(
656        _state: &mut Self,
657        _proxy: &WlSeat,
658        _event: wayland_client::protocol::wl_seat::Event,
659        _data: &(),
660        _conn: &Connection,
661        _qh: &QueueHandle<Self>,
662    ) {
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_window_id_creation() {
672        let id = WindowId::new("test-123");
673        assert_eq!(id.as_str(), "test-123");
674    }
675}