use crate::LOGGER; use crate::backend::window_manager_hypr::HyprlandComms; use freedesktop_desktop_entry::{default_paths, get_languages_from_env}; use glib::clone; use glib::prelude::Cast; use gtk::gdk::{Display, Monitor}; use gtk::prelude::{ BoxExt, ButtonExt, DisplayExt, EditableExt, EntryExt, GtkWindowExt, ListModelExt, MonitorExt, WidgetExt, }; use gtk::{Align, Application, ApplicationWindow, Orientation, PolicyType, gdk}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use jmb_bar::theme; use jmb_logger_rs::*; use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern}; use nucleo_matcher::{Config, Matcher}; use std::cell::RefCell; use std::collections::HashMap; use std::process::Command; use std::rc::Rc; #[derive(Clone)] pub struct RunMenu { window: ApplicationWindow, entry: gtk::Entry, entries: Rc>>>, } impl RunMenu { pub fn create(application: Application) -> Self { let window = ApplicationWindow::builder() .application(&application) .title("JmbRunMenu") .default_width(500) .css_classes(["run-menu"]) .build(); window.init_layer_shell(); window.set_layer(Layer::Overlay); window.set_halign(Align::Center); window.set_width_request(500); window.set_anchor(Edge::Top, true); window.set_keyboard_mode(KeyboardMode::Exclusive); let v_box = gtk::Box::new(Orientation::Vertical, 0); let entry = gtk::Entry::new(); entry.add_css_class("entry"); let scrolled = gtk::ScrolledWindow::new(); scrolled.set_policy(PolicyType::Never, PolicyType::Automatic); scrolled.set_vexpand(false); scrolled.set_propagate_natural_height(true); scrolled.set_min_content_height(0); scrolled.set_max_content_height(300); let content_box = gtk::Box::new(Orientation::Vertical, 0); content_box.add_css_class("content-box"); content_box.set_hexpand(true); scrolled.set_child(Some(&content_box)); v_box.set_vexpand(false); v_box.set_valign(Align::Start); v_box.append(&entry); v_box.append(&scrolled); window.set_child(Some(&v_box)); let display = Display::default().expect("Couldn't get display"); let monitor = HyprlandComms::cursor_monitor() .and_then(|id| display.monitors().item(id as u32)) .expect("Couldn't get monitor") .downcast::() .ok(); window.set_monitor(monitor.as_ref()); if let Some(monitor) = monitor { let geometry = monitor.geometry(); let top_margin = (geometry.height()) / 2; window.set_margin(Edge::Top, top_margin - 100); } else { window.set_valign(Align::Center); } let window_clone = window.clone(); let instance = Self { window: window_clone, entry: entry.clone(), entries: Rc::new(RefCell::new(HashMap::new())), }; let instance_clone = instance.clone(); let matcher_rc = Rc::new(RefCell::new(Matcher::new(Config::DEFAULT))); entry.connect_changed(clone!( #[strong] content_box, move |entry| { while let Some(child) = content_box.first_child() { content_box.remove(&child); } if entry.text_length() > 0 { let text = entry.text(); if let Some(eval) = text.strip_prefix("=") { let new_eval = eval.replace("**", "^"); if new_eval != eval { let cursor = entry.position() as usize; let removed_before_cursor = text[..cursor].matches("**").count(); let new_cursor = cursor - removed_before_cursor; glib::idle_add_local_once(clone!( #[weak] entry, move || { entry.set_text(&format!("={new_eval}")); entry.set_position(new_cursor as i32); } )); } if let Ok(result) = meval::eval_str(eval) { let label = gtk::Label::new(Some(&format!("{}", result))); label.add_css_class("exec-button"); label.set_halign(Align::Start); label.set_hexpand(true); content_box.append(&label); instance_clone.window.set_default_size(500, -1); } } else { instance_clone.list_matching( entry, &content_box, &mut matcher_rc.borrow_mut(), ); instance_clone.window.set_default_size(500, -1); } } } )); entry.connect_has_focus_notify(clone!( #[strong] content_box, move |e| { if let Some(child) = content_box.first_child() { if e.has_focus() { child.add_css_class("suggested"); } else { child.remove_css_class("suggested"); } } } )); entry.connect_activate(clone!( #[strong] content_box, move |_| { if let Some(child) = content_box.first_child() { if let Ok(button) = child.clone().downcast::() { button.activate(); } else if let Ok(label) = child.downcast::() { display.clipboard().set_text(&label.text()); label.set_markup(&format!( "{} - Copied", label.text(), theme::LIGHTGRAY )); } } } )); let instance_clone = instance.clone(); let key_controller = gtk::EventControllerKey::new(); key_controller.connect_key_pressed(move |_, key, _, _| { if key == gdk::Key::Escape { instance_clone.hide(); glib::Propagation::Stop } else { glib::Propagation::Proceed } }); window.add_controller(key_controller); instance } pub fn hide(&self) { self.window.set_visible(false); self.entry.set_text(""); } pub fn show(&mut self) { self.load_desktop_files(); self.window.present(); } pub fn toggle(&mut self) { if self.window.is_visible() { self.hide(); } else { self.show(); } } fn create_entry(&self, content_box: >k::Box, entry: &str) -> gtk::Button { let entry_clone = entry.to_owned(); let button = gtk::Button::new(); button.set_halign(Align::Fill); button.set_hexpand(true); let label = gtk::Label::new(Some(entry)); label.set_halign(Align::Start); button.set_child(Some(&label)); button.add_css_class("exec-button"); let entry_map = self.entries.clone(); let instance = self.clone(); button.connect_clicked(move |_| { let map = entry_map.borrow(); let exec = map.get(&entry_clone); if let Some(exec) = exec { let executable = &exec[0]; if let Err(err) = Command::new(executable).args(&exec[1..]).spawn() { warn!(&LOGGER, "Couldn't execute app: {err}"); } instance.hide(); } }); content_box.append(&button); button } fn list_matching( &self, entry: >k::Entry, content_box: >k::Box, matcher: &mut Matcher, ) -> bool { let keys: Vec = self.entries.borrow().keys().cloned().collect(); let mut first = true; let mut added = false; for (entry, _) in Pattern::parse(&entry.text(), CaseMatching::Ignore, Normalization::Smart) .match_list(keys, matcher) .iter() { let button = self.create_entry(content_box, entry); if first { button.add_css_class("suggested"); first = false; } added = true; } added } fn load_desktop_files(&mut self) { let locales = get_languages_from_env(); let entries = freedesktop_desktop_entry::Iter::new(default_paths()) .entries(Some(&locales)) .collect::>(); let mut entry_map = self.entries.borrow_mut(); entry_map.clear(); for entry in entries { if let Ok(exec) = entry.parse_exec() { let name = entry.full_name(&locales); if let Some(name) = name { entry_map.insert(name.to_string(), exec); } } } } }