improv: Separate components & merge plugins binary with launcher service
This commit is contained in:
parent
43a4229ba7
commit
88acf0a74e
41 changed files with 219 additions and 152 deletions
|
|
@ -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
|
||||
|
|
|
|||
117
src/lib.rs
117
src/lib.rs
|
|
@ -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)>,
|
||||
}
|
||||
|
|
|
|||
12
src/main.rs
12
src/main.rs
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
76
src/plugins/external/load.rs
vendored
76
src/plugins/external/load.rs
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
192
src/plugins/external/mod.rs
vendored
192
src/plugins/external/mod.rs
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
472
src/service.rs
472
src/service.rs
|
|
@ -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")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue