Files
jmb-bar/src/ui/run_menu.rs
jmb05 495210c229
Some checks failed
Build Project / Build Linux (push) Failing after 1m2s
Random commit
2026-11-03 01:33:33 +01:00

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: &gtk::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: &gtk::Entry,
content_box: &gtk::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);
}
}
}
}
}