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