refactor modes into separate folder
This commit is contained in:
parent
dd46e210e1
commit
0467217c23
16 changed files with 1073 additions and 1018 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1917,6 +1917,7 @@ name = "worf-warden"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"log",
|
||||||
"worf",
|
"worf",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ edition = "2024"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
worf = {path = "../../worf"}
|
worf = {path = "../../worf"}
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
|
log = "0.4.27"
|
||||||
|
|
||||||
# todo re-add this
|
# todo re-add this
|
||||||
#[features]
|
#[features]
|
||||||
|
|
|
@ -125,11 +125,7 @@ fn keyboard_type(text: &str) {
|
||||||
|
|
||||||
fn keyboard_tab() {
|
fn keyboard_tab() {
|
||||||
Command::new("ydotool")
|
Command::new("ydotool")
|
||||||
.arg("key")
|
.arg("TAB")
|
||||||
.arg("-d")
|
|
||||||
.arg("50")
|
|
||||||
.arg("15:1")
|
|
||||||
.arg("15:0")
|
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to execute ydotool");
|
.expect("Failed to execute ydotool");
|
||||||
}
|
}
|
||||||
|
@ -334,7 +330,7 @@ fn main() -> Result<(), String> {
|
||||||
let config = config::load_config(Some(&args)).unwrap_or(args);
|
let config = config::load_config(Some(&args)).unwrap_or(args);
|
||||||
|
|
||||||
if !groups().contains("input") {
|
if !groups().contains("input") {
|
||||||
eprintln!("User must be in input group. 'sudo usermod -aG input $USER', then login again");
|
log::error!("User must be in input group. 'sudo usermod -aG input $USER', then login again");
|
||||||
std::process::exit(1)
|
std::process::exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Instant;
|
||||||
|
|
||||||
use crossbeam::channel;
|
use crossbeam::channel;
|
||||||
use crossbeam::channel::Sender;
|
use crossbeam::channel::Sender;
|
||||||
use gdk4::Display;
|
use gdk4::Display;
|
||||||
use gdk4::gio::File;
|
use gdk4::gio::File;
|
||||||
use gdk4::glib::{MainContext, Propagation, timeout_add_local};
|
use gdk4::glib::{MainContext, Propagation};
|
||||||
use gdk4::prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt};
|
use gdk4::prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt};
|
||||||
use gtk4::glib::ControlFlow;
|
use gtk4::glib::ControlFlow;
|
||||||
use gtk4::prelude::{
|
use gtk4::prelude::{
|
||||||
|
@ -1011,7 +1011,7 @@ fn send_selected_item<T>(
|
||||||
T: Clone + Send + 'static,
|
T: Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
let ui_clone = Rc::clone(&ui);
|
let ui_clone = Rc::clone(&ui);
|
||||||
ui.window.connect_hide(move |w| {
|
ui.window.connect_hide(move |_| {
|
||||||
if let Err(e) = meta.selected_sender.send(Ok(Selection {
|
if let Err(e) = meta.selected_sender.send(Ok(Selection {
|
||||||
menu: selected_item.clone(),
|
menu: selected_item.clone(),
|
||||||
custom_key: custom_key.clone(),
|
custom_key: custom_key.clone(),
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Configuration and command line parsing
|
||||||
|
pub mod config;
|
||||||
|
/// Desktop action like parsing desktop files and launching programs
|
||||||
|
pub mod desktop;
|
||||||
|
/// All things related to the user interface
|
||||||
|
pub mod gui;
|
||||||
|
/// Out of the box supported modes, like drun, dmenu, etc...
|
||||||
|
pub mod modes;
|
||||||
|
|
||||||
|
|
||||||
/// Defines error the lib can encounter
|
/// Defines error the lib can encounter
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
@ -64,12 +74,3 @@ impl fmt::Display for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration and command line parsing
|
|
||||||
pub mod config;
|
|
||||||
/// Desktop action like parsing desktop files and launching programs
|
|
||||||
pub mod desktop;
|
|
||||||
/// All things related to the user interface
|
|
||||||
pub mod gui;
|
|
||||||
/// Out of the box supported modes, like drun, dmenu, etc...
|
|
||||||
pub mod mode;
|
|
||||||
|
|
|
@ -1,991 +0,0 @@
|
||||||
use crate::config::{Config, SortOrder, expand_path};
|
|
||||||
use crate::desktop::{
|
|
||||||
copy_to_clipboard, create_file_if_not_exists, find_desktop_files, get_locale_variants,
|
|
||||||
is_executable, load_cache_file, lookup_name_with_locale, save_cache_file, spawn_fork,
|
|
||||||
};
|
|
||||||
use crate::gui::{ItemProvider, MenuItem};
|
|
||||||
use crate::{Error, gui};
|
|
||||||
use freedesktop_file_parser::EntryType;
|
|
||||||
use rayon::prelude::*;
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::ffi::CString;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::os::unix::fs::FileTypeExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::Instant;
|
|
||||||
use std::{env, fs, io};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
struct DRunCache {
|
|
||||||
desktop_entry: String,
|
|
||||||
run_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct DRunProvider<T: Clone> {
|
|
||||||
items: Option<Vec<MenuItem<T>>>,
|
|
||||||
cache_path: Option<PathBuf>,
|
|
||||||
cache: HashMap<String, i64>,
|
|
||||||
data: T,
|
|
||||||
no_actions: bool,
|
|
||||||
sort_order: SortOrder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone + Send + Sync> DRunProvider<T> {
|
|
||||||
fn new(menu_item_data: T, no_actions: bool, sort_order: SortOrder) -> Self {
|
|
||||||
let (cache_path, d_run_cache) = load_d_run_cache();
|
|
||||||
DRunProvider {
|
|
||||||
items: None,
|
|
||||||
cache_path,
|
|
||||||
cache: d_run_cache,
|
|
||||||
data: menu_item_data,
|
|
||||||
no_actions,
|
|
||||||
sort_order,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
fn load(&self) -> Vec<MenuItem<T>> {
|
|
||||||
let locale_variants = get_locale_variants();
|
|
||||||
let default_icon = "application-x-executable".to_string();
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
let entries: Vec<MenuItem<T>> = find_desktop_files()
|
|
||||||
.into_par_iter()
|
|
||||||
.filter(|file| {
|
|
||||||
!file.entry.no_display.unwrap_or(false)
|
|
||||||
&& !file.entry.hidden.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.filter_map(|file| {
|
|
||||||
let name = lookup_name_with_locale(
|
|
||||||
&locale_variants,
|
|
||||||
&file.entry.name.variants,
|
|
||||||
&file.entry.name.default,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (action, working_dir) = match &file.entry.entry_type {
|
|
||||||
EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
|
|
||||||
_ => return 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");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon = file
|
|
||||||
.entry
|
|
||||||
.icon
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.content.clone())
|
|
||||||
.or(Some(default_icon.clone()));
|
|
||||||
|
|
||||||
let sort_score = *self.cache.get(&name).unwrap_or(&0) as f64;
|
|
||||||
|
|
||||||
let mut entry = MenuItem::new(
|
|
||||||
name.clone(),
|
|
||||||
icon.clone(),
|
|
||||||
action.clone(),
|
|
||||||
Vec::new(),
|
|
||||||
working_dir.clone(),
|
|
||||||
sort_score,
|
|
||||||
Some(self.data.clone()),
|
|
||||||
);
|
|
||||||
if !self.no_actions {
|
|
||||||
for action in file.actions.values() {
|
|
||||||
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())
|
|
||||||
.unwrap_or("application-x-executable".to_string());
|
|
||||||
|
|
||||||
|
|
||||||
entry.sub_elements.push(MenuItem::new(
|
|
||||||
action_name,
|
|
||||||
Some(action_icon),
|
|
||||||
action.exec.clone(),
|
|
||||||
Vec::new(),
|
|
||||||
working_dir.clone(),
|
|
||||||
0.0,
|
|
||||||
Some(self.data.clone()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(entry)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut seen_actions = HashSet::new();
|
|
||||||
let mut entries: Vec<MenuItem<T>> = entries
|
|
||||||
.into_iter()
|
|
||||||
.filter(|entry| seen_actions.insert(entry.action.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"parsing desktop files took {}ms",
|
|
||||||
start.elapsed().as_millis()
|
|
||||||
);
|
|
||||||
|
|
||||||
gui::apply_sort(&mut entries, &self.sort_order);
|
|
||||||
entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemProvider<i32> for RunProvider {
|
|
||||||
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<i32>>) {
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(self.load().clone());
|
|
||||||
}
|
|
||||||
(false, self.items.clone().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<i32>) -> (bool, Option<Vec<MenuItem<i32>>>) {
|
|
||||||
(false, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct RunProvider {
|
|
||||||
items: Option<Vec<MenuItem<i32>>>,
|
|
||||||
cache_path: Option<PathBuf>,
|
|
||||||
cache: HashMap<String, i64>,
|
|
||||||
sort_order: SortOrder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunProvider {
|
|
||||||
fn new(sort_order: SortOrder) -> Self {
|
|
||||||
let (cache_path, d_run_cache) = load_run_cache();
|
|
||||||
RunProvider {
|
|
||||||
items: None,
|
|
||||||
cache_path,
|
|
||||||
cache: d_run_cache,
|
|
||||||
sort_order,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
fn load(&self) -> Vec<MenuItem<i32>> {
|
|
||||||
let path_var = env::var("PATH").unwrap_or_default();
|
|
||||||
let paths = env::split_paths(&path_var);
|
|
||||||
|
|
||||||
let entries: Vec<_> = paths
|
|
||||||
.filter(|dir| dir.is_dir())
|
|
||||||
.flat_map(|dir| {
|
|
||||||
fs::read_dir(dir)
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let path = entry.path();
|
|
||||||
if !is_executable(&path) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = path.file_name()?.to_str()?.to_string();
|
|
||||||
let sort_score = *self.cache.get(&label).unwrap_or(&0) as f64;
|
|
||||||
|
|
||||||
Some(MenuItem::new(
|
|
||||||
label,
|
|
||||||
None,
|
|
||||||
path.to_str().map(ToString::to_string),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
sort_score,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut seen_actions = HashSet::new();
|
|
||||||
let mut entries: Vec<MenuItem<i32>> = entries
|
|
||||||
.into_iter()
|
|
||||||
.filter(|entry| {
|
|
||||||
entry
|
|
||||||
.action
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|action| action.split('/').next_back())
|
|
||||||
.is_some_and(|cmd| seen_actions.insert(cmd.to_string()))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
gui::apply_sort(&mut entries, &self.sort_order);
|
|
||||||
entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone + Send + Sync> ItemProvider<T> for DRunProvider<T> {
|
|
||||||
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(self.load().clone());
|
|
||||||
}
|
|
||||||
(false, self.items.clone().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
|
||||||
(false, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct FileItemProvider<T: Clone> {
|
|
||||||
last_result: Option<Vec<MenuItem<T>>>,
|
|
||||||
menu_item_data: T,
|
|
||||||
sort_order: SortOrder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> FileItemProvider<T> {
|
|
||||||
fn new(menu_item_data: T, sort_order: SortOrder) -> Self {
|
|
||||||
FileItemProvider {
|
|
||||||
last_result: None,
|
|
||||||
menu_item_data,
|
|
||||||
sort_order,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_icon_for_name(path: &Path) -> String {
|
|
||||||
let type_result = fs::symlink_metadata(path)
|
|
||||||
.map(|meta| meta.file_type())
|
|
||||||
.map(|file_type| {
|
|
||||||
if file_type.is_symlink() {
|
|
||||||
Some("edit-redo")
|
|
||||||
} else if file_type.is_char_device() {
|
|
||||||
Some("input-keyboard")
|
|
||||||
} else if file_type.is_block_device() {
|
|
||||||
Some("drive-harddisk")
|
|
||||||
} else if file_type.is_socket() {
|
|
||||||
Some("network-transmit-receive")
|
|
||||||
} else if file_type.is_fifo() {
|
|
||||||
Some("rotation-allowed")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(Some("system-lock-screen"));
|
|
||||||
|
|
||||||
if let Some(tr) = type_result {
|
|
||||||
return tr.to_owned();
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(mime) = tree_magic_mini::from_filepath(path) else {
|
|
||||||
return "image-not-found".to_string();
|
|
||||||
};
|
|
||||||
|
|
||||||
if mime.starts_with("image") {
|
|
||||||
return "image-x-generic".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if mime.starts_with("inode") {
|
|
||||||
return mime.replace('/', "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
if mime.starts_with("text") {
|
|
||||||
return if mime.contains("plain") {
|
|
||||||
"text-x-generic".to_string()
|
|
||||||
} else if mime.contains("python") {
|
|
||||||
"text-x-script".to_string()
|
|
||||||
} else if mime.contains("html") {
|
|
||||||
"text-html".to_string()
|
|
||||||
} else {
|
|
||||||
"text-x-generic".to_string()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if mime.starts_with("application") {
|
|
||||||
return if mime.contains("octet") {
|
|
||||||
"application-x-executable".to_string()
|
|
||||||
} else if mime.contains("tar")
|
|
||||||
|| mime.contains("lz")
|
|
||||||
|| mime.contains("zip")
|
|
||||||
|| mime.contains("7z")
|
|
||||||
|| mime.contains("xz")
|
|
||||||
{
|
|
||||||
"package-x-generic".to_string()
|
|
||||||
} else {
|
|
||||||
"text-html".to_string()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("unsupported mime type {mime}");
|
|
||||||
"application-x-generic".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
|
|
||||||
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
|
||||||
let default_path = if let Some(home) = dirs::home_dir() {
|
|
||||||
home.display().to_string()
|
|
||||||
} else {
|
|
||||||
"/".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut trimmed_search = search.unwrap_or(&default_path).to_owned();
|
|
||||||
if !trimmed_search.starts_with('/')
|
|
||||||
&& !trimmed_search.starts_with('~')
|
|
||||||
&& !trimmed_search.starts_with('$')
|
|
||||||
{
|
|
||||||
trimmed_search = format!("{default_path}/{trimmed_search}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = expand_path(&trimmed_search);
|
|
||||||
let mut items: Vec<MenuItem<T>> = Vec::new();
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
if let Some(last) = &self.last_result {
|
|
||||||
return (false, last.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.is_dir() {
|
|
||||||
items.push(MenuItem::new(
|
|
||||||
trimmed_search.clone(),
|
|
||||||
Some(FileItemProvider::<T>::resolve_icon_for_name(&path)),
|
|
||||||
Some(format!("xdg-open {}", path.display())),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
100.0,
|
|
||||||
Some(self.menu_item_data.clone()),
|
|
||||||
));
|
|
||||||
|
|
||||||
if let Ok(entries) = path.read_dir() {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
if let Some(mut path_str) =
|
|
||||||
entry.path().to_str().map(std::string::ToString::to_string)
|
|
||||||
{
|
|
||||||
if trimmed_search.starts_with('~') {
|
|
||||||
if let Some(home_dir) = dirs::home_dir() {
|
|
||||||
if let Some(home_str) = home_dir.to_str() {
|
|
||||||
path_str = path_str.replace(home_str, "~");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.path().is_dir() {
|
|
||||||
path_str.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push(MenuItem::new(
|
|
||||||
path_str.clone(),
|
|
||||||
Some(FileItemProvider::<T>::resolve_icon_for_name(&entry.path())),
|
|
||||||
Some(format!("xdg-open {path_str}")),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
0.0,
|
|
||||||
Some(self.menu_item_data.clone()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.push({
|
|
||||||
MenuItem::new(
|
|
||||||
trimmed_search.clone(),
|
|
||||||
Some(FileItemProvider::<T>::resolve_icon_for_name(
|
|
||||||
&PathBuf::from(&trimmed_search),
|
|
||||||
)),
|
|
||||||
Some(format!("xdg-open {trimmed_search}")),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
0.0,
|
|
||||||
Some(self.menu_item_data.clone()),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
gui::apply_sort(&mut items, &self.sort_order);
|
|
||||||
|
|
||||||
self.last_result = Some(items.clone());
|
|
||||||
(true, items)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
|
||||||
(false, self.last_result.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct SshProvider<T: Clone> {
|
|
||||||
elements: Vec<MenuItem<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> SshProvider<T> {
|
|
||||||
fn new(menu_item_data: T, order: &SortOrder) -> Self {
|
|
||||||
let re = Regex::new(r"(?m)^\s*Host\s+(.+)$").unwrap();
|
|
||||||
let mut items: Vec<_> = dirs::home_dir()
|
|
||||||
.map(|home| home.join(".ssh").join("config"))
|
|
||||||
.filter(|path| path.exists())
|
|
||||||
.map(|path| fs::read_to_string(&path).unwrap_or_default())
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|content| {
|
|
||||||
re.captures_iter(&content)
|
|
||||||
.flat_map(|cap| {
|
|
||||||
cap[1]
|
|
||||||
.split_whitespace()
|
|
||||||
.map(|host| {
|
|
||||||
log::debug!("found ssh host {host}");
|
|
||||||
MenuItem::new(
|
|
||||||
host.to_owned(),
|
|
||||||
Some("computer".to_owned()),
|
|
||||||
Some(format!("ssh {host}")),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
0.0,
|
|
||||||
Some(menu_item_data.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
gui::apply_sort(&mut items, order);
|
|
||||||
Self { elements: items }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> ItemProvider<T> for SshProvider<T> {
|
|
||||||
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
|
||||||
(false, self.elements.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
|
||||||
(false, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MathProvider<T: Clone> {
|
|
||||||
menu_item_data: T,
|
|
||||||
elements: Vec<MenuItem<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> MathProvider<T> {
|
|
||||||
fn new(menu_item_data: T) -> Self {
|
|
||||||
Self {
|
|
||||||
menu_item_data,
|
|
||||||
elements: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_elements(&mut self, elements: &mut Vec<MenuItem<T>>) {
|
|
||||||
self.elements.append(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> ItemProvider<T> for MathProvider<T> {
|
|
||||||
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
|
||||||
if let Some(search_text) = search {
|
|
||||||
let result = match meval::eval_str(search_text) {
|
|
||||||
Ok(result) => result.to_string(),
|
|
||||||
Err(e) => format!("failed to calculate {e:?}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let item = MenuItem::new(
|
|
||||||
result,
|
|
||||||
None,
|
|
||||||
search.map(String::from),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
0.0,
|
|
||||||
Some(self.menu_item_data.clone()),
|
|
||||||
);
|
|
||||||
let mut result = vec![item];
|
|
||||||
result.append(&mut self.elements.clone());
|
|
||||||
(true, result)
|
|
||||||
} else {
|
|
||||||
(false, self.elements.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
|
||||||
(false, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct EmojiProvider<T: Clone> {
|
|
||||||
elements: Vec<MenuItem<T>>,
|
|
||||||
#[allow(dead_code)] // needed for the detection of mode in 'auto'
|
|
||||||
menu_item_data: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> EmojiProvider<T> {
|
|
||||||
fn new(data: T, sort_order: &SortOrder) -> Self {
|
|
||||||
let emoji = emoji::search::search_annotation_all("");
|
|
||||||
let mut menus = emoji
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| {
|
|
||||||
MenuItem::new(
|
|
||||||
format!("{} — Category: {} — Name: {}", e.glyph, e.group, e.name),
|
|
||||||
None,
|
|
||||||
Some(format!("emoji {}", e.glyph)),
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
0.0,
|
|
||||||
Some(data.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
gui::apply_sort(&mut menus, sort_order);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
elements: menus,
|
|
||||||
menu_item_data: data.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> ItemProvider<T> for EmojiProvider<T> {
|
|
||||||
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
|
||||||
(false, self.elements.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
|
||||||
(false, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct DMenuProvider {
|
|
||||||
items: Vec<MenuItem<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DMenuProvider {
|
|
||||||
fn new(sort_order: &SortOrder) -> Result<DMenuProvider, Error> {
|
|
||||||
log::debug!("parsing stdin");
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin()
|
|
||||||
.read_to_string(&mut input)
|
|
||||||
.expect("Failed to read from stdin");
|
|
||||||
|
|
||||||
let mut items: Vec<MenuItem<String>> = input
|
|
||||||
.lines()
|
|
||||||
.rev()
|
|
||||||
.map(|s| MenuItem::new(s.to_string(), None, None, vec![], None, 0.0, None))
|
|
||||||
.collect();
|
|
||||||
log::debug!("parsed stdin");
|
|
||||||
gui::apply_sort(&mut items, sort_order);
|
|
||||||
Ok(Self { items })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemProvider<String> for DMenuProvider {
|
|
||||||
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) {
|
|
||||||
(false, self.items.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) {
|
|
||||||
(false, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum AutoRunType {
|
|
||||||
Math,
|
|
||||||
DRun,
|
|
||||||
File,
|
|
||||||
Ssh,
|
|
||||||
Emoji,
|
|
||||||
// WebSearch,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct AutoItemProvider {
|
|
||||||
drun: DRunProvider<AutoRunType>,
|
|
||||||
file: FileItemProvider<AutoRunType>,
|
|
||||||
math: MathProvider<AutoRunType>,
|
|
||||||
ssh: SshProvider<AutoRunType>,
|
|
||||||
emoji: EmojiProvider<AutoRunType>,
|
|
||||||
last_mode: Option<AutoRunType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AutoItemProvider {
|
|
||||||
fn new(config: &Config) -> Self {
|
|
||||||
AutoItemProvider {
|
|
||||||
drun: DRunProvider::new(AutoRunType::DRun, config.no_actions(), config.sort_order()),
|
|
||||||
file: FileItemProvider::new(AutoRunType::File, config.sort_order()),
|
|
||||||
math: MathProvider::new(AutoRunType::Math),
|
|
||||||
ssh: SshProvider::new(AutoRunType::Ssh, &config.sort_order()),
|
|
||||||
emoji: EmojiProvider::new(AutoRunType::Emoji, &config.sort_order()),
|
|
||||||
last_mode: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_auto_elements(
|
|
||||||
&mut self,
|
|
||||||
search_opt: Option<&str>,
|
|
||||||
) -> (bool, Vec<MenuItem<AutoRunType>>) {
|
|
||||||
// return ssh and drun items
|
|
||||||
let (changed, mut items) = self.drun.get_elements(search_opt);
|
|
||||||
items.append(&mut self.ssh.get_elements(search_opt).1);
|
|
||||||
if self.last_mode == Some(AutoRunType::DRun) {
|
|
||||||
(changed, items)
|
|
||||||
} else {
|
|
||||||
self.last_mode = Some(AutoRunType::DRun);
|
|
||||||
(true, items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemProvider<AutoRunType> for AutoItemProvider {
|
|
||||||
fn get_elements(&mut self, search_opt: Option<&str>) -> (bool, Vec<MenuItem<AutoRunType>>) {
|
|
||||||
let search = match search_opt {
|
|
||||||
Some(s) if !s.trim().is_empty() => s.trim(),
|
|
||||||
_ => return self.default_auto_elements(search_opt),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (mode, (changed, items)) =
|
|
||||||
if MathProvider::<AutoRunType>::contains_math_functions_or_starts_with_number(search) {
|
|
||||||
(AutoRunType::Math, self.math.get_elements(search_opt))
|
|
||||||
} else if search.starts_with('$') || search.starts_with('/') || search.starts_with('~')
|
|
||||||
{
|
|
||||||
(AutoRunType::File, self.file.get_elements(search_opt))
|
|
||||||
} else if search.starts_with("ssh") {
|
|
||||||
(AutoRunType::Ssh, self.ssh.get_elements(search_opt))
|
|
||||||
} else if search.starts_with("emoji") {
|
|
||||||
(AutoRunType::Emoji, self.emoji.get_elements(search_opt))
|
|
||||||
} else {
|
|
||||||
return self.default_auto_elements(search_opt);
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.last_mode.as_ref().is_some_and(|m| m == &mode) {
|
|
||||||
(changed, items)
|
|
||||||
} else {
|
|
||||||
self.last_mode = Some(mode);
|
|
||||||
(true, items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sub_elements(
|
|
||||||
&mut self,
|
|
||||||
item: &MenuItem<AutoRunType>,
|
|
||||||
) -> (bool, Option<Vec<MenuItem<AutoRunType>>>) {
|
|
||||||
let (changed, items) = self.get_elements(Some(item.label.as_ref()));
|
|
||||||
(changed, Some(items))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the drun mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Will return `Err` if it was not able to spawn the process
|
|
||||||
pub fn d_run(config: &Config) -> Result<(), Error> {
|
|
||||||
let provider = DRunProvider::new(0, config.no_actions(), config.sort_order());
|
|
||||||
let cache_path = provider.cache_path.clone();
|
|
||||||
let mut cache = provider.cache.clone();
|
|
||||||
|
|
||||||
// todo ues a arc instead of cloning the config
|
|
||||||
let selection_result = gui::show(config.clone(), provider, false, None, None);
|
|
||||||
match selection_result {
|
|
||||||
Ok(s) => update_drun_cache_and_run(cache_path, &mut cache, s.menu)?,
|
|
||||||
Err(_) => {
|
|
||||||
log::error!("No item selected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the run mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Will return `Err` if it was not able to spawn the process
|
|
||||||
pub fn run(config: &Config) -> Result<(), Error> {
|
|
||||||
let provider = RunProvider::new(config.sort_order());
|
|
||||||
let cache_path = provider.cache_path.clone();
|
|
||||||
let mut cache = provider.cache.clone();
|
|
||||||
|
|
||||||
let selection_result = gui::show(config.clone(), provider, false, None, None);
|
|
||||||
match selection_result {
|
|
||||||
Ok(s) => update_run_cache_and_run(cache_path, &mut cache, s.menu)?,
|
|
||||||
Err(_) => {
|
|
||||||
log::error!("No item selected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the auto mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Will return `Err`
|
|
||||||
/// * if it was not able to spawn the process
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// Panics if an internal static regex cannot be passed anymore, should never happen
|
|
||||||
pub fn auto(config: &Config) -> Result<(), Error> {
|
|
||||||
let mut provider = AutoItemProvider::new(config);
|
|
||||||
let cache_path = provider.drun.cache_path.clone();
|
|
||||||
let mut cache = provider.drun.cache.clone();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// todo ues a arc instead of cloning the config
|
|
||||||
let selection_result = gui::show(
|
|
||||||
config.clone(),
|
|
||||||
provider.clone(),
|
|
||||||
true,
|
|
||||||
Some(
|
|
||||||
vec!["ssh", "emoji", "^\\$\\w+"]
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| Regex::new(s).unwrap())
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Ok(selection_result) = selection_result {
|
|
||||||
let mut selection_result = selection_result.menu;
|
|
||||||
if let Some(data) = &selection_result.data {
|
|
||||||
match data {
|
|
||||||
AutoRunType::Math => {
|
|
||||||
provider.math.elements.push(selection_result);
|
|
||||||
}
|
|
||||||
AutoRunType::DRun => {
|
|
||||||
update_drun_cache_and_run(cache_path, &mut cache, selection_result)?;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
AutoRunType::File => {
|
|
||||||
if let Some(action) = selection_result.action {
|
|
||||||
spawn_fork(&action, selection_result.working_dir.as_ref())?;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
AutoRunType::Ssh => {
|
|
||||||
ssh_launch(&selection_result, config)?;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
AutoRunType::Emoji => {
|
|
||||||
if let Some(action) = selection_result.action {
|
|
||||||
copy_to_clipboard(action)?;
|
|
||||||
} else {
|
|
||||||
return Err(Error::MissingAction);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if selection_result.label.starts_with("ssh") {
|
|
||||||
selection_result.label = selection_result.label.chars().skip(4).collect();
|
|
||||||
ssh_launch(&selection_result, config)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::error!("No item selected");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the file browser mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Will return `Err`
|
|
||||||
/// * if it was not able to spawn the process
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// In case an internal regex does not parse anymore, this should never happen
|
|
||||||
pub fn file(config: &Config) -> Result<(), Error> {
|
|
||||||
let provider = FileItemProvider::new(0, config.sort_order());
|
|
||||||
|
|
||||||
// todo ues a arc instead of cloning the config
|
|
||||||
let selection_result = gui::show(
|
|
||||||
config.clone(),
|
|
||||||
provider,
|
|
||||||
false,
|
|
||||||
Some(vec![Regex::new("^\\$\\w+").unwrap()]),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
if let Some(action) = selection_result.menu.action {
|
|
||||||
spawn_fork(&action, selection_result.menu.working_dir.as_ref())
|
|
||||||
} else {
|
|
||||||
Err(Error::MissingAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ssh_launch<T: Clone>(menu_item: &MenuItem<T>, config: &Config) -> Result<(), Error> {
|
|
||||||
let ssh_cmd = if let Some(action) = &menu_item.action {
|
|
||||||
action.clone()
|
|
||||||
} else {
|
|
||||||
let cmd = config
|
|
||||||
.term()
|
|
||||||
.map(|s| format!("{s} ssh {}", menu_item.label));
|
|
||||||
if let Some(cmd) = cmd {
|
|
||||||
cmd
|
|
||||||
} else {
|
|
||||||
return Err(Error::MissingAction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd = format!(
|
|
||||||
"{} bash -c \"source ~/.bashrc; {ssh_cmd}\"",
|
|
||||||
config.term().unwrap_or_default()
|
|
||||||
);
|
|
||||||
spawn_fork(&cmd, menu_item.working_dir.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the ssh mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Will return `Err`
|
|
||||||
/// * if it was not able to spawn the process
|
|
||||||
/// * if it didn't find a terminal
|
|
||||||
pub fn ssh(config: &Config) -> Result<(), Error> {
|
|
||||||
let provider = SshProvider::new(0, &config.sort_order());
|
|
||||||
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
|
||||||
if let Ok(mi) = selection_result {
|
|
||||||
ssh_launch(&mi.menu, config)?;
|
|
||||||
} else {
|
|
||||||
log::error!("No item selected");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the math mode
|
|
||||||
pub fn math(config: &Config) {
|
|
||||||
let mut calc: Vec<MenuItem<String>> = vec![];
|
|
||||||
loop {
|
|
||||||
let mut provider = MathProvider::new(String::new());
|
|
||||||
provider.add_elements(&mut calc.clone());
|
|
||||||
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
|
||||||
if let Ok(mi) = selection_result {
|
|
||||||
calc.push(mi.menu);
|
|
||||||
} else {
|
|
||||||
log::error!("No item selected");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the emoji mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Forwards errors from the gui. See `gui::show` for details.
|
|
||||||
pub fn emoji(config: &Config) -> Result<(), Error> {
|
|
||||||
let provider = EmojiProvider::new(0, &config.sort_order());
|
|
||||||
let selection_result = gui::show(config.clone(), provider, true, None, None)?;
|
|
||||||
match selection_result.menu.action {
|
|
||||||
None => Err(Error::MissingAction),
|
|
||||||
Some(action) => copy_to_clipboard(action),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the dmenu mode
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Forwards errors from the gui. See `gui::show` for details.
|
|
||||||
pub fn dmenu(config: &Config) -> Result<(), Error> {
|
|
||||||
let provider = DMenuProvider::new(&config.sort_order())?;
|
|
||||||
|
|
||||||
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
|
||||||
match selection_result {
|
|
||||||
Ok(s) => {
|
|
||||||
println!("{}", s.menu.label);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(_) => Err(Error::InvalidSelection),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Error::MissingAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_run_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 run cache {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(action) = selection_result.action {
|
|
||||||
let program = CString::new(action).unwrap();
|
|
||||||
let args = [program.clone()];
|
|
||||||
|
|
||||||
// This replaces the current process image
|
|
||||||
nix::unistd::execvp(&program, &args).map_err(|e| Error::RunFailed(e.to_string()))?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::MissingAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_d_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) {
|
|
||||||
let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun"));
|
|
||||||
load_cache(cache_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) {
|
|
||||||
let cache_path = dirs::cache_dir().map(|x| x.join("worf-run"));
|
|
||||||
load_cache(cache_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_cache(cache_path: Option<PathBuf>) -> (Option<PathBuf>, HashMap<String, i64>) {
|
|
||||||
let cache = {
|
|
||||||
if let Some(ref cache_path) = cache_path {
|
|
||||||
if let Err(e) = create_file_if_not_exists(cache_path) {
|
|
||||||
log::warn!("No drun cache file and cannot create: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load_cache_file(cache_path.as_ref()).unwrap_or_default()
|
|
||||||
};
|
|
||||||
(cache_path, cache)
|
|
||||||
}
|
|
182
worf/src/lib/modes/auto.rs
Normal file
182
worf/src/lib/modes/auto.rs
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
use regex::Regex;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::desktop::{copy_to_clipboard, spawn_fork};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
use crate::modes::math::MathProvider;
|
||||||
|
use crate::modes::drun::{update_drun_cache_and_run, DRunProvider};
|
||||||
|
use crate::modes::emoji::EmojiProvider;
|
||||||
|
use crate::modes::file::FileItemProvider;
|
||||||
|
use crate::modes::ssh;
|
||||||
|
use crate::modes::ssh::SshProvider;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
enum AutoRunType {
|
||||||
|
Math,
|
||||||
|
DRun,
|
||||||
|
File,
|
||||||
|
Ssh,
|
||||||
|
Emoji,
|
||||||
|
// WebSearch,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AutoItemProvider {
|
||||||
|
drun: DRunProvider<AutoRunType>,
|
||||||
|
file: FileItemProvider<AutoRunType>,
|
||||||
|
math: MathProvider<AutoRunType>,
|
||||||
|
ssh: SshProvider<AutoRunType>,
|
||||||
|
emoji: EmojiProvider<AutoRunType>,
|
||||||
|
last_mode: Option<AutoRunType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoItemProvider {
|
||||||
|
fn new(config: &Config) -> Self {
|
||||||
|
AutoItemProvider {
|
||||||
|
drun: DRunProvider::new(AutoRunType::DRun, config.no_actions(), config.sort_order()),
|
||||||
|
file: FileItemProvider::new(AutoRunType::File, config.sort_order()),
|
||||||
|
math: MathProvider::new(AutoRunType::Math),
|
||||||
|
ssh: SshProvider::new(AutoRunType::Ssh, &config.sort_order()),
|
||||||
|
emoji: EmojiProvider::new(AutoRunType::Emoji, &config.sort_order()),
|
||||||
|
last_mode: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_auto_elements(
|
||||||
|
&mut self,
|
||||||
|
search_opt: Option<&str>,
|
||||||
|
) -> (bool, Vec<MenuItem<AutoRunType>>) {
|
||||||
|
// return ssh and drun items
|
||||||
|
let (changed, mut items) = self.drun.get_elements(search_opt);
|
||||||
|
items.append(&mut self.ssh.get_elements(search_opt).1);
|
||||||
|
if self.last_mode == Some(AutoRunType::DRun) {
|
||||||
|
(changed, items)
|
||||||
|
} else {
|
||||||
|
self.last_mode = Some(AutoRunType::DRun);
|
||||||
|
(true, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) -> (bool, Vec<MenuItem<AutoRunType>>) {
|
||||||
|
let search = match search_opt {
|
||||||
|
Some(s) if !s.trim().is_empty() => s.trim(),
|
||||||
|
_ => return self.default_auto_elements(search_opt),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mode, (changed, items)) =
|
||||||
|
if contains_math_functions_or_starts_with_number(search) {
|
||||||
|
(AutoRunType::Math, self.math.get_elements(search_opt))
|
||||||
|
} else if search.starts_with('$') || search.starts_with('/') || search.starts_with('~')
|
||||||
|
{
|
||||||
|
(AutoRunType::File, self.file.get_elements(search_opt))
|
||||||
|
} else if search.starts_with("ssh") {
|
||||||
|
(AutoRunType::Ssh, self.ssh.get_elements(search_opt))
|
||||||
|
} else if search.starts_with("emoji") {
|
||||||
|
(AutoRunType::Emoji, self.emoji.get_elements(search_opt))
|
||||||
|
} else {
|
||||||
|
return self.default_auto_elements(search_opt);
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.last_mode.as_ref().is_some_and(|m| m == &mode) {
|
||||||
|
(changed, items)
|
||||||
|
} else {
|
||||||
|
self.last_mode = Some(mode);
|
||||||
|
(true, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(
|
||||||
|
&mut self,
|
||||||
|
item: &MenuItem<AutoRunType>,
|
||||||
|
) -> (bool, Option<Vec<MenuItem<AutoRunType>>>) {
|
||||||
|
let (changed, items) = self.get_elements(Some(item.label.as_ref()));
|
||||||
|
(changed, Some(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Shows the auto mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return `Err`
|
||||||
|
/// * if it was not able to spawn the process
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if an internal static regex cannot be passed anymore, should never happen
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let mut provider = AutoItemProvider::new(config);
|
||||||
|
let cache_path = provider.drun.cache_path.clone();
|
||||||
|
let mut cache = provider.drun.cache.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// todo ues a arc instead of cloning the config
|
||||||
|
let selection_result = gui::show(
|
||||||
|
config.clone(),
|
||||||
|
provider.clone(),
|
||||||
|
true,
|
||||||
|
Some(
|
||||||
|
vec!["ssh", "emoji", "^\\$\\w+"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| Regex::new(s).unwrap())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(selection_result) = selection_result {
|
||||||
|
let mut selection_result = selection_result.menu;
|
||||||
|
if let Some(data) = &selection_result.data {
|
||||||
|
match data {
|
||||||
|
AutoRunType::Math => {
|
||||||
|
provider.math.elements.push(selection_result);
|
||||||
|
}
|
||||||
|
AutoRunType::DRun => {
|
||||||
|
update_drun_cache_and_run(cache_path, &mut cache, selection_result)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AutoRunType::File => {
|
||||||
|
if let Some(action) = selection_result.action {
|
||||||
|
spawn_fork(&action, selection_result.working_dir.as_ref())?;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AutoRunType::Ssh => {
|
||||||
|
ssh::launch(&selection_result, config)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AutoRunType::Emoji => {
|
||||||
|
if let Some(action) = selection_result.action {
|
||||||
|
copy_to_clipboard(action)?;
|
||||||
|
} else {
|
||||||
|
return Err(Error::MissingAction);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if selection_result.label.starts_with("ssh") {
|
||||||
|
selection_result.label = selection_result.label.chars().skip(4).collect();
|
||||||
|
ssh::launch(&selection_result, config)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("No item selected");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
56
worf/src/lib/modes/dmenu.rs
Normal file
56
worf/src/lib/modes/dmenu.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use std::io;
|
||||||
|
use std::io::Read;
|
||||||
|
use crate::config::{Config, SortOrder};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct DMenuProvider {
|
||||||
|
items: Vec<MenuItem<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DMenuProvider {
|
||||||
|
fn new(sort_order: &SortOrder) -> DMenuProvider {
|
||||||
|
log::debug!("parsing stdin");
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_to_string(&mut input)
|
||||||
|
.expect("Failed to read from stdin");
|
||||||
|
|
||||||
|
let mut items: Vec<MenuItem<String>> = input
|
||||||
|
.lines()
|
||||||
|
.rev()
|
||||||
|
.map(|s| MenuItem::new(s.to_string(), None, None, vec![], None, 0.0, None))
|
||||||
|
.collect();
|
||||||
|
log::debug!("parsed stdin");
|
||||||
|
gui::apply_sort(&mut items, sort_order);
|
||||||
|
Self { items }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ItemProvider<String> for DMenuProvider {
|
||||||
|
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) {
|
||||||
|
(false, self.items.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) {
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Shows the dmenu mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Forwards errors from the gui. See `gui::show` for details.
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let provider = DMenuProvider::new(&config.sort_order());
|
||||||
|
|
||||||
|
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
||||||
|
match selection_result {
|
||||||
|
Ok(s) => {
|
||||||
|
println!("{}", s.menu.label);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(_) => Err(Error::InvalidSelection),
|
||||||
|
}
|
||||||
|
}
|
203
worf/src/lib/modes/drun.rs
Normal file
203
worf/src/lib/modes/drun.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
use freedesktop_file_parser::EntryType;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::config::{Config, SortOrder};
|
||||||
|
use crate::desktop::{find_desktop_files, get_locale_variants, lookup_name_with_locale, save_cache_file, spawn_fork};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
use crate::modes::load_cache;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
struct DRunCache {
|
||||||
|
desktop_entry: String,
|
||||||
|
run_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct DRunProvider<T: Clone> {
|
||||||
|
items: Option<Vec<MenuItem<T>>>,
|
||||||
|
pub(crate) cache_path: Option<PathBuf>,
|
||||||
|
pub(crate) cache: HashMap<String, i64>,
|
||||||
|
data: T,
|
||||||
|
no_actions: bool,
|
||||||
|
sort_order: SortOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Send + Sync> ItemProvider<T> for DRunProvider<T> {
|
||||||
|
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
||||||
|
if self.items.is_none() {
|
||||||
|
self.items = Some(self.load().clone());
|
||||||
|
}
|
||||||
|
(false, self.items.clone().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Send + Sync> DRunProvider<T> {
|
||||||
|
pub(crate) fn new(menu_item_data: T, no_actions: bool, sort_order: SortOrder) -> Self {
|
||||||
|
let (cache_path, d_run_cache) = load_d_run_cache();
|
||||||
|
DRunProvider {
|
||||||
|
items: None,
|
||||||
|
cache_path,
|
||||||
|
cache: d_run_cache,
|
||||||
|
data: menu_item_data,
|
||||||
|
no_actions,
|
||||||
|
sort_order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn load(&self) -> Vec<MenuItem<T>> {
|
||||||
|
let locale_variants = get_locale_variants();
|
||||||
|
let default_icon = "application-x-executable".to_string();
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
let entries: Vec<MenuItem<T>> = find_desktop_files()
|
||||||
|
.into_par_iter()
|
||||||
|
.filter(|file| {
|
||||||
|
!file.entry.no_display.unwrap_or(false)
|
||||||
|
&& !file.entry.hidden.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.filter_map(|file| {
|
||||||
|
let name = lookup_name_with_locale(
|
||||||
|
&locale_variants,
|
||||||
|
&file.entry.name.variants,
|
||||||
|
&file.entry.name.default,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (action, working_dir) = match &file.entry.entry_type {
|
||||||
|
EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
|
||||||
|
_ => return 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");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon = file
|
||||||
|
.entry
|
||||||
|
.icon
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.content.clone())
|
||||||
|
.or(Some(default_icon.clone()));
|
||||||
|
|
||||||
|
let sort_score = *self.cache.get(&name).unwrap_or(&0) as f64;
|
||||||
|
|
||||||
|
let mut entry = MenuItem::new(
|
||||||
|
name.clone(),
|
||||||
|
icon.clone(),
|
||||||
|
action.clone(),
|
||||||
|
Vec::new(),
|
||||||
|
working_dir.clone(),
|
||||||
|
sort_score,
|
||||||
|
Some(self.data.clone()),
|
||||||
|
);
|
||||||
|
if !self.no_actions {
|
||||||
|
for action in file.actions.values() {
|
||||||
|
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())
|
||||||
|
.unwrap_or("application-x-executable".to_string());
|
||||||
|
|
||||||
|
|
||||||
|
entry.sub_elements.push(MenuItem::new(
|
||||||
|
action_name,
|
||||||
|
Some(action_icon),
|
||||||
|
action.exec.clone(),
|
||||||
|
Vec::new(),
|
||||||
|
working_dir.clone(),
|
||||||
|
0.0,
|
||||||
|
Some(self.data.clone()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(entry)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut seen_actions = HashSet::new();
|
||||||
|
let mut entries: Vec<MenuItem<T>> = entries
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entry| seen_actions.insert(entry.action.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"parsing desktop files took {}ms",
|
||||||
|
start.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
|
||||||
|
gui::apply_sort(&mut entries, &self.sort_order);
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_d_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) {
|
||||||
|
let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun"));
|
||||||
|
load_cache(cache_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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(Error::MissingAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the drun mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return `Err` if it was not able to spawn the process
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let provider = DRunProvider::new(0, config.no_actions(), config.sort_order());
|
||||||
|
let cache_path = provider.cache_path.clone();
|
||||||
|
let mut cache = provider.cache.clone();
|
||||||
|
|
||||||
|
// todo ues a arc instead of cloning the config
|
||||||
|
let selection_result = gui::show(config.clone(), provider, false, None, None);
|
||||||
|
match selection_result {
|
||||||
|
Ok(s) => update_drun_cache_and_run(cache_path, &mut cache, s.menu)?,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("No item selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
60
worf/src/lib/modes/emoji.rs
Normal file
60
worf/src/lib/modes/emoji.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use crate::config::{Config, SortOrder};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::desktop::copy_to_clipboard;
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct EmojiProvider<T: Clone> {
|
||||||
|
elements: Vec<MenuItem<T>>,
|
||||||
|
#[allow(dead_code)] // needed for the detection of mode in 'auto'
|
||||||
|
menu_item_data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> EmojiProvider<T> {
|
||||||
|
pub(crate) fn new(data: T, sort_order: &SortOrder) -> Self {
|
||||||
|
let emoji = emoji::search::search_annotation_all("");
|
||||||
|
let mut menus = emoji
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| {
|
||||||
|
MenuItem::new(
|
||||||
|
format!("{} — Category: {} — Name: {}", e.glyph, e.group, e.name),
|
||||||
|
None,
|
||||||
|
Some(format!("emoji {}", e.glyph)),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
Some(data.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
gui::apply_sort(&mut menus, sort_order);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
elements: menus,
|
||||||
|
menu_item_data: data.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> ItemProvider<T> for EmojiProvider<T> {
|
||||||
|
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
||||||
|
(false, self.elements.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the emoji mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Forwards errors from the gui. See `gui::show` for details.
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let provider = EmojiProvider::new(0, &config.sort_order());
|
||||||
|
let selection_result = gui::show(config.clone(), provider, true, None, None)?;
|
||||||
|
match selection_result.menu.action {
|
||||||
|
None => Err(Error::MissingAction),
|
||||||
|
Some(action) => copy_to_clipboard(action),
|
||||||
|
}
|
||||||
|
}
|
214
worf/src/lib/modes/file.rs
Normal file
214
worf/src/lib/modes/file.rs
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::FileTypeExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use regex::Regex;
|
||||||
|
use crate::config::{expand_path, Config, SortOrder};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::desktop::spawn_fork;
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct FileItemProvider<T: Clone> {
|
||||||
|
last_result: Option<Vec<MenuItem<T>>>,
|
||||||
|
menu_item_data: T,
|
||||||
|
sort_order: SortOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> FileItemProvider<T> {
|
||||||
|
pub(crate) fn new(menu_item_data: T, sort_order: SortOrder) -> Self {
|
||||||
|
FileItemProvider {
|
||||||
|
last_result: None,
|
||||||
|
menu_item_data,
|
||||||
|
sort_order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_icon_for_name(path: &Path) -> String {
|
||||||
|
let type_result = fs::symlink_metadata(path)
|
||||||
|
.map(|meta| meta.file_type())
|
||||||
|
.map(|file_type| {
|
||||||
|
if file_type.is_symlink() {
|
||||||
|
Some("edit-redo")
|
||||||
|
} else if file_type.is_char_device() {
|
||||||
|
Some("input-keyboard")
|
||||||
|
} else if file_type.is_block_device() {
|
||||||
|
Some("drive-harddisk")
|
||||||
|
} else if file_type.is_socket() {
|
||||||
|
Some("network-transmit-receive")
|
||||||
|
} else if file_type.is_fifo() {
|
||||||
|
Some("rotation-allowed")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(Some("system-lock-screen"));
|
||||||
|
|
||||||
|
if let Some(tr) = type_result {
|
||||||
|
return tr.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(mime) = tree_magic_mini::from_filepath(path) else {
|
||||||
|
return "image-not-found".to_string();
|
||||||
|
};
|
||||||
|
|
||||||
|
if mime.starts_with("image") {
|
||||||
|
return "image-x-generic".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if mime.starts_with("inode") {
|
||||||
|
return mime.replace('/', "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
if mime.starts_with("text") {
|
||||||
|
return if mime.contains("plain") {
|
||||||
|
"text-x-generic".to_string()
|
||||||
|
} else if mime.contains("python") {
|
||||||
|
"text-x-script".to_string()
|
||||||
|
} else if mime.contains("html") {
|
||||||
|
"text-html".to_string()
|
||||||
|
} else {
|
||||||
|
"text-x-generic".to_string()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if mime.starts_with("application") {
|
||||||
|
return if mime.contains("octet") {
|
||||||
|
"application-x-executable".to_string()
|
||||||
|
} else if mime.contains("tar")
|
||||||
|
|| mime.contains("lz")
|
||||||
|
|| mime.contains("zip")
|
||||||
|
|| mime.contains("7z")
|
||||||
|
|| mime.contains("xz")
|
||||||
|
{
|
||||||
|
"package-x-generic".to_string()
|
||||||
|
} else {
|
||||||
|
"text-html".to_string()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("unsupported mime type {mime}");
|
||||||
|
"application-x-generic".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
|
||||||
|
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
||||||
|
let default_path = if let Some(home) = dirs::home_dir() {
|
||||||
|
home.display().to_string()
|
||||||
|
} else {
|
||||||
|
"/".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut trimmed_search = search.unwrap_or(&default_path).to_owned();
|
||||||
|
if !trimmed_search.starts_with('/')
|
||||||
|
&& !trimmed_search.starts_with('~')
|
||||||
|
&& !trimmed_search.starts_with('$')
|
||||||
|
{
|
||||||
|
trimmed_search = format!("{default_path}/{trimmed_search}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = expand_path(&trimmed_search);
|
||||||
|
let mut items: Vec<MenuItem<T>> = Vec::new();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
if let Some(last) = &self.last_result {
|
||||||
|
return (false, last.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
items.push(MenuItem::new(
|
||||||
|
trimmed_search.clone(),
|
||||||
|
Some(FileItemProvider::<T>::resolve_icon_for_name(&path)),
|
||||||
|
Some(format!("xdg-open {}", path.display())),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
100.0,
|
||||||
|
Some(self.menu_item_data.clone()),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Ok(entries) = path.read_dir() {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if let Some(mut path_str) =
|
||||||
|
entry.path().to_str().map(std::string::ToString::to_string)
|
||||||
|
{
|
||||||
|
if trimmed_search.starts_with('~') {
|
||||||
|
if let Some(home_dir) = dirs::home_dir() {
|
||||||
|
if let Some(home_str) = home_dir.to_str() {
|
||||||
|
path_str = path_str.replace(home_str, "~");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
path_str.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(MenuItem::new(
|
||||||
|
path_str.clone(),
|
||||||
|
Some(FileItemProvider::<T>::resolve_icon_for_name(&entry.path())),
|
||||||
|
Some(format!("xdg-open {path_str}")),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
Some(self.menu_item_data.clone()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
MenuItem::new(
|
||||||
|
trimmed_search.clone(),
|
||||||
|
Some(FileItemProvider::<T>::resolve_icon_for_name(
|
||||||
|
&PathBuf::from(&trimmed_search),
|
||||||
|
)),
|
||||||
|
Some(format!("xdg-open {trimmed_search}")),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
Some(self.menu_item_data.clone()),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
gui::apply_sort(&mut items, &self.sort_order);
|
||||||
|
|
||||||
|
self.last_result = Some(items.clone());
|
||||||
|
(true, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
||||||
|
(false, self.last_result.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Shows the file browser mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return `Err`
|
||||||
|
/// * if it was not able to spawn the process
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// In case an internal regex does not parse anymore, this should never happen
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let provider = FileItemProvider::new(0, config.sort_order());
|
||||||
|
|
||||||
|
// todo ues a arc instead of cloning the config
|
||||||
|
let selection_result = gui::show(
|
||||||
|
config.clone(),
|
||||||
|
provider,
|
||||||
|
false,
|
||||||
|
Some(vec![Regex::new("^\\$\\w+").unwrap()]),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
if let Some(action) = selection_result.menu.action {
|
||||||
|
spawn_fork(&action, selection_result.menu.working_dir.as_ref())
|
||||||
|
} else {
|
||||||
|
Err(Error::MissingAction)
|
||||||
|
}
|
||||||
|
}
|
67
worf/src/lib/modes/math.rs
Normal file
67
worf/src/lib/modes/math.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::gui;
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct MathProvider<T: Clone> {
|
||||||
|
menu_item_data: T,
|
||||||
|
pub(crate) elements: Vec<MenuItem<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> MathProvider<T> {
|
||||||
|
pub(crate) fn new(menu_item_data: T) -> Self {
|
||||||
|
Self {
|
||||||
|
menu_item_data,
|
||||||
|
elements: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn add_elements(&mut self, elements: &mut Vec<MenuItem<T>>) {
|
||||||
|
self.elements.append(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> ItemProvider<T> for MathProvider<T> {
|
||||||
|
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
||||||
|
if let Some(search_text) = search {
|
||||||
|
let result = match meval::eval_str(search_text) {
|
||||||
|
Ok(result) => result.to_string(),
|
||||||
|
Err(e) => format!("failed to calculate {e:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = MenuItem::new(
|
||||||
|
result,
|
||||||
|
None,
|
||||||
|
search.map(String::from),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
Some(self.menu_item_data.clone()),
|
||||||
|
);
|
||||||
|
let mut result = vec![item];
|
||||||
|
result.append(&mut self.elements.clone());
|
||||||
|
(true, result)
|
||||||
|
} else {
|
||||||
|
(false, self.elements.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the math mode
|
||||||
|
pub fn show(config: &Config) {
|
||||||
|
let mut calc: Vec<MenuItem<String>> = vec![];
|
||||||
|
loop {
|
||||||
|
let mut provider = MathProvider::new(String::new());
|
||||||
|
provider.add_elements(&mut calc.clone());
|
||||||
|
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
||||||
|
if let Ok(mi) = selection_result {
|
||||||
|
calc.push(mi.menu);
|
||||||
|
} else {
|
||||||
|
log::error!("No item selected");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
worf/src/lib/modes/mod.rs
Normal file
26
worf/src/lib/modes/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::desktop::{create_file_if_not_exists, load_cache_file};
|
||||||
|
|
||||||
|
pub mod dmenu;
|
||||||
|
pub mod file;
|
||||||
|
pub mod math;
|
||||||
|
pub mod auto;
|
||||||
|
pub mod drun;
|
||||||
|
pub mod run;
|
||||||
|
pub mod ssh;
|
||||||
|
pub mod emoji;
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) fn load_cache(cache_path: Option<PathBuf>) -> (Option<PathBuf>, HashMap<String, i64>) {
|
||||||
|
let cache = {
|
||||||
|
if let Some(ref cache_path) = cache_path {
|
||||||
|
if let Err(e) = create_file_if_not_exists(cache_path) {
|
||||||
|
log::warn!("No drun cache file and cannot create: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load_cache_file(cache_path.as_ref()).unwrap_or_default()
|
||||||
|
};
|
||||||
|
(cache_path, cache)
|
||||||
|
}
|
144
worf/src/lib/modes/run.rs
Normal file
144
worf/src/lib/modes/run.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::{env, fs};
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::config::{Config, SortOrder};
|
||||||
|
use crate::desktop::{is_executable, save_cache_file};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
use crate::modes::load_cache;
|
||||||
|
|
||||||
|
impl ItemProvider<i32> for RunProvider {
|
||||||
|
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<i32>>) {
|
||||||
|
if self.items.is_none() {
|
||||||
|
self.items = Some(self.load().clone());
|
||||||
|
}
|
||||||
|
(false, self.items.clone().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<i32>) -> (bool, Option<Vec<MenuItem<i32>>>) {
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RunProvider {
|
||||||
|
items: Option<Vec<MenuItem<i32>>>,
|
||||||
|
cache_path: Option<PathBuf>,
|
||||||
|
cache: HashMap<String, i64>,
|
||||||
|
sort_order: SortOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunProvider {
|
||||||
|
fn new(sort_order: SortOrder) -> Self {
|
||||||
|
let (cache_path, d_run_cache) = load_run_cache();
|
||||||
|
RunProvider {
|
||||||
|
items: None,
|
||||||
|
cache_path,
|
||||||
|
cache: d_run_cache,
|
||||||
|
sort_order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn load(&self) -> Vec<MenuItem<i32>> {
|
||||||
|
let path_var = env::var("PATH").unwrap_or_default();
|
||||||
|
let paths = env::split_paths(&path_var);
|
||||||
|
|
||||||
|
let entries: Vec<_> = paths
|
||||||
|
.filter(|dir| dir.is_dir())
|
||||||
|
.flat_map(|dir| {
|
||||||
|
fs::read_dir(dir)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let path = entry.path();
|
||||||
|
if !is_executable(&path) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = path.file_name()?.to_str()?.to_string();
|
||||||
|
let sort_score = *self.cache.get(&label).unwrap_or(&0) as f64;
|
||||||
|
|
||||||
|
Some(MenuItem::new(
|
||||||
|
label,
|
||||||
|
None,
|
||||||
|
path.to_str().map(ToString::to_string),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
sort_score,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut seen_actions = HashSet::new();
|
||||||
|
let mut entries: Vec<MenuItem<i32>> = entries
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entry| {
|
||||||
|
entry
|
||||||
|
.action
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|action| action.split('/').next_back())
|
||||||
|
.is_some_and(|cmd| seen_actions.insert(cmd.to_string()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
gui::apply_sort(&mut entries, &self.sort_order);
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn load_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) {
|
||||||
|
let cache_path = dirs::cache_dir().map(|x| x.join("worf-run"));
|
||||||
|
load_cache(cache_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_run_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 run cache {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(action) = selection_result.action {
|
||||||
|
let program = CString::new(action).unwrap();
|
||||||
|
let args = [program.clone()];
|
||||||
|
|
||||||
|
// This replaces the current process image
|
||||||
|
nix::unistd::execvp(&program, &args).map_err(|e| Error::RunFailed(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::MissingAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Shows the run mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return `Err` if it was not able to spawn the process
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let provider = RunProvider::new(config.sort_order());
|
||||||
|
let cache_path = provider.cache_path.clone();
|
||||||
|
let mut cache = provider.cache.clone();
|
||||||
|
|
||||||
|
let selection_result = gui::show(config.clone(), provider, false, None, None);
|
||||||
|
match selection_result {
|
||||||
|
Ok(s) => update_run_cache_and_run(cache_path, &mut cache, s.menu)?,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("No item selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
95
worf/src/lib/modes/ssh.rs
Normal file
95
worf/src/lib/modes/ssh.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use std::fs;
|
||||||
|
use regex::Regex;
|
||||||
|
use crate::config::{Config, SortOrder};
|
||||||
|
use crate::{gui, Error};
|
||||||
|
use crate::desktop::spawn_fork;
|
||||||
|
use crate::gui::{ItemProvider, MenuItem};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct SshProvider<T: Clone> {
|
||||||
|
elements: Vec<MenuItem<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> SshProvider<T> {
|
||||||
|
pub(crate) fn new(menu_item_data: T, order: &SortOrder) -> Self {
|
||||||
|
let re = Regex::new(r"(?m)^\s*Host\s+(.+)$").unwrap();
|
||||||
|
let mut items: Vec<_> = dirs::home_dir()
|
||||||
|
.map(|home| home.join(".ssh").join("config"))
|
||||||
|
.filter(|path| path.exists())
|
||||||
|
.map(|path| fs::read_to_string(&path).unwrap_or_default())
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|content| {
|
||||||
|
re.captures_iter(&content)
|
||||||
|
.flat_map(|cap| {
|
||||||
|
cap[1]
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|host| {
|
||||||
|
log::debug!("found ssh host {host}");
|
||||||
|
MenuItem::new(
|
||||||
|
host.to_owned(),
|
||||||
|
Some("computer".to_owned()),
|
||||||
|
Some(format!("ssh {host}")),
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
Some(menu_item_data.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
gui::apply_sort(&mut items, order);
|
||||||
|
Self { elements: items }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> ItemProvider<T> for SshProvider<T> {
|
||||||
|
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
|
||||||
|
(false, self.elements.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn launch<T: Clone>(menu_item: &MenuItem<T>, config: &Config) -> Result<(), Error> {
|
||||||
|
let ssh_cmd = if let Some(action) = &menu_item.action {
|
||||||
|
action.clone()
|
||||||
|
} else {
|
||||||
|
let cmd = config
|
||||||
|
.term()
|
||||||
|
.map(|s| format!("{s} ssh {}", menu_item.label));
|
||||||
|
if let Some(cmd) = cmd {
|
||||||
|
cmd
|
||||||
|
} else {
|
||||||
|
return Err(Error::MissingAction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmd = format!(
|
||||||
|
"{} bash -c \"source ~/.bashrc; {ssh_cmd}\"",
|
||||||
|
config.term().unwrap_or_default()
|
||||||
|
);
|
||||||
|
spawn_fork(&cmd, menu_item.working_dir.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the ssh mode
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return `Err`
|
||||||
|
/// * if it was not able to spawn the process
|
||||||
|
/// * if it didn't find a terminal
|
||||||
|
pub fn show(config: &Config) -> Result<(), Error> {
|
||||||
|
let provider = SshProvider::new(0, &config.sort_order());
|
||||||
|
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
||||||
|
if let Ok(mi) = selection_result {
|
||||||
|
launch(&mi.menu, config)?;
|
||||||
|
} else {
|
||||||
|
log::error!("No item selected");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ use std::env;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use worf_lib::config::Mode;
|
use worf_lib::config::Mode;
|
||||||
use worf_lib::{Error, config, mode};
|
use worf_lib::{Error, config, mode, modes};
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
env_logger::Builder::new()
|
env_logger::Builder::new()
|
||||||
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned()))
|
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned()))
|
||||||
|
@ -14,17 +14,17 @@ fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
if let Some(show) = &config.show() {
|
if let Some(show) = &config.show() {
|
||||||
let result = match show {
|
let result = match show {
|
||||||
Mode::Run => mode::run(&config),
|
Mode::Run => modes::run::show(&config),
|
||||||
Mode::Drun => mode::d_run(&config),
|
Mode::Drun => modes::drun::show(&config),
|
||||||
Mode::Dmenu => mode::dmenu(&config),
|
Mode::Dmenu => modes::dmenu::show(&config),
|
||||||
Mode::File => mode::file(&config),
|
Mode::File => modes::file::show(&config),
|
||||||
Mode::Math => {
|
Mode::Math => {
|
||||||
mode::math(&config);
|
modes::math::show(&config);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Mode::Ssh => mode::ssh(&config),
|
Mode::Ssh => modes::ssh::show(&config),
|
||||||
Mode::Emoji => mode::emoji(&config),
|
Mode::Emoji => modes::emoji::show(&config),
|
||||||
Mode::Auto => mode::auto(&config),
|
Mode::Auto => modes::auto::show(&config),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
|
|
Loading…
Add table
Reference in a new issue