mirror of
https://github.com/anyrun-org/anyrun.git
synced 2024-10-04 03:48:00 +03:00
Anyrun!
This commit is contained in:
commit
990bf12b79
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);
|
Loading…
Reference in New Issue
Block a user