improv: Separate components & merge plugins binary with launcher service

This commit is contained in:
Michael Aaron Murphy 2021-08-14 14:19:42 +02:00
parent 43a4229ba7
commit 88acf0a74e
41 changed files with 219 additions and 152 deletions

View file

@ -1,7 +1,7 @@
use futures_codec::{FramedRead, LinesCodec};
use futures_lite::{AsyncRead, Stream, StreamExt};
use serde::Deserialize;
use smol::Unblock;
use blocking::Unblock;
use std::io;
/// stdin with AsyncRead support

View file

@ -1,115 +1,124 @@
mod codec;
mod plugins;
mod service;
pub use self::codec::*;
pub use self::plugins::*;
pub use self::service::Service;
use serde::{Deserialize, Serialize};
use slab::Slab;
use std::{borrow::Cow, path::PathBuf};
pub type PluginKey = usize;
/// u32 value defining the generation of an indice.
pub type Generation = u32;
pub type Indice = u32;
pub enum Event {
Request(Request),
Response((PluginKey, PluginResponse)),
PluginExit(PluginKey),
Help(async_oneshot::Sender<Slab<PluginHelp>>),
}
/// u32 value defining the indice of a slot.
pub type Indice = u32;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum IconSource {
// Locate by name or path
// Locate by name or path.
Name(Cow<'static, str>),
// Icon is a mime type
// Icon is a mime type.
Mime(Cow<'static, str>),
// Window Entity ID
// Window Entity ID.
Window((Generation, Indice)),
}
// Launcher frontends shall send these requests to the launcher service.
/// Sent from a plugin to the launcher service.
#[derive(Debug, Deserialize, Serialize)]
pub enum PluginResponse {
/// Append a new search item to the launcher.
Append(PluginSearchResult),
/// Clear all results in the launcher list.
Clear,
/// Close the launcher.
Close,
// Notifies that a .desktop entry should be launched by the frontend.
DesktopEntry(PathBuf),
/// Update the text in the launcher.
Fill(String),
/// Indicoates that a plugin is finished with its queries.
Finished,
}
/// Search information from a plugin to be sorted and filtered by the launcher service.
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PluginSearchResult {
/// Numeric identifier tracked by the plugin.
pub id: Indice,
/// The name / title.
pub name: String,
/// The description / subtitle.
pub description: String,
/// Extra words to match when sorting and filtering.
pub keywords: Option<Vec<String>>,
/// Icon to display in the frontend.
pub icon: Option<IconSource>,
/// Command that is executed by this result, used for sorting and filtering.
pub exec: Option<String>,
/// Designates that this search item refers to a window.
pub window: Option<(Generation, Indice)>,
}
// Sent to the input pipe of the launcher service, and disseminated to its plugins.
#[derive(Debug, Deserialize, Serialize)]
pub enum Request {
/// Activate on the selected item
/// Activate on the selected item.
Activate(Indice),
/// Perform a tab completion from the selected item
/// Perform a tab completion from the selected item.
Complete(Indice),
/// Request to end the service
/// Request to end the service.
Exit,
/// Requests to cancel any active searches
/// Requests to cancel any active searches.
Interrupt,
/// Request to close the selected item
/// Request to close the selected item.
Quit(Indice),
/// Perform a search in our database
/// Perform a search in our database.
Search(String),
}
/// Launcher frontends shall react to these responses from the launcher service.
/// Sent from the launcher service to a frontend.
#[derive(Debug, Deserialize, Serialize)]
pub enum Response {
// An operation was performed and the frontend may choose to exit its process.
Close,
// Notifies that a .desktop entry should be launched by the frontend
// Notifies that a .desktop entry should be launched by the frontend.
DesktopEntry(PathBuf),
// The frontend should clear its search results and display a new list
// The frontend should clear its search results and display a new list.
Update(Vec<SearchResult>),
// An item was selected that resulted in a need to autofill the launcher
// An item was selected that resulted in a need to autofill the launcher.
Fill(String),
}
#[derive(Debug, Deserialize, Serialize)]
pub enum PluginResponse {
/// Append a new search item to the launcher
Append(SearchMeta),
/// Clear all results in the launcher list
Clear,
/// Close the launcher
Close,
// Notifies that a .desktop entry should be launched by the frontend
DesktopEntry(PathBuf),
/// Update the text in the launcher
Fill(String),
/// Indicoates that a plugin is finished with its queries
Finished,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct SearchMeta {
pub id: Indice,
pub name: String,
pub description: String,
pub keywords: Option<Vec<String>>,
pub icon: Option<IconSource>,
pub exec: Option<String>,
pub window: Option<(Generation, Indice)>,
}
/// Serialized response to launcher frontend about a search result.
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResult {
/// Numeric identifier tracked by the plugin.
pub id: Indice,
/// The name / title.
pub name: String,
/// The description / subtitle.
pub description: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
/// Icon to display in the frontend for this item
pub icon: Option<IconSource>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
/// Icon to display in the frontend for this plugin
pub category_icon: Option<IconSource>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
/// Designates that this search item refers to a window.
pub window: Option<(Generation, Indice)>,
}

View file

@ -1,12 +0,0 @@
use pop_launcher::Service;
use std::io;
fn main() {
tracing_subscriber::fmt()
.with_writer(io::stderr)
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let stdout = io::stdout();
smol::block_on(Service::new(stdout.lock()).exec());
}

View file

@ -1,99 +0,0 @@
use regex::Regex;
use serde::Deserialize;
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
#[derive(Debug, Default, Deserialize)]
pub struct PluginConfig {
pub name: Cow<'static, str>,
pub description: Cow<'static, str>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
pub bin: Option<PluginBinary>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
pub icon: Option<crate::IconSource>,
#[serde(default)]
pub query: PluginQuery,
}
#[derive(Debug, Default, Deserialize)]
pub struct PluginBinary {
path: Cow<'static, str>,
#[serde(default)]
args: Vec<Cow<'static, str>>,
}
#[derive(Debug, Default, Deserialize)]
pub struct PluginQuery {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
pub help: Option<Cow<'static, str>>,
#[serde(default)]
pub isolate: bool,
#[serde(default)]
pub no_sort: bool,
#[serde(default)]
pub persistent: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::unwrap_or_skip"
)]
pub regex: Option<Cow<'static, str>>,
}
pub fn load(source: &Path, config_path: &Path) -> Option<(PathBuf, PluginConfig, Option<Regex>)> {
if let Ok(config_bytes) = std::fs::read_to_string(&config_path) {
let config = match ron::from_str::<PluginConfig>(&config_bytes) {
Ok(config) => config,
Err(why) => {
tracing::error!("malformed config at {}: {}", config_path.display(), why);
return None;
}
};
let exec = if let Some(bin) = config.bin.as_ref() {
if bin.path.starts_with('/') {
PathBuf::from((*bin.path).to_owned())
} else {
source.join(bin.path.as_ref())
}
} else {
tracing::error!(
"bin field is missing from config at {}",
config_path.display()
);
return None;
};
let regex = config
.query
.regex
.as_ref()
.and_then(|p| Regex::new(&*p).ok());
return Some((exec, config, regex));
}
tracing::error!("I/O error reading config at {}", config_path.display());
None
}

View file

@ -1,76 +0,0 @@
use crate::PluginConfig;
use flume::Sender;
use futures_lite::{Stream, StreamExt};
use regex::Regex;
use std::path::{Path, PathBuf};
/// Fetches plugins installed on the system in parallel.
pub async fn from_paths(tx: Sender<(PathBuf, PluginConfig, Option<Regex>)>) {
const PLUGIN_PATHS: &[&str] = &[
// User plugins
".local/share/pop-launcher/plugins/",
// System plugins configured by admin
"/etc/pop-launcher/plugins/",
// Distribution plugins
"/usr/lib/pop-launcher/plugins/",
];
let mut futures = Vec::new();
// Searches plugin paths from highest to least priority.
// User plugins will override distribution plugins.
for path in PLUGIN_PATHS {
let path_buf;
#[allow(deprecated)]
let path = if !path.starts_with('/') {
path_buf = std::env::home_dir()
.expect("user does not have home dir")
.join(path);
path_buf.as_path()
} else {
Path::new(&path)
};
let loadable_plugins = from_path(path);
futures_lite::pin!(loadable_plugins);
// Spawn a background task to parse the config for each plugin found.
while let Some((source, config)) = loadable_plugins.next().await {
let tx = tx.clone();
let future = smol::unblock(move || {
if let Some(plugin) = crate::plugins::config::load(&source, &config) {
let _ = tx.send(plugin);
}
});
futures.push(smol::spawn(future))
}
// Ensures that plugins are loaded in the order that they were spawned.
for future in futures.drain(..) {
future.await;
}
}
}
/// Loads all plugin information found in the given path.
pub fn from_path(path: &Path) -> impl Stream<Item = (PathBuf, PathBuf)> + '_ {
gen_z::gen_z(move |mut z| async move {
if let Ok(readdir) = path.read_dir() {
for entry in readdir.filter_map(Result::ok) {
let source = entry.path();
if !source.is_dir() {
continue;
}
let config = source.join("plugin.ron");
if !config.exists() {
continue;
}
z.send((source, config)).await;
}
}
})
}

