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 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, };
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 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 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 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 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}