Skip to main content

hyprshell_windows_lib/switch/
root.rs

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    // gtk
33    window: gtk::ApplicationWindow,
34    controller: Option<gtk::EventController>,
35    /// Regex for removing HTML tags from strings
36    remove_html: Regex,
37    /// Factory for workspace mode (workspaces)
38    items: FactoryVecDeque<Workspaces>,
39    /// Factory for non-workspace mode (clients)
40    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        // TODO add a check in config check so these always succeed
236        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            // Get clients for this workspace
333            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        // Set active workspace
363        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        // Set active client
395        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        // Update workspace active state
437        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        // Clear old active
454        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        // Set new active
465        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        // Clear UI
481        {
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                // Defer execution to ensure window is hidden first
502                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            // TODO: close on killed to let user close window themself
549            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                // TODO: close on killed to let user close window themself
577                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            // all initial thumbnails are loaded
651            // remove initial thumbnail burst timer
652            if let Some(h) = self.timer_handle.take() {
653                h.remove();
654            }
655            // start new slower timer if thumbnail_refresh_ms is set
656            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}