Added Kidex plugin for file search.

This commit is contained in:
Kirottu
2023-04-05 21:00:13 +03:00
parent d48a48bd31
commit b6f8bc631d
7 changed files with 313 additions and 67 deletions
Cargo.lockCargo.tomlREADME.md
anyrun-interface/src
anyrun/src
plugins/kidex

40
Cargo.lock generated

@ -978,6 +978,26 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kidex"
version = "0.1.0"
dependencies = [
"abi_stable",
"anyrun-plugin",
"fuzzy-matcher",
"kidex-common",
"open",
]
[[package]]
name = "kidex-common"
version = "0.1.0"
source = "git+https://github.com/Kirottu/kidex#afcbf54b37dc47b914e0c8f873cb1f11c69674a7"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -1214,6 +1234,16 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "open"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8"
dependencies = [
"pathdiff",
"windows-sys 0.42.0",
]
[[package]]
name = "openssl"
version = "0.10.45"
@ -1333,6 +1363,12 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "percent-encoding"
version = "2.2.0"
@ -1660,9 +1696,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.91"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"itoa",
"ryu",

@ -7,5 +7,6 @@ members = [
"plugins/symbols",
"plugins/rink",
"plugins/shell",
"plugins/kidex",
"plugins/translate",
]

@ -76,6 +76,8 @@ Anyrun requires plugins to function, as they provide the results for input. The
- Calculator & unit conversion
- [Shell](plugins/shell)
- Run shell commands
- [Kidex](plugins/kidex)
- File search provided by [Kidex](https://github.com/Kirottu/kidex)
## Configuration

@ -50,7 +50,8 @@ pub enum HandleResult {
/// Shut down the program
Close,
/// Refresh the items. Useful if the runner wants to alter results in place.
Refresh,
/// The inner value can set an exclusive mode for the plugin.
Refresh(bool),
/// Copy the content, due to how copying works it must be done like this.
Copy(RVec<u8>),
}

@ -1,4 +1,4 @@
use std::{cell::RefCell, env, fs, path::PathBuf, rc::Rc, time::Duration};
use std::{cell::RefCell, env, fs, mem, path::PathBuf, rc::Rc, time::Duration};
use abi_stable::std_types::{ROption, RVec};
use anyrun_interface::{HandleResult, Match, PluginInfo, PluginRef, PollResult};
@ -42,6 +42,9 @@ enum PostRunAction {
/// Some data that needs to be shared between various parts
struct RuntimeData {
args: Args,
/// A plugin may request exclusivity which is set with this
exclusive: Option<PluginView>,
plugins: Vec<PluginView>,
post_run_action: PostRunAction,
}
@ -100,6 +103,8 @@ fn main() {
override_plugins,
config_dir,
},
exclusive: None,
plugins: Vec::new(),
post_run_action: PostRunAction::None,
});
-1 // Magic GTK number to continue running
@ -216,61 +221,60 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
.build();
// Load plugins from the paths specified in the config file
let plugins = Rc::new(
plugins
.iter()
.map(|plugin_path| {
let mut user_path = PathBuf::from(&format!("{}/plugins", config_dir));
let mut global_path = PathBuf::from("/etc/anyrun/plugins");
user_path.extend(plugin_path.iter());
global_path.extend(plugin_path.iter());
runtime_data.borrow_mut().as_mut().unwrap().plugins = plugins
.iter()
.map(|plugin_path| {
// Load the plugin's dynamic library.
let mut user_path = PathBuf::from(&format!("{}/plugins", config_dir));
let mut global_path = PathBuf::from("/etc/anyrun/plugins");
user_path.extend(plugin_path.iter());
global_path.extend(plugin_path.iter());
// Load the plugin's dynamic library.
let plugin = if plugin_path.is_absolute() {
abi_stable::library::lib_header_from_path(plugin_path)
} else if user_path.exists() {
abi_stable::library::lib_header_from_path(&user_path)
} else {
abi_stable::library::lib_header_from_path(&global_path)
}
.and_then(|plugin| plugin.init_root_module::<PluginRef>())
.expect("Failed to load plugin");
// Load the plugin's dynamic library.
let plugin = if plugin_path.is_absolute() {
abi_stable::library::lib_header_from_path(plugin_path)
} else if user_path.exists() {
abi_stable::library::lib_header_from_path(&user_path)
} else {
abi_stable::library::lib_header_from_path(&global_path)
}
.and_then(|plugin| plugin.init_root_module::<PluginRef>())
.expect("Failed to load plugin");
// Run the plugin's init code to init static resources etc.
plugin.init()(config_dir.clone().into());
// Run the plugin's init code to init static resources etc.
plugin.init()(config_dir.clone().into());
let plugin_box = gtk::Box::builder()
let plugin_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(10)
.name(style_names::PLUGIN)
.build();
plugin_box.add(&create_info_box(&plugin.info()()));
plugin_box.add(
&gtk::Separator::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(10)
.name(style_names::PLUGIN)
.build();
plugin_box.add(&create_info_box(&plugin.info()()));
plugin_box.add(
&gtk::Separator::builder()
.orientation(gtk::Orientation::Horizontal)
.name(style_names::PLUGIN)
.build(),
);
let list = gtk::ListBox::builder()
.name(style_names::PLUGIN)
.hexpand(true)
.build();
.build(),
);
let list = gtk::ListBox::builder()
.name(style_names::PLUGIN)
.hexpand(true)
.build();
plugin_box.add(&list);
plugin_box.add(&list);
let row = gtk::ListBoxRow::builder().name(style_names::PLUGIN).build();
row.add(&plugin_box);
let row = gtk::ListBoxRow::builder().name(style_names::PLUGIN).build();
row.add(&plugin_box);
main_list.add(&row);
main_list.add(&row);
PluginView { plugin, row, list }
})
.collect::<Vec<PluginView>>(),
);
PluginView { plugin, row, list }
})
.collect::<Vec<PluginView>>();
// Connect selection events to avoid completely messing up selection logic
for plugin_view in plugins.iter() {
let plugins_clone = plugins.clone();
for plugin_view in runtime_data.borrow().as_ref().unwrap().plugins.iter() {
let plugins_clone = runtime_data.borrow().as_ref().unwrap().plugins.clone();
plugin_view.list.connect_row_selected(move |list, row| {
if row.is_some() {
let combined_matches = plugins_clone
@ -303,9 +307,9 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
.build();
// Refresh the matches when text input changes
let plugins_clone = plugins.clone();
let runtime_data_clone = runtime_data.clone();
entry.connect_changed(move |entry| {
refresh_matches(entry.text().to_string(), plugins_clone.clone())
refresh_matches(entry.text().to_string(), runtime_data_clone.clone())
});
// Handle other key presses for selection control and all other things that may be needed
@ -321,7 +325,11 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
// Handle selections
constants::Down | constants::Tab | constants::Up => {
// Combine all of the matches into a `Vec` to allow for easier handling of the selection
let combined_matches = plugins
let combined_matches = runtime_data
.borrow()
.as_ref()
.unwrap()
.plugins
.iter()
.flat_map(|view| {
view.list.children().into_iter().map(|child| {
@ -335,7 +343,11 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
.collect::<Vec<(gtk::ListBoxRow, gtk::ListBox)>>();
// Get the selected match
let (selected_match, selected_list) = match plugins
let (selected_match, selected_list) = match runtime_data
.borrow()
.as_ref()
.unwrap()
.plugins
.iter()
.find_map(|view| view.list.selected_row().map(|row| (row, view.list.clone())))
{
@ -395,9 +407,14 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
}
// Handle when the selected match is "activated"
constants::Return => {
let (selected_match, plugin) = match plugins
let mut _runtime_data = runtime_data.borrow_mut();
let (selected_match, plugin_view) = match _runtime_data
.as_ref()
.unwrap()
.plugins
.iter()
.find_map(|view| view.list.selected_row().map(|row| (row, view.plugin)))
.find_map(|view| view.list.selected_row().map(|row| (row, view)))
{
Some(selected) => selected,
None => {
@ -406,19 +423,25 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
};
// Perform actions based on the result of handling the selection
match plugin.handle_selection()(unsafe {
match plugin_view.plugin.handle_selection()(unsafe {
(*selected_match.data::<Match>("match").unwrap().as_ptr()).clone()
}) {
HandleResult::Close => {
window.close();
Inhibit(true)
}
HandleResult::Refresh => {
refresh_matches(entry_clone.text().to_string(), plugins.clone());
HandleResult::Refresh(exclusive) => {
if exclusive {
_runtime_data.as_mut().unwrap().exclusive = Some(plugin_view.clone());
} else {
_runtime_data.as_mut().unwrap().exclusive = None;
}
mem::drop(_runtime_data); // Drop the mutable borrow
refresh_matches(entry_clone.text().into(), runtime_data.clone());
Inhibit(false)
}
HandleResult::Copy(bytes) => {
runtime_data.borrow_mut().as_mut().unwrap().post_run_action =
_runtime_data.as_mut().unwrap().post_run_action =
PostRunAction::Copy(bytes.into());
window.close();
Inhibit(true)
@ -441,7 +464,11 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
main_list.show();
}
fn handle_matches(plugin_view: PluginView, plugins: Rc<Vec<PluginView>>, matches: RVec<Match>) {
fn handle_matches(
plugin_view: PluginView,
runtime_data: Rc<RefCell<Option<RuntimeData>>>,
matches: RVec<Match>,
) {
// Clear out the old matches from the list
for widget in plugin_view.list.children() {
plugin_view.list.remove(&widget);
@ -534,7 +561,11 @@ fn handle_matches(plugin_view: PluginView, plugins: Rc<Vec<PluginView>>, matches
// Refresh the items in the view
plugin_view.row.show_all();
let combined_matches = plugins
let combined_matches = runtime_data
.borrow()
.as_ref()
.unwrap()
.plugins
.iter()
.flat_map(|view| {
view.list.children().into_iter().map(|child| {
@ -595,27 +626,40 @@ fn create_info_box(info: &PluginInfo) -> gtk::Box {
}
/// Refresh the matches from the plugins
fn refresh_matches(input: String, plugins: Rc<Vec<PluginView>>) {
for plugin_view in plugins.iter() {
fn refresh_matches(input: String, runtime_data: Rc<RefCell<Option<RuntimeData>>>) {
for plugin_view in runtime_data.borrow().as_ref().unwrap().plugins.iter() {
let id = plugin_view.plugin.get_matches()(input.clone().into());
let plugin_view = plugin_view.clone();
let plugins = plugins.clone();
let runtime_data_clone = runtime_data.clone();
// If the input is empty, skip getting matches and just clear everything out.
if input.is_empty() {
handle_matches(plugin_view, plugins, RVec::new());
handle_matches(plugin_view, runtime_data_clone, RVec::new());
// If a plugin has requested exclusivity, respect it
} else if let Some(exclusive) = &runtime_data.borrow().as_ref().unwrap().exclusive {
if plugin_view.plugin.info() == exclusive.plugin.info() {
glib::timeout_add_local(Duration::from_micros(1000), move || {
async_match(plugin_view.clone(), runtime_data_clone.clone(), id)
});
} else {
handle_matches(plugin_view.clone(), runtime_data_clone, RVec::new());
}
} else {
glib::timeout_add_local(Duration::from_micros(1000), move || {
async_match(plugin_view.clone(), plugins.clone(), id)
async_match(plugin_view.clone(), runtime_data_clone.clone(), id)
});
}
}
}
/// Handle the asynchronously running match task
fn async_match(plugin_view: PluginView, plugins: Rc<Vec<PluginView>>, id: u64) -> glib::Continue {
fn async_match(
plugin_view: PluginView,
runtime_data: Rc<RefCell<Option<RuntimeData>>>,
id: u64,
) -> glib::Continue {
match plugin_view.plugin.poll_matches()(id) {
PollResult::Ready(matches) => {
handle_matches(plugin_view, plugins, matches);
handle_matches(plugin_view, runtime_data, matches);
glib::Continue(false)
}
PollResult::Pending => glib::Continue(true),

16
plugins/kidex/Cargo.toml Normal file

@ -0,0 +1,16 @@
[package]
name = "kidex"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyrun-plugin = { path = "../../anyrun-plugin" }
kidex-common = { git = "https://github.com/Kirottu/kidex", features = ["util"] }
abi_stable = "0.11.1"
fuzzy-matcher = "0.3.7"
open = "3.2.0"

146
plugins/kidex/src/lib.rs Normal file

@ -0,0 +1,146 @@
use abi_stable::std_types::{ROption, RString, RVec};
use anyrun_plugin::{anyrun_interface::HandleResult, *};
use fuzzy_matcher::FuzzyMatcher;
use kidex_common::IndexEntry;
use std::{os::unix::prelude::OsStrExt, process::Command};
pub struct State {
index: Vec<(usize, IndexEntry)>,
selection: Option<IndexEntry>,
}
enum IndexAction {
Open,
CopyPath,
Back,
}
impl From<u64> for IndexAction {
fn from(value: u64) -> Self {
match value {
0 => Self::Open,
1 => Self::CopyPath,
2 => Self::Back,
_ => unreachable!(),
}
}
}
pub fn handler(selection: Match, state: &mut State) -> HandleResult {
match &state.selection {
Some(index_entry) => match selection.id.unwrap().into() {
IndexAction::Open => {
if let Err(why) = Command::new("xdg-open").arg(&index_entry.path).spawn() {
println!("Error running xdg-open: {}", why);
}
HandleResult::Close
}
IndexAction::CopyPath => {
HandleResult::Copy(index_entry.path.clone().into_os_string().as_bytes().into())
}
IndexAction::Back => {
state.selection = None;
HandleResult::Refresh(false)
}
},
None => {
let (_, index_entry) = state
.index
.iter()
.find(|(id, _)| selection.id == ROption::RSome(*id as u64))
.unwrap();
state.selection = Some(index_entry.clone());
HandleResult::Refresh(true)
}
}
}
pub fn init(_config_dir: RString) -> State {
State {
index: match kidex_common::util::get_index(None) {
Ok(index) => index.into_iter().enumerate().collect(),
Err(why) => {
println!("Failed to get kidex index: {}", why);
Vec::new()
}
},
selection: None,
}
}
pub fn get_matches(input: RString, state: &mut State) -> RVec<Match> {
match &state.selection {
Some(index_entry) => {
let path = index_entry.path.to_string_lossy();
vec![
Match {
title: "Open File".into(),
description: ROption::RSome(path.clone().into()),
id: ROption::RSome(IndexAction::Open as u64),
icon: ROption::RSome("document-open".into()),
},
Match {
title: "Copy Path".into(),
description: ROption::RSome(path.into()),
id: ROption::RSome(IndexAction::CopyPath as u64),
icon: ROption::RSome("edit-copy".into()),
},
Match {
title: "Back".into(),
description: ROption::RNone,
id: ROption::RSome(IndexAction::Back as u64),
icon: ROption::RSome("edit-undo".into()),
},
]
.into()
}
None => {
let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().smart_case();
let mut index = state
.index
.clone()
.into_iter()
.filter_map(|(id, index_entry)| {
matcher
.fuzzy_match(&index_entry.path.as_os_str().to_string_lossy(), &input)
.map(|val| (index_entry, id, val))
})
.collect::<Vec<_>>();
index.sort_by(|a, b| b.2.cmp(&a.2));
index.truncate(3);
index
.into_iter()
.map(|(entry_index, id, _)| Match {
title: entry_index
.path
.file_name()
.map(|name| name.to_string_lossy().into())
.unwrap_or("N/A".into()),
icon: ROption::RSome(if entry_index.directory {
"folder".into()
} else {
"text-x-generic".into()
}),
description: entry_index
.path
.parent()
.map(|path| path.display().to_string().into())
.into(),
id: ROption::RSome(id as u64),
})
.collect()
}
}
}
pub fn info() -> PluginInfo {
PluginInfo {
name: "Kidex".into(),
icon: "folder".into(),
}
}
plugin!(init, info, get_matches, handler, State);