improve theme and add examples, support clicks
This commit is contained in:
parent
962f6a8688
commit
141e9ac124
13 changed files with 991 additions and 430 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2105,6 +2105,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smithay-client-toolkit",
|
"smithay-client-toolkit",
|
||||||
|
"strsim 0.11.1",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
@ -28,3 +28,4 @@ calloop = "0.14.2"
|
||||||
crossbeam = "0.8.4"
|
crossbeam = "0.8.4"
|
||||||
libc = "0.2.171"
|
libc = "0.2.171"
|
||||||
freedesktop-file-parser = "0.1.0"
|
freedesktop-file-parser = "0.1.0"
|
||||||
|
strsim = "0.11.1"
|
||||||
|
|
23
README.md
23
README.md
|
@ -1,10 +1,8 @@
|
||||||
# Worf
|
# Worf
|
||||||
|
|
||||||
Worf is a clone of [wofi](https://github.com/SimplyCEO/wofi) written in rust.
|
Worf is yet another dmenu style launcher, heavily inspired by wofi but written in Rust on top of GTK4.
|
||||||
Although no code was taken over, the original project is great and to honor their license this tool is licensed under the same GPLV3 terms.
|
It supports a lot of things the same way wofi does, so migrating to worf is easy, but things I did not
|
||||||
|
deemed necessary where dropped from worf. See breaking changes section for details.
|
||||||
* Wofis css files are supported
|
|
||||||
* Wofis command line flags are supported
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
@ -20,10 +18,19 @@ layerrule = blur, worf
|
||||||
* Window switcher for hyprland
|
* Window switcher for hyprland
|
||||||
|
|
||||||
## Breaking changes to Wofi
|
## Breaking changes to Wofi
|
||||||
* Error messages differ
|
* Runtime behaviour is not guaranteed to be the same and won't ever be, this includes error messages and themes.
|
||||||
|
* Themes in general are mostly compatible. Worf is using the same entity ids,
|
||||||
|
because worf is build on GTK4 instead of GTK3 there will be differences in the look and feel.
|
||||||
* Configuration files are not 100% compatible, Worf is using toml files instead, for most part this only means strings have to be quoted
|
* Configuration files are not 100% compatible, Worf is using toml files instead, for most part this only means strings have to be quoted
|
||||||
* Themes are not 100% compatible
|
* Color files are not supported
|
||||||
|
|
||||||
|
## Dropped configuration options
|
||||||
|
* stylesheet -> use style instead
|
||||||
|
* color / colors -> GTK4 does not support color files
|
||||||
|
|
||||||
|
## New options
|
||||||
|
* --fuzzy-length: Defines how long a string must be be
|
||||||
|
|
||||||
|
|
||||||
## Not supported
|
## Not supported
|
||||||
* Wofi has a C-API, that is not and won't be supported. As of now there are no plans to provide a Rust API either.
|
* Wofi has a C-API, that is not and won't be supported.
|
||||||
|
|
38
src/args.rs
38
src/args.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::lib::config::{Align, MatchMethod, Orientation};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -51,10 +52,6 @@ pub struct Args {
|
||||||
#[clap(short = 's', long = "style")]
|
#[clap(short = 's', long = "style")]
|
||||||
style: Option<String>,
|
style: Option<String>,
|
||||||
|
|
||||||
/// Selects a colors file to use
|
|
||||||
#[clap(short = 'C', long = "color")]
|
|
||||||
color: Option<String>,
|
|
||||||
|
|
||||||
/// Runs in dmenu mode
|
/// Runs in dmenu mode
|
||||||
#[clap(short = 'd', long = "dmenu")]
|
#[clap(short = 'd', long = "dmenu")]
|
||||||
dmenu: bool,
|
dmenu: bool,
|
||||||
|
@ -117,7 +114,7 @@ pub struct Args {
|
||||||
|
|
||||||
/// Sets the matching method, default is contains
|
/// Sets the matching method, default is contains
|
||||||
#[clap(short = 'M', long = "matching")]
|
#[clap(short = 'M', long = "matching")]
|
||||||
matching: Option<String>,
|
matching: Option<MatchMethod>,
|
||||||
|
|
||||||
/// Allows case insensitive searching
|
/// Allows case insensitive searching
|
||||||
#[clap(short = 'i', long = "insensitive")]
|
#[clap(short = 'i', long = "insensitive")]
|
||||||
|
@ -170,4 +167,35 @@ pub struct Args {
|
||||||
/// Runs command for the displayed entries, without changing the output. %s for the real string
|
/// Runs command for the displayed entries, without changing the output. %s for the real string
|
||||||
#[clap(short = 'r', long = "pre-display-cmd")]
|
#[clap(short = 'r', long = "pre-display-cmd")]
|
||||||
pre_display_cmd: Option<String>,
|
pre_display_cmd: Option<String>,
|
||||||
|
|
||||||
|
/// Defines how good a fuzzy match must be, to be shown.
|
||||||
|
#[clap(long = "fuzzy-min-score")]
|
||||||
|
fuzzy_min_score: Option<f64>,
|
||||||
|
|
||||||
|
/// Size of displayed images
|
||||||
|
#[clap(long = "image-size")]
|
||||||
|
image_size: Option<i32>,
|
||||||
|
|
||||||
|
/// Orientation of main window
|
||||||
|
#[clap(long = "orientation")]
|
||||||
|
orientation: Option<Orientation>,
|
||||||
|
|
||||||
|
/// Orientation of the row box, defining if label is below or at the side.
|
||||||
|
#[clap(long = "row-box-orientation")]
|
||||||
|
row_bow_orientation: Option<Orientation>,
|
||||||
|
|
||||||
|
/// Specifies the horizontal align for the entire scrolled area,
|
||||||
|
/// it can be any of fill, start, end, or center, default is fill.
|
||||||
|
#[clap(long = "halign")]
|
||||||
|
pub halign: Option<Align>,
|
||||||
|
//// Specifies the horizontal align for the individual entries,
|
||||||
|
// it can be any of fill, start, end, or center, default is fill.
|
||||||
|
#[clap(long = "content-halign")]
|
||||||
|
pub content_halign: Option<Align>,
|
||||||
|
|
||||||
|
/// Specifies the vertical align for the entire scrolled area, it can be any of fill, start, e
|
||||||
|
/// nd, or center, the default is orientation dependent. If vertical then it defaults to
|
||||||
|
/// start, if horizontal it defaults to center.
|
||||||
|
#[clap(long = "valign")]
|
||||||
|
pub valign: Option<Align>,
|
||||||
}
|
}
|
||||||
|
|
290
src/gui.rs
290
src/gui.rs
|
@ -1,290 +0,0 @@
|
||||||
use crate::config::Config;
|
|
||||||
use anyhow::{Context, anyhow};
|
|
||||||
use crossbeam::channel;
|
|
||||||
use crossbeam::channel::Sender;
|
|
||||||
use gdk4::gio::File;
|
|
||||||
use gdk4::glib::Propagation;
|
|
||||||
use gdk4::prelude::{Cast, DisplayExt, MonitorExt};
|
|
||||||
use gdk4::{Display, Key};
|
|
||||||
use gtk4::prelude::{
|
|
||||||
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt,
|
|
||||||
FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt,
|
|
||||||
};
|
|
||||||
use gtk4::{Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, Image, Label, ListBox, ListBoxRow, PolicyType, ScrolledWindow, SearchEntry, Widget};
|
|
||||||
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
|
|
||||||
use gtk4_layer_shell::{KeyboardMode, LayerShell};
|
|
||||||
use log::{debug, error, info};
|
|
||||||
use std::process::exit;
|
|
||||||
use hyprland::ctl::output::create;
|
|
||||||
use hyprland::ctl::plugin::list;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MenuItem {
|
|
||||||
pub label: String, // todo support empty label?
|
|
||||||
pub icon_path: Option<String>,
|
|
||||||
pub action: Option<String>,
|
|
||||||
pub sub_elements: Vec<MenuItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show(config: Config, elements: Vec<MenuItem>) -> anyhow::Result<(i32)> {
|
|
||||||
// Load CSS
|
|
||||||
let provider = CssProvider::new();
|
|
||||||
let css_file_path = File::for_path("/home/me/.config/wofi/style.css");
|
|
||||||
|
|
||||||
provider.load_from_file(&css_file_path);
|
|
||||||
// Apply CSS to the display
|
|
||||||
let display = Display::default().expect("Could not connect to a display");
|
|
||||||
gtk4::style_context_add_provider_for_display(
|
|
||||||
&display,
|
|
||||||
&provider,
|
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
let display = Display::default().expect("Could not connect to a display");
|
|
||||||
// Apply CSS to the display
|
|
||||||
gtk4::style_context_add_provider_for_display(
|
|
||||||
&display,
|
|
||||||
&provider,
|
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
// No need for application_id unless you want portal support
|
|
||||||
let app = Application::builder().application_id("worf").build();
|
|
||||||
let (sender, receiver) = channel::bounded(1);
|
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
|
||||||
// Create a toplevel undecorated window
|
|
||||||
let window = ApplicationWindow::builder()
|
|
||||||
.application(app)
|
|
||||||
.decorated(false)
|
|
||||||
.resizable(false)
|
|
||||||
.default_width(20)
|
|
||||||
.default_height(20)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
window.set_widget_name("window");
|
|
||||||
|
|
||||||
config.normal_window.map(|normal| {
|
|
||||||
if !normal {
|
|
||||||
window.set_layer(gtk4_layer_shell::Layer::Overlay);
|
|
||||||
window.init_layer_shell();
|
|
||||||
window.set_keyboard_mode(KeyboardMode::Exclusive);
|
|
||||||
window.set_namespace(Some("worf"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let outer_box = gtk4::Box::new(Orientation::Vertical, 0);
|
|
||||||
outer_box.set_widget_name("outer-box");
|
|
||||||
window.set_child(Some(&outer_box));
|
|
||||||
|
|
||||||
let entry = SearchEntry::new();
|
|
||||||
entry.set_widget_name("input");
|
|
||||||
entry.set_css_classes(&["input"]);
|
|
||||||
entry.set_placeholder_text(config.prompt.as_deref());
|
|
||||||
|
|
||||||
// Example `search` and `password_char` usage
|
|
||||||
// let password_char = Some('*');
|
|
||||||
// todo\
|
|
||||||
// if let Some(c) = password_char {
|
|
||||||
// let entry_casted: Entry = entry.clone().upcast();
|
|
||||||
// entry_casted.set_visibility(false);
|
|
||||||
// entry_casted.set_invisible_char(c);
|
|
||||||
// }
|
|
||||||
|
|
||||||
outer_box.append(&entry);
|
|
||||||
|
|
||||||
let scroll = ScrolledWindow::new();
|
|
||||||
scroll.set_widget_name("scroll");
|
|
||||||
scroll.set_hexpand(true);
|
|
||||||
scroll.set_vexpand(true);
|
|
||||||
|
|
||||||
let hide_scroll = false; // todo
|
|
||||||
if hide_scroll {
|
|
||||||
scroll.set_policy(PolicyType::External, PolicyType::External);
|
|
||||||
}
|
|
||||||
|
|
||||||
outer_box.append(&scroll);
|
|
||||||
|
|
||||||
let inner_box = FlowBox::new();
|
|
||||||
inner_box.set_widget_name("inner-box");
|
|
||||||
inner_box.set_css_classes(&["inner-box"]);
|
|
||||||
|
|
||||||
inner_box.set_selection_mode(gtk4::SelectionMode::Browse);
|
|
||||||
inner_box.set_max_children_per_line(1); // todo change to `columns` variable
|
|
||||||
//inner_box.set_orientation(Orientation::Horizontal); // or Vertical
|
|
||||||
inner_box.set_halign(Align::Fill);
|
|
||||||
inner_box.set_valign(Align::Start);
|
|
||||||
inner_box.set_activate_on_single_click(true);
|
|
||||||
|
|
||||||
for entry in &elements {
|
|
||||||
add_menu_item(&inner_box, &entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set focus after everything is realized
|
|
||||||
inner_box.connect_map(|fb| {
|
|
||||||
fb.grab_focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0);
|
|
||||||
wrapper_box.set_homogeneous(true);
|
|
||||||
wrapper_box.append(&inner_box);
|
|
||||||
scroll.set_child(Some(&wrapper_box));
|
|
||||||
|
|
||||||
// todo implement search function
|
|
||||||
// // Dummy filter and sort funcs – replace with actual logic
|
|
||||||
// inner_box.set_filter_func(Some(Box::new(|_child| {
|
|
||||||
// true // filter logic here
|
|
||||||
// })));
|
|
||||||
// inner_box.set_sort_func(Some(Box::new(|child1, child2| {
|
|
||||||
// child1.widget_name().cmp(&child2.widget_name())
|
|
||||||
// })));
|
|
||||||
|
|
||||||
// Create key event controller
|
|
||||||
let entry_clone = entry.clone();
|
|
||||||
setup_key_event_handler(&window, entry_clone, inner_box, app.clone(), sender.clone());
|
|
||||||
|
|
||||||
window.show();
|
|
||||||
|
|
||||||
// Get the display where the window resides
|
|
||||||
let display = window.display();
|
|
||||||
|
|
||||||
// Get the monitor that the window is on (use window's coordinates to find this)
|
|
||||||
window.surface().map(|surface| {
|
|
||||||
let monitor = display.monitor_at_surface(&surface);
|
|
||||||
if let Some(monitor) = monitor {
|
|
||||||
let geometry = monitor.geometry();
|
|
||||||
config.width.as_ref().map(|width| {
|
|
||||||
percent_or_absolute(&width, geometry.width())
|
|
||||||
.map(|w| window.set_width_request(w))
|
|
||||||
});
|
|
||||||
config.height.as_ref().map(|height| {
|
|
||||||
percent_or_absolute(&height, geometry.height())
|
|
||||||
.map(|h| window.set_height_request(h))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
error!("failed to get monitor to init window size");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let empty_array: [&str; 0] = [];
|
|
||||||
|
|
||||||
app.run_with_args(&empty_array);
|
|
||||||
let selected_index = receiver.recv()?;
|
|
||||||
Ok(selected_index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_key_event_handler(
|
|
||||||
window: &ApplicationWindow,
|
|
||||||
entry_clone: SearchEntry,
|
|
||||||
inner_box: FlowBox,
|
|
||||||
app: Application,
|
|
||||||
sender: Sender<i32>,
|
|
||||||
) {
|
|
||||||
let key_controller = EventControllerKey::new();
|
|
||||||
key_controller.connect_key_pressed(move |_, key_value, _, _| {
|
|
||||||
match key_value {
|
|
||||||
Key::Escape => exit(1), // todo better way to do this?
|
|
||||||
Key::Return => {
|
|
||||||
for s in &inner_box.selected_children() {
|
|
||||||
// let element : &Option<&EntryElement> = &elements.get(s.index() as usize);
|
|
||||||
// if let Some(element) = *element {
|
|
||||||
// debug!("Running action on element with name {}", element.label);
|
|
||||||
// (element.action)();
|
|
||||||
// }
|
|
||||||
if let Err(e) = sender.send(s.index()) {
|
|
||||||
error!("failed to send selected child {e:?}")
|
|
||||||
}
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if let Some(c) = key_value.name() {
|
|
||||||
// Only proceed if it's a single alphanumeric character
|
|
||||||
if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) {
|
|
||||||
let current = entry_clone.text().to_string();
|
|
||||||
entry_clone.set_text(&format!("{current}{c}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
// Add the controller to the window
|
|
||||||
window.add_controller(key_controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) {
|
|
||||||
let parent: Widget = if !entry_element.sub_elements.is_empty() {
|
|
||||||
let expander = Expander::new(None);
|
|
||||||
expander.set_widget_name("expander-box");
|
|
||||||
expander.set_halign(Align::Fill);
|
|
||||||
|
|
||||||
let menu_row = create_menu_row(entry_element);
|
|
||||||
expander.set_label_widget(Some(&menu_row));
|
|
||||||
|
|
||||||
let list_box = ListBox::new();
|
|
||||||
list_box.set_widget_name("entry");
|
|
||||||
|
|
||||||
// todo multi nesting is not supported yet.
|
|
||||||
for sub_item in entry_element.sub_elements.iter(){
|
|
||||||
list_box.append(&create_menu_row(sub_item));
|
|
||||||
}
|
|
||||||
|
|
||||||
expander.set_child(Some(&list_box));
|
|
||||||
expander.upcast()
|
|
||||||
} else {
|
|
||||||
create_menu_row(entry_element).upcast()
|
|
||||||
};
|
|
||||||
|
|
||||||
parent.set_halign(Align::Start);
|
|
||||||
|
|
||||||
let child = FlowBoxChild::new();
|
|
||||||
child.set_widget_name("entry");
|
|
||||||
child.set_child(Some(&parent));
|
|
||||||
|
|
||||||
inner_box.append(&child);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_menu_row(menu_item: &MenuItem) -> Widget {
|
|
||||||
let row = ListBoxRow::new();
|
|
||||||
row.set_widget_name("entry");
|
|
||||||
row.set_hexpand(true);
|
|
||||||
row.set_halign(Align::Start);
|
|
||||||
|
|
||||||
let row_box = gtk4::Box::new(Orientation::Horizontal, 0);
|
|
||||||
row.set_child(Some(&row_box));
|
|
||||||
|
|
||||||
if let Some(image_path) = &menu_item.icon_path {
|
|
||||||
// todo check config too
|
|
||||||
let image = Image::from_icon_name(image_path);
|
|
||||||
image.set_pixel_size(24);
|
|
||||||
image.set_widget_name("img");
|
|
||||||
row_box.append(&image);
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = Label::new(Some(&menu_item.label));
|
|
||||||
|
|
||||||
label.set_widget_name("unselected");
|
|
||||||
row_box.append(&label);
|
|
||||||
|
|
||||||
|
|
||||||
row.upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn percent_or_absolute(value: &String, base_value: i32) -> Option<i32> {
|
|
||||||
if value.contains("%") {
|
|
||||||
let value = value.replace("%", "");
|
|
||||||
let value = value.trim();
|
|
||||||
match value.parse::<i32>() {
|
|
||||||
Ok(n) => {
|
|
||||||
let result = ((n as f32 / 100.0) * base_value as f32) as i32;
|
|
||||||
Some(result)
|
|
||||||
}
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value.parse::<i32>().ok()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,41 @@
|
||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
|
use crate::lib::system;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use clap::ValueEnum;
|
||||||
use gtk4::prelude::ToValue;
|
use gtk4::prelude::ToValue;
|
||||||
use merge::Merge;
|
use merge::Merge;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum MatchMethod {
|
||||||
|
Fuzzy,
|
||||||
|
Contains,
|
||||||
|
MultiContains,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Orientation {
|
||||||
|
Vertical,
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Align {
|
||||||
|
Fill,
|
||||||
|
Start,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Merge, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Merge, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
/// Defines the path to the stylesheet being used.
|
||||||
|
/// Defaults to XDG_CONFIG_DIR/worf/style.css
|
||||||
|
/// If XDG_CONFIG_DIR is not defined $HOME/.config will be used instead
|
||||||
|
#[serde(default = "default_style")]
|
||||||
pub style: Option<String>,
|
pub style: Option<String>,
|
||||||
pub stylesheet: Option<String>,
|
|
||||||
pub color: Option<String>,
|
|
||||||
pub colors: Option<String>,
|
|
||||||
pub show: Option<String>,
|
pub show: Option<String>,
|
||||||
pub mode: Option<String>,
|
pub mode: Option<String>,
|
||||||
#[serde(default = "default_width")]
|
#[serde(default = "default_width")]
|
||||||
|
@ -32,24 +57,46 @@ pub struct Config {
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub exec_search: Option<bool>,
|
pub exec_search: Option<bool>,
|
||||||
pub hide_scroll: Option<bool>,
|
pub hide_scroll: Option<bool>,
|
||||||
pub matching: Option<String>,
|
|
||||||
|
/// Defines how matching is done
|
||||||
|
#[serde(default = "default_match_method")]
|
||||||
|
pub matching: Option<MatchMethod>,
|
||||||
pub insensitive: Option<bool>,
|
pub insensitive: Option<bool>,
|
||||||
pub parse_search: Option<bool>,
|
pub parse_search: Option<bool>,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
pub no_actions: Option<bool>,
|
pub no_actions: Option<bool>,
|
||||||
pub lines: Option<u32>,
|
pub lines: Option<u32>,
|
||||||
|
/// Defines how many columns are shown per row
|
||||||
|
#[serde(default = "default_columns")]
|
||||||
pub columns: Option<u32>,
|
pub columns: Option<u32>,
|
||||||
pub sort_order: Option<String>,
|
pub sort_order: Option<String>,
|
||||||
pub gtk_dark: Option<bool>,
|
pub gtk_dark: Option<bool>,
|
||||||
pub search: Option<String>,
|
pub search: Option<String>,
|
||||||
pub monitor: Option<String>,
|
pub monitor: Option<String>,
|
||||||
pub pre_display_cmd: Option<String>,
|
pub pre_display_cmd: Option<String>,
|
||||||
pub orientation: Option<String>,
|
/// Defines how the entries root container are ordered
|
||||||
pub halign: Option<String>,
|
/// Default is vertical
|
||||||
pub content_halign: Option<String>,
|
#[serde(default = "default_orientation")]
|
||||||
pub valign: Option<String>,
|
pub orientation: Option<Orientation>,
|
||||||
|
/// Specifies the horizontal align for the entire scrolled area,
|
||||||
|
/// it can be any of fill, start, end, or center, default is fill.
|
||||||
|
#[serde(default = "default_halign")]
|
||||||
|
pub halign: Option<Align>,
|
||||||
|
//// Specifies the horizontal align for the individual entries,
|
||||||
|
// it can be any of fill, start, end, or center, default is fill.
|
||||||
|
#[serde(default = "default_content_halign")]
|
||||||
|
pub content_halign: Option<Align>,
|
||||||
|
|
||||||
|
/// Specifies the vertical align for the entire scrolled area, it can be any of fill, start, e
|
||||||
|
/// nd, or center, the default is orientation dependent. If vertical then it defaults to
|
||||||
|
/// start, if horizontal it defaults to center.
|
||||||
|
pub valign: Option<Align>,
|
||||||
|
|
||||||
pub filter_rate: Option<u32>,
|
pub filter_rate: Option<u32>,
|
||||||
pub image_size: Option<u32>,
|
/// Specifies the image size when enabled.
|
||||||
|
/// Defaults to 32.
|
||||||
|
#[serde(default = "default_image_size")]
|
||||||
|
pub image_size: Option<i32>,
|
||||||
pub key_up: Option<String>,
|
pub key_up: Option<String>,
|
||||||
pub key_down: Option<String>,
|
pub key_down: Option<String>,
|
||||||
pub key_left: Option<String>,
|
pub key_left: Option<String>,
|
||||||
|
@ -73,19 +120,28 @@ pub struct Config {
|
||||||
pub copy_exec: Option<String>,
|
pub copy_exec: Option<String>,
|
||||||
pub single_click: Option<bool>,
|
pub single_click: Option<bool>,
|
||||||
pub pre_display_exec: Option<bool>,
|
pub pre_display_exec: Option<bool>,
|
||||||
|
|
||||||
|
// Exclusive options
|
||||||
|
/// Minimum score for the fuzzy finder to accept a match.
|
||||||
|
/// Must be a value between 0 and 1
|
||||||
|
/// Defaults to 0.1.
|
||||||
|
#[serde(default = "default_fuzzy_min_score")]
|
||||||
|
pub fuzzy_min_score: Option<f64>,
|
||||||
|
|
||||||
|
/// Defines how the content in the row box is aligned
|
||||||
|
/// Defaults to vertical
|
||||||
|
#[serde(default = "default_row_box_orientation")]
|
||||||
|
pub row_bow_orientation: Option<Orientation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Config {
|
Config {
|
||||||
style: None,
|
style: default_style(),
|
||||||
stylesheet: None,
|
|
||||||
color: None,
|
|
||||||
colors: None,
|
|
||||||
show: None,
|
show: None,
|
||||||
mode: None,
|
mode: None,
|
||||||
width: None,
|
width: default_width(),
|
||||||
height: None,
|
height: default_height(),
|
||||||
prompt: None,
|
prompt: None,
|
||||||
xoffset: None,
|
xoffset: None,
|
||||||
x: None,
|
x: None,
|
||||||
|
@ -105,18 +161,18 @@ impl Default for Config {
|
||||||
location: None,
|
location: None,
|
||||||
no_actions: None,
|
no_actions: None,
|
||||||
lines: None,
|
lines: None,
|
||||||
columns: None,
|
columns: default_columns(),
|
||||||
sort_order: None,
|
sort_order: None,
|
||||||
gtk_dark: None,
|
gtk_dark: None,
|
||||||
search: None,
|
search: None,
|
||||||
monitor: None,
|
monitor: None,
|
||||||
pre_display_cmd: None,
|
pre_display_cmd: None,
|
||||||
orientation: None,
|
orientation: default_row_box_orientation(),
|
||||||
halign: None,
|
halign: default_halign(),
|
||||||
content_halign: None,
|
content_halign: default_content_halign(),
|
||||||
valign: None,
|
valign: None,
|
||||||
filter_rate: None,
|
filter_rate: None,
|
||||||
image_size: None,
|
image_size: default_image_size(),
|
||||||
key_up: None,
|
key_up: None,
|
||||||
key_down: None,
|
key_down: None,
|
||||||
key_left: None,
|
key_left: None,
|
||||||
|
@ -139,10 +195,32 @@ impl Default for Config {
|
||||||
copy_exec: None,
|
copy_exec: None,
|
||||||
single_click: None,
|
single_click: None,
|
||||||
pre_display_exec: None,
|
pre_display_exec: None,
|
||||||
|
fuzzy_min_score: default_fuzzy_min_score(),
|
||||||
|
row_bow_orientation: default_row_box_orientation(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_row_box_orientation() -> Option<Orientation> {
|
||||||
|
Some(Orientation::Horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_orientation() -> Option<Orientation> {
|
||||||
|
Some(Orientation::Vertical)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_halign() -> Option<Align> {
|
||||||
|
Some(Align::Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_content_halign() -> Option<Align> {
|
||||||
|
Some(Align::Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_columns() -> Option<u32> {
|
||||||
|
Some(1)
|
||||||
|
}
|
||||||
|
|
||||||
fn default_normal_window() -> Option<bool> {
|
fn default_normal_window() -> Option<bool> {
|
||||||
Some(false)
|
Some(false)
|
||||||
}
|
}
|
||||||
|
@ -224,18 +302,44 @@ fn default_normal_window() -> Option<bool> {
|
||||||
// key_default = "Ctrl-c";
|
// key_default = "Ctrl-c";
|
||||||
// char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default);
|
// char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default);
|
||||||
|
|
||||||
fn default_height() -> Option<String> {
|
fn default_style() -> Option<String> {
|
||||||
|
system::config_path(None)
|
||||||
|
.ok()
|
||||||
|
.and_then(|pb| Some(pb.display().to_string()))
|
||||||
|
.or_else(|| {
|
||||||
|
log::error!("no stylesheet found, using system styles");
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_height() -> Option<String> {
|
||||||
Some("40%".to_owned())
|
Some("40%".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_width() -> Option<String> {
|
pub fn default_width() -> Option<String> {
|
||||||
Some("50%".to_owned())
|
Some("50%".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_password_char() -> Option<String> {
|
pub fn default_password_char() -> Option<String> {
|
||||||
Some("*".to_owned())
|
Some("*".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_fuzzy_min_length() -> Option<i32> {
|
||||||
|
Some(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_fuzzy_min_score() -> Option<f64> {
|
||||||
|
Some(0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_match_method() -> Option<MatchMethod> {
|
||||||
|
Some(MatchMethod::Contains)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_image_size() -> Option<i32> {
|
||||||
|
Some(32)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn merge_config_with_args(config: &mut Config, args: &Args) -> anyhow::Result<Config> {
|
pub fn merge_config_with_args(config: &mut Config, args: &Args) -> anyhow::Result<Config> {
|
||||||
let args_json = serde_json::to_value(args)?;
|
let args_json = serde_json::to_value(args)?;
|
||||||
let mut config_json = serde_json::to_value(config)?;
|
let mut config_json = serde_json::to_value(config)?;
|
|
@ -121,19 +121,14 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<Pa
|
||||||
fs::read_dir(folder).ok().map(|entries| {
|
fs::read_dir(folder).ok().map(|entries| {
|
||||||
entries
|
entries
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.filter(|entry| {
|
.filter_map(|entry| entry.path().canonicalize().ok())
|
||||||
entry
|
|
||||||
.file_type()
|
|
||||||
.ok()
|
|
||||||
.is_some_and(|file_type| file_type.is_file())
|
|
||||||
})
|
|
||||||
.filter(|entry| {
|
.filter(|entry| {
|
||||||
entry
|
entry
|
||||||
.file_name()
|
.file_name()
|
||||||
.to_str()
|
.and_then(|e| e.to_str()) // Handle the Option here.
|
||||||
.is_some_and(|name| file_name.is_match(name))
|
.map(|name| file_name.is_match(name))
|
||||||
|
.unwrap_or(false) // Handle the case where the file name is not a valid string.
|
||||||
})
|
})
|
||||||
.map(|entry| entry.path())
|
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -142,6 +137,7 @@ pub(crate) fn find_desktop_files() -> Vec<DesktopFile> {
|
||||||
let mut paths = vec![
|
let mut paths = vec![
|
||||||
PathBuf::from("/usr/share/applications"),
|
PathBuf::from("/usr/share/applications"),
|
||||||
PathBuf::from("/usr/local/share/applications"),
|
PathBuf::from("/usr/local/share/applications"),
|
||||||
|
PathBuf::from("/var/lib/flatpak/exports/share/applications"),
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(home) = home_dir() {
|
if let Some(home) = home_dir() {
|
||||||
|
@ -166,7 +162,6 @@ pub(crate) fn find_desktop_files() -> Vec<DesktopFile> {
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get_locale_variants() -> Vec<String> {
|
pub fn get_locale_variants() -> Vec<String> {
|
||||||
let locale = env::var("LC_ALL")
|
let locale = env::var("LC_ALL")
|
||||||
.or_else(|_| env::var("LC_MESSAGES"))
|
.or_else(|_| env::var("LC_MESSAGES"))
|
544
src/lib/gui.rs
Normal file
544
src/lib/gui.rs
Normal file
|
@ -0,0 +1,544 @@
|
||||||
|
use crate::lib::config;
|
||||||
|
use crate::lib::config::{Config, MatchMethod};
|
||||||
|
use anyhow::{Context, anyhow};
|
||||||
|
use crossbeam::channel;
|
||||||
|
use crossbeam::channel::Sender;
|
||||||
|
use gdk4::gio::{File, Menu};
|
||||||
|
use gdk4::glib::{GString, Propagation, Unichar};
|
||||||
|
use gdk4::prelude::{Cast, DisplayExt, ListModelExtManual, MonitorExt};
|
||||||
|
use gdk4::{Display, Key};
|
||||||
|
use gtk4::prelude::{
|
||||||
|
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt,
|
||||||
|
FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt,
|
||||||
|
WidgetExt,
|
||||||
|
};
|
||||||
|
use gtk4::{
|
||||||
|
Align, Entry, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label,
|
||||||
|
ListBox, ListBoxRow, Ordering, PolicyType, Revealer, ScrolledWindow, SearchEntry, Widget, gdk,
|
||||||
|
};
|
||||||
|
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
|
||||||
|
use gtk4_layer_shell::{KeyboardMode, LayerShell};
|
||||||
|
use hyprland::ctl::output::create;
|
||||||
|
use hyprland::ctl::plugin::list;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use std::process::exit;
|
||||||
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
|
||||||
|
type ArcMenuMap = Arc<Mutex<HashMap<FlowBoxChild, MenuItem>>>;
|
||||||
|
type MenuItemSender = Sender<Result<MenuItem, anyhow::Error>>;
|
||||||
|
|
||||||
|
impl Into<Orientation> for config::Orientation {
|
||||||
|
fn into(self) -> Orientation {
|
||||||
|
match self {
|
||||||
|
config::Orientation::Vertical => Orientation::Vertical,
|
||||||
|
config::Orientation::Horizontal => Orientation::Horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Align> for config::Align {
|
||||||
|
fn into(self) -> Align {
|
||||||
|
match self {
|
||||||
|
config::Align::Fill => Align::Fill,
|
||||||
|
config::Align::Start => Align::Start,
|
||||||
|
config::Align::Center => Align::Center,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MenuItem {
|
||||||
|
pub label: String, // todo support empty label?
|
||||||
|
pub icon_path: Option<String>,
|
||||||
|
pub action: Option<String>,
|
||||||
|
pub sub_elements: Vec<MenuItem>,
|
||||||
|
pub working_dir: Option<String>,
|
||||||
|
pub initial_sort_score: i64,
|
||||||
|
pub search_sort_score: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(config: Config, elements: Vec<MenuItem>) -> Result<MenuItem, anyhow::Error> {
|
||||||
|
if let Some(ref css) = config.style {
|
||||||
|
let provider = CssProvider::new();
|
||||||
|
let css_file_path = File::for_path(css);
|
||||||
|
provider.load_from_file(&css_file_path);
|
||||||
|
let display = Display::default().expect("Could not connect to a display");
|
||||||
|
gtk4::style_context_add_provider_for_display(
|
||||||
|
&display,
|
||||||
|
&provider,
|
||||||
|
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need for application_id unless you want portal support
|
||||||
|
let app = Application::builder().application_id("worf").build();
|
||||||
|
let (sender, receiver) = channel::bounded(1);
|
||||||
|
|
||||||
|
app.connect_activate(move |app| {
|
||||||
|
build_ui(&config, &elements, sender.clone(), app);
|
||||||
|
});
|
||||||
|
|
||||||
|
let empty_array: [&str; 0] = [];
|
||||||
|
app.run_with_args(&empty_array);
|
||||||
|
let selection = receiver.recv()?;
|
||||||
|
selection
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(
|
||||||
|
config: &Config,
|
||||||
|
elements: &Vec<MenuItem>,
|
||||||
|
sender: Sender<Result<MenuItem, anyhow::Error>>,
|
||||||
|
app: &Application,
|
||||||
|
) {
|
||||||
|
// Create a toplevel undecorated window
|
||||||
|
let window = ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.decorated(false)
|
||||||
|
.resizable(false)
|
||||||
|
.default_width(20)
|
||||||
|
.default_height(20)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
window.set_widget_name("window");
|
||||||
|
|
||||||
|
config.normal_window.map(|normal| {
|
||||||
|
if !normal {
|
||||||
|
// Initialize the window as a layer
|
||||||
|
window.init_layer_shell();
|
||||||
|
window.set_layer(gtk4_layer_shell::Layer::Overlay);
|
||||||
|
window.set_keyboard_mode(KeyboardMode::Exclusive);
|
||||||
|
window.set_namespace(Some("worf"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0);
|
||||||
|
outer_box.set_widget_name("outer-box");
|
||||||
|
|
||||||
|
window.set_child(Some(&outer_box));
|
||||||
|
|
||||||
|
let entry = SearchEntry::new();
|
||||||
|
entry.set_widget_name("input");
|
||||||
|
entry.set_css_classes(&["input"]);
|
||||||
|
entry.set_placeholder_text(config.prompt.as_deref());
|
||||||
|
entry.set_sensitive(false);
|
||||||
|
outer_box.append(&entry);
|
||||||
|
|
||||||
|
let scroll = ScrolledWindow::new();
|
||||||
|
scroll.set_widget_name("scroll");
|
||||||
|
scroll.set_hexpand(true);
|
||||||
|
scroll.set_vexpand(true);
|
||||||
|
// if let Some(valign) = config.valign {
|
||||||
|
// scroll.set_valign(valign.into());
|
||||||
|
// } else {
|
||||||
|
// if config.orientation.unwrap() == config::Orientation::Horizontal {
|
||||||
|
// scroll.set_valign(Align::Center);
|
||||||
|
// } else {
|
||||||
|
// scroll.set_valign(Align::Start);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
let hide_scroll = false; // todo
|
||||||
|
if hide_scroll {
|
||||||
|
scroll.set_policy(PolicyType::External, PolicyType::External);
|
||||||
|
}
|
||||||
|
|
||||||
|
outer_box.append(&scroll);
|
||||||
|
|
||||||
|
let inner_box = FlowBox::new();
|
||||||
|
inner_box.set_widget_name("inner-box");
|
||||||
|
inner_box.set_css_classes(&["inner-box"]);
|
||||||
|
inner_box.set_hexpand(true);
|
||||||
|
inner_box.set_vexpand(false);
|
||||||
|
|
||||||
|
inner_box.set_selection_mode(gtk4::SelectionMode::Browse);
|
||||||
|
inner_box.set_max_children_per_line(config.columns.unwrap());
|
||||||
|
inner_box.set_activate_on_single_click(true);
|
||||||
|
|
||||||
|
let mut list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
for entry in elements {
|
||||||
|
list_items
|
||||||
|
.lock()
|
||||||
|
.unwrap() // panic here ok? deadlock?
|
||||||
|
.insert(
|
||||||
|
add_menu_item(
|
||||||
|
&inner_box,
|
||||||
|
&entry,
|
||||||
|
&config,
|
||||||
|
sender.clone(),
|
||||||
|
list_items.clone(),
|
||||||
|
app.clone(),
|
||||||
|
),
|
||||||
|
entry.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let items_clone = list_items.clone();
|
||||||
|
inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_clone));
|
||||||
|
|
||||||
|
// Set focus after everything is realized
|
||||||
|
inner_box.connect_map(|fb| {
|
||||||
|
fb.grab_focus();
|
||||||
|
fb.invalidate_sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0);
|
||||||
|
wrapper_box.append(&inner_box);
|
||||||
|
scroll.set_child(Some(&wrapper_box));
|
||||||
|
|
||||||
|
setup_key_event_handler(
|
||||||
|
&window,
|
||||||
|
entry.clone(),
|
||||||
|
inner_box,
|
||||||
|
app.clone(),
|
||||||
|
sender.clone(),
|
||||||
|
list_items.clone(),
|
||||||
|
config.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.show();
|
||||||
|
|
||||||
|
let display = window.display();
|
||||||
|
window.surface().map(|surface| {
|
||||||
|
// todo this does not work for multi monitor systems
|
||||||
|
let monitor = display.monitor_at_surface(&surface);
|
||||||
|
if let Some(monitor) = monitor {
|
||||||
|
let geometry = monitor.geometry();
|
||||||
|
config.width.as_ref().map(|width| {
|
||||||
|
percent_or_absolute(&width, geometry.width()).map(|w| window.set_width_request(w))
|
||||||
|
});
|
||||||
|
config.height.as_ref().map(|height| {
|
||||||
|
percent_or_absolute(&height, geometry.height())
|
||||||
|
.map(|h| window.set_height_request(h))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log::error!("failed to get monitor to init window size");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_key_event_handler(
|
||||||
|
window: &ApplicationWindow,
|
||||||
|
entry_clone: SearchEntry,
|
||||||
|
inner_box: FlowBox,
|
||||||
|
app: Application,
|
||||||
|
sender: MenuItemSender,
|
||||||
|
list_items: Arc<Mutex<HashMap<FlowBoxChild, MenuItem>>>,
|
||||||
|
config: Config,
|
||||||
|
) {
|
||||||
|
let key_controller = EventControllerKey::new();
|
||||||
|
|
||||||
|
key_controller.connect_key_pressed(move |_, key_value, _, _| {
|
||||||
|
match key_value {
|
||||||
|
Key::Escape => {
|
||||||
|
if let Err(e) = sender.send(Err(anyhow!("No item selected"))) {
|
||||||
|
log::error!("failed to send message {e}");
|
||||||
|
}
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
Key::Return => {
|
||||||
|
if let Err(e) = handle_selected_item(&sender, &app, &inner_box, &list_items) {
|
||||||
|
log::error!("{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::BackSpace => {
|
||||||
|
let mut items = list_items.lock().unwrap();
|
||||||
|
let mut query = entry_clone.text().to_string();
|
||||||
|
query.pop();
|
||||||
|
|
||||||
|
entry_clone.set_text(&query);
|
||||||
|
filter_widgets(&query, &mut items, &config, &inner_box);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut items = list_items.lock().unwrap();
|
||||||
|
if let Some(c) = key_value.to_unicode() {
|
||||||
|
let current = entry_clone.text().to_string();
|
||||||
|
let query = format!("{current}{c}");
|
||||||
|
entry_clone.set_text(&query);
|
||||||
|
filter_widgets(&query, &mut items, &config, &inner_box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Propagation::Proceed
|
||||||
|
});
|
||||||
|
window.add_controller(key_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_menu_items(
|
||||||
|
child1: &FlowBoxChild,
|
||||||
|
child2: &FlowBoxChild,
|
||||||
|
items_lock: &Mutex<HashMap<FlowBoxChild, MenuItem>>,
|
||||||
|
) -> Ordering {
|
||||||
|
let lock = items_lock.lock().unwrap();
|
||||||
|
let m1 = lock.get(child1);
|
||||||
|
let m2 = lock.get(child2);
|
||||||
|
|
||||||
|
match (m1, m2) {
|
||||||
|
(Some(menu1), Some(menu2)) => {
|
||||||
|
if menu1.search_sort_score != 0.0 || menu2.search_sort_score != 0.0 {
|
||||||
|
if menu1.search_sort_score > menu2.search_sort_score {
|
||||||
|
Ordering::Smaller
|
||||||
|
} else {
|
||||||
|
Ordering::Larger
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if menu1.initial_sort_score > menu2.initial_sort_score {
|
||||||
|
Ordering::Smaller
|
||||||
|
} else {
|
||||||
|
Ordering::Larger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(_), None) => Ordering::Larger,
|
||||||
|
(None, Some(_)) => Ordering::Smaller,
|
||||||
|
(None, None) => Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_selected_item(
|
||||||
|
sender: &MenuItemSender,
|
||||||
|
app: &Application,
|
||||||
|
inner_box: &FlowBox,
|
||||||
|
lock_arc: &ArcMenuMap,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for s in inner_box.selected_children() {
|
||||||
|
let list_items = lock_arc.lock().unwrap();
|
||||||
|
let item = list_items.get(&s);
|
||||||
|
if let Some(item) = item {
|
||||||
|
if let Err(e) = sender.send(Ok(item.clone())) {
|
||||||
|
log::error!("failed to send message {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.quit();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err("selected item cannot be resolved".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_menu_item(
|
||||||
|
inner_box: &FlowBox,
|
||||||
|
entry_element: &MenuItem,
|
||||||
|
config: &Config,
|
||||||
|
sender: MenuItemSender,
|
||||||
|
lock_arc: ArcMenuMap,
|
||||||
|
app: Application,
|
||||||
|
) -> FlowBoxChild {
|
||||||
|
let parent: Widget = if !entry_element.sub_elements.is_empty() {
|
||||||
|
let expander = Expander::new(None);
|
||||||
|
expander.set_widget_name("expander-box");
|
||||||
|
expander.set_hexpand(true);
|
||||||
|
|
||||||
|
let menu_row = create_menu_row(
|
||||||
|
entry_element,
|
||||||
|
config,
|
||||||
|
lock_arc.clone(),
|
||||||
|
sender.clone(),
|
||||||
|
app.clone(),
|
||||||
|
inner_box.clone(),
|
||||||
|
);
|
||||||
|
expander.set_label_widget(Some(&menu_row));
|
||||||
|
|
||||||
|
let list_box = ListBox::new();
|
||||||
|
list_box.set_hexpand(true);
|
||||||
|
list_box.set_halign(Align::Fill);
|
||||||
|
|
||||||
|
for sub_item in &entry_element.sub_elements {
|
||||||
|
let sub_row = create_menu_row(
|
||||||
|
sub_item,
|
||||||
|
config,
|
||||||
|
lock_arc.clone(),
|
||||||
|
sender.clone(),
|
||||||
|
app.clone(),
|
||||||
|
inner_box.clone(),
|
||||||
|
);
|
||||||
|
sub_row.set_hexpand(true);
|
||||||
|
sub_row.set_halign(Align::Fill);
|
||||||
|
sub_row.set_widget_name("entry");
|
||||||
|
list_box.append(&sub_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
expander.set_child(Some(&list_box));
|
||||||
|
expander.upcast()
|
||||||
|
} else {
|
||||||
|
create_menu_row(
|
||||||
|
entry_element,
|
||||||
|
config,
|
||||||
|
lock_arc.clone(),
|
||||||
|
sender.clone(),
|
||||||
|
app.clone(),
|
||||||
|
inner_box.clone(),
|
||||||
|
)
|
||||||
|
.upcast()
|
||||||
|
};
|
||||||
|
|
||||||
|
parent.set_halign(Align::Fill);
|
||||||
|
parent.set_valign(Align::Start);
|
||||||
|
parent.set_hexpand(true);
|
||||||
|
|
||||||
|
let child = FlowBoxChild::new();
|
||||||
|
child.set_widget_name("entry");
|
||||||
|
child.set_child(Some(&parent));
|
||||||
|
child.set_hexpand(true);
|
||||||
|
child.set_vexpand(false);
|
||||||
|
|
||||||
|
inner_box.append(&child);
|
||||||
|
child
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_menu_row(
|
||||||
|
menu_item: &MenuItem,
|
||||||
|
config: &Config,
|
||||||
|
lock_arc: ArcMenuMap,
|
||||||
|
sender: MenuItemSender,
|
||||||
|
app: Application,
|
||||||
|
inner_box: FlowBox,
|
||||||
|
) -> Widget {
|
||||||
|
let row = ListBoxRow::new();
|
||||||
|
row.set_hexpand(true);
|
||||||
|
row.set_halign(Align::Fill);
|
||||||
|
row.set_widget_name("row");
|
||||||
|
|
||||||
|
let click = GestureClick::new();
|
||||||
|
click.set_button(gdk::BUTTON_PRIMARY);
|
||||||
|
click.connect_pressed(move |_gesture, n_press, _x, _y| {
|
||||||
|
if n_press == 2 {
|
||||||
|
if let Err(e) = handle_selected_item(&sender, &app, &inner_box, &lock_arc) {
|
||||||
|
log::error!("{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
row.add_controller(click);
|
||||||
|
|
||||||
|
let row_box = gtk4::Box::new(config.row_bow_orientation.unwrap().into(), 0);
|
||||||
|
row_box.set_hexpand(true);
|
||||||
|
row_box.set_vexpand(false);
|
||||||
|
row_box.set_halign(Align::Fill);
|
||||||
|
|
||||||
|
row.set_child(Some(&row_box));
|
||||||
|
|
||||||
|
if let Some(image_path) = &menu_item.icon_path {
|
||||||
|
let image = Image::from_icon_name(image_path);
|
||||||
|
image.set_pixel_size(
|
||||||
|
config
|
||||||
|
.image_size
|
||||||
|
.unwrap_or(config::default_image_size().unwrap()),
|
||||||
|
);
|
||||||
|
image.set_widget_name("img");
|
||||||
|
row_box.append(&image);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = Label::new(Some(&menu_item.label));
|
||||||
|
label.set_hexpand(true);
|
||||||
|
row_box.append(&label);
|
||||||
|
|
||||||
|
if config.content_halign.unwrap() == config::Align::Start
|
||||||
|
|| config.content_halign.unwrap() == config::Align::Fill
|
||||||
|
{
|
||||||
|
label.set_xalign(0.0);
|
||||||
|
}
|
||||||
|
row.upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_widgets(
|
||||||
|
query: &str,
|
||||||
|
items: &mut HashMap<FlowBoxChild, MenuItem>,
|
||||||
|
config: &Config,
|
||||||
|
inner_box: &FlowBox,
|
||||||
|
) {
|
||||||
|
if items.is_empty() {
|
||||||
|
items.iter().for_each(|(child, _)| {
|
||||||
|
child.set_visible(true);
|
||||||
|
});
|
||||||
|
if let Some(child) = inner_box.first_child() {
|
||||||
|
child.grab_focus();
|
||||||
|
let fb = child.downcast::<FlowBoxChild>();
|
||||||
|
if let Ok(fb) = fb {
|
||||||
|
inner_box.select_child(&fb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = query.to_owned().to_lowercase();
|
||||||
|
let mut highest_score = -1.0;
|
||||||
|
let mut fb: Option<&FlowBoxChild> = None;
|
||||||
|
items.iter_mut().for_each(|(flowbox_child, mut menu_item)| {
|
||||||
|
let menu_item_search = format!(
|
||||||
|
"{} {}",
|
||||||
|
menu_item
|
||||||
|
.action
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| a.to_lowercase())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
&menu_item.label.to_lowercase()
|
||||||
|
);
|
||||||
|
|
||||||
|
let matching = if let Some(matching) = &config.matching {
|
||||||
|
matching
|
||||||
|
} else {
|
||||||
|
&config::default_match_method().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (search_sort_score, visible) = match matching {
|
||||||
|
MatchMethod::Fuzzy => {
|
||||||
|
let score = strsim::normalized_levenshtein(&query, &menu_item_search);
|
||||||
|
(score, score > config.fuzzy_min_score.unwrap())
|
||||||
|
}
|
||||||
|
MatchMethod::Contains => {
|
||||||
|
if menu_item_search.contains(&query) {
|
||||||
|
(1.0, true)
|
||||||
|
} else {
|
||||||
|
(0.0, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MatchMethod::MultiContains => {
|
||||||
|
let score = query
|
||||||
|
.split(' ')
|
||||||
|
.filter(|i| menu_item_search.contains(i))
|
||||||
|
.map(|_| 1.0)
|
||||||
|
.sum();
|
||||||
|
(score, score > 0.0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
menu_item.search_sort_score = search_sort_score;
|
||||||
|
if visible {
|
||||||
|
highest_score = search_sort_score;
|
||||||
|
fb = Some(flowbox_child);
|
||||||
|
}
|
||||||
|
|
||||||
|
flowbox_child.set_visible(visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(top_item) = fb {
|
||||||
|
inner_box.select_child(top_item);
|
||||||
|
top_item.grab_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percent_or_absolute(value: &String, base_value: i32) -> Option<i32> {
|
||||||
|
if value.contains("%") {
|
||||||
|
let value = value.replace("%", "").trim().to_string();
|
||||||
|
match value.parse::<i32>() {
|
||||||
|
Ok(n) => Some(((n as f32 / 100.0) * base_value as f32) as i32),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value.parse::<i32>().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_sort_scores(items: &mut Vec<MenuItem>) {
|
||||||
|
let mut regular_score = items.len() as i64;
|
||||||
|
items.sort_by(|l, r| r.label.cmp(&l.label));
|
||||||
|
|
||||||
|
for item in items.iter_mut() {
|
||||||
|
if item.initial_sort_score == 0 {
|
||||||
|
item.initial_sort_score = regular_score;
|
||||||
|
regular_score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
src/lib/mod.rs
Normal file
4
src/lib/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod config;
|
||||||
|
pub mod desktop;
|
||||||
|
pub mod gui;
|
||||||
|
pub mod system;
|
31
src/lib/system.rs
Normal file
31
src/lib/system.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn home_dir() -> Result<String, anyhow::Error> {
|
||||||
|
env::var("HOME").map_err(|e| anyhow::anyhow!("$HOME not set: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn conf_home() -> Result<String, anyhow::Error> {
|
||||||
|
env::var("XDG_CONF_HOME").map_err(|e| anyhow::anyhow!("XDG_CONF_HOME not set: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_path(config_path: Option<String>) -> Result<PathBuf, anyhow::Error> {
|
||||||
|
config_path
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.and_then(|p| p.canonicalize().ok().filter(|c| c.exists()))
|
||||||
|
.or_else(|| {
|
||||||
|
[
|
||||||
|
conf_home().ok().map(PathBuf::from),
|
||||||
|
home_dir()
|
||||||
|
.ok()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|c| c.join(".config")),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|base| base.join("worf").join("style.css"))
|
||||||
|
.find_map(|p| p.canonicalize().ok())
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("Could not find a valid config file."))
|
||||||
|
}
|
176
src/main.rs
176
src/main.rs
|
@ -1,11 +1,16 @@
|
||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
#![allow(clippy::implicit_return)]
|
#![allow(clippy::implicit_return)]
|
||||||
|
|
||||||
|
// todo resolve paths like ~/
|
||||||
|
|
||||||
use crate::args::{Args, Mode};
|
use crate::args::{Args, Mode};
|
||||||
use crate::config::{Config, merge_config_with_args};
|
use crate::lib::config::{Config, merge_config_with_args};
|
||||||
use crate::desktop::{default_icon, find_desktop_files, get_locale_variants};
|
use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants};
|
||||||
use crate::gui::MenuItem;
|
use crate::lib::gui;
|
||||||
|
use crate::lib::gui::MenuItem;
|
||||||
|
use anyhow::{Error, anyhow};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use freedesktop_file_parser::{DesktopAction, EntryType};
|
||||||
use gdk4::prelude::Cast;
|
use gdk4::prelude::Cast;
|
||||||
use gtk4::prelude::{
|
use gtk4::prelude::{
|
||||||
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt,
|
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt,
|
||||||
|
@ -22,51 +27,40 @@ use std::process::{Command, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::{env, fs, time};
|
use std::{env, fs, time};
|
||||||
use freedesktop_file_parser::{DesktopAction, EntryType};
|
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
mod config;
|
mod lib;
|
||||||
mod desktop;
|
|
||||||
mod gui;
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
gtk4::init()?;
|
gtk4::init()?;
|
||||||
|
|
||||||
env_logger::Builder::new()
|
env_logger::Builder::new()
|
||||||
// todo change to info as default
|
// todo change to error as default
|
||||||
.parse_filters(&std::env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_owned()))
|
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_owned()))
|
||||||
.init();
|
.init();
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let home_dir = std::env::var("HOME")?;
|
let home_dir = env::var("HOME")?;
|
||||||
let config_path = args
|
let config_path = args
|
||||||
.config
|
.config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| PathBuf::from(c))
|
.map(|c| PathBuf::from(c))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
std::env::var("XDG_CONF_HOME")
|
env::var("XDG_CONF_HOME")
|
||||||
.map_or(
|
.map_or(
|
||||||
PathBuf::from(home_dir.clone()).join(".config"),
|
PathBuf::from(home_dir.clone()).join(".config"),
|
||||||
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
|
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
|
||||||
)
|
)
|
||||||
.join("wofi") // todo change to worf
|
.join("worf")
|
||||||
.join("config")
|
.join("config")
|
||||||
});
|
});
|
||||||
// todo use this?
|
|
||||||
let colors_dir = std::env::var("XDG_CACHE_HOME")
|
|
||||||
.map_or(
|
|
||||||
PathBuf::from(home_dir.clone()).join(".cache"),
|
|
||||||
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
|
|
||||||
)
|
|
||||||
.join("wal")
|
|
||||||
.join("colors");
|
|
||||||
|
|
||||||
let drun_cache = std::env::var("XDG_CACHE_HOME")
|
let drun_cache = env::var("XDG_CACHE_HOME")
|
||||||
.map_or(
|
.map_or(
|
||||||
PathBuf::from(home_dir.clone()).join(".cache"),
|
PathBuf::from(home_dir.clone()).join(".cache"),
|
||||||
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
|
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
|
||||||
)
|
)
|
||||||
.join("worf-drun"); // todo change to worf
|
.join("worf-drun");
|
||||||
|
|
||||||
let toml_content = fs::read_to_string(config_path)?;
|
let toml_content = fs::read_to_string(config_path)?;
|
||||||
let mut config: Config = toml::from_str(&toml_content)?; // todo bail out properly
|
let mut config: Config = toml::from_str(&toml_content)?; // todo bail out properly
|
||||||
|
@ -104,8 +98,12 @@ fn drun(mut config: Config) -> anyhow::Result<()> {
|
||||||
for file in find_desktop_files().iter().filter(|f| {
|
for file in find_desktop_files().iter().filter(|f| {
|
||||||
f.entry.hidden.map_or(true, |hidden| !hidden)
|
f.entry.hidden.map_or(true, |hidden| !hidden)
|
||||||
&& f.entry.no_display.map_or(true, |no_display| !no_display)
|
&& f.entry.no_display.map_or(true, |no_display| !no_display)
|
||||||
// todo handle not shown in?
|
|
||||||
}) {
|
}) {
|
||||||
|
let (action, working_dir) = match &file.entry.entry_type {
|
||||||
|
EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
|
||||||
|
_ => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
let name = lookup_name_with_locale(
|
let name = lookup_name_with_locale(
|
||||||
&locale_variants,
|
&locale_variants,
|
||||||
&file.entry.name.variants,
|
&file.entry.name.variants,
|
||||||
|
@ -115,14 +113,26 @@ fn drun(mut config: Config) -> anyhow::Result<()> {
|
||||||
debug!("Skipping desktop entry without name {file:?}")
|
debug!("Skipping desktop entry without name {file:?}")
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon = file.entry.icon.as_ref().map(|s| s.content.clone());
|
let icon = file
|
||||||
debug!("file, name={name:?}, icon={icon:?}");
|
.entry
|
||||||
|
.icon
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.content.clone())
|
||||||
|
.or(Some(default_icon.clone()));
|
||||||
|
debug!("file, name={name:?}, icon={icon:?}, action={action:?}");
|
||||||
|
let mut sort_score = 0.0;
|
||||||
|
if name.as_ref().unwrap().contains("ox") {
|
||||||
|
sort_score = 999.0;
|
||||||
|
}
|
||||||
|
|
||||||
let mut entry = MenuItem {
|
let mut entry = MenuItem {
|
||||||
label: name.unwrap(),
|
label: name.unwrap(),
|
||||||
icon_path: icon.clone(),
|
icon_path: icon.clone(),
|
||||||
action: None,
|
action,
|
||||||
sub_elements: Vec::default(),
|
sub_elements: Vec::default(),
|
||||||
|
working_dir: working_dir.clone(),
|
||||||
|
initial_sort_score: 0,
|
||||||
|
search_sort_score: sort_score,
|
||||||
};
|
};
|
||||||
|
|
||||||
file.actions.iter().for_each(|(_, action)| {
|
file.actions.iter().for_each(|(_, action)| {
|
||||||
|
@ -131,95 +141,83 @@ fn drun(mut config: Config) -> anyhow::Result<()> {
|
||||||
&action.name.variants,
|
&action.name.variants,
|
||||||
&action.name.default,
|
&action.name.default,
|
||||||
);
|
);
|
||||||
let action_icon = action.icon.as_ref().map(|s| s.content.clone()).or(icon.as_ref().map(|s| s.clone()));
|
let action_icon = action
|
||||||
|
.icon
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.content.clone())
|
||||||
|
.or(icon.as_ref().map(|s| s.clone()));
|
||||||
|
|
||||||
debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}");
|
debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}");
|
||||||
|
|
||||||
let sub_entry = MenuItem {
|
let sub_entry = MenuItem {
|
||||||
label: action_name.unwrap().trim().to_owned(),
|
label: action_name.unwrap().trim().to_owned(),
|
||||||
icon_path: action_icon,
|
icon_path: action_icon,
|
||||||
action: None,
|
action: action.exec.clone(),
|
||||||
sub_elements: Vec::default(),
|
sub_elements: Vec::default(),
|
||||||
|
working_dir: working_dir.clone(),
|
||||||
|
initial_sort_score: 0,
|
||||||
|
search_sort_score: 0.0,
|
||||||
};
|
};
|
||||||
entry.sub_elements.push(sub_entry);
|
entry.sub_elements.push(sub_entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
|
|
||||||
// let desktop = Some("desktop entry");
|
|
||||||
// let locale =
|
|
||||||
// env::var("LC_ALL")
|
|
||||||
// .or_else(|_| env::var("LC_MESSAGES"))
|
|
||||||
// .or_else(|_| env::var("LANG"))
|
|
||||||
// .unwrap_or_else(|_| "en_US.UTF-8".to_string()).split_once(".").map(|(k,_)| k.to_owned().to_lowercase());
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// if let Some(desktop_entry) = file.get("desktop entry") {
|
|
||||||
// let icon = desktop_entry
|
|
||||||
// .get("icon")
|
|
||||||
// .and_then(|x| x.as_ref().map(|x| x.to_owned()));
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// let Some(exec) = desktop_entry.get("exec")
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// .and_then(|x| x.as_ref()) else {
|
|
||||||
// warn!("Skipping desktop file {file:#?}");
|
|
||||||
// continue;
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// if let Some((cmd, _)) = exec.split_once(' ') {
|
|
||||||
// if !PathBuf::from(cmd).exists() {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let name = desktop_entry
|
|
||||||
// .get("name")
|
|
||||||
// .and_then(|x| x.as_ref().map(|x| x.to_owned()));
|
|
||||||
//
|
|
||||||
// if let Some(name) = name {
|
|
||||||
// entries.push({
|
|
||||||
// EntryElement {
|
|
||||||
// label: name,
|
|
||||||
// icon_path: icon,
|
|
||||||
// action: Some(exec.clone()),
|
|
||||||
// sub_elements: None,
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.sort_by(|l, r| l.label.cmp(&r.label));
|
gui::initialize_sort_scores(&mut entries);
|
||||||
if config.prompt.is_none() {
|
|
||||||
config.prompt = Some("drun".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo ues a arc instead of cloning the config
|
// todo ues a arc instead of cloning the config
|
||||||
let selected_index = gui::show(config.clone(), entries.clone())?;
|
let selection_result = gui::show(config.clone(), entries.clone());
|
||||||
entries.get(selected_index as usize).map(|e| {
|
match selection_result {
|
||||||
e.action.as_ref().map(|a| {
|
Ok(selected_item) => {
|
||||||
spawn_fork(&a);
|
if let Some(action) = selected_item.action {
|
||||||
})
|
spawn_fork(&action, &selected_item.working_dir)?
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_fork(cmd: &str) {
|
fn spawn_fork(cmd: &str, working_dir: &Option<String>) -> anyhow::Result<()> {
|
||||||
// todo fork this for real
|
|
||||||
// todo probably remove arguments?
|
// todo probably remove arguments?
|
||||||
|
// todo support working dir
|
||||||
|
// todo fix actions
|
||||||
|
// todo graphical disk map icon not working
|
||||||
// Unix-like systems (Linux, macOS)
|
// Unix-like systems (Linux, macOS)
|
||||||
let _ = Command::new(cmd)
|
|
||||||
|
let parts = cmd.split(' ').collect::<Vec<_>>();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err(anyhow!("empty command passed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dir) = working_dir {
|
||||||
|
env::set_current_dir(dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let exec = parts[0];
|
||||||
|
let args: Vec<_> = parts
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.filter(|arg| !arg.starts_with("%"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let _ = Command::new(exec)
|
||||||
|
.args(args)
|
||||||
.stdin(Stdio::null()) // Disconnect stdin
|
.stdin(Stdio::null()) // Disconnect stdin
|
||||||
.stdout(Stdio::null()) // Disconnect stdout
|
.stdout(Stdio::null()) // Disconnect stdout
|
||||||
.stderr(Stdio::null()) // Disconnect stderr
|
.stderr(Stdio::null()) // Disconnect stderr
|
||||||
|
.pre_exec(|| {
|
||||||
|
libc::setsid();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.spawn();
|
.spawn();
|
||||||
sleep(time::Duration::from_secs(30));
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// fn main() -> anyhow::Result<()> {
|
// fn main() -> anyhow::Result<()> {
|
||||||
|
|
69
styles/compact.css
Normal file
69
styles/compact.css
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
* {
|
||||||
|
font-family: DejaVu;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window {
|
||||||
|
all: unset;
|
||||||
|
background-color: rgba(33, 33, 33, 0.8); /* Matches #212121BB */
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box {
|
||||||
|
/* The name of the search bar */
|
||||||
|
/* The name of the scrolled window containing all of the entries */
|
||||||
|
border: 2px solid rgba(63, 81, 181, 1);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box #input {
|
||||||
|
background-color: rgba(32, 32, 32, 0.6);
|
||||||
|
color: #f2f2f2;
|
||||||
|
border-bottom: 2px solid rgba(214, 174, 0, 1);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active {
|
||||||
|
all: unset;
|
||||||
|
background-color: rgba(32, 32, 32, 0.6);
|
||||||
|
color: #f2f2f2;
|
||||||
|
border-bottom: 2px solid rgba(214, 174, 2, 1);
|
||||||
|
padding: 1.2rem 1.2rem 1.2rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box #scroll {
|
||||||
|
/* The name of the box containing all of the entries */
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box {
|
||||||
|
/* The name of all entries */
|
||||||
|
/* The name of all boxes shown when expanding */
|
||||||
|
/* entries with multiple actions */
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(32, 32, 32, 0.1);
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
/* The name of all images in entries displayed in image mode */
|
||||||
|
/* The name of all the text in entries */
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry #img {
|
||||||
|
width: 1rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry:selected {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#row:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0);;
|
||||||
|
outline: inherit;
|
||||||
|
outline-color: inherit;
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
outline: inherit;
|
||||||
|
outline-color: inherit;
|
||||||
|
}
|
69
styles/launcher.css
Normal file
69
styles/launcher.css
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
* {
|
||||||
|
font-family: DejaVu;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window {
|
||||||
|
all: unset;
|
||||||
|
background-color: rgba(33, 33, 33, 0.8); /* Matches #212121BB */
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box {
|
||||||
|
/* The name of the search bar */
|
||||||
|
/* The name of the scrolled window containing all of the entries */
|
||||||
|
border: 2px solid rgba(63, 81, 181, 1);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box #input {
|
||||||
|
background-color: rgba(32, 32, 32, 0.6);
|
||||||
|
color: #f2f2f2;
|
||||||
|
border-bottom: 2px solid rgba(214, 174, 0, 1);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active {
|
||||||
|
all: unset;
|
||||||
|
background-color: rgba(32, 32, 32, 0.6);
|
||||||
|
color: #f2f2f2;
|
||||||
|
border-bottom: 2px solid rgba(214, 174, 2, 1);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window #outer-box #scroll {
|
||||||
|
/* The name of the box containing all of the entries */
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box {
|
||||||
|
/* The name of all entries */
|
||||||
|
/* The name of all boxes shown when expanding */
|
||||||
|
/* entries with multiple actions */
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(32, 32, 32, 0.1);
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry #img {
|
||||||
|
width: 1rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry:selected {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#row:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0);;
|
||||||
|
outline: inherit;
|
||||||
|
outline-color: inherit;
|
||||||
|
|
||||||
|
}
|
||||||
|
#window #outer-box #scroll #inner-box #entry:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
outline: inherit;
|
||||||
|
outline-color: inherit;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue