This commit is contained in:
Kirottu 2022-12-29 23:56:32 +02:00
commit 990bf12b79
No known key found for this signature in database
GPG Key ID: 4034355FE9181021
17 changed files with 2069 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1065
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[workspace]
members = [
"anyrun",
"anyrun-plugin",
"anyrun-interface",
"plugins/applications",
"plugins/symbols",
"plugins/web-search",
]

View 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"

View 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
View 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
View 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
View 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
View 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
View 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: &gtk::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(
&gtk::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::<&gtk::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::<&gtk::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(
&gtk::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(
&gtk::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(
&gtk::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(
&gtk::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),
}
}

View 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"

View 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);

View 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())
}

View 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"

View 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);

View 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"

View 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);