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 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 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 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 if let Some(h) = self.timer_handle.take() {
480 h.remove();
481 }
482 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}