271 lines
9.4 KiB
Rust
271 lines
9.4 KiB
Rust
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<RefCell<HashMap<String, Vec<String>>>>,
|
|
}
|
|
|
|
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::<Monitor>()
|
|
.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::<gtk::Button>() {
|
|
button.activate();
|
|
} else if let Ok(label) = child.downcast::<gtk::Label>() {
|
|
display.clipboard().set_text(&label.text());
|
|
label.set_markup(&format!(
|
|
"{}<span foreground=\"{}\"><i> - Copied</i></span>",
|
|
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<String> = 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::<Vec<_>>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|