Added Kidex plugin for file search.
This commit is contained in:
40
Cargo.lock
generated
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: >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),
|
||||
|
16
plugins/kidex/Cargo.toml
Normal file
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
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);
|
Reference in New Issue
Block a user