View file

@ -1,192 +0,0 @@
pub mod load;
use std::{
io,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use crate::{Plugin, PluginResponse, Request};
use async_oneshot::oneshot;
use flume::Sender;
use futures_lite::{AsyncWriteExt, FutureExt, StreamExt};
use smol::{
process::{Child, Command, Stdio},
Task,
};
use tracing::{event, Level};
pub struct ExternalPlugin {
tx: Sender<PluginResponse>,
name: String,
pub cmd: PathBuf,
pub args: Vec<String>,
process: Option<(Task<()>, Child, async_oneshot::Sender<()>)>,
detached: Arc<AtomicBool>,
searching: Arc<AtomicBool>,
}
impl ExternalPlugin {
pub fn new(name: String, cmd: PathBuf, args: Vec<String>, tx: Sender<PluginResponse>) -> Self {
Self {
name,
tx,
cmd,
args,
process: None,
detached: Arc::default(),
searching: Arc::default(),
}
}
pub fn launch(&mut self) -> Option<&mut (Task<()>, Child, async_oneshot::Sender<()>)> {
event!(Level::DEBUG, "{}: launching plugin", self.name());
let child = Command::new(&self.cmd)
.args(&self.args)
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.ok();
if let Some(mut child) = child {
if let Some(stdout) = child.stdout.take() {
let detached = self.detached.clone();
let searching = self.searching.clone();
let (trip_tx, trip_rx) = oneshot::<()>();
let tx = self.tx.clone();
let name = self.name().to_owned();
// Spawn a background task to forward JSON responses from the child process.
let task = smol::spawn(async move {
let tx_ = tx.clone();
let searching_ = searching.clone();
let name_ = name.clone();
// Future for directly handling the JSON output from the process.
let responder = async move {
let mut requests = crate::json_input_stream(stdout);
while let Some(result) = requests.next().await {
match result {
Ok(response) => {
if let PluginResponse::Finished = response {
searching_.store(false, Ordering::SeqCst);
}
tracing::debug!("{}: responding with {:?}", name_, response);
let _ = tx_.send(response);
}
Err(why) => {
event!(Level::ERROR, "{}: serde error: {:?}", name_, why);
}
}
}
tracing::debug!("{}: exiting from responder", name_);
};
let trip = async move {
let _ = trip_rx.await;
};
let _ = responder.or(trip).await;
// Ensure that a task that was searching sends a finished signal if it dies.
if searching.swap(false, Ordering::SeqCst) {
let _ = tx.send(PluginResponse::Finished);
}
detached.store(true, Ordering::SeqCst);
event!(Level::DEBUG, "{}: detached plugin", name);
});
self.process = Some((task, child, trip_tx));
}
}
self.process.as_mut()
}
pub async fn process_check(&mut self) {
if let Some(mut child) = self.process.take() {
match child.1.try_status() {
Err(_) | Ok(Some(_)) => {
child.0.cancel().await;
}
Ok(None) => self.process = Some(child),
}
if self.detached.swap(false, Ordering::SeqCst) {
self.process = None;
}
}
}
pub async fn query(&mut self, event: &Request) -> io::Result<()> {
self.process_check().await;
if self.process.is_none() {
tracing::debug!("{}: relaunching process", self.name());
self.launch();
}
if let Some((_, child, _)) = self.process.as_mut() {
if let Some(stdin) = child.stdin.as_mut() {
if let Ok(mut serialized) = serde_json::to_vec(event) {
serialized.push(b'\n');
let _ = stdin.write_all(&serialized).await?;
tracing::debug!("{}: sent message to external process", self.name());
}
return Ok(());
}
}
Err(io::Error::new(
io::ErrorKind::NotConnected,
"child process could not be reached",
))
}
}
#[async_trait::async_trait]
impl Plugin for ExternalPlugin {
async fn activate(&mut self, id: u32) {
let _ = self.query(&Request::Activate(id)).await;
}
async fn complete(&mut self, id: u32) {
let _ = self.query(&Request::Complete(id)).await;
}
fn exit(&mut self) {
if let Some((_, _, mut trigger)) = self.process.take() {
let _ = trigger.send(());
}
}
async fn interrupt(&mut self) {
let _ = self.query(&Request::Interrupt).await;
}
fn name(&self) -> &str {
&self.name
}
async fn search(&mut self, query: &str) {
if self.query(&Request::Search(query.to_owned())).await.is_ok() {
self.searching.store(true, Ordering::SeqCst);
} else {
let _ = self.tx.send_async(PluginResponse::Finished).await;
}
}
async fn quit(&mut self, id: u32) {
let _ = self.query(&Request::Quit(id)).await;
}
}

View file

@ -1,95 +0,0 @@
use crate::{Event, IconSource, Plugin, PluginConfig, PluginQuery, PluginResponse, SearchMeta};
use flume::Sender;
use slab::Slab;
use std::borrow::Cow;
pub const REGEX: Cow<'static, str> = Cow::Borrowed("^(\\?).*");
pub const CONFIG: PluginConfig = PluginConfig {
name: Cow::Borrowed("Help"),
description: Cow::Borrowed("Show available plugin prefixes"),
bin: None,
query: PluginQuery {
help: None,
isolate: true,
no_sort: true,
persistent: false,
regex: None,
},
icon: Some(IconSource::Name(Cow::Borrowed("system-help-symbolic"))),
};
pub struct PluginHelp {
pub name: String,
pub description: String,
pub help: Option<String>,
}
pub struct HelpPlugin {
pub details: Slab<PluginHelp>,
pub internal: Sender<Event>,
pub tx: Sender<PluginResponse>,
}
impl HelpPlugin {
pub fn new(internal: Sender<Event>, tx: Sender<PluginResponse>) -> Self {
Self {
details: Slab::new(),
internal,
tx,
}
}
async fn reload(&mut self) {
let (tx, rx) = async_oneshot::oneshot();
let _ = self.internal.send_async(Event::Help(tx)).await;
self.details = rx.await.expect("internal error fetching help info");
}
}
#[async_trait::async_trait]
impl Plugin for HelpPlugin {
async fn activate(&mut self, id: u32) {
if let Some(detail) = self.details.get(id as usize) {
if let Some(help) = detail.help.as_ref() {
let _ = self.tx.send_async(PluginResponse::Fill(help.clone())).await;
}
}
}
async fn complete(&mut self, id: u32) {
self.activate(id).await
}
fn exit(&mut self) {}
async fn interrupt(&mut self) {}
fn name(&self) -> &str {
"help"
}
async fn search(&mut self, _query: &str) {
if self.details.is_empty() {
self.reload().await;
}
for (id, detail) in self.details.iter() {
if detail.help.is_some() {
let _ = self
.tx
.send_async(PluginResponse::Append(SearchMeta {
id: id as u32,
name: detail.name.clone(),
description: detail.description.clone(),
..Default::default()
}))
.await;
}
}
let _ = self.tx.send_async(PluginResponse::Finished).await;
}
async fn quit(&mut self, _id: u32) {}
}

View file

@ -1,124 +0,0 @@
mod config;
pub(crate) mod external;
pub mod help;
pub use self::config::{PluginBinary, PluginConfig, PluginQuery};
pub use self::external::ExternalPlugin;
pub use self::help::{HelpPlugin, PluginHelp};
use crate::Request;
use async_trait::async_trait;
use flume::{Receiver, Sender};
use regex::Regex;
#[async_trait]
pub trait Plugin
where
Self: Sized + Send,
{
/// Activate the selected ID from this plugin
async fn activate(&mut self, id: u32);
async fn complete(&mut self, id: u32);
fn exit(&mut self);
async fn interrupt(&mut self);
fn name(&self) -> &str;
async fn search(&mut self, query: &str);
async fn quit(&mut self, id: u32);
async fn run(&mut self, rx: Receiver<Request>) {
while let Ok(request) = rx.recv_async().await {
tracing::event!(
tracing::Level::DEBUG,
"{}: received {:?}",
self.name(),
request
);
match request {
Request::Search(query) => self.search(&query).await,
Request::Interrupt => self.interrupt().await,
Request::Activate(id) => self.activate(id).await,
Request::Complete(id) => self.complete(id).await,
Request::Quit(id) => self.quit(id).await,
Request::Exit => {
self.exit();
break;
}
}
}
tracing::event!(tracing::Level::DEBUG, "{}: exiting plugin", self.name());
}
}
/// Stores all information relevant for communicating with a plugin
///
/// Plugins may be requested to exit, and relaunched at any point in the future.
pub struct PluginConnector {
/// The deserialized configuration file for this plugin
pub config: PluginConfig,
/// Code that is executed to prepare a new instance of
/// this plugin to spawn as a background service
pub init: Box<dyn Fn() -> Sender<Request>>,
/// A compiled regular expression that a query must match
/// for the launcher service to justify spawning and sending
/// queries to this plugin
pub regex: Option<Regex>,
/// The sender of the spawned background service that will be
/// forwarded to the launncher service
pub sender: Option<Sender<Request>>,
}
impl PluginConnector {
pub fn new(
config: PluginConfig,
regex: Option<Regex>,
init: Box<dyn Fn() -> Sender<Request> + Send>,
) -> Self {
Self {
config,
init,
regex,
sender: None,
}
}
pub fn details(&self) -> PluginHelp {
PluginHelp {
name: self.config.name.as_ref().to_owned(),
description: self.config.description.as_ref().to_owned(),
help: self
.config
.query
.help
.as_ref()
.map(|x| x.as_ref().to_owned()),
}
}
/// Obtains the sender for sending messages to this plugin.
///
/// If the sender is absent, the plugin is relaunched with a new one.
pub fn sender_exec(&mut self) -> &mut Sender<Request> {
let &mut Self {
ref mut sender,
ref init,
..
} = self;
sender.get_or_insert_with(|| init())
}
/// Drops the sender, which will subsequently drop the plugin forwarder attached to it
pub fn sender_drop(&mut self) {
self.sender = None;
}
}

View file

@ -1,472 +0,0 @@
use crate::*;
use flume::{unbounded, Receiver, Sender};
use futures_lite::{future, StreamExt};
use regex::Regex;
use slab::Slab;
use std::io::Write;
pub struct Service<O> {
active_search: Vec<(PluginKey, SearchMeta)>,
awaiting_results: usize,
last_query: String,
output: O,
plugins: Slab<PluginConnector>,
no_sort: bool,
search_scheduled: bool,
}
impl<O: Write> Service<O> {
pub fn new(output: O) -> Self {
Self {
active_search: Vec::new(),
awaiting_results: 0,
last_query: String::new(),
output,
plugins: Slab::new(),
no_sort: false,
search_scheduled: false,
}
}
pub async fn exec(mut self) {
let (service_tx, service_rx) = unbounded();
{
let (plugins_tx, plugins_rx) = unbounded();
let plugin_loader = plugins::external::load::from_paths(plugins_tx);
let plugin_receiver = async {
while let Ok((exec, config, regex)) = plugins_rx.recv_async().await {
tracing::info!("found plugin \"{}\"", exec.display());
if self
.plugins
.iter()
.any(|(_, p)| p.config.name == config.name)
{
tracing::info!("ignoring plugin");
continue;
}
let name = String::from(config.name.as_ref());
self.register_plugin(service_tx.clone(), config, regex, move |tx| {
ExternalPlugin::new(name.clone(), exec.clone(), Vec::new(), tx)
});
}
};
future::zip(plugin_loader, plugin_receiver).await;
}
let internal = service_tx.clone();
self.register_plugin(
service_tx.clone(),
plugins::help::CONFIG,
Some(Regex::new(plugins::help::REGEX.as_ref()).expect("failed to compile help regex")),
move |tx| HelpPlugin::new(internal.clone(), tx),
);
let f1 = request_handler(service_tx);
let f2 = self.response_handler(service_rx);
future::zip(f1, f2).await;
}
async fn response_handler(&mut self, service_rx: Receiver<Event>) {
while let Ok(event) = service_rx.recv_async().await {
match event {
Event::Request(request) => {
match request {
Request::Search(query) => self.search(query),
Request::Interrupt => self.interrupt(),
Request::Activate(id) => self.activate(id),
Request::Complete(id) => self.complete(id),
Request::Quit(id) => self.quit(id),
// When requested to exit, the service will forward that
// request to all of its plugins before exiting itself
Request::Exit => {
for (_key, plugin) in self.plugins.iter_mut() {
let tx = plugin.sender_exec();
let _ = tx.send(Request::Exit);
}
break;
}
}
}
Event::Response((plugin, response)) => match response {
PluginResponse::Append(item) => self.append(plugin, item),
PluginResponse::Clear => self.clear(),
PluginResponse::Close => self.close(),
PluginResponse::Fill(text) => self.fill(text),
PluginResponse::Finished => self.finished(plugin),
PluginResponse::DesktopEntry(path) => {
self.respond(&Response::DesktopEntry(path));
}
},
// When a plugin has exited, the sender attached to the plugin will be dropped
Event::PluginExit(plugin_id) => {
if let Some(plugin) = self.plugins.get_mut(plugin_id) {
plugin.sender_drop();
}
}
Event::Help(mut sender) => {
let mut details = Slab::new();
for (_, plugin) in self.plugins.iter() {
details.insert(plugin.details());
}
let _ = sender.send(details);
}
}
}
}
fn register_plugin<P: Plugin, I: Fn(Sender<PluginResponse>) -> P + Send + Sync + 'static>(
&mut self,
service_tx: Sender<Event>,
config: PluginConfig,
regex: Option<regex::Regex>,
init: I,
) {
let (plugin_tx, plugin_rx) = unbounded();
let entry = self.plugins.vacant_entry();
let id = entry.key();
let init = std::sync::Arc::new(init);
entry.insert(PluginConnector::new(
config,
regex,
Box::new(move || {
let (request_tx, request_rx) = unbounded();
let init = init.clone();
let plugin_tx = plugin_tx.clone();
let plugin_rx = plugin_rx.clone();
let service_tx = service_tx.clone();
smol::spawn(async move {
let mut plugin = init(plugin_tx);
let f1 = plugin.run(request_rx);
let f2 = plugin_forwarder(id, plugin_rx, service_tx);
future::zip(f1, f2).await;
})
.detach();
request_tx
}),
));
}
fn activate(&mut self, id: u32) {
if let Some((plugin, meta)) = self.search_result(id as usize) {
let _ = plugin.sender_exec().send(Request::Activate(meta.id));
}
}
fn append(&mut self, plugin: PluginKey, append: SearchMeta) {
self.active_search.push((plugin, append));
}
fn clear(&mut self) {
self.active_search.clear();
}
fn close(&mut self) {
self.respond(&Response::Close);
}
fn complete(&mut self, id: u32) {
if let Some((plugin, meta)) = self.search_result(id as usize) {
let _ = plugin.sender_exec().send(Request::Complete(meta.id));
}
}
fn fill(&mut self, text: String) {
self.respond(&Response::Fill(text));
}
fn finished(&mut self, _plugin: PluginKey) {
self.awaiting_results -= 1;
if self.awaiting_results == 0 {
if self.search_scheduled {
self.search(String::new());
return;
}
let search_list = self.sort();
self.respond(&Response::Update(search_list))
}
}
fn interrupt(&mut self) {
for (_, plugin) in self.plugins.iter_mut() {
if let Some(sender) = plugin.sender.as_mut() {
let _ = sender.send(Request::Interrupt);
}
}
}
fn quit(&mut self, id: u32) {
if let Some((plugin, meta)) = self.search_result(id as usize) {
let _ = plugin.sender_exec().send(Request::Quit(meta.id));
}
}
/// Serializes the launcher's response to stdout
fn respond<E: serde::Serialize>(&mut self, event: &E) {
if let Ok(mut vec) = serde_json::to_vec(event) {
vec.push(b'\n');
let _ = self.output.write_all(&vec);
}
}
fn search(&mut self, query: String) {
if self.awaiting_results > 0 {
tracing::debug!("backing off from search until plugins are ready");
if !self.search_scheduled {
self.interrupt();
self.search_scheduled = true;
}
return;
}
self.active_search.clear();
if !self.search_scheduled {
self.last_query = query;
}
self.search_scheduled = false;
let query = self.last_query.as_str();
let mut query_queue = Vec::new();
let mut isolated = None;
let requires_persistence = query.is_empty();
for (key, plugin) in self.plugins.iter_mut() {
// Avoid sending queries to plugins which are not matched
if let Some(regex) = plugin.regex.as_ref() {
if !regex.is_match(query) {
continue;
}
}
if requires_persistence && !plugin.config.query.persistent {
continue;
}
if plugin.config.query.isolate {
isolated = Some(key);
break;
}
query_queue.push(key);
}
if let Some(isolated) = isolated {
if let Some(plugin) = self.plugins.get_mut(isolated) {
if plugin
.sender_exec()
.send(Request::Search(query.to_owned()))
.is_ok()
{
tracing::debug!("submitted query to {}", plugin.config.name);
self.awaiting_results += 1;
self.no_sort = plugin.config.query.no_sort;
}
}
} else {
for plugin in query_queue {
if let Some(plugin) = self.plugins.get_mut(plugin) {
if plugin
.sender_exec()
.send(Request::Search(query.to_owned()))
.is_ok()
{
tracing::debug!("submitted query to {}", plugin.config.name);
self.awaiting_results += 1;
}
}
}
}
}
/// From a given position ID, fetch the search result and its associated plugin
fn search_result(&mut self, id: usize) -> Option<(&mut PluginConnector, &mut SearchMeta)> {
let &mut Self {
ref mut active_search,
ref mut plugins,
..
} = self;
active_search
.get_mut(id)
.and_then(move |(plugin_id, meta)| {
plugins.get_mut(*plugin_id).map(|plugin| (plugin, meta))
})
}
fn sort(&mut self) -> Vec<SearchResult> {
let &mut Self {
ref mut active_search,
ref mut no_sort,
ref last_query,
ref plugins,
..
} = self;
let query = &last_query.to_ascii_lowercase();
use std::cmp::Ordering;
if *no_sort {
*no_sort = false;
} else {
active_search.sort_by(|a, b| {
fn calculate_weight(meta: &SearchMeta, query: &str) -> usize {
let mut weight = 0;
let name = meta.name.to_ascii_lowercase();
let description = meta.description.to_ascii_lowercase();
let exec = meta
.exec
.as_ref()
.map(|exec| exec.to_ascii_lowercase())
.unwrap_or_default();
if !name.starts_with(query) {
weight = 1;
if !name.contains(query) {
weight = strsim::damerau_levenshtein(&name, query)
.min(strsim::damerau_levenshtein(&description, query));
if let Some(keywords) = meta.keywords.as_ref() {
for keyword in keywords.iter() {
let keyword = keyword.to_ascii_lowercase();
weight = if keyword.starts_with(query)
|| keyword.contains(query)
{
1
} else {
weight.min(strsim::damerau_levenshtein(query, &keyword) + 1)
}
}
}
}
}
if exec.contains(query) {
weight = if exec.starts_with(query) {
weight.min(2)
} else {
weight.min(strsim::damerau_levenshtein(query, &exec))
}
}
weight
}
let a_weight = calculate_weight(&a.1, query);
let b_weight = calculate_weight(&b.1, query);
match a_weight.cmp(&b_weight) {
Ordering::Equal => {
let a_len = a.1.name.len();
let b_len = b.1.name.len();
a_len.cmp(&b_len)
}
other => other,
}
});
}
let take = if last_query.starts_with('/') | last_query.starts_with('~') {
100
} else {
8
};
let mut windows = Vec::with_capacity(take);
let mut non_windows = Vec::with_capacity(take);
let search_results =
active_search
.iter()
.take(take)
.enumerate()
.map(|(id, (plugin, meta))| SearchResult {
id: id as u32,
name: meta.name.clone(),
description: meta.description.clone(),
icon: meta.icon.clone(),
category_icon: plugins
.get(*plugin)
.and_then(|conn| conn.config.icon.clone()),
window: meta.window,
});
for result in search_results {
if result.window.is_some() {
windows.push(result);
} else {
non_windows.push(result)
}
}
windows.append(&mut non_windows);
windows
}
}
async fn plugin_forwarder(
plugin_id: PluginKey,
receiver: Receiver<PluginResponse>,
forwarder: Sender<Event>,
) {
while let Ok(response) = receiver.recv_async().await {
let _ = forwarder.send(Event::Response((plugin_id, response)));
}
let _ = forwarder.send(Event::PluginExit(plugin_id));
}
/// Handles Requests received from a frontend
async fn request_handler(tx: Sender<Event>) {
let mut requested_to_exit = false;
let mut request_stream = json_input_stream(async_stdin());
while let Some(result) = request_stream.next().await {
match result {
Ok(request) => {
if let Request::Exit = request {
requested_to_exit = true
}
let _ = tx.send(Event::Request(request));
if requested_to_exit {
break;
}
}
Err(why) => {
tracing::error!("Request JSON is malformed: {}", why);
}
}
}
tracing::debug!("no longer listening for requests")
}