diff --git a/Cargo.lock b/Cargo.lock index a364d18..de9c77e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index dc61ef8..8b2edad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ members = [ "plugins/symbols", "plugins/rink", "plugins/shell", + "plugins/kidex", "plugins/translate", ] \ No newline at end of file diff --git a/README.md b/README.md index 408c387..906605d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/anyrun-interface/src/lib.rs b/anyrun-interface/src/lib.rs index e287d9f..99c4e11 100644 --- a/anyrun-interface/src/lib.rs +++ b/anyrun-interface/src/lib.rs @@ -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>), } diff --git a/anyrun/src/main.rs b/anyrun/src/main.rs index f5bea99..cb759a9 100644 --- a/anyrun/src/main.rs +++ b/anyrun/src/main.rs @@ -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: >k::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( + >k::Separator::builder() .orientation(gtk::Orientation::Horizontal) - .spacing(10) .name(style_names::PLUGIN) - .build(); - plugin_box.add(&create_info_box(&plugin.info()())); - plugin_box.add( - >k::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: >k::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: >k::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: >k::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: >k::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: >k::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: >k::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), diff --git a/plugins/kidex/Cargo.toml b/plugins/kidex/Cargo.toml new file mode 100644 index 0000000..66623f0 --- /dev/null +++ b/plugins/kidex/Cargo.toml @@ -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" diff --git a/plugins/kidex/src/lib.rs b/plugins/kidex/src/lib.rs new file mode 100644 index 0000000..7b2c6d4 --- /dev/null +++ b/plugins/kidex/src/lib.rs @@ -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);