Skip to main content

hyprshell_launcher_lib/
root.rs

1use crate::plugin::{LaunchItem, get_child_launch_items_from_parent, match_launch_item};
2use crate::plugins;
3use crate::plugins_boxes::{
4    LauncherPlugins, LauncherPluginsInit, LauncherPluginsInput, LauncherPluginsOutput,
5};
6use crate::result::{LauncherResults, LauncherResultsInit, LauncherResultsOutput};
7use config_lib::{Launcher, Modifier};
8use core_lib::transfer::Identifier;
9use core_lib::{Direction, LAUNCHER_NAMESPACE, WarnWithDetails};
10use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
11use relm4::adw::gdk::ModifierType;
12use relm4::adw::prelude::*;
13use relm4::adw::{gdk, glib, gtk};
14use relm4::factory::FactoryVecDeque;
15use relm4::gtk::{EventController, EventControllerKey, Orientation, PropagationPhase};
16use relm4::{ComponentParts, ComponentSender, SimpleComponent};
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::rc::Rc;
20use tracing::{trace, warn};
21
22#[derive(Debug)]
23pub struct LauncherRoot {
24    settings: Launcher,
25
26    ui: LauncherUI,
27    data: LauncherData,
28
29    switching: bool,
30    data_dir: Rc<PathBuf>,
31}
32
33#[derive(Debug)]
34pub enum LauncherRootInput {
35    SetLauncher(Launcher),
36    OpenLauncher,
37    CloseLauncher,
38    LaunchPlugin(char),
39    LaunchIndex(usize),
40    Escape,
41    Return,
42    Switch(Direction, bool),
43    Type,
44}
45
46#[derive(Debug)]
47pub struct LauncherRootInit {
48    pub launcher: Launcher,
49    pub data_dir: Rc<PathBuf>,
50}
51
52#[derive(Debug)]
53pub enum LauncherRootOutput {
54    Switch(Direction, bool),
55    /// `do_switch`: if true, opens program / does switch and closes, if false only closes
56    Close(bool),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60enum ActivationOutcome {
61    OpenChildMode,
62    Launched,
63    NotLaunched,
64}
65
66#[relm4::component(pub)]
67impl SimpleComponent for LauncherRoot {
68    type Init = LauncherRootInit;
69    type Input = LauncherRootInput;
70    type Output = LauncherRootOutput;
71
72    view! {
73        #[root]
74        gtk::ApplicationWindow {
75            set_css_classes: &["window"],
76            set_default_size: (20, 20),
77            gtk::Box {
78                set_css_classes: &["launcher"],
79                set_orientation: Orientation::Vertical,
80                set_spacing: 4,
81                #[watch]
82                set_width_request: i32::from(model.settings.width),
83                #[local_ref]
84                entrye -> gtk::Entry {
85                    set_css_classes: &["launcher-input"],
86                    connect_changed => LauncherRootInput::Type,
87                },
88                #[local_ref]
89                resultse -> gtk::Box {
90                    set_orientation: Orientation::Vertical,
91                    set_css_classes: &["launcher-results"],
92                    set_spacing: 3,
93                },
94                #[local_ref]
95                pluginse -> gtk::Box {
96                    set_orientation: Orientation::Horizontal,
97                    set_css_classes: &["launcher-plugins"],
98                    set_spacing: 4,
99                }
100            }
101        }
102    }
103
104    fn init(
105        init: Self::Init,
106        root: Self::Root,
107        sender: ComponentSender<Self>,
108    ) -> ComponentParts<Self> {
109        let entry = gtk::Entry::new();
110        let results: FactoryVecDeque<LauncherResults> = FactoryVecDeque::builder()
111            .launch(gtk::Box::default())
112            .forward(sender.input_sender(), |r| match r {
113                LauncherResultsOutput::Clicked(idx) => {
114                    LauncherRootInput::LaunchIndex(idx.current_index())
115                }
116            });
117        let plugins: FactoryVecDeque<LauncherPlugins> = FactoryVecDeque::builder()
118            .launch(gtk::Box::default())
119            .forward(sender.input_sender(), |r| match r {
120                LauncherPluginsOutput::Clicked(ch) => LauncherRootInput::LaunchPlugin(ch),
121            });
122
123        let model = Self {
124            settings: init.launcher,
125            data_dir: init.data_dir,
126            ui: LauncherUI {
127                window: root.clone(),
128                entry,
129                results,
130                plugins,
131                controller: None,
132            },
133            data: LauncherData::default(),
134            switching: false, // enter when nothing was done launches program
135        };
136
137        let entrye = &model.ui.entry;
138        let resultse = &model.ui.results.widget().clone();
139        let pluginse = &model.ui.plugins.widget().clone();
140        let widgets = view_output!();
141
142        // ensure that the entry is always focused
143        let entry_2 = model.ui.entry.clone();
144        let window_2 = root.clone();
145        glib::timeout_add_local(std::time::Duration::from_millis(200), move || {
146            if window_2.is_visible() {
147                entry_2.grab_focus_without_selecting();
148            }
149            glib::ControlFlow::Continue
150        });
151        plugins::init();
152
153        let window = &root;
154        window.init_layer_shell();
155        window.set_namespace(Some(LAUNCHER_NAMESPACE));
156        window.set_layer(Layer::Overlay);
157        window.set_anchor(Edge::Top, true);
158        window.set_margin(Edge::Top, 0);
159        window.set_exclusive_zone(-1);
160        window.set_keyboard_mode(KeyboardMode::Exclusive);
161        ComponentParts { model, widgets }
162    }
163
164    fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
165        match message {
166            LauncherRootInput::SetLauncher(launcher) => {
167                self.settings = launcher;
168                self.setup_keyboard_controller(&sender);
169            }
170            LauncherRootInput::OpenLauncher => {
171                self.reset_data();
172                self.load_static_items();
173                self.load_static_plugins();
174                self.handle_type();
175                self.open_launcher();
176            }
177            LauncherRootInput::CloseLauncher => self.close_launcher(),
178            LauncherRootInput::LaunchPlugin(char) => {
179                trace!("Closing launcher with char: {}", char);
180                if let Some(iden) = self.data.static_plugins.get(&char) {
181                    plugins::launch(
182                        iden,
183                        &self.ui.entry.text(),
184                        self.settings.default_terminal.as_deref(),
185                        &self.data_dir,
186                        None,
187                    );
188                } else {
189                    warn!("No match found for char: {}", char);
190                }
191                sender
192                    .output_sender()
193                    .emit(LauncherRootOutput::Close(false));
194            }
195            LauncherRootInput::LaunchIndex(index) => {
196                trace!("Closing launcher with index: {}", index);
197                match self.activate_selected(index) {
198                    ActivationOutcome::OpenChildMode => (),
199                    ActivationOutcome::Launched => {
200                        sender
201                            .output_sender()
202                            .emit(LauncherRootOutput::Close(false));
203                    }
204                    ActivationOutcome::NotLaunched => {}
205                }
206            }
207            LauncherRootInput::Escape => {
208                if self.data.active_parent.is_some() {
209                    self.data.active_parent = None;
210                    if let Some(text) = self.data.parent_text.take()
211                        && let Some(cursor) = self.data.parent_cursor.take()
212                    {
213                        self.ui.entry.set_text(&text);
214                        self.ui.entry.set_position(cursor);
215                    }
216                    self.handle_type();
217                } else {
218                    sender
219                        .output_sender()
220                        .emit(LauncherRootOutput::Close(false));
221                }
222            }
223            LauncherRootInput::Type => {
224                self.switching = false;
225                self.handle_type();
226            }
227            LauncherRootInput::Switch(dir, ws) => {
228                self.switching = true;
229                sender
230                    .output_sender()
231                    .emit(LauncherRootOutput::Switch(dir, ws));
232            }
233            LauncherRootInput::Return => {
234                if self.switching {
235                    sender.output_sender().emit(LauncherRootOutput::Close(true));
236                } else {
237                    sender
238                        .input_sender()
239                        .emit(LauncherRootInput::LaunchIndex(0));
240                }
241            }
242        }
243    }
244}
245
246impl LauncherRoot {
247    fn reset_data(&mut self) {
248        self.data.active_parent = None;
249        self.data.parent_text = None;
250        self.data.parent_cursor = None;
251        self.data.active_results.clear();
252        self.switching = false;
253    }
254
255    fn load_static_items(&mut self) {
256        self.data.static_items.clear();
257        for opt in plugins::get_static_items(&self.settings.plugins, &self.data_dir) {
258            self.data.static_items.push(opt);
259        }
260    }
261
262    fn load_static_plugins(&mut self) {
263        let plugins = plugins::get_static_plugins(
264            &self.settings.plugins,
265            self.settings.default_terminal.as_deref(),
266        );
267        let mut plugins_lock = self.ui.plugins.guard();
268        plugins_lock.clear();
269        for opt in plugins {
270            self.data.static_plugins.insert(opt.key, opt.iden.clone());
271            plugins_lock.push_back(LauncherPluginsInit {
272                opt,
273                launch_modifier: self.settings.launch_modifier,
274            });
275        }
276    }
277
278    fn setup_keyboard_controller(&mut self, sender: &ComponentSender<Self>) {
279        let event_controller = EventControllerKey::new();
280        let plugin_keys = plugins::get_static_options_chars(&self.settings.plugins);
281        let sender_2 = sender.clone();
282        let launcher = self.settings.clone();
283        let entry = self.ui.entry.clone();
284        event_controller.set_propagation_phase(PropagationPhase::Capture);
285        event_controller.connect_key_pressed(move |_, key, _, modt| {
286            trace!("input: {key:?}");
287            let text_empty = entry.text().is_empty();
288            handle_key(&launcher, text_empty, key, modt, &plugin_keys, &sender_2)
289        });
290        if let Some(controller) = self.ui.controller.take() {
291            self.ui.entry.remove_controller(&controller);
292        }
293        self.ui.entry.add_controller(event_controller);
294    }
295    fn open_launcher(&self) {
296        trace!("Showing window {:?}", self.ui.window.id());
297        self.ui.window.set_visible(true);
298        self.ui.entry.grab_focus();
299        self.ui.entry.set_text("");
300        exec_lib::set_no_follow_mouse(None).warn_details("Failed to set follow mouse");
301    }
302    fn close_launcher(&self) {
303        trace!("Hiding window {:?}", self.ui.window.id());
304        self.ui.window.set_visible(false);
305        exec_lib::reset_no_follow_mouse().warn_details("Failed to reset follow mouse");
306    }
307
308    fn activate_selected(&mut self, index: usize) -> ActivationOutcome {
309        let Some(item) = self.data.active_results.get(index).map(|entry| &entry.item) else {
310            return ActivationOutcome::NotLaunched;
311        };
312        if item.item.children.is_empty() {
313            plugins::launch(
314                &item.item.iden,
315                &self.ui.entry.text(),
316                self.settings.default_terminal.as_deref(),
317                &self.data_dir,
318                item.args.as_deref(),
319            );
320            ActivationOutcome::Launched
321        } else {
322            self.data.parent_text = Some(self.ui.entry.text().into());
323            self.data.parent_cursor = Some(self.ui.entry.position());
324            self.data.active_parent = Some(item.item.clone());
325            self.ui.entry.set_text("");
326            self.handle_type();
327            ActivationOutcome::OpenChildMode
328        }
329    }
330
331    fn handle_type(&mut self) {
332        let text: &str = &self.ui.entry.text();
333
334        let mut dynamic_results = Vec::new();
335        let mut results = Vec::new();
336        if !text.is_empty() || self.settings.show_when_empty {
337            if let Some(parent) = self.data.active_parent.as_ref() {
338                for opt in get_child_launch_items_from_parent(parent) {
339                    results.push(opt);
340                }
341            } else {
342                if !text.is_empty() {
343                    for opt in plugins::get_input_driven_launch_items(&self.settings.plugins, text)
344                    {
345                        dynamic_results.push(opt);
346                    }
347                }
348                results.extend(self.data.static_items.clone());
349            }
350        }
351
352        let mut results: Vec<_> = results
353            .into_iter()
354            .filter_map(|item| match_launch_item(item, text))
355            .collect();
356        // reverse sorting, so that the most relevant items are at the top
357        results.sort_by_key(|b| std::cmp::Reverse(b.score));
358        dynamic_results.extend(results);
359
360        let max_items = self.settings.max_items.min(9) as usize;
361        let dynamic: Vec<_> = dynamic_results
362            .into_iter()
363            .enumerate()
364            .map(|(idx, item)| LauncherResultsInit {
365                item,
366                key: match idx {
367                    0 => "Return".to_string(),
368                    i => format!("{}+{i}", self.settings.launch_modifier),
369                },
370            })
371            .take(max_items)
372            .collect();
373
374        self.data.active_results.clone_from(&dynamic);
375        let mut results_lock = self.ui.results.guard();
376        results_lock.clear();
377        for item in dynamic {
378            results_lock.push_back(item);
379        }
380
381        self.ui
382            .plugins
383            .broadcast(LauncherPluginsInput::SetEnabled(!text.is_empty()));
384    }
385}
386
387#[allow(clippy::too_many_lines)]
388fn handle_key(
389    launcher: &Launcher,
390    text_empty: bool,
391    key: gdk::Key,
392    modt: ModifierType,
393    plugin_keys: &[gdk::Key],
394    sender: &ComponentSender<LauncherRoot>,
395) -> glib::Propagation {
396    let launch_mod = match launcher.launch_modifier {
397        Modifier::Ctrl => modt == ModifierType::CONTROL_MASK,
398        Modifier::Alt => modt == ModifierType::ALT_MASK,
399        Modifier::Super => modt == ModifierType::SUPER_MASK,
400        Modifier::None => false,
401    };
402    trace!(
403        "key: {}{:?}, mods: {:?}, launch_mod: {}, launch_modifier: {}",
404        key, key, modt, launch_mod, launcher.launch_modifier
405    );
406    if launch_mod && plugin_keys.contains(&key) {
407        if let Some(ch) = key.name().unwrap_or_default().to_string().pop() {
408            sender
409                .input_sender()
410                .emit(LauncherRootInput::LaunchPlugin(ch));
411        }
412        return glib::Propagation::Stop;
413    }
414
415    match (launch_mod, key) {
416        (_, gdk::Key::Escape) => {
417            sender.input_sender().emit(LauncherRootInput::Escape);
418            glib::Propagation::Stop
419        }
420        (_, gdk::Key::Tab) => {
421            sender
422                .input_sender()
423                .emit(LauncherRootInput::Switch(Direction::Right, false));
424            glib::Propagation::Stop
425        }
426        (_, gdk::Key::ISO_Left_Tab | gdk::Key::grave | gdk::Key::dead_grave) => {
427            sender
428                .input_sender()
429                .emit(LauncherRootInput::Switch(Direction::Left, false));
430            glib::Propagation::Stop
431        }
432        (true, gdk::Key::h) => {
433            sender
434                .input_sender()
435                .emit(LauncherRootInput::Switch(Direction::Left, true));
436            glib::Propagation::Stop
437        }
438        (true, gdk::Key::l) => {
439            sender
440                .input_sender()
441                .emit(LauncherRootInput::Switch(Direction::Right, true));
442            glib::Propagation::Stop
443        }
444        (_, gdk::Key::Left) => {
445            if !text_empty {
446                // allow using with text in launcher
447                return glib::Propagation::Proceed;
448            }
449            sender
450                .input_sender()
451                .emit(LauncherRootInput::Switch(Direction::Left, true));
452            glib::Propagation::Stop
453        }
454        (_, gdk::Key::Right) => {
455            if !text_empty {
456                // allow using with text in launcher
457                return glib::Propagation::Proceed;
458            }
459            sender
460                .input_sender()
461                .emit(LauncherRootInput::Switch(Direction::Right, true));
462            glib::Propagation::Stop
463        }
464        (_, gdk::Key::Up) | (true, gdk::Key::k) => {
465            sender
466                .input_sender()
467                .emit(LauncherRootInput::Switch(Direction::Up, true));
468            glib::Propagation::Stop
469        }
470        (_, gdk::Key::Down) | (true, gdk::Key::j) => {
471            sender
472                .input_sender()
473                .emit(LauncherRootInput::Switch(Direction::Down, true));
474            glib::Propagation::Stop
475        }
476        (_, gdk::Key::Return) => {
477            sender.input_sender().emit(LauncherRootInput::Return);
478            glib::Propagation::Stop
479        }
480        (true, gdk::Key::_1) => {
481            sender
482                .input_sender()
483                .emit(LauncherRootInput::LaunchIndex(1));
484            glib::Propagation::Stop
485        }
486        (true, gdk::Key::_2) => {
487            sender
488                .input_sender()
489                .emit(LauncherRootInput::LaunchIndex(2));
490            glib::Propagation::Stop
491        }
492        (true, gdk::Key::_3) => {
493            sender
494                .input_sender()
495                .emit(LauncherRootInput::LaunchIndex(3));
496            glib::Propagation::Stop
497        }
498        (true, gdk::Key::_4) => {
499            sender
500                .input_sender()
501                .emit(LauncherRootInput::LaunchIndex(4));
502            glib::Propagation::Stop
503        }
504        (true, gdk::Key::_5) => {
505            sender
506                .input_sender()
507                .emit(LauncherRootInput::LaunchIndex(5));
508            glib::Propagation::Stop
509        }
510        (true, gdk::Key::_6) => {
511            sender
512                .input_sender()
513                .emit(LauncherRootInput::LaunchIndex(6));
514            glib::Propagation::Stop
515        }
516        (true, gdk::Key::_7) => {
517            sender
518                .input_sender()
519                .emit(LauncherRootInput::LaunchIndex(7));
520            glib::Propagation::Stop
521        }
522        (true, gdk::Key::_8) => {
523            sender
524                .input_sender()
525                .emit(LauncherRootInput::LaunchIndex(8));
526            glib::Propagation::Stop
527        }
528        (true, gdk::Key::_9) => {
529            sender
530                .input_sender()
531                .emit(LauncherRootInput::LaunchIndex(9));
532            glib::Propagation::Stop
533        }
534        _ => glib::Propagation::Proceed,
535    }
536}
537
538#[derive(Debug, Default)]
539struct LauncherData {
540    static_items: Vec<LaunchItem>,
541    static_plugins: HashMap<char, Identifier>,
542
543    active_results: Vec<LauncherResultsInit>,
544
545    active_parent: Option<LaunchItem>,
546    parent_text: Option<Box<str>>,
547    parent_cursor: Option<i32>,
548}
549
550#[derive(Debug)]
551struct LauncherUI {
552    window: gtk::ApplicationWindow,
553    entry: gtk::Entry,
554    results: FactoryVecDeque<LauncherResults>,
555    plugins: FactoryVecDeque<LauncherPlugins>,
556    controller: Option<EventController>,
557}