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 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 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 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 if let Some(h) = self.timer_handle.take() {
487 h.remove();
488 }
489 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}