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};
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 sender.input(SwitchRootInput::RefreshThumbnails);
317 ControlFlow::Continue
318 },
319 ));
320 }
321 }
322
323 fn populate_workspace_mode(&mut self, hypr_data: &HyprlandData, scale: f64, active: Active) {
324 let mut lock = self.items.guard();
325 lock.clear();
326
327 for (wid, workspace_data) in &hypr_data.workspaces {
328 if !workspace_data.any_client_enabled {
329 trace!("skipping workspace {} with no enabled clients", wid);
330 continue;
331 }
332 let workspace_clients: Vec<_> = hypr_data
334 .clients
335 .iter()
336 .filter(|(_, client)| client.workspace == *wid && client.enabled)
337 .map(|(id, data)| (*id, data.clone()))
338 .collect();
339
340 let Some(monitor) = hypr_data.monitors.find_by_first(&workspace_data.monitor) else {
341 error!(
342 "Workspace {} has invalid monitor {}",
343 wid, workspace_data.monitor
344 );
345 continue;
346 };
347 lock.push_back(WorkspacesInit {
348 monitor_data: monitor.clone(),
349 remove_html: self.remove_html.clone(),
350 id: *wid,
351 data: workspace_data.clone(),
352 scale,
353 clients: workspace_clients,
354 #[cfg(feature = "live_windows")]
355 live_thumbnails: self.live_thumbnails,
356 #[cfg(feature = "live_windows")]
357 live_thumbnails_icons: self.live_thumbnails_icons,
358 });
359 }
360 drop(lock);
361
362 for (idx, item) in self.items.iter().enumerate() {
364 if item.workspace_id == active.workspace {
365 self.items.send(idx, WorkspacesInput::SetActive(true));
366 break;
367 }
368 }
369 }
370
371 fn populate_clients_only_mode(&mut self, hypr_data: &HyprlandData, scale: f64, active: Active) {
372 let mut lock = self.clients_only.guard();
373 lock.clear();
374
375 for (id, client) in &hypr_data.clients {
376 if !client.enabled {
377 continue;
378 }
379 let Some(monitor) = hypr_data.monitors.find_by_first(&client.monitor) else {
380 error!("Client {} has invalid monitor {}", id, client.monitor);
381 continue;
382 };
383 lock.push_back(crate::switch::clients::ClientsInit {
384 id: *id,
385 scale,
386 monitor_data: monitor.clone(),
387 data: client.clone(),
388 #[cfg(feature = "live_windows")]
389 live_thumbnails: self.live_thumbnails,
390 });
391 }
392 drop(lock);
393
394 if let Some(active_id) = active.client {
396 for (idx, item) in self.clients_only.iter().enumerate() {
397 if item.id == active_id {
398 self.clients_only
399 .send(idx, crate::switch::clients::ClientsInput::SetActive(true));
400 break;
401 }
402 }
403 }
404 }
405
406 fn navigate(&mut self, direction: Direction) {
407 let new_active = if self.switch.switch_workspaces {
408 find_next_workspace(
409 direction,
410 true,
411 &self.data.hypr_data,
412 self.data.active,
413 self.general.items_per_row,
414 )
415 } else {
416 find_next_client(
417 direction,
418 true,
419 &self.data.hypr_data,
420 self.data.active,
421 self.general.items_per_row,
422 )
423 };
424
425 let old_active = self.data.active;
426 self.data.active = new_active;
427
428 if self.switch.switch_workspaces {
429 self.update_workspace_active(old_active, new_active);
430 } else {
431 self.update_clients_only_active(old_active, new_active);
432 }
433 }
434
435 fn update_workspace_active(&self, old_active: Active, new_active: Active) {
436 if old_active.workspace != new_active.workspace {
438 for (idx, item) in self.items.iter().enumerate() {
439 if item.workspace_id == old_active.workspace {
440 self.items.send(idx, WorkspacesInput::SetActive(false));
441 }
442 if item.workspace_id == new_active.workspace {
443 self.items.send(idx, WorkspacesInput::SetActive(true));
444 if let Some(cid) = new_active.client {
445 self.items.send(idx, WorkspacesInput::SetActiveClient(cid));
446 }
447 }
448 }
449 }
450 }
451
452 fn update_clients_only_active(&self, old_active: Active, new_active: Active) {
453 if let Some(old_id) = old_active.client {
455 for (idx, item) in self.clients_only.iter().enumerate() {
456 if item.id == old_id {
457 self.clients_only
458 .send(idx, crate::switch::clients::ClientsInput::SetActive(false));
459 break;
460 }
461 }
462 }
463
464 if let Some(new_id) = new_active.client {
466 for (idx, item) in self.clients_only.iter().enumerate() {
467 if item.id == new_id {
468 self.clients_only
469 .send(idx, crate::switch::clients::ClientsInput::SetActive(true));
470 break;
471 }
472 }
473 }
474 }
475
476 fn close_switch(&mut self, do_switch: bool) {
477 trace!("Hiding window {:?}", self.window.id());
478 self.window.set_visible(false);
479
480 {
482 let mut lock = self.items.guard();
483 lock.clear();
484 }
485 {
486 let mut lock = self.clients_only.guard();
487 lock.clear();
488 }
489
490 if do_switch {
491 if let Some(id) = self.data.active.client {
492 debug!(
493 "Switching to client {}",
494 self.data
495 .hypr_data
496 .clients
497 .iter()
498 .find(|(cid, _)| *cid == id)
499 .map_or_else(|| "<Unknown>".to_string(), |(_, c)| c.title.clone())
500 );
501 glib::idle_add_local(move || {
503 if let Err(e) = switch_client(id) {
504 tracing::warn!("Failed to switch to client {id:?}: {e}");
505 }
506 ControlFlow::Break
507 });
508 } else {
509 let id = self.data.active.workspace;
510 debug!(
511 "Switching to workspace {}",
512 self.data
513 .hypr_data
514 .workspaces
515 .iter()
516 .find(|(wid, _)| *wid == id)
517 .map_or_else(|| "<Unknown>".to_string(), |(_, w)| w.name.clone())
518 );
519 glib::idle_add_local(move || {
520 if let Err(e) = switch_workspace(id) {
521 tracing::warn!("Failed to switch to workspace {id:?}: {e}");
522 }
523 ControlFlow::Break
524 });
525 }
526 }
527 #[cfg(feature = "live_windows")]
528 {
529 if let Some(handle) = self.timer_handle.take() {
530 handle.remove();
531 }
532 self.capture_manager = None;
533 }
534 }
535
536 fn close_item(&self) {
537 if self.switch.switch_workspaces {
538 self.kill_workspace_clients();
539 } else {
540 self.kill_active_client();
541 }
542 }
543
544 fn kill_active_client(&self) {
545 if let Some(id) = self.data.active.client
546 && let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT)
547 {
548 tracing::warn!("Failed to kill client {id}: {e}");
550 }
551 }
552
553 fn kill_workspace_clients(&self) {
554 let workspace_id = self.data.active.workspace;
555 debug!(
556 "Killing all clients in workspace {}",
557 self.data
558 .hypr_data
559 .workspaces
560 .iter()
561 .find(|(wid, _)| *wid == workspace_id)
562 .map_or_else(|| workspace_id.to_string(), |(_, w)| w.name.clone())
563 );
564
565 let clients_to_kill: Vec<_> = self
566 .data
567 .hypr_data
568 .clients
569 .iter()
570 .filter(|(_, client)| client.workspace == workspace_id)
571 .map(|(id, _)| *id)
572 .collect();
573
574 for client_id in clients_to_kill {
575 if let Err(e) = exec_lib::kill::kill_client_blocking(client_id, KILL_TIMEOUT) {
576 tracing::warn!("Failed to kill client {client_id}: {e}");
578 }
579 }
580 }
581
582 fn reload_switch(&mut self) {
583 let (hypr_data, _) = match collect_data(&SortConfig {
584 filter_current_monitor: self.switch.filter_by_current_monitor,
585 filter_current_workspace: self.switch.filter_by_current_workspace,
586 filter_same_class: self.switch.filter_by_same_class,
587 sort_recent: true,
588 exclude_workspaces: if self.switch.exclude_workspaces.is_empty() {
589 None
590 } else {
591 Some(self.switch.exclude_workspaces.clone())
592 },
593 }) {
594 Ok(data) => data,
595 Err(e) => {
596 error!("Failed to collect data: {}", e);
597 return;
598 }
599 };
600
601 while match self.data.active {
602 Active {
603 client: Some(id), ..
604 } => hypr_data.clients.find_by_first(&id).is_none(),
605 Active { workspace: id, .. } => hypr_data.workspaces.find_by_first(&id).is_none(),
606 } {
607 self.data.active = if self.switch.switch_workspaces {
608 find_next_workspace(
609 Direction::Right,
610 true,
611 &hypr_data,
612 self.data.active,
613 self.general.items_per_row,
614 )
615 } else {
616 find_next_client(
617 Direction::Right,
618 true,
619 &hypr_data,
620 self.data.active,
621 self.general.items_per_row,
622 )
623 };
624 }
625
626 self.data = SwitchData {
627 active: self.data.active,
628 hypr_data: hypr_data.clone(),
629 };
630
631 if self.switch.switch_workspaces {
632 self.populate_workspace_mode(&hypr_data, self.general.scale, self.data.active);
633 } else {
634 self.populate_clients_only_mode(&hypr_data, self.general.scale, self.data.active);
635 }
636 }
637
638 #[cfg(feature = "live_windows")]
639 fn refresh_thumbnails(&mut self, sender: &ComponentSender<Self>) {
640 use relm4::adw::gdk::Display;
641 let Some(mgr) = &mut self.capture_manager else {
642 return;
643 };
644 let Some(display) = Display::default() else {
645 return;
646 };
647 let mut captures = refresh_captures(mgr, &display, !self.thumbnail_burst);
648 if self.thumbnail_burst && mgr.pending_count() == 0 {
649 self.thumbnail_burst = false;
650 if let Some(h) = self.timer_handle.take() {
653 h.remove();
654 }
655 if self.thumbnail_refresh_ms != 0 {
657 trace!("Switching from thumbnail_burst refresh to slow refresh");
658 let sender = sender.clone();
659 self.timer_handle = Some(glib::timeout_add_local(
660 Duration::from_millis(self.thumbnail_refresh_ms),
661 move || {
662 sender.input(SwitchRootInput::RefreshThumbnails);
663 ControlFlow::Continue
664 },
665 ));
666 } else {
667 trace!("All initial thumbnail captures loaded");
668 }
669 }
670
671 if self.switch.switch_workspaces {
672 for (client_id, texture) in captures {
673 for (idx, _) in self.items.iter().enumerate() {
674 self.items.send(
675 idx,
676 WorkspacesInput::UpdateClientThumbnail(client_id, texture.clone()),
677 );
678 }
679 }
680 } else {
681 for (idx, item) in self.clients_only.iter().enumerate() {
682 if let Some(texture) = captures.remove(&item.id) {
683 self.clients_only.send(
684 idx,
685 crate::switch::clients::ClientsInput::UpdateThumbnail(texture),
686 );
687 }
688 }
689 }
690 }
691}
692
693fn handle_key(
694 key: Key,
695 s_key: Key,
696 kill_key: Key,
697 event_sender: &ComponentSender<SwitchRoot>,
698) -> glib::Propagation {
699 match key {
700 Key::Escape => {
701 event_sender
702 .input_sender()
703 .emit(SwitchRootInput::CloseSwitch(false));
704 glib::Propagation::Stop
705 }
706 k if k == s_key || k == Key::l || k == Key::Right => {
707 event_sender
708 .input_sender()
709 .emit(SwitchRootInput::Switch(Direction::Right));
710 glib::Propagation::Stop
711 }
712 Key::ISO_Left_Tab | Key::grave | Key::dead_grave | Key::h | Key::Left => {
713 event_sender
714 .input_sender()
715 .emit(SwitchRootInput::Switch(Direction::Left));
716 glib::Propagation::Stop
717 }
718 Key::j | Key::Down => {
719 event_sender
720 .input_sender()
721 .emit(SwitchRootInput::Switch(Direction::Down));
722 glib::Propagation::Stop
723 }
724 Key::k | Key::Up => {
725 event_sender
726 .input_sender()
727 .emit(SwitchRootInput::Switch(Direction::Up));
728 glib::Propagation::Stop
729 }
730 k if k == kill_key || k == Key::Delete => {
731 event_sender
732 .input_sender()
733 .emit(SwitchRootInput::CloseCurrentItem);
734 glib::Propagation::Stop
735 }
736 _ => glib::Propagation::Proceed,
737 }
738}
739
740#[derive(Debug)]
741pub struct SwitchData {
742 pub active: Active,
743 pub hypr_data: HyprlandData,
744}
745
746impl Default for SwitchData {
747 fn default() -> Self {
748 Self {
749 active: Active {
750 client: None,
751 workspace: -1,
752 monitor: -1,
753 },
754 hypr_data: HyprlandData::default(),
755 }
756 }
757}