1use crate::data::{SortConfig, collect_data};
2use crate::next::{find_next_client, find_next_workspace};
3#[cfg(feature = "live_windows")]
4use crate::shared::refresh_captures;
5use crate::shared::{Workspaces, WorkspacesInit, WorkspacesInput};
6use core_lib::{Active, ByFirst, Direction, HyprlandData, SWITCH_NAMESPACE};
7use exec_lib::switch::{switch_client, switch_workspace};
8#[cfg(feature = "live_windows")]
9use exec_lib::wayland_capture::CaptureManager;
10use gtk4_layer_shell::{KeyboardMode, Layer, LayerShell};
11use regex::Regex;
12use relm4::adw::glib::ControlFlow;
13use relm4::adw::gtk;
14use relm4::adw::gtk::glib;
15use relm4::adw::prelude::*;
16use relm4::gtk::gdk::Key;
17use relm4::gtk::{EventControllerKey, Orientation, SelectionMode};
18use relm4::prelude::*;
19use std::time::Duration;
20use tracing::{debug, error, trace, warn};
21
22const KILL_TIMEOUT: Duration = Duration::from_millis(200);
23#[cfg(feature = "live_windows")]
24const THUMBNAIL_BURST_MS: u64 = 8;
25
26#[derive(Debug)]
27pub struct SwitchRoot {
28 general: config_lib::WindowsGeneral,
29 switch: config_lib::Switch,
30 open: bool,
31 data: SwitchData,
32 window: gtk::ApplicationWindow,
34 controller: Option<gtk::EventController>,
35 remove_html: Regex,
37 items: FactoryVecDeque<Workspaces>,
39 clients_only: FactoryVecDeque<crate::switch::clients::Clients>,
41
42 #[cfg(feature = "live_windows")]
43 live_thumbnails: bool,
44 #[cfg(feature = "live_windows")]
45 live_thumbnails_icons: bool,
46
47 #[cfg(feature = "live_windows")]
48 capture_manager: Option<CaptureManager>,
49 #[cfg(feature = "live_windows")]
50 timer_handle: Option<glib::SourceId>,
51 #[cfg(feature = "live_windows")]
52 thumbnail_refresh_ms: u64,
53 #[cfg(feature = "live_windows")]
54 thumbnail_burst: bool,
55}
56
57#[derive(Debug)]
58pub enum SwitchRootInput {
59 SetSwitch(config_lib::Switch),
60 SetGeneral(config_lib::WindowsGeneral),
61 OpenSwitch(Direction),
62 Switch(Direction),
63 CloseSwitch(bool),
64 CloseCurrentItem,
65 ReloadSwitch,
66 #[cfg(feature = "live_windows")]
67 RefreshThumbnails,
68}
69
70#[derive(Debug)]
71pub struct SwitchRootInit {
72 pub general: config_lib::WindowsGeneral,
73 pub switch: config_lib::Switch,
74 pub thumbnail_refresh_ms: u64,
75}
76
77#[derive(Debug)]
78pub enum SwitchRootOutput {}
79
80#[relm4::component(pub)]
81impl SimpleComponent for SwitchRoot {
82 type Init = SwitchRootInit;
83 type Input = SwitchRootInput;
84 type Output = SwitchRootOutput;
85
86 view! {
87 #[root]
88 gtk::ApplicationWindow {
89 set_css_classes: &["window"],
90 set_default_size: (100, 100),
91 match model.switch.switch_workspaces {
92 true => {
93 #[local_ref]
94 itemsw -> gtk::FlowBox {
95 set_css_classes: &["monitor"],
96 set_selection_mode: SelectionMode::None,
97 set_orientation: Orientation::Horizontal,
98 #[watch]
99 set_max_children_per_line: u32::from(model.general.items_per_row),
100 #[watch]
101 set_min_children_per_line: u32::from(model.general.items_per_row),
102 }
103 }
104 false => {
105 #[local_ref]
106 clients_only_w -> gtk::FlowBox {
107 set_css_classes: &["monitor"],
108 set_selection_mode: SelectionMode::None,
109 set_orientation: Orientation::Horizontal,
110 #[watch]
111 set_max_children_per_line: u32::from(model.general.items_per_row),
112 #[watch]
113 set_min_children_per_line: u32::from(model.general.items_per_row),
114 }
115 }
116 }
117 }
118 }
119
120 fn init(
121 init: Self::Init,
122 root: Self::Root,
123 sender: ComponentSender<Self>,
124 ) -> ComponentParts<Self> {
125 trace!("Initializing SwitchRoot");
126
127 let items: FactoryVecDeque<Workspaces> = FactoryVecDeque::builder()
128 .launch(gtk::FlowBox::default())
129 .detach();
130
131 let clients_only: FactoryVecDeque<crate::switch::clients::Clients> =
132 FactoryVecDeque::builder()
133 .launch(gtk::FlowBox::default())
134 .detach();
135
136 let model = Self {
137 general: init.general,
138 switch: init.switch,
139 open: false,
140 window: root.clone(),
141 controller: None,
142 remove_html: Regex::new(r"<[^>]*>").expect("invalid regex"),
143 data: SwitchData::default(),
144 items,
145 clients_only,
146 #[cfg(feature = "live_windows")]
147 live_thumbnails: std::env::var_os("HYPRSHELL_EXPERIMENTAL").is_some_and(|v| v == "1"),
148 #[cfg(feature = "live_windows")]
149 live_thumbnails_icons: std::env::var_os("HYPRSHELL_EXPERIMENTAL_ICONS")
150 .is_none_or(|v| v != "0"),
151 #[cfg(feature = "live_windows")]
152 capture_manager: None,
153 #[cfg(feature = "live_windows")]
154 timer_handle: None,
155 #[cfg(feature = "live_windows")]
156 thumbnail_refresh_ms: init.thumbnail_refresh_ms,
157 #[cfg(feature = "live_windows")]
158 thumbnail_burst: false,
159 };
160
161 let itemsw: gtk::FlowBox = model.items.widget().clone();
162 let clients_only_w: gtk::FlowBox = model.clients_only.widget().clone();
163 let widgets = view_output!();
164
165 let window = &root;
166 window.init_layer_shell();
167 window.set_namespace(Some(SWITCH_NAMESPACE));
168 window.set_layer(Layer::Overlay);
169 window.set_keyboard_mode(KeyboardMode::Exclusive);
170 sender
171 .input_sender()
172 .emit(SwitchRootInput::SetSwitch(model.switch.clone()));
173 ComponentParts { model, widgets }
174 }
175
176 fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
177 trace!("switch::root::update: {message:?}");
178 match message {
179 SwitchRootInput::SetSwitch(switch) => {
180 self.switch = switch;
181 self.setup_keyboard_controller(&sender);
182 }
183 SwitchRootInput::SetGeneral(general) => {
184 self.general = general;
185 self.setup_keyboard_controller(&sender);
186 }
187 SwitchRootInput::OpenSwitch(direction) => {
188 if self.open {
189 sender
190 .input_sender()
191 .emit(SwitchRootInput::Switch(direction));
192 } else {
193 self.open = true;
194 self.open_switch(direction, &sender);
195 }
196 }
197 SwitchRootInput::Switch(direction) => {
198 if self.open {
199 self.navigate(direction);
200 } else {
201 trace!("not open");
202 }
203 }
204 SwitchRootInput::CloseSwitch(do_switch) => {
205 if self.open {
206 self.open = false;
207 self.close_switch(do_switch);
208 } else {
209 trace!("not open");
210 }
211 }
212 SwitchRootInput::CloseCurrentItem => {
213 if self.open {
214 self.close_item();
215 } else {
216 trace!("not open");
217 }
218 sender.input_sender().emit(SwitchRootInput::ReloadSwitch);
219 }
220 SwitchRootInput::ReloadSwitch => {
221 if self.open {
222 self.reload_switch();
223 } else {
224 trace!("not open");
225 }
226 }
227 #[cfg(feature = "live_windows")]
228 SwitchRootInput::RefreshThumbnails => self.refresh_thumbnails(&sender),
229 }
230 }
231}
232
233impl SwitchRoot {
234 fn setup_keyboard_controller(&mut self, sender: &ComponentSender<Self>) {
235 if let Some(k) = Key::from_name(self.switch.key.to_string()) {
237 if let Some(kk) = Key::from_name(self.switch.kill_key.to_string()) {
238 let key_controller = EventControllerKey::new();
239 let sender_2 = sender.clone();
240 key_controller.connect_key_pressed(move |_, key, _, _| {
241 trace!("Key pressed: {:?}", key);
242 handle_key(key, k, kk, &sender_2.clone())
243 });
244 if let Some(controller) = self.controller.take() {
245 self.window.remove_controller(&controller);
246 }
247 self.window.add_controller(key_controller);
248 } else {
249 error!("Invalid kill key name: {}", self.switch.kill_key);
250 }
251 } else {
252 error!("Invalid key name: {}", self.switch.key);
253 }
254 }
255
256 #[allow(unused_variables)]
257 fn open_switch(&mut self, direction: Direction, sender: &ComponentSender<Self>) {
258 let (hypr_data, active_prev) = match collect_data(&SortConfig {
259 filter_current_monitor: self.switch.filter_by_current_monitor,
260 filter_current_workspace: self.switch.filter_by_current_workspace,
261 filter_same_class: self.switch.filter_by_same_class,
262 sort_recent: true,
263 exclude_workspaces: if self.switch.exclude_workspaces.is_empty() {
264 None
265 } else {
266 Some(self.switch.exclude_workspaces.clone())
267 },
268 }) {
269 Ok(data) => data,
270 Err(e) => {
271 error!("Failed to collect data: {}", e);
272 return;
273 }
274 };
275
276 let active = if self.switch.switch_workspaces {
277 find_next_workspace(
278 direction,
279 true,
280 &hypr_data,
281 active_prev,
282 self.general.items_per_row,
283 )
284 } else {
285 find_next_client(
286 direction,
287 true,
288 &hypr_data,
289 active_prev,
290 self.general.items_per_row,
291 )
292 };
293 self.data = SwitchData {
294 active,
295 hypr_data: hypr_data.clone(),
296 };
297
298 trace!("Showing window {:?}", self.window.id());
299 self.window.set_visible(true);
300 self.window.grab_focus();
301
302 if self.switch.switch_workspaces {
303 self.populate_workspace_mode(&hypr_data, self.general.scale, self.data.active);
304 } else {
305 self.populate_clients_only_mode(&hypr_data, self.general.scale, self.data.active);
306 }
307
308 #[cfg(feature = "live_windows")]
309 if self.live_thumbnails {
310 self.capture_manager = CaptureManager::new().map_err(|e| error!("{e}")).ok();
311 self.thumbnail_burst = true;
312 let sender = sender.clone();
313 self.timer_handle = Some(glib::timeout_add_local(
314 Duration::from_millis(THUMBNAIL_BURST_MS),
315 move || {
316 if sender
317 .input_sender()
318 .send(SwitchRootInput::RefreshThumbnails)
319 .is_err()
320 {
321 warn!("Failed to send refresh thumbnails");
322 return ControlFlow::Break;
323 }
324 ControlFlow::Continue
325 },
326 ));
327 }
328 }
329
330 fn populate_workspace_mode(&mut self, hypr_data: &HyprlandData, scale: f64, active: Active) {
331 let mut lock = self.items.guard();
332 lock.clear();
333
334 for (wid, workspace_data) in &hypr_data.workspaces {
335 if !workspace_data.any_client_enabled {
336 trace!("skipping workspace {} with no enabled clients", wid);
337 continue;
338 }
339 let workspace_clients: Vec<_> = hypr_data
341 .clients
342 .iter()
343 .filter(|(_, client)| client.workspace == *wid && client.enabled)
344 .map(|(id, data)| (*id, data.clone()))
345 .collect();
346
347 let Some(monitor) = hypr_data.monitors.find_by_first(&workspace_data.monitor) else {
348 error!(
349 "Workspace {} has invalid monitor {}",
350 wid, workspace_data.monitor
351 );
352 continue;
353 };
354 lock.push_back(WorkspacesInit {
355 monitor_data: monitor.clone(),
356 remove_html: self.remove_html.clone(),
357 id: *wid,
358 data: workspace_data.clone(),
359 scale,
360 clients: workspace_clients,
361 #[cfg(feature = "live_windows")]
362 live_thumbnails: self.live_thumbnails,
363 #[cfg(feature = "live_windows")]
364 live_thumbnails_icons: self.live_thumbnails_icons,
365 });
366 }
367 drop(lock);
368
369 for (idx, item) in self.items.iter().enumerate() {
371 if item.workspace_id == active.workspace {
372 self.items.send(idx, WorkspacesInput::SetActive(true));
373 break;
374 }
375 }
376 }
377
378 fn populate_clients_only_mode(&mut self, hypr_data: &HyprlandData, scale: f64, active: Active) {
379 let mut lock = self.clients_only.guard();
380 lock.clear();
381
382 for (id, client) in &hypr_data.clients {
383 if !client.enabled {
384 continue;
385 }
386 let Some(monitor) = hypr_data.monitors.find_by_first(&client.monitor) else {
387 error!("Client {} has invalid monitor {}", id, client.monitor);
388 continue;
389 };
390 lock.push_back(crate::switch::clients::ClientsInit {
391 id: *id,
392 scale,
393 monitor_data: monitor.clone(),
394 data: client.clone(),
395 #[cfg(feature = "live_windows")]
396 live_thumbnails: self.live_thumbnails,
397 });
398 }
399 drop(lock);
400
401 if let Some(active_id) = active.client {
403 for (idx, item) in self.clients_only.iter().enumerate() {
404 if item.id == active_id {
405 self.clients_only
406 .send(idx, crate::switch::clients::ClientsInput::SetActive(true));
407 break;
408 }
409 }
410 }
411 }
412
413 fn navigate(&mut self, direction: Direction) {
414 let new_active = if self.switch.switch_workspaces {
415 find_next_workspace(
416 direction,
417 true,
418 &self.data.hypr_data,
419 self.data.active,
420 self.general.items_per_row,
421 )
422 } else {
423 find_next_client(
424 direction,
425 true,
426 &self.data.hypr_data,
427 self.data.active,
428 self.general.items_per_row,
429 )
430 };
431
432 let old_active = self.data.active;
433 self.data.active = new_active;
434
435 if self.switch.switch_workspaces {
436 self.update_workspace_active(old_active, new_active);
437 } else {
438 self.update_clients_only_active(old_active, new_active);
439 }
440 }
441
442 fn update_workspace_active(&self, old_active: Active, new_active: Active) {
443 if old_active.workspace != new_active.workspace {
445 for (idx, item) in self.items.iter().enumerate() {
446 if item.workspace_id == old_active.workspace {
447 self.items.send(idx, WorkspacesInput::SetActive(false));
448 }
449 if item.workspace_id == new_active.workspace {
450 self.items.send(idx, WorkspacesInput::SetActive(true));
451 if let Some(cid) = new_active.client {
452 self.items.send(idx, WorkspacesInput::SetActiveClient(cid));
453 }
454 }
455 }
456 }
457 }
458
459 fn update_clients_only_active(&self, old_active: Active, new_active: Active) {
460 if let Some(old_id) = old_active.client {
462 for (idx, item) in self.clients_only.iter().enumerate() {
463 if item.id == old_id {
464 self.clients_only
465 .send(idx, crate::switch::clients::ClientsInput::SetActive(false));
466 break;
467 }
468 }
469 }
470
471 if let Some(new_id) = new_active.client {
473 for (idx, item) in self.clients_only.iter().enumerate() {
474 if item.id == new_id {
475 self.clients_only
476 .send(idx, crate::switch::clients::ClientsInput::SetActive(true));
477 break;
478 }
479 }
480 }
481 }
482
483 fn close_switch(&mut self, do_switch: bool) {
484 trace!("Hiding window {:?}", self.window.id());
485 self.window.set_visible(false);
486
487 {
489 let mut lock = self.items.guard();
490 lock.clear();
491 }
492 {
493 let mut lock = self.clients_only.guard();
494 lock.clear();
495 }
496
497 if do_switch {
498 if let Some(id) = self.data.active.client {
499 debug!(
500 "Switching to client {}",
501 self.data
502 .hypr_data
503 .clients
504 .iter()
505 .find(|(cid, _)| *cid == id)
506 .map_or_else(|| "<Unknown>".to_string(), |(_, c)| c.title.clone())
507 );
508 glib::idle_add_local(move || {
510 if let Err(e) = switch_client(id) {
511 warn!("Failed to switch to client {id:?}: {e}");
512 }
513 ControlFlow::Break
514 });
515 } else {
516 let id = self.data.active.workspace;
517 debug!(
518 "Switching to workspace {}",
519 self.data
520 .hypr_data
521 .workspaces
522 .iter()
523 .find(|(wid, _)| *wid == id)
524 .map_or_else(|| "<Unknown>".to_string(), |(_, w)| w.name.clone())
525 );
526 glib::idle_add_local(move || {
527 if let Err(e) = switch_workspace(id) {
528 tracing::warn!("Failed to switch to workspace {id:?}: {e}");
529 }
530 ControlFlow::Break
531 });
532 }
533 }
534 #[cfg(feature = "live_windows")]
535 {
536 if let Some(handle) = self.timer_handle.take() {
537 handle.remove();
538 }
539 self.capture_manager = None;
540 }
541 }
542
543 fn close_item(&self) {
544 if self.switch.switch_workspaces {
545 self.kill_workspace_clients();
546 } else {
547 self.kill_active_client();
548 }
549 }
550
551 fn kill_active_client(&self) {
552 if let Some(id) = self.data.active.client
553 && let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT)
554 {
555 tracing::warn!("Failed to kill client {id}: {e}");
557 }
558 }
559
560 fn kill_workspace_clients(&self) {
561 let workspace_id = self.data.active.workspace;
562 debug!(
563 "Killing all clients in workspace {}",
564 self.data
565 .hypr_data
566 .workspaces
567 .iter()
568 .find(|(wid, _)| *wid == workspace_id)
569 .map_or_else(|| workspace_id.to_string(), |(_, w)| w.name.clone())
570 );
571
572 let clients_to_kill: Vec<_> = self
573 .data
574 .hypr_data
575 .clients
576 .iter()
577 .filter(|(_, client)| client.workspace == workspace_id)
578 .map(|(id, _)| *id)
579 .collect();
580
581 for client_id in clients_to_kill {
582 if let Err(e) = exec_lib::kill::kill_client_blocking(client_id, KILL_TIMEOUT) {
583 tracing::warn!("Failed to kill client {client_id}: {e}");
585 }
586 }
587 }
588
589 fn reload_switch(&mut self) {
590 let (hypr_data, _) = match collect_data(&SortConfig {
591 filter_current_monitor: self.switch.filter_by_current_monitor,
592 filter_current_workspace: self.switch.filter_by_current_workspace,
593 filter_same_class: self.switch.filter_by_same_class,
594 sort_recent: true,
595 exclude_workspaces: if self.switch.exclude_workspaces.is_empty() {
596 None
597 } else {
598 Some(self.switch.exclude_workspaces.clone())
599 },
600 }) {
601 Ok(data) => data,
602 Err(e) => {
603 error!("Failed to collect data: {}", e);
604 return;
605 }
606 };
607
608 while match self.data.active {
609 Active {
610 client: Some(id), ..
611 } => hypr_data.clients.find_by_first(&id).is_none(),
612 Active { workspace: id, .. } => hypr_data.workspaces.find_by_first(&id).is_none(),
613 } {
614 self.data.active = if self.switch.switch_workspaces {
615 find_next_workspace(
616 Direction::Right,
617 true,
618 &hypr_data,
619 self.data.active,
620 self.general.items_per_row,
621 )
622 } else {
623 find_next_client(
624 Direction::Right,
625 true,
626 &hypr_data,
627 self.data.active,
628 self.general.items_per_row,
629 )
630 };
631 }
632
633 self.data = SwitchData {
634 active: self.data.active,
635 hypr_data: hypr_data.clone(),
636 };
637
638 if self.switch.switch_workspaces {
639 self.populate_workspace_mode(&hypr_data, self.general.scale, self.data.active);
640 } else {
641 self.populate_clients_only_mode(&hypr_data, self.general.scale, self.data.active);
642 }
643 }
644
645 #[cfg(feature = "live_windows")]
646 fn refresh_thumbnails(&mut self, sender: &ComponentSender<Self>) {
647 use relm4::adw::gdk::Display;
648 let Some(mgr) = &mut self.capture_manager else {
649 return;
650 };
651 let Some(display) = Display::default() else {
652 return;
653 };
654 let mut captures = refresh_captures(mgr, &display, !self.thumbnail_burst);
655 if self.thumbnail_burst && mgr.pending_count() == 0 {
656 self.thumbnail_burst = false;
657 if let Some(h) = self.timer_handle.take() {
660 h.remove();
661 }
662 if self.thumbnail_refresh_ms != 0 {
664 trace!("Switching from thumbnail_burst refresh to slow refresh");
665 let sender = sender.clone();
666 self.timer_handle = Some(glib::timeout_add_local(
667 Duration::from_millis(self.thumbnail_refresh_ms),
668 move || {
669 if sender
670 .input_sender()
671 .send(SwitchRootInput::RefreshThumbnails)
672 .is_err()
673 {
674 warn!("Failed to send refresh thumbnails");
675 return ControlFlow::Break;
676 }
677 ControlFlow::Continue
678 },
679 ));
680 } else {
681 trace!("All initial thumbnail captures loaded");
682 }
683 }
684
685 if self.switch.switch_workspaces {
686 for (client_id, texture) in captures {
687 for (idx, _) in self.items.iter().enumerate() {
688 self.items.send(
689 idx,
690 WorkspacesInput::UpdateClientThumbnail(client_id, texture.clone()),
691 );
692 }
693 }
694 } else {
695 for (idx, item) in self.clients_only.iter().enumerate() {
696 if let Some(texture) = captures.remove(&item.id) {
697 self.clients_only.send(
698 idx,
699 crate::switch::clients::ClientsInput::UpdateThumbnail(texture),
700 );
701 }
702 }
703 }
704 }
705}
706
707fn handle_key(
708 key: Key,
709 s_key: Key,
710 kill_key: Key,
711 event_sender: &ComponentSender<SwitchRoot>,
712) -> glib::Propagation {
713 match key {
714 Key::Escape => {
715 event_sender
716 .input_sender()
717 .emit(SwitchRootInput::CloseSwitch(false));
718 glib::Propagation::Stop
719 }
720 k if k == s_key || k == Key::l || k == Key::Right => {
721 event_sender
722 .input_sender()
723 .emit(SwitchRootInput::Switch(Direction::Right));
724 glib::Propagation::Stop
725 }
726 Key::ISO_Left_Tab | Key::grave | Key::dead_grave | Key::h | Key::Left => {
727 event_sender
728 .input_sender()
729 .emit(SwitchRootInput::Switch(Direction::Left));
730 glib::Propagation::Stop
731 }
732 Key::j | Key::Down => {
733 event_sender
734 .input_sender()
735 .emit(SwitchRootInput::Switch(Direction::Down));
736 glib::Propagation::Stop
737 }
738 Key::k | Key::Up => {
739 event_sender
740 .input_sender()
741 .emit(SwitchRootInput::Switch(Direction::Up));
742 glib::Propagation::Stop
743 }
744 k if k == kill_key || k == Key::Delete => {
745 event_sender
746 .input_sender()
747 .emit(SwitchRootInput::CloseCurrentItem);
748 glib::Propagation::Stop
749 }
750 _ => glib::Propagation::Proceed,
751 }
752}
753
754#[derive(Debug)]
755pub struct SwitchData {
756 pub active: Active,
757 pub hypr_data: HyprlandData,
758}
759
760impl Default for SwitchData {
761 fn default() -> Self {
762 Self {
763 active: Active {
764 client: None,
765 workspace: -1,
766 monitor: -1,
767 },
768 hypr_data: HyprlandData::default(),
769 }
770 }
771}