1use 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
29fn 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
38fn get_wayland_timeout() -> Duration {
40 static TIMEOUT: OnceLock<Duration> = OnceLock::new();
41 *TIMEOUT.get_or_init(wayland_timeout)
42}
43
44fn 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 conn.flush()
61 .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
62
63 event_queue
65 .dispatch_pending(state)
66 .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
67
68 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 let remaining = timeout - elapsed;
79 let timeout_ms = remaining.as_millis().min(100) as i32;
80
81 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 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 event_queue
108 .dispatch_pending(state)
109 .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
110
111 event_queue
113 .roundtrip(state)
114 .map_err(|e| Error::WaylandConnection(Box::new(e)))?;
115 return Ok(());
116 }
117 }
118}
119
120#[derive(Debug, Default)]
122struct PendingToplevel {
123 identifier: Option<String>,
124 app_id: Option<String>,
125 title: Option<String>,
126 is_activated: bool,
127}
128
129struct EnumerationState {
135 #[allow(dead_code)]
136 list: ExtForeignToplevelListV1,
137 info: ZcosmicToplevelInfoV1,
138 pending: HashMap<u32, PendingToplevel>,
139 cosmic_pending: HashMap<u32, u32>, 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
167pub 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 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 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 roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
202 tracing::debug!("enumerate_windows: roundtrip 2 complete (cosmic state events)");
203
204 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 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 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 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
271impl 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 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 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 for chunk in state_bytes.chunks_exact(4) {
394 let state_value =
397 u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
398 tracing::debug!(" State value: {}", state_value);
399 if state_value == 2 {
401 tracing::debug!(" -> Window is ACTIVATED");
402 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
421struct 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)>, 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 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 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
511pub 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 roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
523
524 if !state.request_cosmic_handle(&qh) {
526 return Err(Error::WindowNotFound {
527 identifier: identifier.to_string(),
528 });
529 }
530
531 roundtrip_with_timeout(&conn, &mut event_queue, &mut state)?;
533
534 state.activate();
536
537 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
550impl 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}