add auto mode
This commit is contained in:
parent
1da13e94fa
commit
f558131233
13 changed files with 1011 additions and 362 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -436,6 +436,12 @@ dependencies = [
|
|||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "freedesktop-file-parser"
|
||||
version = "0.1.3"
|
||||
|
@ -1013,6 +1019,16 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meval"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.7"
|
||||
|
@ -1033,6 +1049,12 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
@ -1726,6 +1748,7 @@ dependencies = [
|
|||
"hyprland",
|
||||
"libc",
|
||||
"log",
|
||||
"meval",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -42,3 +42,4 @@ freedesktop-file-parser = "0.1.3"
|
|||
strsim = "0.11.1"
|
||||
dirs = "6.0.0"
|
||||
which = "7.0.3"
|
||||
meval = "0.2.0"
|
||||
|
|
|
@ -1,13 +1,28 @@
|
|||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{env, fs};
|
||||
use std::{env, fmt, fs};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::{Error, anyhow};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigurationError {
|
||||
Open(String),
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigurationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ConfigurationError::Open(e) => write!(f, "{e}"),
|
||||
ConfigurationError::Parse(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
|
||||
pub enum MatchMethod {
|
||||
Fuzzy,
|
||||
|
@ -46,6 +61,9 @@ pub enum Mode {
|
|||
|
||||
/// reads from stdin and displays options which when selected will be output to stdout.
|
||||
Dmenu,
|
||||
|
||||
/// tries to determine automatically what to do
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -62,6 +80,7 @@ impl FromStr for Mode {
|
|||
"run" => Ok(Mode::Run),
|
||||
"drun" => Ok(Mode::Drun),
|
||||
"dmenu" => Ok(Mode::Dmenu),
|
||||
"auto" => Ok(Mode::Auto),
|
||||
_ => Err(ArgsError::InvalidParameter(
|
||||
format!("{s} is not a valid argument show this, see help for details").to_owned(),
|
||||
)),
|
||||
|
@ -598,6 +617,17 @@ pub fn style_path(full_path: Option<String>) -> Result<PathBuf, anyhow::Error> {
|
|||
resolve_path(full_path, alternative_paths.into_iter().collect())
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return Err when it cannot resolve any path or no style is found
|
||||
pub fn conf_path(full_path: Option<String>) -> Result<PathBuf, anyhow::Error> {
|
||||
let alternative_paths = path_alternatives(
|
||||
vec![dirs::config_dir()],
|
||||
&PathBuf::from("worf").join("config"),
|
||||
);
|
||||
resolve_path(full_path, alternative_paths.into_iter().collect())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn path_alternatives(base_paths: Vec<Option<PathBuf>>, sub_path: &PathBuf) -> Vec<PathBuf> {
|
||||
base_paths
|
||||
|
@ -635,41 +665,28 @@ pub fn resolve_path(
|
|||
/// * cannot parse the config file
|
||||
/// * no config file exists
|
||||
/// * config file and args cannot be merged
|
||||
pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> {
|
||||
let home_dir = env::var("HOME")?;
|
||||
let config_path = args_opt.as_ref().map(|c| {
|
||||
c.config.as_ref().map_or_else(
|
||||
|| {
|
||||
env::var("XDG_CONF_HOME")
|
||||
.map_or(
|
||||
PathBuf::from(home_dir.clone()).join(".config"),
|
||||
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
|
||||
)
|
||||
.join("worf")
|
||||
.join("config")
|
||||
},
|
||||
PathBuf::from,
|
||||
)
|
||||
});
|
||||
|
||||
pub fn load_config(args_opt: Option<Config>) -> Result<Config, ConfigurationError> {
|
||||
let config_path = conf_path(args_opt.as_ref().map(|c| c.config.clone()).flatten());
|
||||
match config_path {
|
||||
Some(path) => {
|
||||
let toml_content = fs::read_to_string(path)?;
|
||||
let mut config: Config = toml::from_str(&toml_content)?;
|
||||
Ok(path) => {
|
||||
let toml_content =
|
||||
fs::read_to_string(path).map_err(|e| ConfigurationError::Open(format!("{e}")))?;
|
||||
let mut config: Config = toml::from_str(&toml_content)
|
||||
.map_err(|e| ConfigurationError::Parse(format!("{e}")))?;
|
||||
|
||||
if let Some(args) = args_opt {
|
||||
let mut merge_result = merge_config_with_args(&mut config, &args)?;
|
||||
let mut merge_result = merge_config_with_args(&mut config, &args)
|
||||
.map_err(|e| ConfigurationError::Parse(format!("{e}")))?;
|
||||
|
||||
if merge_result.prompt.is_none() {
|
||||
match &merge_result.show {
|
||||
None => {}
|
||||
Some(mode) => {
|
||||
match mode {
|
||||
Mode::Run => merge_result.prompt = Some("run".to_owned()),
|
||||
Mode::Drun => merge_result.prompt = Some("drun".to_owned()),
|
||||
Mode::Dmenu => merge_result.prompt = Some("dmenu".to_owned()),
|
||||
}
|
||||
}
|
||||
Some(mode) => match mode {
|
||||
Mode::Run => merge_result.prompt = Some("run".to_owned()),
|
||||
Mode::Drun => merge_result.prompt = Some("drun".to_owned()),
|
||||
Mode::Dmenu => merge_result.prompt = Some("dmenu".to_owned()),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -678,9 +695,32 @@ pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> {
|
|||
Ok(config)
|
||||
}
|
||||
}
|
||||
None => Err(anyhow!("No config file found")),
|
||||
|
||||
Err(e) => Err(ConfigurationError::Open(format!("{e}"))),
|
||||
}
|
||||
}
|
||||
pub fn expand_path(input: &str) -> PathBuf {
|
||||
let mut path = input.to_string();
|
||||
|
||||
// Expand ~ to home directory
|
||||
if path.starts_with("~") {
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
path = path.replacen("~", home_dir.to_str().unwrap_or(""), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Expand $VAR style environment variables
|
||||
if path.contains('$') {
|
||||
for (key, value) in env::vars() {
|
||||
let var_pattern = format!("${}", key);
|
||||
if path.contains(&var_pattern) {
|
||||
path = path.replace(&var_pattern, &value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::anyhow;
|
||||
use freedesktop_file_parser::DesktopFile;
|
||||
use gdk4::Display;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{IconLookupFlags, IconTheme, TextDirection};
|
||||
use home::home_dir;
|
||||
|
@ -10,88 +11,71 @@ use std::path::Path;
|
|||
use std::path::PathBuf;
|
||||
use std::{env, fs, string};
|
||||
|
||||
pub struct IconResolver {
|
||||
cache: HashMap<String, String>,
|
||||
#[derive(Debug)]
|
||||
pub enum DesktopError {
|
||||
MissingIcon,
|
||||
}
|
||||
|
||||
impl Default for IconResolver {
|
||||
#[must_use]
|
||||
fn default() -> IconResolver {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl IconResolver {
|
||||
#[must_use]
|
||||
pub fn new() -> IconResolver {
|
||||
IconResolver {
|
||||
cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_path(&mut self, icon_name: &str) -> String {
|
||||
if let Some(icon_path) = self.cache.get(icon_name) {
|
||||
info!("Fetching {icon_name} from cache");
|
||||
return icon_path.to_owned();
|
||||
}
|
||||
|
||||
info!("Loading icon for {icon_name}");
|
||||
let icon = fetch_icon_from_theme(icon_name)
|
||||
.or_else(|_| {
|
||||
fetch_icon_from_common_dirs(icon_name).map_or_else(
|
||||
|| Err(anyhow::anyhow!("Missing file")), // Return an error here
|
||||
Ok,
|
||||
)
|
||||
})
|
||||
.or_else(|_| {
|
||||
warn!("Missing icon for {icon_name}, using fallback");
|
||||
default_icon()
|
||||
});
|
||||
|
||||
self.cache
|
||||
.entry(icon_name.to_owned())
|
||||
.or_insert_with(|| icon.unwrap_or_default())
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
//
|
||||
// #[derive(Clone)]
|
||||
// pub struct IconResolver {
|
||||
// cache: HashMap<String, String>,
|
||||
// }
|
||||
//
|
||||
// impl Default for IconResolver {
|
||||
// #[must_use]
|
||||
// fn default() -> IconResolver {
|
||||
// Self::new()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl IconResolver {
|
||||
// #[must_use]
|
||||
// pub fn new() -> IconResolver {
|
||||
// IconResolver {
|
||||
// cache: HashMap::new(),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// pub fn icon_path_no_cache(&self, icon_name: &str) -> Result<String, DesktopError> {
|
||||
// let icon = fetch_icon_from_theme(icon_name)
|
||||
// .or_else(|_|
|
||||
// fetch_icon_from_common_dirs(icon_name)
|
||||
// .or_else(|_| default_icon()));
|
||||
//
|
||||
// icon
|
||||
// }
|
||||
//
|
||||
// pub fn icon_path(&mut self, icon_name: &str) -> String {
|
||||
// if let Some(icon_path) = self.cache.get(icon_name) {
|
||||
// return icon_path.to_owned();
|
||||
// }
|
||||
//
|
||||
// let icon = self.icon_path_no_cache(icon_name);
|
||||
//
|
||||
// self.cache
|
||||
// .entry(icon_name.to_owned())
|
||||
// .or_insert_with(|| icon.unwrap_or_default())
|
||||
// .to_owned()
|
||||
// }
|
||||
// }
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if no icon can be found
|
||||
pub fn default_icon() -> anyhow::Result<String> {
|
||||
fetch_icon_from_theme("image-missing")
|
||||
pub fn default_icon() -> Result<String, DesktopError> {
|
||||
fetch_icon_from_theme("image-missing").map_err(|e| DesktopError::MissingIcon)
|
||||
}
|
||||
|
||||
// fn fetch_icon_from_desktop_file(icon_name: &str) -> Option<String> {
|
||||
// // find_desktop_files().into_iter().find_map(|desktop_file| {
|
||||
// // desktop_file
|
||||
// // .get("Desktop Entry")
|
||||
// // .filter(|desktop_entry| {
|
||||
// // desktop_entry
|
||||
// // .get("Exec")
|
||||
// // .and_then(|opt| opt.as_ref())
|
||||
// // .is_some_and(|exec| exec.to_lowercase().contains(icon_name))
|
||||
// // })
|
||||
// // .map(|desktop_entry| {
|
||||
// // desktop_entry
|
||||
// // .get("Icon")
|
||||
// // .and_then(|opt| opt.as_ref())
|
||||
// // .map(ToOwned::to_owned)
|
||||
// // .unwrap_or_default()
|
||||
// // })
|
||||
// // })
|
||||
// //todo
|
||||
// None
|
||||
// }
|
||||
|
||||
fn fetch_icon_from_theme(icon_name: &str) -> anyhow::Result<String> {
|
||||
fn fetch_icon_from_theme(icon_name: &str) -> Result<String, DesktopError> {
|
||||
let display = gtk4::gdk::Display::default();
|
||||
if display.is_none() {
|
||||
log::error!("Failed to get display");
|
||||
}
|
||||
|
||||
let display = display.unwrap();
|
||||
let display = Display::default().expect("Failed to get default display");
|
||||
let theme = IconTheme::for_display(&display);
|
||||
|
||||
let icon = theme.lookup_icon(
|
||||
icon_name,
|
||||
&[],
|
||||
|
@ -106,17 +90,25 @@ fn fetch_icon_from_theme(icon_name: &str) -> anyhow::Result<String> {
|
|||
.and_then(|file| file.path())
|
||||
.and_then(|path| path.to_str().map(string::ToString::to_string))
|
||||
{
|
||||
None => Err(anyhow!("Cannot find file")),
|
||||
None => {
|
||||
let path = PathBuf::from("/usr/share/icons")
|
||||
.join(theme.theme_name())
|
||||
.join(format!("{icon_name}.svg"));
|
||||
if path.exists() {
|
||||
Ok(path.display().to_string())
|
||||
} else {
|
||||
Err(DesktopError::MissingIcon)
|
||||
}
|
||||
}
|
||||
Some(i) => Ok(i),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> {
|
||||
pub fn fetch_icon_from_common_dirs(icon_name: &str) -> Result<String, DesktopError> {
|
||||
let mut paths = vec![
|
||||
PathBuf::from("/usr/local/share/icons"),
|
||||
PathBuf::from("/usr/share/icons"),
|
||||
PathBuf::from("/usr/share/pixmaps"),
|
||||
// /usr/share/icons contains the theme icons, handled via separate function
|
||||
];
|
||||
|
||||
if let Some(home) = home_dir() {
|
||||
|
@ -133,6 +125,7 @@ fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> {
|
|||
find_file_case_insensitive(dir.as_path(), &formatted_name)
|
||||
.and_then(|files| files.first().map(|f| f.to_string_lossy().into_owned()))
|
||||
})
|
||||
.ok_or_else(|| DesktopError::MissingIcon)
|
||||
}
|
||||
|
||||
fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<PathBuf>> {
|
||||
|
@ -156,7 +149,7 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<Pa
|
|||
/// # Errors
|
||||
///
|
||||
/// Will return Err when it cannot parse the internal regex
|
||||
pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
|
||||
pub fn find_desktop_files() -> Vec<DesktopFile> {
|
||||
let mut paths = vec![
|
||||
PathBuf::from("/usr/share/applications"),
|
||||
PathBuf::from("/usr/local/share/applications"),
|
||||
|
@ -168,6 +161,7 @@ pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
|
|||
}
|
||||
|
||||
if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") {
|
||||
// todo use dirs:: instead
|
||||
paths.push(PathBuf::from(xdg_data_home).join(".applications"));
|
||||
}
|
||||
|
||||
|
@ -175,7 +169,7 @@ pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
|
|||
paths.push(PathBuf::from(xdg_data_dir).join(".applications"));
|
||||
}
|
||||
|
||||
let regex = &Regex::new("(?i).*\\.desktop$")?;
|
||||
let regex = &Regex::new("(?i).*\\.desktop$").unwrap();
|
||||
|
||||
let p: Vec<_> = paths
|
||||
.into_iter()
|
||||
|
@ -190,7 +184,7 @@ pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(p)
|
||||
p
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
434
src/lib/gui.rs
434
src/lib/gui.rs
|
@ -1,4 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -19,15 +20,21 @@ use gtk4::{
|
|||
ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk,
|
||||
};
|
||||
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
|
||||
use gtk4_layer_shell::{KeyboardMode, LayerShell};
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, LayerShell};
|
||||
use log;
|
||||
|
||||
use crate::config;
|
||||
use crate::config::{Animation, Config, MatchMethod};
|
||||
|
||||
type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>;
|
||||
type ArcProvider<T> = Arc<Mutex<dyn ItemProvider<T>>>;
|
||||
type MenuItemSender<T> = Sender<Result<MenuItem<T>, anyhow::Error>>;
|
||||
|
||||
pub trait ItemProvider<T: std::clone::Clone> {
|
||||
fn get_elements(&mut self, search: Option<&str>) -> Vec<MenuItem<T>>;
|
||||
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> Option<Vec<MenuItem<T>>>;
|
||||
}
|
||||
|
||||
impl From<config::Orientation> for Orientation {
|
||||
fn from(orientation: config::Orientation) -> Self {
|
||||
match orientation {
|
||||
|
@ -47,8 +54,8 @@ impl From<config::Align> for Align {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MenuItem<T> {
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct MenuItem<T: Clone> {
|
||||
pub label: String, // todo support empty label?
|
||||
pub icon_path: Option<String>,
|
||||
pub action: Option<String>,
|
||||
|
@ -61,12 +68,19 @@ pub struct MenuItem<T> {
|
|||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone> AsRef<MenuItem<T>> for MenuItem<T> {
|
||||
fn as_ref(&self) -> &MenuItem<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return Err when the channel between the UI and this is broken
|
||||
pub fn show<T>(config: Config, elements: Vec<MenuItem<T>>) -> Result<MenuItem<T>, anyhow::Error>
|
||||
pub fn show<T, P>(config: Config, item_provider: P) -> Result<MenuItem<T>, anyhow::Error>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
P: ItemProvider<T> + 'static + Clone,
|
||||
{
|
||||
if let Some(ref css) = config.style {
|
||||
let provider = CssProvider::new();
|
||||
|
@ -85,7 +99,7 @@ where
|
|||
let (sender, receiver) = channel::bounded(1);
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
build_ui(&config, &elements, &sender, app);
|
||||
build_ui(&config, item_provider.clone(), &sender, app);
|
||||
});
|
||||
|
||||
let gtk_args: [&str; 0] = [];
|
||||
|
@ -93,13 +107,14 @@ where
|
|||
receiver.recv()?
|
||||
}
|
||||
|
||||
fn build_ui<T>(
|
||||
fn build_ui<T, P>(
|
||||
config: &Config,
|
||||
elements: &Vec<MenuItem<T>>,
|
||||
item_provider: P,
|
||||
sender: &Sender<Result<MenuItem<T>, anyhow::Error>>,
|
||||
app: &Application,
|
||||
) where
|
||||
T: Clone + 'static,
|
||||
P: ItemProvider<T> + 'static,
|
||||
{
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
|
@ -119,6 +134,9 @@ fn build_ui<T>(
|
|||
window.set_namespace(Some("worf"));
|
||||
}
|
||||
|
||||
/// todo make this configurable
|
||||
window.set_anchor(Edge::Top, true);
|
||||
|
||||
let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0);
|
||||
outer_box.set_widget_name("outer-box");
|
||||
|
||||
|
@ -126,7 +144,7 @@ fn build_ui<T>(
|
|||
entry.set_widget_name("input");
|
||||
entry.set_css_classes(&["input"]);
|
||||
entry.set_placeholder_text(config.prompt.as_deref());
|
||||
entry.set_sensitive(false);
|
||||
entry.set_can_focus(false);
|
||||
outer_box.append(&entry);
|
||||
|
||||
let scroll = ScrolledWindow::new();
|
||||
|
@ -145,6 +163,7 @@ fn build_ui<T>(
|
|||
inner_box.set_css_classes(&["inner-box"]);
|
||||
inner_box.set_hexpand(true);
|
||||
inner_box.set_vexpand(false);
|
||||
|
||||
if let Some(halign) = config.halign {
|
||||
inner_box.set_halign(halign.into());
|
||||
}
|
||||
|
@ -161,27 +180,29 @@ fn build_ui<T>(
|
|||
inner_box.set_max_children_per_line(config.columns.unwrap());
|
||||
inner_box.set_activate_on_single_click(true);
|
||||
|
||||
let item_provider = Arc::new(Mutex::new(item_provider));
|
||||
let list_items: ArcMenuMap<T> = 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, &list_items, app, &window),
|
||||
entry.clone(),
|
||||
);
|
||||
}
|
||||
build_ui_from_menu_items(
|
||||
&item_provider.lock().unwrap().get_elements(None),
|
||||
&list_items,
|
||||
&inner_box,
|
||||
&config,
|
||||
&sender,
|
||||
&app,
|
||||
&window,
|
||||
);
|
||||
|
||||
let items_sort = Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items);
|
||||
inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_sort));
|
||||
let items_sort = ArcMenuMap::clone(&list_items);
|
||||
inner_box.set_sort_func(move |child1, child2| {
|
||||
sort_menu_items_by_score(child1, child2, items_sort.clone())
|
||||
});
|
||||
|
||||
let items_focus = Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items);
|
||||
let items_focus = ArcMenuMap::clone(&list_items);
|
||||
inner_box.connect_map(move |fb| {
|
||||
fb.grab_focus();
|
||||
fb.invalidate_sort();
|
||||
|
||||
let mut item_lock = items_focus.lock().unwrap();
|
||||
select_first_visible_child(&mut *item_lock, fb);
|
||||
select_first_visible_child(&items_focus, fb);
|
||||
});
|
||||
|
||||
let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0);
|
||||
|
@ -194,8 +215,9 @@ fn build_ui<T>(
|
|||
inner_box,
|
||||
app.clone(),
|
||||
sender.clone(),
|
||||
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items),
|
||||
ArcMenuMap::clone(&list_items),
|
||||
config.clone(),
|
||||
item_provider,
|
||||
);
|
||||
|
||||
window.set_child(Widget::NONE);
|
||||
|
@ -203,66 +225,169 @@ fn build_ui<T>(
|
|||
animate_window_show(config.clone(), window.clone(), outer_box);
|
||||
}
|
||||
|
||||
fn build_ui_from_menu_items<T: Clone + 'static>(
|
||||
items: &Vec<MenuItem<T>>,
|
||||
list_items: &ArcMenuMap<T>,
|
||||
inner_box: &FlowBox,
|
||||
config: &Config,
|
||||
sender: &MenuItemSender<T>,
|
||||
app: &Application,
|
||||
window: &ApplicationWindow,
|
||||
) {
|
||||
{
|
||||
let mut arc_lock = list_items.lock().unwrap();
|
||||
inner_box.unset_sort_func();
|
||||
|
||||
loop {
|
||||
if let Some(b) = inner_box.child_at_index(0) {
|
||||
inner_box.remove(&b);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for entry in items {
|
||||
arc_lock.insert(
|
||||
add_menu_item(&inner_box, entry, config, sender, &list_items, app, window),
|
||||
(*entry).clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let lic = list_items.clone();
|
||||
inner_box
|
||||
.set_sort_func(move |child2, child1| sort_menu_items_by_score(child1, child2, lic.clone()));
|
||||
inner_box.invalidate_sort();
|
||||
}
|
||||
|
||||
fn setup_key_event_handler<T: Clone + 'static>(
|
||||
window: &ApplicationWindow,
|
||||
entry_clone: SearchEntry,
|
||||
entry: SearchEntry,
|
||||
inner_box: FlowBox,
|
||||
app: Application,
|
||||
sender: MenuItemSender<T>,
|
||||
list_items: Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>,
|
||||
config: Config,
|
||||
item_provider: ArcProvider<T>,
|
||||
) {
|
||||
let key_controller = EventControllerKey::new();
|
||||
|
||||
let window_clone = window.clone();
|
||||
let entry_clone = entry.clone();
|
||||
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}");
|
||||
}
|
||||
close_gui(app.clone(), window_clone.clone(), &config);
|
||||
}
|
||||
Key::Return => {
|
||||
if let Err(e) = handle_selected_item(
|
||||
&sender,
|
||||
app.clone(),
|
||||
window_clone.clone(),
|
||||
&config,
|
||||
&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
|
||||
handle_key_press(
|
||||
&entry_clone,
|
||||
&inner_box,
|
||||
&app,
|
||||
&sender,
|
||||
&list_items,
|
||||
&config,
|
||||
&item_provider,
|
||||
&window_clone,
|
||||
&key_value,
|
||||
)
|
||||
});
|
||||
|
||||
window.add_controller(key_controller);
|
||||
}
|
||||
|
||||
fn sort_menu_items<T>(
|
||||
fn handle_key_press<T: Clone + 'static>(
|
||||
search_entry: &SearchEntry,
|
||||
inner_box: &FlowBox,
|
||||
app: &Application,
|
||||
sender: &MenuItemSender<T>,
|
||||
list_items: &ArcMenuMap<T>,
|
||||
config: &Config,
|
||||
item_provider: &ArcProvider<T>,
|
||||
window_clone: &ApplicationWindow,
|
||||
keyboard_key: &Key,
|
||||
) -> Propagation {
|
||||
let update_view = |query: &String, items: Vec<MenuItem<T>>| {
|
||||
build_ui_from_menu_items(
|
||||
&items,
|
||||
&list_items,
|
||||
&inner_box,
|
||||
&config,
|
||||
&sender,
|
||||
&app,
|
||||
&window_clone,
|
||||
);
|
||||
filter_widgets(query, list_items, &config, &inner_box);
|
||||
select_first_visible_child(&list_items, &inner_box);
|
||||
};
|
||||
|
||||
let update_view_from_provider = |query: &String| {
|
||||
let filtered_list = item_provider.lock().unwrap().get_elements(Some(&query));
|
||||
update_view(query, filtered_list)
|
||||
};
|
||||
|
||||
match keyboard_key {
|
||||
&Key::Escape => {
|
||||
if let Err(e) = sender.send(Err(anyhow!("No item selected"))) {
|
||||
log::error!("failed to send message {e}");
|
||||
}
|
||||
close_gui(app.clone(), window_clone.clone(), &config);
|
||||
}
|
||||
&Key::Return => {
|
||||
if let Err(e) = handle_selected_item(
|
||||
&sender,
|
||||
app.clone(),
|
||||
window_clone.clone(),
|
||||
&config,
|
||||
&inner_box,
|
||||
&list_items,
|
||||
) {
|
||||
log::error!("{e}");
|
||||
}
|
||||
}
|
||||
&Key::BackSpace => {
|
||||
let mut query = search_entry.text().to_string();
|
||||
query.pop();
|
||||
|
||||
search_entry.set_text(&query);
|
||||
update_view_from_provider(&query);
|
||||
}
|
||||
&Key::Tab => {
|
||||
if let Some(fb) = inner_box.selected_children().first() {
|
||||
if let Some(child) = fb.child() {
|
||||
let expander = child.downcast::<Expander>().ok();
|
||||
if let Some(expander) = expander {
|
||||
expander.set_expanded(true);
|
||||
} else {
|
||||
let lock = list_items.lock().unwrap();
|
||||
let menu_item = lock.get(fb);
|
||||
if let Some(menu_item) = menu_item {
|
||||
if let Some(new_items) =
|
||||
item_provider.lock().unwrap().get_sub_elements(&menu_item)
|
||||
{
|
||||
let query = menu_item.label.clone();
|
||||
drop(lock);
|
||||
|
||||
search_entry.set_text(&query);
|
||||
update_view(&query, new_items);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Propagation::Stop;
|
||||
}
|
||||
_ => {
|
||||
if let Some(c) = keyboard_key.to_unicode() {
|
||||
let current = search_entry.text().to_string();
|
||||
let query = format!("{current}{c}");
|
||||
search_entry.set_text(&query);
|
||||
update_view_from_provider(&query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
}
|
||||
|
||||
fn sort_menu_items_by_score<T: std::clone::Clone>(
|
||||
child1: &FlowBoxChild,
|
||||
child2: &FlowBoxChild,
|
||||
items_lock: &Mutex<HashMap<FlowBoxChild, MenuItem<T>>>,
|
||||
items_lock: ArcMenuMap<T>,
|
||||
) -> Ordering {
|
||||
let lock = items_lock.lock().unwrap();
|
||||
let m1 = lock.get(child1);
|
||||
|
@ -302,21 +427,21 @@ fn animate_window_show(config: Config, window: ApplicationWindow, outer_box: gtk
|
|||
let monitor = display.monitor_at_surface(&surface);
|
||||
if let Some(monitor) = monitor {
|
||||
let geometry = monitor.geometry();
|
||||
let Some(target_width) = percent_or_absolute(&config.width.unwrap(), geometry.width())
|
||||
let Some(target_width) = percent_or_absolute(config.width.as_ref(), geometry.width())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(target_height) =
|
||||
percent_or_absolute(&config.height.unwrap(), geometry.height())
|
||||
percent_or_absolute(config.height.as_ref(), geometry.height())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
animate_window(
|
||||
window.clone(),
|
||||
config.show_animation.unwrap(),
|
||||
config.show_animation_time.unwrap(),
|
||||
config.show_animation.unwrap_or(Animation::None),
|
||||
config.show_animation_time.unwrap_or(0),
|
||||
target_height,
|
||||
target_width,
|
||||
move || {
|
||||
|
@ -348,8 +473,8 @@ where
|
|||
|
||||
animate_window(
|
||||
window,
|
||||
config.hide_animation.unwrap(),
|
||||
config.hide_animation_time.unwrap(),
|
||||
config.hide_animation.unwrap_or(Animation::None),
|
||||
config.hide_animation_time.unwrap_or(0),
|
||||
target_h,
|
||||
target_w,
|
||||
on_done_func,
|
||||
|
@ -563,7 +688,7 @@ fn add_menu_item<T: Clone + 'static>(
|
|||
create_menu_row(
|
||||
entry_element,
|
||||
config,
|
||||
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc),
|
||||
ArcMenuMap::clone(lock_arc),
|
||||
sender.clone(),
|
||||
app.clone(),
|
||||
window.clone(),
|
||||
|
@ -579,7 +704,7 @@ fn add_menu_item<T: Clone + 'static>(
|
|||
let menu_row = create_menu_row(
|
||||
entry_element,
|
||||
config,
|
||||
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc),
|
||||
ArcMenuMap::clone(lock_arc),
|
||||
sender.clone(),
|
||||
app.clone(),
|
||||
window.clone(),
|
||||
|
@ -595,7 +720,7 @@ fn add_menu_item<T: Clone + 'static>(
|
|||
let sub_row = create_menu_row(
|
||||
sub_item,
|
||||
config,
|
||||
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc),
|
||||
ArcMenuMap::clone(lock_arc),
|
||||
sender.clone(),
|
||||
app.clone(),
|
||||
window.clone(),
|
||||
|
@ -659,7 +784,13 @@ fn create_menu_row<T: Clone + 'static>(
|
|||
|
||||
row.add_controller(click);
|
||||
|
||||
let row_box = gtk4::Box::new(config.row_bow_orientation.unwrap().into(), 0);
|
||||
let row_box = gtk4::Box::new(
|
||||
config
|
||||
.row_bow_orientation
|
||||
.unwrap_or(config::Orientation::Horizontal)
|
||||
.into(),
|
||||
0,
|
||||
);
|
||||
row_box.set_hexpand(true);
|
||||
row_box.set_vexpand(false);
|
||||
row_box.set_halign(Align::Fill);
|
||||
|
@ -690,86 +821,97 @@ fn create_menu_row<T: Clone + 'static>(
|
|||
label.set_wrap(true);
|
||||
row_box.append(&label);
|
||||
|
||||
if config.content_halign.unwrap() == config::Align::Start
|
||||
|| config.content_halign.unwrap() == config::Align::Fill
|
||||
if config
|
||||
.content_halign
|
||||
.is_some_and(|c| c == config::Align::Start)
|
||||
|| config
|
||||
.content_halign
|
||||
.is_some_and(|c| c == config::Align::Fill)
|
||||
{
|
||||
label.set_xalign(0.0);
|
||||
}
|
||||
row.upcast()
|
||||
}
|
||||
|
||||
fn filter_widgets<T>(
|
||||
fn filter_widgets<T: Clone>(
|
||||
query: &str,
|
||||
items: &mut HashMap<FlowBoxChild, MenuItem<T>>,
|
||||
item_arc: &ArcMenuMap<T>,
|
||||
config: &Config,
|
||||
inner_box: &FlowBox,
|
||||
) {
|
||||
if items.is_empty() {
|
||||
for (child, _) in items.iter() {
|
||||
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);
|
||||
{
|
||||
let mut items = item_arc.lock().unwrap();
|
||||
if items.is_empty() {
|
||||
for (child, _) in items.iter() {
|
||||
child.set_visible(true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let query = query.to_owned().to_lowercase();
|
||||
for (flowbox_child, menu_item) in items.iter_mut() {
|
||||
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)
|
||||
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);
|
||||
}
|
||||
}
|
||||
MatchMethod::MultiContains => {
|
||||
let score = query
|
||||
.split(' ')
|
||||
.filter(|i| menu_item_search.contains(i))
|
||||
.map(|_| 1.0)
|
||||
.sum();
|
||||
(score, score > 0.0)
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
menu_item.search_sort_score = search_sort_score;
|
||||
flowbox_child.set_visible(visible);
|
||||
let query = query.to_owned().to_lowercase();
|
||||
for (flowbox_child, menu_item) in items.iter_mut() {
|
||||
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_or(config::default_fuzzy_min_score().unwrap_or(0.0)),
|
||||
)
|
||||
}
|
||||
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;
|
||||
flowbox_child.set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
select_first_visible_child(items, inner_box);
|
||||
inner_box.invalidate_sort();
|
||||
}
|
||||
|
||||
fn select_first_visible_child<T>(
|
||||
items: &mut HashMap<FlowBoxChild, MenuItem<T>>,
|
||||
inner_box: &FlowBox,
|
||||
) {
|
||||
fn select_first_visible_child<T: Clone>(lock: &ArcMenuMap<T>, inner_box: &FlowBox) {
|
||||
let items = lock.lock().unwrap();
|
||||
for i in 0..items.len() {
|
||||
let i_32 = i.try_into().unwrap_or(i32::MAX);
|
||||
if let Some(child) = inner_box.child_at_index(i_32) {
|
||||
|
@ -784,21 +926,25 @@ fn select_first_visible_child<T>(
|
|||
// allowed because truncating is fine, we do no need the precision
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn percent_or_absolute(value: &str, 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,
|
||||
fn percent_or_absolute(value: Option<&String>, base_value: i32) -> Option<i32> {
|
||||
if let Some(value) = value {
|
||||
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()
|
||||
}
|
||||
} else {
|
||||
value.parse::<i32>().ok()
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// highly unlikely that we are dealing with > i64 items
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
pub fn initialize_sort_scores<T>(items: &mut [MenuItem<T>]) {
|
||||
pub fn initialize_sort_scores<T: std::clone::Clone>(items: &mut [MenuItem<T>]) {
|
||||
let mut regular_score = items.len() as i64;
|
||||
items.sort_by(|l, r| l.label.cmp(&r.label));
|
||||
|
||||
|
|
504
src/lib/mode.rs
504
src/lib/mode.rs
|
@ -1,13 +1,17 @@
|
|||
use crate::config::Config;
|
||||
use crate::config::{Config, expand_path};
|
||||
use crate::desktop::{
|
||||
default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale,
|
||||
};
|
||||
use crate::gui;
|
||||
use crate::gui::MenuItem;
|
||||
use anyhow::{Context, anyhow};
|
||||
use crate::gui::{ItemProvider, MenuItem};
|
||||
use crate::{config, desktop, gui};
|
||||
use anyhow::{Context, Error, anyhow};
|
||||
use freedesktop_file_parser::EntryType;
|
||||
use gtk4::Image;
|
||||
use libc::option;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::prelude::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
@ -19,127 +23,416 @@ struct DRunCache {
|
|||
run_count: usize,
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if it was not able to spawn the process
|
||||
pub fn d_run(config: &Config) -> anyhow::Result<()> {
|
||||
let locale_variants = get_locale_variants();
|
||||
let default_icon = default_icon().unwrap_or_default();
|
||||
#[derive(Clone)]
|
||||
struct DRunProvider<T: std::clone::Clone> {
|
||||
items: Vec<MenuItem<T>>,
|
||||
cache_path: Option<PathBuf>,
|
||||
cache: HashMap<String, i64>,
|
||||
}
|
||||
|
||||
let (cache_path, mut d_run_cache) = load_d_run_cache();
|
||||
impl<T: Clone> DRunProvider<T> {
|
||||
fn new(menu_item_data: T) -> Self {
|
||||
let locale_variants = get_locale_variants();
|
||||
let default_icon = default_icon().unwrap_or_default();
|
||||
|
||||
let mut entries: Vec<MenuItem<String>> = Vec::new();
|
||||
for file in find_desktop_files().ok().iter().flatten().filter(|f| {
|
||||
f.entry.hidden.is_none_or(|hidden| !hidden)
|
||||
&& f.entry.no_display.is_none_or(|no_display| !no_display)
|
||||
}) {
|
||||
let Some(name) = lookup_name_with_locale(
|
||||
&locale_variants,
|
||||
&file.entry.name.variants,
|
||||
&file.entry.name.default,
|
||||
) else {
|
||||
log::warn!("Skipping desktop entry without name {file:?}");
|
||||
continue;
|
||||
};
|
||||
let (cache_path, d_run_cache) = load_d_run_cache();
|
||||
|
||||
let (action, working_dir) = match &file.entry.entry_type {
|
||||
EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let cmd_exists = action
|
||||
.as_ref()
|
||||
.and_then(|a| {
|
||||
a.split(' ')
|
||||
.next()
|
||||
.map(|cmd| cmd.replace('"', ""))
|
||||
.map(|cmd| PathBuf::from(&cmd).exists() || which::which(&cmd).is_ok())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !cmd_exists {
|
||||
log::warn!(
|
||||
"Skipping desktop entry for {name:?} because action {action:?} does not exist"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let icon = file
|
||||
.entry
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|s| s.content.clone())
|
||||
.or(Some(default_icon.clone()));
|
||||
log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}");
|
||||
let sort_score = d_run_cache.get(&name).unwrap_or(&0);
|
||||
|
||||
let mut entry: MenuItem<String> = MenuItem {
|
||||
label: name,
|
||||
icon_path: icon.clone(),
|
||||
action,
|
||||
sub_elements: Vec::default(),
|
||||
working_dir: working_dir.clone(),
|
||||
initial_sort_score: -(*sort_score),
|
||||
search_sort_score: 0.0,
|
||||
data: None,
|
||||
};
|
||||
|
||||
file.actions.iter().for_each(|(_, action)| {
|
||||
if let Some(action_name) = lookup_name_with_locale(
|
||||
let mut entries: Vec<MenuItem<T>> = Vec::new();
|
||||
for file in find_desktop_files().iter().filter(|f| {
|
||||
f.entry.hidden.is_none_or(|hidden| !hidden)
|
||||
&& f.entry.no_display.is_none_or(|no_display| !no_display)
|
||||
}) {
|
||||
let Some(name) = lookup_name_with_locale(
|
||||
&locale_variants,
|
||||
&action.name.variants,
|
||||
&action.name.default,
|
||||
) {
|
||||
let action_icon = action
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|s| s.content.clone())
|
||||
.or(icon.clone());
|
||||
&file.entry.name.variants,
|
||||
&file.entry.name.default,
|
||||
) else {
|
||||
log::warn!("Skipping desktop entry without name {file:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}");
|
||||
let (action, working_dir) = match &file.entry.entry_type {
|
||||
EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let sub_entry = MenuItem {
|
||||
label: action_name,
|
||||
icon_path: action_icon,
|
||||
action: action.exec.clone(),
|
||||
sub_elements: Vec::default(),
|
||||
working_dir: working_dir.clone(),
|
||||
initial_sort_score: 0, // subitems are never sorted right now.
|
||||
search_sort_score: 0.0,
|
||||
data: None,
|
||||
};
|
||||
entry.sub_elements.push(sub_entry);
|
||||
}
|
||||
});
|
||||
let cmd_exists = action
|
||||
.as_ref()
|
||||
.and_then(|a| {
|
||||
a.split(' ')
|
||||
.next()
|
||||
.map(|cmd| cmd.replace('"', ""))
|
||||
.map(|cmd| PathBuf::from(&cmd).exists() || which::which(&cmd).is_ok())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
entries.push(entry);
|
||||
if !cmd_exists {
|
||||
log::warn!(
|
||||
"Skipping desktop entry for {name:?} because action {action:?} does not exist"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let icon = file
|
||||
.entry
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|s| s.content.clone())
|
||||
.or(Some(default_icon.clone()));
|
||||
log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}");
|
||||
let sort_score = d_run_cache.get(&name).unwrap_or(&0);
|
||||
|
||||
let mut entry: MenuItem<T> = MenuItem {
|
||||
label: name,
|
||||
icon_path: icon.clone(),
|
||||
action,
|
||||
sub_elements: Vec::default(),
|
||||
working_dir: working_dir.clone(),
|
||||
initial_sort_score: -(*sort_score),
|
||||
search_sort_score: 0.0,
|
||||
data: Some(menu_item_data.clone()),
|
||||
};
|
||||
|
||||
file.actions.iter().for_each(|(_, action)| {
|
||||
if let Some(action_name) = lookup_name_with_locale(
|
||||
&locale_variants,
|
||||
&action.name.variants,
|
||||
&action.name.default,
|
||||
) {
|
||||
let action_icon = action
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|s| s.content.clone())
|
||||
.or(icon.clone());
|
||||
|
||||
log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}");
|
||||
|
||||
let sub_entry = MenuItem {
|
||||
label: action_name,
|
||||
icon_path: action_icon,
|
||||
action: action.exec.clone(),
|
||||
sub_elements: Vec::default(),
|
||||
working_dir: working_dir.clone(),
|
||||
initial_sort_score: 0, // subitems are never sorted right now.
|
||||
search_sort_score: 0.0,
|
||||
data: None,
|
||||
};
|
||||
entry.sub_elements.push(sub_entry);
|
||||
}
|
||||
});
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
gui::initialize_sort_scores(&mut entries);
|
||||
|
||||
DRunProvider {
|
||||
items: entries,
|
||||
cache_path,
|
||||
cache: d_run_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::clone::Clone> ItemProvider<T> for DRunProvider<T> {
|
||||
fn get_elements(&mut self, _: Option<&str>) -> Vec<MenuItem<T>> {
|
||||
self.items.clone()
|
||||
}
|
||||
|
||||
gui::initialize_sort_scores(&mut entries);
|
||||
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> Option<Vec<MenuItem<T>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// todo ues a arc instead of cloning the config
|
||||
let selection_result = gui::show(config.clone(), entries.clone());
|
||||
match selection_result {
|
||||
Ok(selected_item) => {
|
||||
if let Some(cache) = cache_path {
|
||||
*d_run_cache.entry(selected_item.label).or_insert(0) += 1;
|
||||
if let Err(e) = save_cache_file(&cache, &d_run_cache) {
|
||||
log::warn!("cannot save drun cache {e:?}");
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum AutoRunType {
|
||||
Math,
|
||||
DRun,
|
||||
File,
|
||||
Ssh,
|
||||
WebSearch,
|
||||
Emoji,
|
||||
Run,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AutoItemProvider {
|
||||
drun_provider: DRunProvider<AutoRunType>,
|
||||
last_result: Option<Vec<MenuItem<AutoRunType>>>,
|
||||
}
|
||||
|
||||
impl AutoItemProvider {
|
||||
fn new() -> Self {
|
||||
AutoItemProvider {
|
||||
drun_provider: DRunProvider::new(AutoRunType::DRun),
|
||||
|
||||
last_result: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_run_handle_files(&mut self, trimmed_search: &str) -> Vec<MenuItem<AutoRunType>> {
|
||||
let folder_icon = "inode-directory";
|
||||
|
||||
let path = config::expand_path(trimmed_search);
|
||||
let mut items: Vec<MenuItem<AutoRunType>> = Vec::new();
|
||||
|
||||
if !path.exists() {
|
||||
if let Some(last) = &self.last_result {
|
||||
if !last.is_empty()
|
||||
&& last.first().is_some_and(|l| {
|
||||
l.as_ref()
|
||||
.data
|
||||
.as_ref()
|
||||
.is_some_and(|t| t == &AutoRunType::File)
|
||||
})
|
||||
{
|
||||
return last.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(action) = selected_item.action {
|
||||
spawn_fork(&action, selected_item.working_dir.as_ref())?;
|
||||
}
|
||||
return vec![];
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
|
||||
if path.is_dir() {
|
||||
for entry in path.read_dir().unwrap() {
|
||||
if let Ok(entry) = entry {
|
||||
let mut path_str = entry.path().to_str().unwrap_or("").to_string();
|
||||
if trimmed_search.starts_with("~") {
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
path_str = path_str.replace(home_dir.to_str().unwrap_or(""), "~");
|
||||
}
|
||||
}
|
||||
|
||||
if entry.path().is_dir() {
|
||||
path_str += "/";
|
||||
}
|
||||
|
||||
items.push({
|
||||
MenuItem {
|
||||
label: path_str.clone(),
|
||||
icon_path: if entry.path().is_dir() {
|
||||
Some(folder_icon.to_owned())
|
||||
} else {
|
||||
Some(resolve_icon_for_name(entry.path()))
|
||||
},
|
||||
action: Some(format!("xdg-open {path_str}")),
|
||||
sub_elements: vec![],
|
||||
working_dir: None,
|
||||
initial_sort_score: 0,
|
||||
search_sort_score: 0.0,
|
||||
data: Some(AutoRunType::File),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
MenuItem {
|
||||
label: trimmed_search.to_owned(),
|
||||
icon_path: Some(resolve_icon_for_name(PathBuf::from(trimmed_search))),
|
||||
action: Some(format!("xdg-open {trimmed_search}")),
|
||||
sub_elements: vec![],
|
||||
working_dir: None,
|
||||
initial_sort_score: 0,
|
||||
search_sort_score: 0.0,
|
||||
data: Some(AutoRunType::File),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.last_result = Some(items.clone());
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_icon_for_name(path: PathBuf) -> String {
|
||||
// todo use https://docs.rs/tree_magic_mini/latest/tree_magic_mini/ instead
|
||||
if let Ok(metadata) = fs::symlink_metadata(&path) {
|
||||
if metadata.file_type().is_symlink() {
|
||||
return "inode-symlink".to_owned();
|
||||
} else if metadata.is_dir() {
|
||||
return "inode-directory".to_owned();
|
||||
} else if metadata.permissions().mode() & 0o111 != 0 {
|
||||
return "application-x-executable".to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
|
||||
match extension.as_str() {
|
||||
"sh" | "py" | "rb" | "pl" | "bash" => "text-x-script".to_owned(),
|
||||
"c" | "cpp" | "rs" | "java" | "js" | "h" | "hpp" => "text-x-generic".to_owned(),
|
||||
"txt" | "md" | "log" => "text-x-generic".to_owned(),
|
||||
"html" | "htm" => "text-html".to_owned(),
|
||||
"jpg" | "jpeg" | "png" | "gif" | "svg" | "webp" => "image-x-generic".to_owned(),
|
||||
"mp3" | "wav" | "ogg" => "audio-x-generic".to_owned(),
|
||||
"mp4" | "mkv" | "avi" => "video-x-generic".to_owned(),
|
||||
"ttf" | "otf" | "woff" => "font-x-generic".to_owned(),
|
||||
"zip" | "tar" | "gz" | "xz" | "7z" | "lz4" => "package-x-generic".to_owned(),
|
||||
"deb" | "rpm" | "apk" => "x-package-repository".to_owned(),
|
||||
"odt" => "x-office-document".to_owned(),
|
||||
"ott" => "x-office-document-template".to_owned(),
|
||||
"ods" => "x-office-spreadsheet".to_owned(),
|
||||
"ots" => "x-office-spreadsheet-template".to_owned(),
|
||||
"odp" => "x-office-presentation".to_owned(),
|
||||
"otp" => "x-office-presentation-template".to_owned(),
|
||||
"odg" => "x-office-drawing".to_owned(),
|
||||
"vcf" => "x-office-addressbook".to_owned(),
|
||||
_ => "application-x-generic".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_math_functions_or_starts_with_number(input: &str) -> bool {
|
||||
// Regex for function names (word boundaries to match whole words)
|
||||
let math_functions = r"\b(sqrt|abs|exp|ln|sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|floor|ceil|round|signum|min|max|pi|e)\b";
|
||||
|
||||
// Regex for strings that start with a number (including decimals)
|
||||
let starts_with_number = r"^\s*[+-]?(\d+(\.\d*)?|\.\d+)";
|
||||
|
||||
let math_regex = Regex::new(math_functions).unwrap();
|
||||
let number_regex = Regex::new(starts_with_number).unwrap();
|
||||
|
||||
math_regex.is_match(input) || number_regex.is_match(input)
|
||||
}
|
||||
|
||||
impl ItemProvider<AutoRunType> for AutoItemProvider {
|
||||
fn get_elements(&mut self, search_opt: Option<&str>) -> Vec<MenuItem<AutoRunType>> {
|
||||
if let Some(search) = search_opt {
|
||||
let trimmed_search = search.trim();
|
||||
if trimmed_search.is_empty() {
|
||||
self.drun_provider.get_elements(search_opt)
|
||||
} else if contains_math_functions_or_starts_with_number(trimmed_search) {
|
||||
let result = match meval::eval_str(trimmed_search) {
|
||||
Ok(result) => result.to_string(),
|
||||
Err(e) => format!("failed to calculate {e:?}"),
|
||||
};
|
||||
|
||||
let item = MenuItem {
|
||||
label: result,
|
||||
icon_path: None,
|
||||
action: None,
|
||||
sub_elements: vec![],
|
||||
working_dir: None,
|
||||
initial_sort_score: 0,
|
||||
search_sort_score: 0.0,
|
||||
data: Some(AutoRunType::Math),
|
||||
};
|
||||
|
||||
return vec![item];
|
||||
} else if trimmed_search.starts_with("$")
|
||||
|| trimmed_search.starts_with("/")
|
||||
|| trimmed_search.starts_with("~")
|
||||
{
|
||||
self.auto_run_handle_files(trimmed_search)
|
||||
} else {
|
||||
return self.drun_provider.get_elements(search_opt);
|
||||
}
|
||||
} else {
|
||||
self.drun_provider.get_elements(search_opt)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sub_elements(
|
||||
&mut self,
|
||||
item: &MenuItem<AutoRunType>,
|
||||
) -> Option<Vec<MenuItem<AutoRunType>>> {
|
||||
Some(self.get_elements(Some(item.label.as_ref())))
|
||||
}
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if it was not able to spawn the process
|
||||
pub fn d_run(config: &mut Config) -> anyhow::Result<()> {
|
||||
let provider = DRunProvider::new("".to_owned());
|
||||
let cache_path = provider.cache_path.clone();
|
||||
let mut cache = provider.cache.clone();
|
||||
if config.prompt.is_none() {
|
||||
config.prompt = Some("drun".to_owned());
|
||||
}
|
||||
|
||||
// todo ues a arc instead of cloning the config
|
||||
let selection_result = gui::show(config.clone(), provider);
|
||||
match selection_result {
|
||||
Ok(s) => {
|
||||
update_drun_cache_and_run(cache_path, &mut cache, s)?;
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("No item selected");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auto(config: &mut Config) -> anyhow::Result<()> {
|
||||
let provider = AutoItemProvider::new();
|
||||
let cache_path = provider.drun_provider.cache_path.clone();
|
||||
let mut cache = provider.drun_provider.cache.clone();
|
||||
|
||||
if config.prompt.is_none() {
|
||||
config.prompt = Some("auto".to_owned());
|
||||
}
|
||||
|
||||
// todo ues a arc instead of cloning the config
|
||||
let selection_result = gui::show(config.clone(), provider);
|
||||
|
||||
match selection_result {
|
||||
Ok(selection_result) => {
|
||||
if let Some(data) = &selection_result.data {
|
||||
match data {
|
||||
AutoRunType::Math => {}
|
||||
AutoRunType::DRun => {
|
||||
update_drun_cache_and_run(cache_path, &mut cache, selection_result)?;
|
||||
}
|
||||
AutoRunType::File => {
|
||||
if let Some(action) = selection_result.action {
|
||||
spawn_fork(&action, selection_result.working_dir.as_ref())?
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
todo!("not supported yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("No item selected");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_drun_cache_and_run<T: Clone>(
|
||||
cache_path: Option<PathBuf>,
|
||||
cache: &mut HashMap<String, i64>,
|
||||
selection_result: MenuItem<T>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(cache_path) = cache_path {
|
||||
*cache.entry(selection_result.label).or_insert(0) += 1;
|
||||
if let Err(e) = save_cache_file(&cache_path, &cache) {
|
||||
log::warn!("cannot save drun cache {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(action) = selection_result.action {
|
||||
spawn_fork(&action, selection_result.working_dir.as_ref())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("cannot find drun action"))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_d_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) {
|
||||
let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun"));
|
||||
let d_run_cache = {
|
||||
|
@ -213,6 +506,7 @@ fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> anyhow::Result<()> {
|
|||
.iter()
|
||||
.skip(1)
|
||||
.filter(|arg| !arg.starts_with('%'))
|
||||
.map(|arg| expand_path(arg))
|
||||
.collect();
|
||||
|
||||
unsafe {
|
||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -1,6 +1,7 @@
|
|||
use std::env;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use worf_lib::config::Mode;
|
||||
use worf_lib::{config, mode};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
|
@ -12,19 +13,22 @@ fn main() -> anyhow::Result<()> {
|
|||
.init();
|
||||
|
||||
let args = config::parse_args();
|
||||
let config = config::load_config(Some(args))?;
|
||||
let mut config = config::load_config(Some(args)).map_err(|e| anyhow!(e))?;
|
||||
|
||||
if let Some(show) = &config.show {
|
||||
match show {
|
||||
config::Mode::Run => {
|
||||
Mode::Run => {
|
||||
todo!("run not implemented")
|
||||
}
|
||||
config::Mode::Drun => {
|
||||
mode::d_run(&config)?;
|
||||
Mode::Drun => {
|
||||
mode::d_run(&mut config)?;
|
||||
}
|
||||
config::Mode::Dmenu => {
|
||||
Mode::Dmenu => {
|
||||
todo!("dmenu not implemented")
|
||||
}
|
||||
Mode::Auto => {
|
||||
mode::auto(&mut config)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
8
styles/dmenu/config.toml
Normal file
8
styles/dmenu/config.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
image_size=0
|
||||
columns=999
|
||||
orientation="Horizontal"
|
||||
row_bow_orientation="Horizontal"
|
||||
content_halign="Center"
|
||||
height="25"
|
||||
width="100%"
|
||||
valign="Start"
|
66
styles/dmenu/style.css
Normal file
66
styles/dmenu/style.css
Normal file
|
@ -0,0 +1,66 @@
|
|||
* {
|
||||
font-family: DejaVu;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#window {
|
||||
all: unset;
|
||||
background-color: rgba(33, 33, 33, 1);
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#window #outer-box {
|
||||
/* The name of the search bar */
|
||||
/* The name of the scrolled window containing all of the entries */
|
||||
border: none;
|
||||
}
|
||||
|
||||
#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);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#window #outer-box #scroll #inner-box #entry {
|
||||
color: #fff;
|
||||
background-color: rgba(32, 32, 32, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
border-bottom: 5px solid rgba(32, 32, 32, 0.1);
|
||||
|
||||
}
|
||||
#window #outer-box #scroll #inner-box #entry #img {
|
||||
|
||||
}
|
||||
|
||||
#window #outer-box #scroll #inner-box #entry:selected {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
#row:hover {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
outline: inherit;
|
||||
}
|
||||
#window #outer-box #scroll #inner-box #entry:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
#label {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
|
@ -3,6 +3,6 @@ columns=6
|
|||
orientation="Vertical"
|
||||
row_bow_orientation="Vertical"
|
||||
content_halign="Center"
|
||||
height="110%"
|
||||
height="105%"
|
||||
width="100%"
|
||||
valign="Start"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
all: unset;
|
||||
background-color: rgba(33, 33, 33, 0.76); /* Matches #212121BB */
|
||||
border-radius: 0;
|
||||
padding-top: 5rem;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
#window #outer-box {
|
||||
|
@ -17,7 +17,7 @@
|
|||
color: #f2f2f2;
|
||||
border-bottom: 2px solid rgba(214, 174, 0, 1);
|
||||
font-size: 1rem;
|
||||
margin: 1rem 40rem;
|
||||
margin: 1rem 50rem 2rem;
|
||||
}
|
||||
|
||||
#window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active {
|
||||
|
|
5
styles/relaxed/config.toml
Normal file
5
styles/relaxed/config.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
image_size=48
|
||||
columns=1
|
||||
height="60%"
|
||||
width="70%"
|
||||
valign="Start"
|
68
styles/relaxed/style.css
Normal file
68
styles/relaxed/style.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
* {
|
||||
font-family: DejaVu;
|
||||
}
|
||||
|
||||
#window {
|
||||
all: unset;
|
||||
background-color: rgba(33, 33, 33, 0.8); /* Matches #212121BB */
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#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 #inner-box #entry {
|
||||
color: #fff;
|
||||
background-color: rgba(32, 32, 32, 0.1);
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-bottom: 5px solid rgba(32, 32, 32, 0.1);
|
||||
|
||||
}
|
||||
#window #outer-box #scroll #inner-box #entry #img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#window #outer-box #scroll #inner-box #entry:selected {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
border-bottom: 5px solid rgba(214, 174, 0, 1);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
#row:hover {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
outline: inherit;
|
||||
}
|
||||
#window #outer-box #scroll #inner-box #entry:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
#label {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
Loading…
Add table
Reference in a new issue