Anyrun!
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
1065
Cargo.lock
generated
Normal file
1065
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"anyrun",
|
||||||
|
"anyrun-plugin",
|
||||||
|
"anyrun-interface",
|
||||||
|
"plugins/applications",
|
||||||
|
"plugins/symbols",
|
||||||
|
"plugins/web-search",
|
||||||
|
]
|
9
anyrun-interface/Cargo.toml
Normal file
9
anyrun-interface/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "anyrun-interface"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
abi_stable = "0.11.1"
|
67
anyrun-interface/src/lib.rs
Normal file
67
anyrun-interface/src/lib.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use abi_stable::{
|
||||||
|
declare_root_module_statics,
|
||||||
|
library::RootModule,
|
||||||
|
package_version_strings,
|
||||||
|
sabi_types::VersionStrings,
|
||||||
|
std_types::{ROption, RString, RVec},
|
||||||
|
StableAbi,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(StableAbi)]
|
||||||
|
#[sabi(kind(Prefix(prefix_ref = PluginRef)))]
|
||||||
|
#[sabi(missing_field(panic))]
|
||||||
|
pub struct Plugin {
|
||||||
|
pub init: extern "C" fn(RString),
|
||||||
|
pub info: extern "C" fn() -> PluginInfo,
|
||||||
|
pub get_matches: extern "C" fn(RString) -> u64,
|
||||||
|
pub poll_matches: extern "C" fn(u64) -> PollResult,
|
||||||
|
pub handle_selection: extern "C" fn(Match) -> HandleResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info of the plugin. Used for the main UI
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(StableAbi, Debug)]
|
||||||
|
pub struct PluginInfo {
|
||||||
|
pub name: RString,
|
||||||
|
/// The icon name from the icon theme in use
|
||||||
|
pub icon: RString,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a match from a plugin
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(StableAbi, Clone)]
|
||||||
|
pub struct Match {
|
||||||
|
pub title: RString,
|
||||||
|
pub description: ROption<RString>,
|
||||||
|
/// The icon name from the icon theme in use
|
||||||
|
pub icon: RString,
|
||||||
|
/// For runners to differentiate between the matches.
|
||||||
|
pub id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For determining how anyrun should proceed after the plugin has handled a match selection
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(StableAbi)]
|
||||||
|
pub enum HandleResult {
|
||||||
|
/// Shut down the program
|
||||||
|
Close,
|
||||||
|
/// Refresh the items. Useful if the runner wants to alter results in place.
|
||||||
|
Refresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(StableAbi)]
|
||||||
|
pub enum PollResult {
|
||||||
|
Ready(RVec<Match>),
|
||||||
|
Pending,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootModule for PluginRef {
|
||||||
|
declare_root_module_statics! {PluginRef}
|
||||||
|
|
||||||
|
const BASE_NAME: &'static str = "anyrun_plugin";
|
||||||
|
const NAME: &'static str = "anyrun_plugin";
|
||||||
|
const VERSION_STRINGS: VersionStrings = package_version_strings!();
|
||||||
|
}
|
10
anyrun-plugin/Cargo.toml
Normal file
10
anyrun-plugin/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "anyrun-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
abi_stable = "0.11.1"
|
||||||
|
anyrun-interface = { path = "../anyrun-interface" }
|
101
anyrun-plugin/src/lib.rs
Normal file
101
anyrun-plugin/src/lib.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
pub use anyrun_interface::{self, Match, PluginInfo};
|
||||||
|
|
||||||
|
/**
|
||||||
|
The macro to create a plugin, handles asynchronous execution of getting the matches and the boilerplate
|
||||||
|
for integrating with `stable_abi`.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
|
||||||
|
* `$init`: Function that takes an `RString` as the only argument, which points to the anyrun config directory. It returns nothing.
|
||||||
|
The path is used for plugin specific config files.
|
||||||
|
**NOTE**: Should not block or block for a long time. If this blocks the main thread will too.
|
||||||
|
|
||||||
|
* `$info`: Function that returns the plugin info as a `PluginInfo` object. Takes no arguments.
|
||||||
|
|
||||||
|
* `$get_matches`: Function that takes the current text input as an `RString` as the only argument, and returns an `RVec<Match>`.
|
||||||
|
This is run asynchronously automatically.
|
||||||
|
|
||||||
|
* `$handler`: The function to handle the selection of an item. Takes a `Match` as it's only argument and returns a `HandleResult` with
|
||||||
|
the appropriate action.
|
||||||
|
**/
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! plugin {
|
||||||
|
($init:ident, $info:ident, $get_matches:ident, $handler:ident) => {
|
||||||
|
mod anyrun_plugin_internal {
|
||||||
|
static THREAD: ::std::sync::Mutex<
|
||||||
|
Option<(
|
||||||
|
::std::thread::JoinHandle<
|
||||||
|
::abi_stable::std_types::RVec<::anyrun_plugin::anyrun_interface::Match>,
|
||||||
|
>,
|
||||||
|
u64,
|
||||||
|
)>,
|
||||||
|
> = ::std::sync::Mutex::new(None);
|
||||||
|
static ID_COUNTER: ::std::sync::atomic::AtomicU64 =
|
||||||
|
::std::sync::atomic::AtomicU64::new(0);
|
||||||
|
|
||||||
|
#[::abi_stable::export_root_module]
|
||||||
|
fn init_root_module() -> ::anyrun_plugin::anyrun_interface::PluginRef {
|
||||||
|
use ::abi_stable::prefix_type::PrefixTypeTrait;
|
||||||
|
::anyrun_plugin::anyrun_interface::Plugin {
|
||||||
|
init,
|
||||||
|
info,
|
||||||
|
get_matches,
|
||||||
|
poll_matches,
|
||||||
|
handle_selection,
|
||||||
|
}
|
||||||
|
.leak_into_prefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::abi_stable::sabi_extern_fn]
|
||||||
|
fn init(config_dir: ::abi_stable::std_types::RString) {
|
||||||
|
super::$init(config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::abi_stable::sabi_extern_fn]
|
||||||
|
fn info() -> ::anyrun_plugin::anyrun_interface::PluginInfo {
|
||||||
|
super::$info()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::abi_stable::sabi_extern_fn]
|
||||||
|
fn get_matches(input: ::abi_stable::std_types::RString) -> u64 {
|
||||||
|
let current_id = ID_COUNTER.load(::std::sync::atomic::Ordering::Relaxed);
|
||||||
|
ID_COUNTER.store(current_id + 1, ::std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
let handle = ::std::thread::spawn(move || super::$get_matches(input));
|
||||||
|
|
||||||
|
*THREAD.lock().unwrap() = Some((handle, current_id));
|
||||||
|
|
||||||
|
current_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::abi_stable::sabi_extern_fn]
|
||||||
|
fn poll_matches(id: u64) -> ::anyrun_plugin::anyrun_interface::PollResult {
|
||||||
|
match THREAD.try_lock() {
|
||||||
|
Ok(thread) => match thread.as_ref() {
|
||||||
|
Some((thread, task_id)) => {
|
||||||
|
if *task_id == id {
|
||||||
|
if !thread.is_finished() {
|
||||||
|
return ::anyrun_plugin::anyrun_interface::PollResult::Pending;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ::anyrun_plugin::anyrun_interface::PollResult::Cancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return ::anyrun_plugin::anyrun_interface::PollResult::Cancelled,
|
||||||
|
},
|
||||||
|
Err(_) => return ::anyrun_plugin::anyrun_interface::PollResult::Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (thread, _) = THREAD.lock().unwrap().take().unwrap();
|
||||||
|
::anyrun_plugin::anyrun_interface::PollResult::Ready(thread.join().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::abi_stable::sabi_extern_fn]
|
||||||
|
fn handle_selection(
|
||||||
|
selection: ::anyrun_plugin::anyrun_interface::Match,
|
||||||
|
) -> ::anyrun_plugin::anyrun_interface::HandleResult {
|
||||||
|
super::$handler(selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
14
anyrun/Cargo.toml
Normal file
14
anyrun/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "anyrun"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
abi_stable = "0.11.1"
|
||||||
|
gtk = "0.16.2"
|
||||||
|
gtk-layer-shell = { version = "0.5.0", features = ["v0_6"] }
|
||||||
|
ron = "0.8.0"
|
||||||
|
serde = { version = "1.0.151", features = ["derive"] }
|
||||||
|
anyrun-interface = { path = "../anyrun-interface" }
|
20
anyrun/res/style.css
Normal file
20
anyrun/res/style.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#window {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
list#main {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
list#plugin {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
label#match-desc {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label#plugin {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
469
anyrun/src/main.rs
Normal file
469
anyrun/src/main.rs
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
env, fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use abi_stable::std_types::{ROption, RVec};
|
||||||
|
use gtk::{gdk, glib, prelude::*};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use anyrun_interface::{HandleResult, Match, PluginInfo, PluginRef, PollResult};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
width: u32,
|
||||||
|
plugins: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PluginView {
|
||||||
|
plugin: PluginRef,
|
||||||
|
row: gtk::ListBoxRow,
|
||||||
|
list: gtk::ListBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
override_plugins: Option<Vec<String>>,
|
||||||
|
config_dir: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod style_names {
|
||||||
|
pub const ENTRY: &str = "entry";
|
||||||
|
pub const MAIN: &str = "main";
|
||||||
|
pub const WINDOW: &str = "window";
|
||||||
|
pub const PLUGIN: &str = "plugin";
|
||||||
|
pub const MATCH: &str = "match";
|
||||||
|
|
||||||
|
pub const MATCH_TITLE: &str = "match-title";
|
||||||
|
pub const MATCH_DESC: &str = "match-desc";
|
||||||
|
pub const TITLE_DESC_BOX: &str = "title-desc-box";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = gtk::Application::new(Some("com.kirottu.anyrun"), Default::default());
|
||||||
|
let args: Rc<RefCell<Option<Args>>> = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
|
app.add_main_option(
|
||||||
|
"override-plugins",
|
||||||
|
glib::Char('o' as i8),
|
||||||
|
glib::OptionFlags::IN_MAIN,
|
||||||
|
glib::OptionArg::StringArray,
|
||||||
|
"Override plugins. Provide paths in same format as in the config file",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
app.add_main_option(
|
||||||
|
"config-dir",
|
||||||
|
glib::Char('c' as i8),
|
||||||
|
glib::OptionFlags::IN_MAIN,
|
||||||
|
glib::OptionArg::String,
|
||||||
|
"Override the config directory from the default (~/.config/anyrun/)",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_clone = args.clone();
|
||||||
|
app.connect_handle_local_options(move |_app, dict| {
|
||||||
|
let override_plugins = dict.lookup::<Vec<String>>("override-plugins").unwrap();
|
||||||
|
let config_dir = dict.lookup::<String>("config-dir").unwrap();
|
||||||
|
|
||||||
|
*args_clone.borrow_mut() = Some(Args {
|
||||||
|
override_plugins,
|
||||||
|
config_dir,
|
||||||
|
});
|
||||||
|
-1 // Magic GTK number to continue running
|
||||||
|
});
|
||||||
|
|
||||||
|
let args_clone = args.clone();
|
||||||
|
app.connect_activate(move |app| activate(app, args_clone.clone()));
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate(app: >k::Application, args: Rc<RefCell<Option<Args>>>) {
|
||||||
|
// Figure out the config dir
|
||||||
|
let config_dir = args
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.config_dir
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(format!(
|
||||||
|
"{}/.config/anyrun",
|
||||||
|
env::var("HOME").expect("Could not determine home directory! Is $HOME set?")
|
||||||
|
));
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
let config: Config = ron::from_str(
|
||||||
|
&fs::read_to_string(format!("{}/config.ron", config_dir))
|
||||||
|
.expect("Unable to read config file!"),
|
||||||
|
)
|
||||||
|
.expect("Config file malformed!");
|
||||||
|
|
||||||
|
// Create the main window
|
||||||
|
let window = gtk::ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.name(style_names::WINDOW)
|
||||||
|
.width_request(config.width as i32)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Init GTK layer shell
|
||||||
|
gtk_layer_shell::init_for_window(&window);
|
||||||
|
gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, true);
|
||||||
|
gtk_layer_shell::set_keyboard_mode(&window, gtk_layer_shell::KeyboardMode::Exclusive);
|
||||||
|
|
||||||
|
// Try to load custom CSS, if it fails load the default CSS
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
if let Err(why) = provider.load_from_path(&format!("{}/style.css", config_dir)) {
|
||||||
|
println!("Failed to load custom CSS: {}", why);
|
||||||
|
provider
|
||||||
|
.load_from_data(include_bytes!("../res/style.css"))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
gtk::StyleContext::add_provider_for_screen(
|
||||||
|
&gdk::Screen::default().expect("Failed to get GDK screen for CSS provider!"),
|
||||||
|
&provider,
|
||||||
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the plugins in the config file, or the plugins specified with the override
|
||||||
|
let plugins = match &args.borrow().as_ref().unwrap().override_plugins {
|
||||||
|
Some(plugins) => plugins.iter().map(|path| PathBuf::from(path)).collect(),
|
||||||
|
None => config.plugins,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure at least one plugin is specified
|
||||||
|
if plugins.len() == 0 {
|
||||||
|
println!("At least one plugin needs to be enabled!");
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main list of plugin views
|
||||||
|
let main_list = gtk::ListBox::builder()
|
||||||
|
.selection_mode(gtk::SelectionMode::None)
|
||||||
|
.name(style_names::MAIN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Load plugins from the paths specified in the config file
|
||||||
|
let plugins = Rc::new(
|
||||||
|
plugins
|
||||||
|
.iter()
|
||||||
|
.map(|plugin_path| {
|
||||||
|
// Load the plugin's dynamic library.
|
||||||
|
let plugin = abi_stable::library::lib_header_from_path(
|
||||||
|
if plugin_path.is_absolute() {
|
||||||
|
plugin_path.clone()
|
||||||
|
} else {
|
||||||
|
let mut path = PathBuf::from(&format!("{}/plugins", config_dir));
|
||||||
|
path.extend(plugin_path.iter());
|
||||||
|
path
|
||||||
|
}
|
||||||
|
.as_path(),
|
||||||
|
)
|
||||||
|
.and_then(|plugin| plugin.init_root_module::<PluginRef>())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Run the plugin's init code to init static resources etc.
|
||||||
|
plugin.init()(config_dir.clone().into());
|
||||||
|
|
||||||
|
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)
|
||||||
|
.name(style_names::PLUGIN)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
let list = gtk::ListBox::builder()
|
||||||
|
.name(style_names::PLUGIN)
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
plugin_box.add(&list);
|
||||||
|
|
||||||
|
let row = gtk::ListBoxRow::builder().name(style_names::PLUGIN).build();
|
||||||
|
row.add(&plugin_box);
|
||||||
|
|
||||||
|
main_list.add(&row);
|
||||||
|
|
||||||
|
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();
|
||||||
|
plugin_view.list.connect_row_selected(move |list, row| {
|
||||||
|
if let Some(_) = row {
|
||||||
|
let combined_matches = plugins_clone
|
||||||
|
.iter()
|
||||||
|
.map(|view| {
|
||||||
|
view.list.children().into_iter().map(|child| {
|
||||||
|
(
|
||||||
|
child.dynamic_cast::<gtk::ListBoxRow>().unwrap(),
|
||||||
|
view.list.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<(gtk::ListBoxRow, gtk::ListBox)>>();
|
||||||
|
|
||||||
|
for (_, _list) in combined_matches {
|
||||||
|
if _list != *list {
|
||||||
|
_list.select_row(None::<>k::ListBoxRow>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text entry box
|
||||||
|
let entry = gtk::Entry::builder()
|
||||||
|
.hexpand(true)
|
||||||
|
.has_focus(true)
|
||||||
|
.name(style_names::ENTRY)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Refresh the matches when text input changes
|
||||||
|
let plugins_clone = plugins.clone();
|
||||||
|
entry.connect_changed(move |entry| {
|
||||||
|
refresh_matches(entry.text().to_string(), plugins_clone.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle other key presses for selection control and all other things that may be needed
|
||||||
|
let entry_clone = entry.clone();
|
||||||
|
let plugins_clone = plugins.clone();
|
||||||
|
window.connect_key_press_event(move |window, event| {
|
||||||
|
use gdk::keys::constants;
|
||||||
|
match event.keyval() {
|
||||||
|
constants::Escape => {
|
||||||
|
window.close();
|
||||||
|
Inhibit(true)
|
||||||
|
}
|
||||||
|
constants::Down | constants::Tab | constants::Up => {
|
||||||
|
let combined_matches = plugins_clone
|
||||||
|
.iter()
|
||||||
|
.map(|view| {
|
||||||
|
view.list.children().into_iter().map(|child| {
|
||||||
|
(
|
||||||
|
child.dynamic_cast::<gtk::ListBoxRow>().unwrap(),
|
||||||
|
view.list.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<(gtk::ListBoxRow, gtk::ListBox)>>();
|
||||||
|
|
||||||
|
let (selected_match, selected_list) = match plugins_clone
|
||||||
|
.iter()
|
||||||
|
.find_map(|view| view.list.selected_row().map(|row| (row, view.list.clone())))
|
||||||
|
{
|
||||||
|
Some(selected) => selected,
|
||||||
|
None => {
|
||||||
|
if event.keyval() != constants::Up {
|
||||||
|
combined_matches[0]
|
||||||
|
.1
|
||||||
|
.select_row(Some(&combined_matches[0].0));
|
||||||
|
}
|
||||||
|
return Inhibit(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selected_list.select_row(None::<>k::ListBoxRow>);
|
||||||
|
|
||||||
|
let index = combined_matches
|
||||||
|
.iter()
|
||||||
|
.position(|(row, _)| *row == selected_match)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match event.keyval() {
|
||||||
|
constants::Down | constants::Tab => {
|
||||||
|
if index < combined_matches.len() - 1 {
|
||||||
|
combined_matches[index + 1]
|
||||||
|
.1
|
||||||
|
.select_row(Some(&combined_matches[index + 1].0));
|
||||||
|
} else {
|
||||||
|
combined_matches[0]
|
||||||
|
.1
|
||||||
|
.select_row(Some(&combined_matches[0].0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constants::Up => {
|
||||||
|
if index > 0 {
|
||||||
|
combined_matches[index - 1]
|
||||||
|
.1
|
||||||
|
.select_row(Some(&combined_matches[index - 1].0));
|
||||||
|
} else {
|
||||||
|
combined_matches[combined_matches.len() - 1]
|
||||||
|
.1
|
||||||
|
.select_row(Some(&combined_matches[combined_matches.len() - 1].0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Inhibit(true)
|
||||||
|
}
|
||||||
|
constants::Return => {
|
||||||
|
let (selected_match, plugin) = match plugins_clone.iter().find_map(|view| {
|
||||||
|
view.list
|
||||||
|
.selected_row()
|
||||||
|
.map(|row| (row, view.plugin.clone()))
|
||||||
|
}) {
|
||||||
|
Some(selected) => selected,
|
||||||
|
None => {
|
||||||
|
return Inhibit(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match 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.clone());
|
||||||
|
Inhibit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Inhibit(false),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let main_vbox = gtk::Box::new(gtk::Orientation::Vertical, 10);
|
||||||
|
main_vbox.add(&entry);
|
||||||
|
window.add(&main_vbox);
|
||||||
|
window.show_all();
|
||||||
|
// Add and show the list later, to avoid showing empty plugin categories on launch
|
||||||
|
main_vbox.add(&main_list);
|
||||||
|
main_list.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_matches(matches: RVec<Match>, plugin_view: PluginView) {
|
||||||
|
for widget in plugin_view.list.children() {
|
||||||
|
plugin_view.list.remove(&widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches.len() == 0 {
|
||||||
|
plugin_view.row.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _match in matches {
|
||||||
|
let hbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.spacing(10)
|
||||||
|
.name(style_names::MATCH)
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
hbox.add(
|
||||||
|
>k::Image::builder()
|
||||||
|
.icon_name(&_match.icon)
|
||||||
|
.name(style_names::MATCH)
|
||||||
|
.pixel_size(32)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
let title = gtk::Label::builder()
|
||||||
|
.name(style_names::MATCH_TITLE)
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.label(&_match.title)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// If a description is present, make a box with it and the title
|
||||||
|
match &_match.description {
|
||||||
|
ROption::RSome(desc) => {
|
||||||
|
let title_desc_box = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.name(style_names::TITLE_DESC_BOX)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.build();
|
||||||
|
title_desc_box.add(&title);
|
||||||
|
title_desc_box.add(
|
||||||
|
>k::Label::builder()
|
||||||
|
.name(style_names::MATCH_DESC)
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.label(desc)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
hbox.add(&title_desc_box);
|
||||||
|
}
|
||||||
|
ROption::RNone => {
|
||||||
|
hbox.add(&title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let row = gtk::ListBoxRow::builder().name(style_names::MATCH).build();
|
||||||
|
row.add(&hbox);
|
||||||
|
// GTK data setting is not type checked, so it is unsafe.
|
||||||
|
// Only `Match` objects are stored though.
|
||||||
|
unsafe {
|
||||||
|
row.set_data("match", _match);
|
||||||
|
}
|
||||||
|
plugin_view.list.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the items in the view
|
||||||
|
plugin_view.row.show_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_info_box(info: &PluginInfo) -> gtk::Box {
|
||||||
|
let info_box = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.name(style_names::PLUGIN)
|
||||||
|
.width_request(200)
|
||||||
|
.expand(false)
|
||||||
|
.spacing(10)
|
||||||
|
.build();
|
||||||
|
info_box.add(
|
||||||
|
>k::Image::builder()
|
||||||
|
.icon_name(&info.icon)
|
||||||
|
.name(style_names::PLUGIN)
|
||||||
|
.pixel_size(48)
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.valign(gtk::Align::Start)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
info_box.add(
|
||||||
|
>k::Label::builder()
|
||||||
|
.label(&info.name)
|
||||||
|
.name(style_names::PLUGIN)
|
||||||
|
.halign(gtk::Align::End)
|
||||||
|
.valign(gtk::Align::Start)
|
||||||
|
.hexpand(true)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
info_box
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the matches from the plugins
|
||||||
|
fn refresh_matches(input: String, plugins: Rc<Vec<PluginView>>) {
|
||||||
|
for plugin_view in plugins.iter() {
|
||||||
|
let id = plugin_view.plugin.get_matches()(input.clone().into());
|
||||||
|
let plugin_view = plugin_view.clone();
|
||||||
|
glib::timeout_add_local(Duration::from_micros(1000), move || {
|
||||||
|
async_match(plugin_view.clone(), id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the asynchronously running match task
|
||||||
|
fn async_match(plugin_view: PluginView, id: u64) -> glib::Continue {
|
||||||
|
match plugin_view.plugin.poll_matches()(id) {
|
||||||
|
PollResult::Ready(matches) => {
|
||||||
|
handle_matches(matches, plugin_view);
|
||||||
|
glib::Continue(false)
|
||||||
|
}
|
||||||
|
PollResult::Pending => glib::Continue(true),
|
||||||
|
PollResult::Cancelled => glib::Continue(false),
|
||||||
|
}
|
||||||
|
}
|
14
plugins/applications/Cargo.toml
Normal file
14
plugins/applications/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "applications"
|
||||||
|
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" }
|
||||||
|
abi_stable = "0.11.1"
|
||||||
|
sublime_fuzzy = "0.7.0"
|
81
plugins/applications/src/lib.rs
Normal file
81
plugins/applications/src/lib.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use abi_stable::std_types::{ROption, RString, RVec};
|
||||||
|
use scrubber::DesktopEntry;
|
||||||
|
use std::{process::Command, sync::RwLock, thread};
|
||||||
|
use anyrun_plugin::{anyrun_interface::HandleResult, *};
|
||||||
|
|
||||||
|
mod scrubber;
|
||||||
|
|
||||||
|
static ENTRIES: RwLock<Vec<(DesktopEntry, u64)>> = RwLock::new(Vec::new());
|
||||||
|
|
||||||
|
pub fn handler(selection: Match) -> HandleResult {
|
||||||
|
let entries = ENTRIES.read().unwrap();
|
||||||
|
let entry = entries
|
||||||
|
.iter()
|
||||||
|
.find_map(|(entry, id)| {
|
||||||
|
if *id == selection.id {
|
||||||
|
Some(entry)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Err(why) = Command::new("sh").arg("-c").arg(&entry.exec).spawn() {
|
||||||
|
println!("Error running desktop entry: {}", why);
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleResult::Close
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(_config_dir: RString) {
|
||||||
|
thread::spawn(|| {
|
||||||
|
*ENTRIES.write().unwrap() = match scrubber::scrubber() {
|
||||||
|
Ok(results) => results,
|
||||||
|
Err(why) => {
|
||||||
|
println!("Error reading desktop entries: {}", why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_matches(input: RString) -> RVec<Match> {
|
||||||
|
if input.len() == 0 {
|
||||||
|
return RVec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = ENTRIES
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(entry, id)| {
|
||||||
|
match sublime_fuzzy::best_match(&input.to_lowercase(), &entry.name.to_lowercase()) {
|
||||||
|
Some(val) => Some((entry, id, val.score())),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<(DesktopEntry, u64, isize)>>();
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
|
||||||
|
entries.truncate(5);
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|(entry, id, _)| Match {
|
||||||
|
title: entry.name.into(),
|
||||||
|
icon: entry.icon.into(),
|
||||||
|
description: ROption::RNone,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info() -> PluginInfo {
|
||||||
|
PluginInfo {
|
||||||
|
name: "Applications".into(),
|
||||||
|
icon: "application-x-executable".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin!(init, info, get_matches, handler);
|
121
plugins/applications/src/scrubber.rs
Normal file
121
plugins/applications/src/scrubber.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use std::{collections::HashMap, env, ffi::OsStr, fs, io};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DesktopEntry {
|
||||||
|
pub exec: String,
|
||||||
|
pub name: String,
|
||||||
|
pub icon: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELD_CODE_LIST: &[&str] = &[
|
||||||
|
"%f", "%F", "%u", "%U", "%d", "%D", "%n", "%N", "%i", "%c", "%k", "%v", "%m",
|
||||||
|
];
|
||||||
|
|
||||||
|
impl DesktopEntry {
|
||||||
|
fn from_dir_entry(entry: &fs::DirEntry) -> Option<Self> {
|
||||||
|
if entry.path().extension() == Some(OsStr::new("desktop")) {
|
||||||
|
let content = match fs::read_to_string(entry.path()) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.starts_with("[") && line != "[Desktop Entry]" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (key, val) = match line.split_once("=") {
|
||||||
|
Some(keyval) => keyval,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
map.insert(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if map.get("Type")? == &"Application"
|
||||||
|
&& match map.get("NoDisplay") {
|
||||||
|
Some(no_display) => !no_display.parse::<bool>().unwrap_or(true),
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Some(DesktopEntry {
|
||||||
|
exec: {
|
||||||
|
let mut exec = map.get("Exec")?.to_string();
|
||||||
|
for field_code in FIELD_CODE_LIST {
|
||||||
|
exec = exec.replace(field_code, "");
|
||||||
|
}
|
||||||
|
exec
|
||||||
|
},
|
||||||
|
name: map.get("Name")?.to_string(),
|
||||||
|
icon: map
|
||||||
|
.get("Icon")
|
||||||
|
.unwrap_or(&"application-x-executable")
|
||||||
|
.to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scrubber() -> Result<Vec<(DesktopEntry, u64)>, Box<dyn std::error::Error>> {
|
||||||
|
// Create iterator over all the files in the XDG_DATA_DIRS
|
||||||
|
// XDG compliancy is cool
|
||||||
|
let mut paths: Vec<Result<fs::DirEntry, io::Error>> = match env::var("XDG_DATA_DIRS") {
|
||||||
|
Ok(data_dirs) => {
|
||||||
|
// The vec for all the DirEntry objects
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
// Parse the XDG_DATA_DIRS variable and list files of all the paths
|
||||||
|
for dir in data_dirs.split(":") {
|
||||||
|
match fs::read_dir(format!("{}/applications/", dir)) {
|
||||||
|
Ok(dir) => {
|
||||||
|
paths.extend(dir);
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
eprintln!("Error reading directory {}: {}", dir, why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Make sure the list of paths isn't empty
|
||||||
|
if paths.is_empty() {
|
||||||
|
return Err("No valid desktop file dirs found!".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return it
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
Err(_) => fs::read_dir("/usr/share/applications")?.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Go through user directory desktop files for overrides
|
||||||
|
let user_path = match env::var("XDG_DATA_HOME") {
|
||||||
|
Ok(data_home) => {
|
||||||
|
format!("{}/applications/", data_home)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
format!(
|
||||||
|
"{}/.local/share/applications/",
|
||||||
|
env::var("HOME").expect("Unable to determine home directory!")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
paths.extend(fs::read_dir(&user_path)?);
|
||||||
|
|
||||||
|
// Keeping track of the entries
|
||||||
|
let mut id = 0;
|
||||||
|
|
||||||
|
Ok(paths
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
id += 1;
|
||||||
|
let entry = match entry {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(_why) => return None,
|
||||||
|
};
|
||||||
|
DesktopEntry::from_dir_entry(&entry).map(|val| (val, id))
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
13
plugins/symbols/Cargo.toml
Normal file
13
plugins/symbols/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "symbols"
|
||||||
|
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" }
|
||||||
|
abi_stable = "0.11.1"
|
27
plugins/symbols/src/lib.rs
Normal file
27
plugins/symbols/src/lib.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use abi_stable::std_types::{ROption, RString, RVec};
|
||||||
|
use anyrun_plugin::{plugin, anyrun_interface::HandleResult, Match, PluginInfo};
|
||||||
|
|
||||||
|
pub fn init(_config_dir: RString) {}
|
||||||
|
|
||||||
|
pub fn info() -> PluginInfo {
|
||||||
|
PluginInfo {
|
||||||
|
name: "Symbols".into(),
|
||||||
|
icon: "emblem-mail".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_matches(input: RString) -> RVec<Match> {
|
||||||
|
vec![Match {
|
||||||
|
title: "Test".into(),
|
||||||
|
description: ROption::RNone,
|
||||||
|
icon: "dialog-warning".into(),
|
||||||
|
id: 0,
|
||||||
|
}]
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handler(selection: Match) -> HandleResult {
|
||||||
|
HandleResult::Close
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin!(init, info, get_matches, handler);
|
13
plugins/web-search/Cargo.toml
Normal file
13
plugins/web-search/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "web-search"
|
||||||
|
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" }
|
||||||
|
abi_stable = "0.11.1"
|
35
plugins/web-search/src/lib.rs
Normal file
35
plugins/web-search/src/lib.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use abi_stable::std_types::{ROption, RString, RVec};
|
||||||
|
use anyrun_plugin::{plugin, anyrun_interface::HandleResult, Match, PluginInfo};
|
||||||
|
|
||||||
|
pub fn init(_config_dir: RString) {}
|
||||||
|
|
||||||
|
pub fn info() -> PluginInfo {
|
||||||
|
PluginInfo {
|
||||||
|
name: "Web search".into(),
|
||||||
|
icon: "system-search".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_matches(input: RString) -> RVec<Match> {
|
||||||
|
vec![
|
||||||
|
Match {
|
||||||
|
title: "DDG it!".into(),
|
||||||
|
description: ROption::RSome(format!(r#"Look up "{}" with DuckDuckGo"#, input).into()),
|
||||||
|
icon: "emblem-web".into(),
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
Match {
|
||||||
|
title: "Startpage it!".into(),
|
||||||
|
description: ROption::RSome(format!(r#"Look up "{}" with Startpage"#, input).into()),
|
||||||
|
icon: "emblem-web".into(),
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handler(selection: Match) -> HandleResult {
|
||||||
|
HandleResult::Close
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin!(init, info, get_matches, handler);
|
Reference in New Issue
Block a user