Skip to main content

hyprshell_windows_lib/overview/
root.rs

1use crate::data::{SortConfig, collect_data};
2use crate::next::{find_next_client, find_next_workspace};
3use crate::overview::window::{
4    OverviewWindow, OverviewWindowData, OverviewWindowInit, OverviewWindowInput,
5    OverviewWindowOutput,
6};
7#[cfg(feature = "live_windows")]
8use crate::shared::refresh_captures;
9use core_lib::{Active, ByFirst, ClientId, Direction, HyprlandData, MonitorId, WorkspaceId};
10use exec_lib::switch::{switch_client, switch_workspace};
11#[cfg(feature = "live_windows")]
12use exec_lib::wayland_capture::CaptureManager;
13use launcher_lib::{LauncherRoot, LauncherRootInit, LauncherRootInput, LauncherRootOutput};
14use relm4::adw::gdk::{Display, Monitor};
15use relm4::adw::glib::ControlFlow;
16use relm4::adw::prelude::*;
17use relm4::adw::{glib, gtk};
18use relm4::prelude::*;
19use std::collections::BTreeMap;
20use std::path::PathBuf;
21use std::rc::Rc;
22use std::time::Duration;
23use tracing::{debug, error, trace};
24
25const KILL_TIMEOUT: Duration = Duration::from_millis(200);
26#[cfg(feature = "live_windows")]
27const THUMBNAIL_BURST_MS: u64 = 8;
28
29#[derive(Debug)]
30pub struct OverviewRoot {
31    general: config_lib::WindowsGeneral,
32    overview: config_lib::Overview,
33    open: bool,
34    data: OverviewData,
35
36    launcher_root: Controller<LauncherRoot>,
37    windows: BTreeMap<MonitorId, Controller<OverviewWindow>>,
38
39    #[cfg(feature = "live_windows")]
40    live_thumbnails: bool,
41
42    #[cfg(feature = "live_windows")]
43    capture_manager: Option<CaptureManager>,
44    #[cfg(feature = "live_windows")]
45    timer_handle: Option<glib::SourceId>,
46    #[cfg(feature = "live_windows")]
47    thumbnail_refresh_ms: u64,
48    #[cfg(feature = "live_windows")]
49    thumbnail_burst: bool,
50}
51
52#[derive(Debug)]
53pub enum OverviewRootInput {
54    SetOverview(config_lib::Overview),
55    SetGeneral(config_lib::WindowsGeneral),
56    OpenOverview,
57    Switch(Direction, bool),
58    CloseOverview(bool),
59    CloseOverviewClick(WorkspaceId),
60    CloseOverviewClickC(ClientId),
61    CloseItem(ClientId),
62    ReloadOverview,
63    #[cfg(feature = "live_windows")]
64    RefreshThumbnails,
65}
66
67#[derive(Debug)]
68pub struct OverviewRootInit {
69    pub general: config_lib::WindowsGeneral,
70    pub overview: config_lib::Overview,
71    pub data_dir: Rc<PathBuf>,
72    pub thumbnail_refresh_ms: u64,
73}
74
75#[derive(Debug)]
76pub enum OverviewRootOutput {}
77
78#[relm4::component(pub)]
79impl SimpleComponent for OverviewRoot {
80    type Init = OverviewRootInit;
81    type Input = OverviewRootInput;
82    type Output = OverviewRootOutput;
83
84    view! {
85        gtk::Window {
86        }
87    }
88    fn init(
89        init: Self::Init,
90        root: Self::Root,
91        sender: ComponentSender<Self>,
92    ) -> ComponentParts<Self> {
93        trace!("Initializing OverviewRoot");
94        let app = relm4::main_application();
95        let mut windows = BTreeMap::new();
96
97        #[cfg(feature = "live_windows")]
98        let live_thumbnails = std::env::var_os("HYPRSHELL_EXPERIMENTAL").is_some_and(|v| v == "1");
99        #[cfg(feature = "live_windows")]
100        let live_thumbnails_icons =
101            std::env::var_os("HYPRSHELL_EXPERIMENTAL_ICONS").is_none_or(|v| v != "0");
102
103        let monitors = exec_lib::collect::get_monitors();
104        let gmonitors = Display::default()
105            .expect("Could not connect to a display")
106            .monitors()
107            .iter()
108            .filter_map(Result::ok)
109            .collect::<Vec<Monitor>>();
110        for gtk_monitor in gmonitors {
111            let monitor_conn = gtk_monitor.connector().unwrap_or_default();
112            if let Some(monitor) = monitors.iter().find(|m| m.connector == monitor_conn) {
113                let overview_window = OverviewWindow::builder();
114                let window = &overview_window.root;
115                app.add_window(window);
116                let overview_window = overview_window
117                    .launch(OverviewWindowInit {
118                        general: init.general.clone(),
119                        gtk_monitor,
120                        #[cfg(feature = "live_windows")]
121                        live_thumbnails,
122                        #[cfg(feature = "live_windows")]
123                        live_thumbnails_icons,
124                    })
125                    .forward(sender.input_sender(), |m| match m {
126                        OverviewWindowOutput::Clicked(ws) => {
127                            OverviewRootInput::CloseOverviewClick(ws)
128                        }
129                        OverviewWindowOutput::ClickedC(cl) => {
130                            OverviewRootInput::CloseOverviewClickC(cl)
131                        }
132                    });
133                windows.entry(monitor.id).insert_entry(overview_window);
134            }
135        }
136        let launcher_root = LauncherRoot::builder();
137        let window = &launcher_root.root;
138        app.add_window(window);
139        let launcher_root = launcher_root
140            .launch(LauncherRootInit {
141                launcher: init.overview.launcher.clone(),
142                data_dir: init.data_dir,
143            })
144            .forward(sender.input_sender(), |msg| match msg {
145                LauncherRootOutput::Switch(dir, ws) => OverviewRootInput::Switch(dir, ws),
146                LauncherRootOutput::Close(do_switch) => OverviewRootInput::CloseOverview(do_switch),
147            });
148
149        let model = Self {
150            general: init.general,
151            overview: init.overview,
152            open: false,
153            windows,
154            launcher_root,
155            data: OverviewData::default(),
156            #[cfg(feature = "live_windows")]
157            live_thumbnails,
158            #[cfg(feature = "live_windows")]
159            capture_manager: None,
160            #[cfg(feature = "live_windows")]
161            timer_handle: None,
162            #[cfg(feature = "live_windows")]
163            thumbnail_refresh_ms: init.thumbnail_refresh_ms,
164            #[cfg(feature = "live_windows")]
165            thumbnail_burst: false,
166        };
167
168        let widgets = view_output!();
169        sender
170            .input_sender()
171            .emit(OverviewRootInput::SetOverview(model.overview.clone()));
172        ComponentParts { model, widgets }
173    }
174
175    fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
176        trace!("overview::root::update: {message:?}");
177        match message {
178            OverviewRootInput::SetOverview(overview) => {
179                self.overview = overview;
180                self.launcher_root.emit(LauncherRootInput::SetLauncher(
181                    self.overview.launcher.clone(),
182                ));
183            }
184            OverviewRootInput::SetGeneral(general) => {
185                for window in self.windows.values_mut() {
186                    window.emit(OverviewWindowInput::SetGeneral(general.clone()));
187                }
188                self.general = general;
189            }
190            OverviewRootInput::OpenOverview => {
191                if self.open {
192                    sender
193                        .input_sender()
194                        .emit(OverviewRootInput::CloseOverview(false));
195                } else {
196                    self.open = true;
197                    self.launcher_root.emit(LauncherRootInput::OpenLauncher);
198                    self.open_overview(&sender);
199                }
200            }
201            OverviewRootInput::Switch(direction, workspace) => {
202                if self.open {
203                    self.navigate(direction, workspace);
204                } else {
205                    trace!("not open");
206                }
207            }
208            OverviewRootInput::CloseOverview(do_switch) => {
209                if self.open {
210                    self.open = false;
211                    self.launcher_root.emit(LauncherRootInput::CloseLauncher);
212                    self.close_overview(do_switch);
213                } else {
214                    trace!("not open");
215                }
216            }
217            OverviewRootInput::CloseItem(id) => {
218                if self.open {
219                    self.close_item(id);
220                } else {
221                    trace!("not open");
222                }
223                sender
224                    .input_sender()
225                    .emit(OverviewRootInput::ReloadOverview);
226            }
227            OverviewRootInput::ReloadOverview => {
228                if self.open {
229                    self.reload_overview();
230                } else {
231                    trace!("not open");
232                }
233            }
234            OverviewRootInput::CloseOverviewClick(ws) => {
235                self.data.active.client = None;
236                self.data.active.workspace = ws;
237                sender
238                    .input_sender()
239                    .emit(OverviewRootInput::CloseOverview(true));
240            }
241            OverviewRootInput::CloseOverviewClickC(cl) => {
242                self.data.active.client = Some(cl);
243                sender
244                    .input_sender()
245                    .emit(OverviewRootInput::CloseOverview(true));
246            }
247            #[cfg(feature = "live_windows")]
248            OverviewRootInput::RefreshThumbnails => self.refresh_thumbnails(&sender),
249        }
250    }
251}
252
253impl OverviewRoot {
254    #[allow(unused_variables)]
255    fn open_overview(&mut self, sender: &ComponentSender<Self>) {
256        let (hypr_data, active) = match collect_data(&SortConfig {
257            filter_current_monitor: self.overview.filter_by_current_monitor,
258            filter_current_workspace: self.overview.filter_by_current_workspace,
259            filter_same_class: self.overview.filter_by_same_class,
260            sort_recent: false,
261            exclude_workspaces: if self.overview.exclude_workspaces.is_empty() {
262                None
263            } else {
264                Some(self.overview.exclude_workspaces.clone())
265            },
266        }) {
267            Ok(data) => data,
268            Err(e) => {
269                error!("Failed to collect data: {}", e);
270                return;
271            }
272        };
273        self.data = OverviewData {
274            active,
275            hypr_data: hypr_data.clone(),
276        };
277        self.render(hypr_data, self.data.active, true);
278
279        #[cfg(feature = "live_windows")]
280        if self.live_thumbnails {
281            self.capture_manager = CaptureManager::new().map_err(|e| error!("{e}")).ok();
282            self.thumbnail_burst = true;
283            let sender = sender.clone();
284            self.timer_handle = Some(glib::timeout_add_local(
285                Duration::from_millis(THUMBNAIL_BURST_MS),
286                move || {
287                    sender.input(OverviewRootInput::RefreshThumbnails);
288                    ControlFlow::Continue
289                },
290            ));
291        }
292    }
293
294    fn navigate(&mut self, direction: Direction, workspace: bool) {
295        let new_active = if workspace {
296            find_next_workspace(
297                direction,
298                false,
299                &self.data.hypr_data,
300                self.data.active,
301                self.general.items_per_row,
302            )
303        } else {
304            if direction == Direction::Up || direction == Direction::Down {
305                error!(
306                    "Clients in overview can only be switched left and right (forwards and backwards)"
307                );
308                return;
309            }
310            find_next_client(
311                direction,
312                false,
313                &self.data.hypr_data,
314                self.data.active,
315                self.general.items_per_row,
316            )
317        };
318
319        let old_active = self.data.active;
320        self.data.active = new_active;
321
322        if new_active != old_active {
323            for window in self.windows.values() {
324                window.emit(OverviewWindowInput::SetActive(old_active, new_active));
325            }
326        }
327    }
328
329    #[allow(clippy::unused_self)]
330    fn close_item(&self, id: ClientId) {
331        if let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT) {
332            // TODO: close on killed to let user close window themself
333            tracing::warn!("Failed to kill client {id}: {e}");
334        }
335    }
336
337    fn close_overview(&mut self, do_switch: bool) {
338        for window in self.windows.values() {
339            window.emit(OverviewWindowInput::CloseOverview);
340        }
341
342        if do_switch {
343            if let Some(id) = self.data.active.client {
344                debug!(
345                    "Switching to client {}",
346                    self.data
347                        .hypr_data
348                        .clients
349                        .iter()
350                        .find(|(cid, _)| *cid == id)
351                        .map_or_else(|| "<Unknown>".to_string(), |(_, c)| c.title.clone())
352                );
353                // Defer execution to ensure window is hidden first
354                glib::idle_add_local(move || {
355                    if let Err(e) = switch_client(id) {
356                        tracing::warn!("Failed to switch to client {id:?}: {e}");
357                    }
358                    ControlFlow::Break
359                });
360            } else {
361                let id = self.data.active.workspace;
362                debug!(
363                    "Switching to workspace {}",
364                    self.data
365                        .hypr_data
366                        .workspaces
367                        .iter()
368                        .find(|(wid, _)| *wid == id)
369                        .map_or_else(|| "<Unknown>".to_string(), |(_, w)| w.name.clone())
370                );
371                glib::idle_add_local(move || {
372                    if let Err(e) = switch_workspace(id) {
373                        tracing::warn!("Failed to switch to workspace {id:?}: {e}");
374                    }
375                    ControlFlow::Break
376                });
377            }
378        }
379        #[cfg(feature = "live_windows")]
380        {
381            if let Some(handle) = self.timer_handle.take() {
382                handle.remove();
383            }
384            self.capture_manager = None;
385        }
386    }
387
388    fn reload_overview(&mut self) {
389        let (hypr_data, _active) = match collect_data(&SortConfig {
390            filter_current_monitor: self.overview.filter_by_current_monitor,
391            filter_current_workspace: self.overview.filter_by_current_workspace,
392            filter_same_class: self.overview.filter_by_same_class,
393            sort_recent: false,
394            exclude_workspaces: if self.overview.exclude_workspaces.is_empty() {
395                None
396            } else {
397                Some(self.overview.exclude_workspaces.clone())
398            },
399        }) {
400            Ok(data) => data,
401            Err(e) => {
402                error!("Failed to collect data: {}", e);
403                return;
404            }
405        };
406
407        while match self.data.active {
408            Active {
409                client: Some(id), ..
410            } => hypr_data.clients.find_by_first(&id).is_none(),
411            Active { workspace: id, .. } => hypr_data.workspaces.find_by_first(&id).is_none(),
412        } {
413            self.data.active = find_next_workspace(
414                Direction::Right,
415                true,
416                &hypr_data,
417                self.data.active,
418                self.general.items_per_row,
419            );
420        }
421
422        self.data = OverviewData {
423            active: self.data.active,
424            hypr_data: hypr_data.clone(),
425        };
426        self.render(hypr_data, self.data.active, false);
427    }
428
429    fn render(&self, hypr_data: HyprlandData, active: Active, open: bool) {
430        let mut mapped_ws = BTreeMap::new();
431        for (i, workspace_data) in hypr_data.workspaces {
432            mapped_ws
433                .entry(workspace_data.monitor)
434                .or_insert_with(Vec::new)
435                .push((i, workspace_data));
436        }
437        let mut mapped_cl = BTreeMap::new();
438        for (i, client_data) in hypr_data.clients {
439            mapped_cl
440                .entry(client_data.monitor)
441                .or_insert_with(Vec::new)
442                .push((i, client_data));
443        }
444
445        for (monitor_id, window) in &self.windows {
446            if let Some(data) = hypr_data.monitors.find_by_first(monitor_id) {
447                // always update, maybe last client got removed
448                let data = OverviewWindowData {
449                    active,
450                    clients: mapped_cl.remove(monitor_id).unwrap_or_default(),
451                    workspaces: mapped_ws.remove(monitor_id).unwrap_or_default(),
452                    monitor: data.clone(),
453                };
454                if open {
455                    window.emit(OverviewWindowInput::OpenOverview((
456                        data,
457                        self.overview.top_offset,
458                    )));
459                } else {
460                    window.emit(OverviewWindowInput::ReloadOverview(data));
461                }
462            }
463        }
464    }
465
466    #[cfg(feature = "live_windows")]
467    fn refresh_thumbnails(&mut self, sender: &ComponentSender<Self>) {
468        let Some(mgr) = &mut self.capture_manager else {
469            return;
470        };
471        let Some(display) = Display::default() else {
472            return;
473        };
474        let captures = refresh_captures(mgr, &display, !self.thumbnail_burst);
475        if self.thumbnail_burst && mgr.pending_count() == 0 {
476            self.thumbnail_burst = false;
477            // all initial thumbnails are loaded
478            // remove initial thumbnail burst timer
479            if let Some(h) = self.timer_handle.take() {
480                h.remove();
481            }
482            // start new slower timer if thumbnail_refresh_ms is set
483            if self.thumbnail_refresh_ms != 0 {
484                trace!("Switching from thumbnail_burst refresh to slow refresh");
485                let sender = sender.clone();
486                self.timer_handle = Some(glib::timeout_add_local(
487                    Duration::from_millis(self.thumbnail_refresh_ms),
488                    move || {
489                        sender.input(OverviewRootInput::RefreshThumbnails);
490                        ControlFlow::Continue
491                    },
492                ));
493            } else {
494                trace!("All initial thumbnail captures loaded");
495            }
496        }
497
498        for (client_id, texture) in captures {
499            for window in self.windows.values() {
500                window.emit(OverviewWindowInput::UpdateClientThumbnail(
501                    client_id,
502                    texture.clone(),
503                ));
504            }
505        }
506    }
507}
508
509#[derive(Debug)]
510pub struct OverviewData {
511    pub active: Active,
512    pub hypr_data: HyprlandData,
513}
514
515impl Default for OverviewData {
516    fn default() -> Self {
517        Self {
518            active: Active {
519                client: None,
520                workspace: -1,
521                monitor: -1,
522            },
523            hypr_data: HyprlandData::default(),
524        }
525    }
526}