1use crate::data::{SortConfig, collect_data};
2use crate::next::{find_next_client, find_next_workspace};
3use crate::shared::{Workspaces, WorkspacesInit, WorkspacesInput};
4use crate::switch::clients::{Clients, ClientsInit};
5use core_lib::{Active, ByFirst, Direction, HyprlandData, SWITCH_NAMESPACE};
6use exec_lib::switch::{switch_client, switch_workspace};
7use gtk4_layer_shell::{KeyboardMode, Layer, LayerShell};
8use regex::Regex;
9use relm4::adw::glib::ControlFlow;
10use relm4::adw::gtk;
11use relm4::adw::gtk::glib;
12use relm4::adw::prelude::*;
13use relm4::gtk::gdk::Key;
14use relm4::gtk::{EventControllerKey, Orientation, SelectionMode};
15use relm4::prelude::*;
16use std::time::Duration;
17use tracing::{debug, error, trace};
18
19const KILL_TIMEOUT: Duration = Duration::from_millis(200);
20
21#[derive(Debug)]
22pub struct SwitchRoot {
23 general: config_lib::WindowsGeneral,
24 switch: config_lib::Switch,
25 open: bool,
26 data: SwitchData,
27 window: gtk::ApplicationWindow,
29 controller: Option<gtk::EventController>,
30 remove_html: Regex,
32 items: FactoryVecDeque<Workspaces>,
34 clients_only: FactoryVecDeque<Clients>,
36}
37
38#[derive(Debug)]
39pub enum SwitchRootInput {
40 SetSwitch(config_lib::Switch),
41 SetGeneral(config_lib::WindowsGeneral),
42 OpenSwitch(Direction),
43 Switch(Direction),
44 CloseSwitch(bool),
45 CloseCurrentItem,
46 ReloadSwitch,
47}
48
49#[derive(Debug)]
50pub struct SwitchRootInit {
51 pub general: config_lib::WindowsGeneral,
52 pub switch: config_lib::Switch,
53}
54
55#[derive(Debug)]
56pub enum SwitchRootOutput {}
57
58#[relm4::component(pub)]
59impl SimpleComponent for SwitchRoot {
60 type Init = SwitchRootInit;
61 type Input = SwitchRootInput;
62 type Output = SwitchRootOutput;
63
64 view! {
65 #[root]
66 gtk::ApplicationWindow {
67 set_css_classes: &["window"],
68 set_default_size: (100, 100),
69 match model.switch.switch_workspaces {
70 true => {
71 #[local_ref]
72 itemsw -> gtk::FlowBox {
73 set_css_classes: &["monitor"],
74 set_selection_mode: SelectionMode::None,
75 set_orientation: Orientation::Horizontal,
76 #[watch]
77 set_max_children_per_line: u32::from(model.general.items_per_row),
78 #[watch]
79 set_min_children_per_line: u32::from(model.general.items_per_row),
80 }
81 }
82 false => {
83 #[local_ref]
84 clients_only_w -> gtk::FlowBox {
85 set_css_classes: &["monitor"],
86 set_selection_mode: SelectionMode::None,
87 set_orientation: Orientation::Horizontal,
88 #[watch]
89 set_max_children_per_line: u32::from(model.general.items_per_row),
90 #[watch]
91 set_min_children_per_line: u32::from(model.general.items_per_row),
92 }
93 }
94 }
95 }
96 }
97
98 fn init(
99 init: Self::Init,
100 root: Self::Root,
101 sender: ComponentSender<Self>,
102 ) -> ComponentParts<Self> {
103 trace!("Initializing SwitchRoot");
104
105 let items: FactoryVecDeque<Workspaces> = FactoryVecDeque::builder()
106 .launch(gtk::FlowBox::default())
107 .detach();
108
109 let clients_only: FactoryVecDeque<Clients> = FactoryVecDeque::builder()
110 .launch(gtk::FlowBox::default())
111 .detach();
112
113 let model = Self {
114 general: init.general,
115 switch: init.switch,
116 open: false,
117 window: root.clone(),
118 controller: None,
119 remove_html: Regex::new(r"<[^>]*>").expect("invalid regex"),
120 data: SwitchData::default(),
121 items,
122 clients_only,
123 };
124
125 let itemsw: gtk::FlowBox = model.items.widget().clone();
126 let clients_only_w: gtk::FlowBox = model.clients_only.widget().clone();
127 let widgets = view_output!();
128
129 let window = &root;
130 window.init_layer_shell();
131 window.set_namespace(Some(SWITCH_NAMESPACE));
132 window.set_layer(Layer::Overlay);
133 window.set_keyboard_mode(KeyboardMode::Exclusive);
134 sender
135 .input_sender()
136 .emit(SwitchRootInput::SetSwitch(model.switch.clone()));
137 ComponentParts { model, widgets }
138 }
139
140 fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
141 trace!("switch::root::update: {message:?}");
142 match message {
143 SwitchRootInput::SetSwitch(switch) => {
144 self.switch = switch;
145 self.setup_keyboard_controller(&sender);
146 }
147 SwitchRootInput::SetGeneral(general) => {
148 self.general = general;
149 self.setup_keyboard_controller(&sender);
150 }
151 SwitchRootInput::OpenSwitch(direction) => {
152 if !self.open {
153 self.open = true;
154 self.open_switch(direction);
155 } else {
156 sender
157 .input_sender()
158 .emit(SwitchRootInput::Switch(direction));
159 }
160 }
161 SwitchRootInput::Switch(direction) => {
162 if self.open {
163 self.navigate(direction);
164 } else {
165 trace!("not open");
166 }
167 }
168 SwitchRootInput::CloseSwitch(do_switch) => {
169 if self.open {
170 self.open = false;
171 self.close_switch(do_switch);
172 } else {
173 trace!("not open");
174 }
175 }
176 SwitchRootInput::CloseCurrentItem => {
177 if self.open {
178 self.close_item();
179 } else {
180 trace!("not open");
181 }
182 sender.input_sender().emit(SwitchRootInput::ReloadSwitch);
183 }
184 SwitchRootInput::ReloadSwitch => {
185 if self.open {
186 self.reload_switch();
187 } else {
188 trace!("not open");
189 }
190 }
191 }
192 }
193}
194
195impl SwitchRoot {
196 fn setup_keyboard_controller(&mut self, sender: &ComponentSender<Self>) {
197 if let Some(k) = Key::from_name(self.switch.key.to_string()) {
199 if let Some(kk) = Key::from_name(self.switch.kill_key.to_string()) {
200 let key_controller = EventControllerKey::new();
201 let sender_2 = sender.clone();
202 key_controller.connect_key_pressed(move |_, key, _, _| {
203 trace!("Key pressed: {:?}", key);
204 handle_key(key, k, kk, sender_2.clone())
205 });
206 if let Some(controller) = self.controller.take() {
207 self.window.remove_controller(&controller);
208 }
209 self.window.add_controller(key_controller);
210 } else {
211 error!("Invalid kill key name: {}", self.switch.kill_key);
212 }
213 } else {
214 error!("Invalid key name: {}", self.switch.key);
215 }
216 }
217
218 fn open_switch(&mut self, direction: Direction) {
219 let (hypr_data, active_prev) = match collect_data(&SortConfig {
220 filter_current_monitor: self.switch.filter_by_current_monitor,
221 filter_current_workspace: self.switch.filter_by_current_workspace,
222 filter_same_class: self.switch.filter_by_same_class,
223 sort_recent: true,
224 exclude_workspaces: if self.switch.exclude_workspaces.is_empty() {
225 None
226 } else {
227 Some(self.switch.exclude_workspaces.clone())
228 },
229 }) {
230 Ok(data) => data,
231 Err(e) => {
232 error!("Failed to collect data: {}", e);
233 return;
234 }
235 };
236
237 let active = if self.switch.switch_workspaces {
238 find_next_workspace(
239 &direction,
240 true,
241 &hypr_data,
242 active_prev,
243 self.general.items_per_row,
244 )
245 } else {
246 find_next_client(
247 &direction,
248 true,
249 &hypr_data,
250 active_prev,
251 self.general.items_per_row,
252 )
253 };
254 self.data = SwitchData {
255 active,
256 hypr_data: hypr_data.clone(),
257 };
258
259 trace!("Showing window {:?}", self.window.id());
260 self.window.set_visible(true);
261 self.window.grab_focus();
262
263 if self.switch.switch_workspaces {
264 self.populate_workspace_mode(&hypr_data, self.general.scale, self.data.active);
265 } else {
266 self.populate_clients_only_mode(&hypr_data, self.general.scale, self.data.active);
267 }
268 }
269
270 fn populate_workspace_mode(&mut self, hypr_data: &HyprlandData, scale: f64, active: Active) {
271 let mut lock = self.items.guard();
272 lock.clear();
273
274 for (wid, workspace_data) in &hypr_data.workspaces {
275 if !workspace_data.any_client_enabled {
276 trace!("skipping workspace {} with no enabled clients", wid);
277 continue;
278 }
279 let workspace_clients: Vec<_> = hypr_data
281 .clients
282 .iter()
283 .filter(|(_, client)| client.workspace == *wid && client.enabled)
284 .map(|(id, data)| (*id, data.clone()))
285 .collect();
286
287 let Some(monitor) = hypr_data.monitors.find_by_first(&workspace_data.monitor) else {
288 error!(
289 "Workspace {} has invalid monitor {}",
290 wid, workspace_data.monitor
291 );
292 continue;
293 };
294 lock.push_back(WorkspacesInit {
295 monitor_data: monitor.clone(),
296 remove_html: self.remove_html.clone(),
297 id: *wid,
298 data: workspace_data.clone(),
299 scale,
300 clients: workspace_clients,
301 });
302 }
303 drop(lock);
304
305 for (idx, item) in self.items.iter().enumerate() {
307 if item.workspace_id == active.workspace {
308 self.items.send(idx, WorkspacesInput::SetActive(true));
309 break;
310 }
311 }
312 }
313
314 fn populate_clients_only_mode(&mut self, hypr_data: &HyprlandData, scale: f64, active: Active) {
315 let mut lock = self.clients_only.guard();
316 lock.clear();
317
318 for (id, client) in &hypr_data.clients {
319 if !client.enabled {
320 continue;
321 }
322 let Some(monitor) = hypr_data.monitors.find_by_first(&client.monitor) else {
323 error!("Client {} has invalid monitor {}", id, client.monitor);
324 continue;
325 };
326 lock.push_back(ClientsInit {
327 id: *id,
328 scale,
329 monitor_data: monitor.clone(),
330 data: client.clone(),
331 });
332 }
333 drop(lock);
334
335 if let Some(active_id) = active.client {
337 for (idx, item) in self.clients_only.iter().enumerate() {
338 if item.id == active_id {
339 self.clients_only
340 .send(idx, crate::switch::clients::ClientsInput::SetActive(true));
341 break;
342 }
343 }
344 }
345 }
346
347 fn navigate(&mut self, direction: Direction) {
348 let new_active = if self.switch.switch_workspaces {
349 find_next_workspace(
350 &direction,
351 true,
352 &self.data.hypr_data,
353 self.data.active,
354 self.general.items_per_row,
355 )
356 } else {
357 find_next_client(
358 &direction,
359 true,
360 &self.data.hypr_data,
361 self.data.active,
362 self.general.items_per_row,
363 )
364 };
365
366 let old_active = self.data.active;
367 self.data.active = new_active;
368
369 if self.switch.switch_workspaces {
370 self.update_workspace_active(old_active, new_active);
371 } else {
372 self.update_clients_only_active(old_active, new_active);
373 }
374 }
375
376 fn update_workspace_active(&mut self, old_active: Active, new_active: Active) {
377 if old_active.workspace != new_active.workspace {
379 for (idx, item) in self.items.iter().enumerate() {
380 if item.workspace_id == old_active.workspace {
381 self.items.send(idx, WorkspacesInput::SetActive(false));
382 }
383 if item.workspace_id == new_active.workspace {
384 self.items.send(idx, WorkspacesInput::SetActive(true));
385 if let Some(cid) = new_active.client {
386 self.items.send(idx, WorkspacesInput::SetActiveClient(cid));
387 }
388 }
389 }
390 }
391 }
392
393 fn update_clients_only_active(&mut self, old_active: Active, new_active: Active) {
394 if let Some(old_id) = old_active.client {
396 for (idx, item) in self.clients_only.iter().enumerate() {
397 if item.id == old_id {
398 self.clients_only
399 .send(idx, crate::switch::clients::ClientsInput::SetActive(false));
400 break;
401 }
402 }
403 }
404
405 if let Some(new_id) = new_active.client {
407 for (idx, item) in self.clients_only.iter().enumerate() {
408 if item.id == new_id {
409 self.clients_only
410 .send(idx, crate::switch::clients::ClientsInput::SetActive(true));
411 break;
412 }
413 }
414 }
415 }
416
417 fn close_switch(&mut self, do_switch: bool) {
418 trace!("Hiding window {:?}", self.window.id());
419 self.window.set_visible(false);
420
421 {
423 let mut lock = self.items.guard();
424 lock.clear();
425 }
426 {
427 let mut lock = self.clients_only.guard();
428 lock.clear();
429 }
430
431 if do_switch {
432 if let Some(id) = self.data.active.client {
433 debug!(
434 "Switching to client {}",
435 self.data
436 .hypr_data
437 .clients
438 .iter()
439 .find(|(cid, _)| *cid == id)
440 .map_or_else(|| "<Unknown>".to_string(), |(_, c)| c.title.clone())
441 );
442 glib::idle_add_local(move || {
444 if let Err(e) = switch_client(id) {
445 tracing::warn!("Failed to switch to client {id:?}: {e}");
446 }
447 ControlFlow::Break
448 });
449 } else {
450 let id = self.data.active.workspace;
451 debug!(
452 "Switching to workspace {}",
453 self.data
454 .hypr_data
455 .workspaces
456 .iter()
457 .find(|(wid, _)| *wid == id)
458 .map_or_else(|| "<Unknown>".to_string(), |(_, w)| w.name.clone())
459 );
460 glib::idle_add_local(move || {
461 if let Err(e) = switch_workspace(id) {
462 tracing::warn!("Failed to switch to workspace {id:?}: {e}");
463 }
464 ControlFlow::Break
465 });
466 }
467 }
468 }
469
470 fn close_item(&mut self) {
471 if self.switch.switch_workspaces {
472 self.kill_workspace_clients();
473 } else {
474 self.kill_active_client();
475 }
476 }
477
478 fn kill_active_client(&self) {
479 if let Some(id) = self.data.active.client {
480 if let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT) {
481 tracing::warn!("Failed to kill client {id}: {e}");
483 }
484 }
485 }
486
487 fn kill_workspace_clients(&self) {
488 let workspace_id = self.data.active.workspace;
489 debug!(
490 "Killing all clients in workspace {}",
491 self.data
492 .hypr_data
493 .workspaces
494 .iter()
495 .find(|(wid, _)| *wid == workspace_id)
496 .map_or_else(|| workspace_id.to_string(), |(_, w)| w.name.clone())
497 );
498
499 let clients_to_kill: Vec<_> = self
500 .data
501 .hypr_data
502 .clients
503 .iter()
504 .filter(|(_, client)| client.workspace == workspace_id)
505 .map(|(id, _)| *id)
506 .collect();
507
508 for client_id in clients_to_kill {
509 if let Err(e) = exec_lib::kill::kill_client_blocking(client_id, KILL_TIMEOUT) {
510 tracing::warn!("Failed to kill client {client_id}: {e}");
512 }
513 }
514 }
515
516 fn reload_switch(&mut self) {
517 let (hypr_data, _) = match collect_data(&SortConfig {
518 filter_current_monitor: self.switch.filter_by_current_monitor,
519 filter_current_workspace: self.switch.filter_by_current_workspace,
520 filter_same_class: self.switch.filter_by_same_class,
521 sort_recent: true,
522 exclude_workspaces: if self.switch.exclude_workspaces.is_empty() {
523 None
524 } else {
525 Some(self.switch.exclude_workspaces.clone())
526 },
527 }) {
528 Ok(data) => data,
529 Err(e) => {
530 error!("Failed to collect data: {}", e);
531 return;
532 }
533 };
534
535 while match self.data.active {
536 Active {
537 client: Some(id), ..
538 } => hypr_data.clients.find_by_first(&id).is_none(),
539 Active { workspace: id, .. } => hypr_data.workspaces.find_by_first(&id).is_none(),
540 } {
541 self.data.active = if self.switch.switch_workspaces {
542 find_next_workspace(
543 &Direction::Right,
544 true,
545 &hypr_data,
546 self.data.active,
547 self.general.items_per_row,
548 )
549 } else {
550 find_next_client(
551 &Direction::Right,
552 true,
553 &hypr_data,
554 self.data.active,
555 self.general.items_per_row,
556 )
557 };
558 }
559
560 self.data = SwitchData {
561 active: self.data.active,
562 hypr_data: hypr_data.clone(),
563 };
564
565 if self.switch.switch_workspaces {
566 self.populate_workspace_mode(&hypr_data, self.general.scale, self.data.active);
567 } else {
568 self.populate_clients_only_mode(&hypr_data, self.general.scale, self.data.active);
569 }
570 }
571}
572
573fn handle_key(
574 key: Key,
575 s_key: Key,
576 kill_key: Key,
577 event_sender: ComponentSender<SwitchRoot>,
578) -> glib::Propagation {
579 match key {
580 Key::Escape => {
581 event_sender
582 .input_sender()
583 .emit(SwitchRootInput::CloseSwitch(false));
584 glib::Propagation::Stop
585 }
586 k if k == s_key || k == Key::l || k == Key::Right => {
587 event_sender
588 .input_sender()
589 .emit(SwitchRootInput::Switch(Direction::Right));
590 glib::Propagation::Stop
591 }
592 Key::ISO_Left_Tab | Key::grave | Key::dead_grave | Key::h | Key::Left => {
593 event_sender
594 .input_sender()
595 .emit(SwitchRootInput::Switch(Direction::Left));
596 glib::Propagation::Stop
597 }
598 Key::j | Key::Down => {
599 event_sender
600 .input_sender()
601 .emit(SwitchRootInput::Switch(Direction::Down));
602 glib::Propagation::Stop
603 }
604 Key::k | Key::Up => {
605 event_sender
606 .input_sender()
607 .emit(SwitchRootInput::Switch(Direction::Up));
608 glib::Propagation::Stop
609 }
610 k if k == kill_key || k == Key::Delete => {
611 event_sender
612 .input_sender()
613 .emit(SwitchRootInput::CloseCurrentItem);
614 glib::Propagation::Stop
615 }
616 _ => glib::Propagation::Proceed,
617 }
618}
619
620#[derive(Debug)]
621pub struct SwitchData {
622 pub active: Active,
623 pub hypr_data: HyprlandData,
624}
625
626impl Default for SwitchData {
627 fn default() -> Self {
628 Self {
629 active: Active {
630 client: None,
631 workspace: -1,
632 monitor: -1,
633 },
634 hypr_data: HyprlandData::default(),
635 }
636 }
637}