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, warn};
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                    if sender
288                        .input_sender()
289                        .send(OverviewRootInput::RefreshThumbnails)
290                        .is_err()
291                    {
292                        warn!("Failed to send refresh thumbnails");
293                        return ControlFlow::Break;
294                    }
295                    ControlFlow::Continue
296                },
297            ));
298        }
299    }
300
301    fn navigate(&mut self, direction: Direction, workspace: bool) {
302        let new_active = if workspace {
303            find_next_workspace(
304                direction,
305                false,
306                &self.data.hypr_data,
307                self.data.active,
308                self.general.items_per_row,
309            )
310        } else {
311            if direction == Direction::Up || direction == Direction::Down {
312                error!(
313                    "Clients in overview can only be switched left and right (forwards and backwards)"
314                );
315                return;
316            }
317            find_next_client(
318                direction,
319                false,
320                &self.data.hypr_data,
321                self.data.active,
322                self.general.items_per_row,
323            )
324        };
325
326        let old_active = self.data.active;
327        self.data.active = new_active;
328
329        if new_active != old_active {
330            for window in self.windows.values() {
331                window.emit(OverviewWindowInput::SetActive(old_active, new_active));
332            }
333        }
334    }
335
336    #[allow(clippy::unused_self)]
337    fn close_item(&self, id: ClientId) {
338        if let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT) {
339            // TODO: close on killed to let user close window themself
340            tracing::warn!("Failed to kill client {id}: {e}");
341        }
342    }
343
344    fn close_overview(&mut self, do_switch: bool) {
345        for window in self.windows.values() {
346            window.emit(OverviewWindowInput::CloseOverview);
347        }
348
349        if do_switch {
350            if let Some(id) = self.data.active.client {
351                debug!(
352                    "Switching to client {}",
353                    self.data
354                        .hypr_data
355                        .clients
356                        .iter()
357                        .find(|(cid, _)| *cid == id)
358                        .map_or_else(|| "<Unknown>".to_string(), |(_, c)| c.title.clone())
359                );
360                // Defer execution to ensure window is hidden first
361                glib::idle_add_local(move || {
362                    if let Err(e) = switch_client(id) {
363                        tracing::warn!("Failed to switch to client {id:?}: {e}");
364                    }
365                    ControlFlow::Break
366                });
367            } else {
368                let id = self.data.active.workspace;
369                debug!(
370                    "Switching to workspace {}",
371                    self.data
372                        .hypr_data
373                        .workspaces
374                        .iter()
375                        .find(|(wid, _)| *wid == id)
376                        .map_or_else(|| "<Unknown>".to_string(), |(_, w)| w.name.clone())
377                );
378                glib::idle_add_local(move || {
379                    if let Err(e) = switch_workspace(id) {
380                        tracing::warn!("Failed to switch to workspace {id:?}: {e}");
381                    }
382                    ControlFlow::Break
383                });
384            }
385        }
386        #[cfg(feature = "live_windows")]
387        {
388            if let Some(handle) = self.timer_handle.take() {
389                handle.remove();
390            }
391            self.capture_manager = None;
392        }
393    }
394
395    fn reload_overview(&mut self) {
396        let (hypr_data, _active) = match collect_data(&SortConfig {
397            filter_current_monitor: self.overview.filter_by_current_monitor,
398            filter_current_workspace: self.overview.filter_by_current_workspace,
399            filter_same_class: self.overview.filter_by_same_class,
400            sort_recent: false,
401            exclude_workspaces: if self.overview.exclude_workspaces.is_empty() {
402                None
403            } else {
404                Some(self.overview.exclude_workspaces.clone())
405            },
406        }) {
407            Ok(data) => data,
408            Err(e) => {
409                error!("Failed to collect data: {}", e);
410                return;
411            }
412        };
413
414        while match self.data.active {
415            Active {
416                client: Some(id), ..
417            } => hypr_data.clients.find_by_first(&id).is_none(),
418            Active { workspace: id, .. } => hypr_data.workspaces.find_by_first(&id).is_none(),
419        } {
420            self.data.active = find_next_workspace(
421                Direction::Right,
422                true,
423                &hypr_data,
424                self.data.active,
425                self.general.items_per_row,
426            );
427        }
428
429        self.data = OverviewData {
430            active: self.data.active,
431            hypr_data: hypr_data.clone(),
432        };
433        self.render(hypr_data, self.data.active, false);
434    }
435
436    fn render(&self, hypr_data: HyprlandData, active: Active, open: bool) {
437        let mut mapped_ws = BTreeMap::new();
438        for (i, workspace_data) in hypr_data.workspaces {
439            mapped_ws
440                .entry(workspace_data.monitor)
441                .or_insert_with(Vec::new)
442                .push((i, workspace_data));
443        }
444        let mut mapped_cl = BTreeMap::new();
445        for (i, client_data) in hypr_data.clients {
446            mapped_cl
447                .entry(client_data.monitor)
448                .or_insert_with(Vec::new)
449                .push((i, client_data));
450        }
451
452        for (monitor_id, window) in &self.windows {
453            if let Some(data) = hypr_data.monitors.find_by_first(monitor_id) {
454                // always update, maybe last client got removed
455                let data = OverviewWindowData {
456                    active,
457                    clients: mapped_cl.remove(monitor_id).unwrap_or_default(),
458                    workspaces: mapped_ws.remove(monitor_id).unwrap_or_default(),
459                    monitor: data.clone(),
460                };
461                if open {
462                    window.emit(OverviewWindowInput::OpenOverview((
463                        data,
464                        self.overview.top_offset,
465                    )));
466                } else {
467                    window.emit(OverviewWindowInput::ReloadOverview(data));
468                }
469            }
470        }
471    }
472
473    #[cfg(feature = "live_windows")]
474    fn refresh_thumbnails(&mut self, sender: &ComponentSender<Self>) {
475        let Some(mgr) = &mut self.capture_manager else {
476            return;
477        };
478        let Some(display) = Display::default() else {
479            return;
480        };
481        let captures = refresh_captures(mgr, &display, !self.thumbnail_burst);
482        if self.thumbnail_burst && mgr.pending_count() == 0 {
483            self.thumbnail_burst = false;
484            // all initial thumbnails are loaded
485            // remove initial thumbnail burst timer
486            if let Some(h) = self.timer_handle.take() {
487                h.remove();
488            }
489            // start new slower timer if thumbnail_refresh_ms is set
490            if self.thumbnail_refresh_ms != 0 {
491                trace!("Switching from thumbnail_burst refresh to slow refresh");
492                let sender = sender.clone();
493                self.timer_handle = Some(glib::timeout_add_local(
494                    Duration::from_millis(self.thumbnail_refresh_ms),
495                    move || {
496                        if sender
497                            .input_sender()
498                            .send(OverviewRootInput::RefreshThumbnails)
499                            .is_err()
500                        {
501                            warn!("Failed to send refresh thumbnails");
502                            return ControlFlow::Break;
503                        }
504                        ControlFlow::Continue
505                    },
506                ));
507            } else {
508                trace!("All initial thumbnail captures loaded");
509            }
510        }
511
512        for (client_id, texture) in captures {
513            for window in self.windows.values() {
514                window.emit(OverviewWindowInput::UpdateClientThumbnail(
515                    client_id,
516                    texture.clone(),
517                ));
518            }
519        }
520    }
521}
522
523#[derive(Debug)]
524pub struct OverviewData {
525    pub active: Active,
526    pub hypr_data: HyprlandData,
527}
528
529impl Default for OverviewData {
530    fn default() -> Self {
531        Self {
532            active: Active {
533                client: None,
534                workspace: -1,
535                monitor: -1,
536            },
537            hypr_data: HyprlandData::default(),
538        }
539    }
540}