Skip to main content

hyprshell_windows_lib/switch/
root.rs

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    // gtk
28    window: gtk::ApplicationWindow,
29    controller: Option<gtk::EventController>,
30    /// Regex for removing HTML tags from strings
31    remove_html: Regex,
32    /// Factory for workspace mode (workspaces)
33    items: FactoryVecDeque<Workspaces>,
34    /// Factory for non-workspace mode (clients)
35    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        // TODO add a check in config check so these always succeed
198        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            // Get clients for this workspace
280            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        // Set active workspace
306        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        // Set active client
336        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        // Update workspace active state
378        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        // Clear old active
395        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        // Set new active
406        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        // Clear UI
422        {
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                // Defer execution to ensure window is hidden first
443                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                // TODO: close on killed to let user close window themself
482                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                // TODO: close on killed to let user close window themself
511                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}