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

46
Cargo.lock generated
View file

@ -761,23 +761,21 @@ dependencies = [
name = "pop-launcher"
version = "1.0.0"
dependencies = [
"anyhow",
"async-io",
"async-oneshot",
"async-trait",
"flume",
"blocking",
"futures-lite",
"futures_codec",
"gen-z",
"regex",
"ron",
"serde",
"serde_json",
"serde_with",
"slab",
]
[[package]]
name = "pop-launcher-bin"
version = "1.0.0"
dependencies = [
"pop-launcher-plugins",
"pop-launcher-service",
"smol",
"strsim",
"toml",
"tracing",
"tracing-subscriber",
]
@ -803,6 +801,32 @@ dependencies = [
"zvariant",
]
[[package]]
name = "pop-launcher-service"
version = "1.0.0"
dependencies = [
"anyhow",
"async-io",
"async-oneshot",
"async-trait",
"flume",
"futures-lite",
"futures_codec",
"gen-z",
"pop-launcher",
"regex",
"ron",
"serde",
"serde_json",
"serde_with",
"slab",
"smol",
"strsim",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "proc-macro-crate"
version = "0.1.5"

View file

@ -2,29 +2,18 @@
name = "pop-launcher"
version = "1.0.0"
authors = ["Michael Aaron Murphy <mmstick@pm.me>"]
description = "Library for writing plugins and frontends for pop-launcher"
repository = "https://github.com/pop-os/launcher"
edition = "2018"
resolver = "2"
[workspace]
members = ["plugins"]
members = ["bin", "plugins", "service"]
[dependencies]
anyhow = "1"
async-io = "1"
async-oneshot = "0.5"
async-trait = "0.1"
flume = "=0.10.7" # Restrict version to build with 1.47.0
futures_codec = "0.4"
futures-lite = "1"
regex = "1.5"
ron = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_with = "1"
slab = "0.4"
smol = "1"
strsim = "0.10"
toml = "0.5"
tracing = "0.1"
tracing-subscriber = { version = "0.2", features = ["fmt"] }
gen-z = "0.1"
futures-lite = "1"
futures_codec = "0.4"
blocking = "1"

View file

@ -12,8 +12,8 @@ endif
LAUNCHER_DIR = $(LIB_PATH)/pop-launcher
SCRIPTS_DIR = $(LAUNCHER_DIR)/scripts
PLUGIN_DIR = $(LAUNCHER_DIR)/plugins
DEFAULT_PLUGINS_BIN = $(PLUGIN_DIR)/pop-launcher-plugins
BIN_DIR = $(BASE_PATH)/bin
BIN = $(BIN_DIR)/pop-launcher
PLUGINS=calc desktop_entries files find pop_shell pulse recent scripts terminal web
@ -31,8 +31,7 @@ ifneq ($(VENDOR),0)
endif
all: extract-vendor
cargo build -p pop-launcher-plugins $(ARGS)
cargo build $(ARGS)
cargo build -p pop-launcher-bin $(ARGS)
clean:
cargo clean
@ -56,42 +55,41 @@ install:
for plugin in $(PLUGINS); do \
dest=$(PLUGIN_DIR)/$${plugin}; \
mkdir -p $${dest}; \
install -Dm0644 plugins/src/plugins/$${plugin}/plugin.ron $${dest}/plugin.ron; \
install -Dm0644 plugins/src/$${plugin}/plugin.ron $${dest}/plugin.ron; \
done
install -Dm0755 target/$(TARGET)/pop-launcher $(BIN_DIR)/pop-launcher
install -Dm0755 target/$(TARGET)/pop-launcher-plugins $(DEFAULT_PLUGINS_BIN)
install -Dm0755 target/$(TARGET)/pop-launcher-bin $(BIN)
# Pop Shell Windows plugin
ln -sf $(DEFAULT_PLUGINS_BIN) $(PLUGIN_DIR)/pop_shell/pop-shell
ln -sf $(BIN) $(PLUGIN_DIR)/pop_shell/pop-shell
# Desktop Entries plugin
ln -sf $(DEFAULT_PLUGINS_BIN) $(PLUGIN_DIR)/desktop_entries/desktop-entries
ln -sf $(BIN) $(PLUGIN_DIR)/desktop_entries/desktop-entries
# Find plugin
ln -sf $(DEFAULT_PLUGINS_BIN) $(PLUGIN_DIR)/find/find
ln -sf $(BIN) $(PLUGIN_DIR)/find/find
# Scripts plugin
ln -sf $(DEFAULT_PLUGINS_BIN) $(PLUGIN_DIR)/scripts/scripts
ln -sf $(BIN) $(PLUGIN_DIR)/scripts/scripts
# Calculator plugin
install -Dm0755 plugins/src/plugins/calc/calc.js $(PLUGIN_DIR)/calc
install -Dm0644 plugins/src/plugins/calc/math.js $(PLUGIN_DIR)/calc
install -Dm0755 plugins/src/calc/calc.js $(PLUGIN_DIR)/calc
install -Dm0644 plugins/src/calc/math.js $(PLUGIN_DIR)/calc
# Files plugin
install -Dm0755 plugins/src/plugins/files/files.js $(PLUGIN_DIR)/files
install -Dm0755 plugins/src/files/files.js $(PLUGIN_DIR)/files
# Recent plugin
install -Dm0755 plugins/src/plugins/recent/recent.js $(PLUGIN_DIR)/recent
install -Dm0755 plugins/src/recent/recent.js $(PLUGIN_DIR)/recent
# Pulse plugin
install -Dm0755 plugins/src/plugins/pulse/pulse.js $(PLUGIN_DIR)/pulse
install -Dm0755 plugins/src/pulse/pulse.js $(PLUGIN_DIR)/pulse
# Terminal plugin
install -Dm0755 plugins/src/plugins/terminal/terminal.js $(PLUGIN_DIR)/terminal
install -Dm0755 plugins/src/terminal/terminal.js $(PLUGIN_DIR)/terminal
# Web plugin
install -Dm0755 plugins/src/plugins/web/web.js $(PLUGIN_DIR)/web
install -Dm0755 plugins/src/web/web.js $(PLUGIN_DIR)/web
# Scripts
mkdir -p $(SCRIPTS_DIR)

View file

@ -12,7 +12,7 @@ Using IPC enables each plugin to isolate their data from other plugin processes
## Script Directories
- User-local plugins: `~/.local/share/pop-shell/scripts`
- User-local scripts: `~/.local/share/pop-shell/scripts`
- System-wide install for system administrators: `/etc/pop-shell/scripts`
- Distribution packaging: `/usr/lib/pop-shell/scripts`
@ -65,7 +65,7 @@ If you are writing a plugin, you should send these events to your stdout.
```rust
pub enum PluginResponse {
/// Append a new search item to the launcher
Append(SearchMeta),
Append(PluginSearchResult),
/// Clear all results in the launcher list
Clear,
/// Close the launcher
@ -81,14 +81,14 @@ pub enum PluginResponse {
#### JSON Equivalent
- `{ "Append": SearchMeta }`,
- `{ "Append": PluginSearchResult }`,
- `"Clear"`,
- `"Close"`,
- `{ "DesktopEntry": string }`
- `{ "Fill": string }`
- `"Finished"`
Where `SearchMeta` is:
Where `PluginSearchResult` is:
```ts
{

14
bin/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "pop-launcher-bin"
version = "1.0.0"
edition = "2018"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pop-launcher-plugins = { path = "../plugins" }
pop-launcher-service = { path = "../service" }
smol = "1"
tracing = "0.1"
tracing-subscriber = "0.2"

View file

@ -1,5 +1,5 @@
mod plugins;
use pop_launcher_plugins as plugins;
use pop_launcher_service::Service;
use smol::block_on;
use std::io;
@ -13,7 +13,12 @@ fn main() {
if let Some(plugin) = std::env::args().next() {
let start = plugin.rfind('/').map(|v| v + 1).unwrap_or(0);
match &plugin.as_str()[start..] {
let cmd = &plugin.as_str()[start..];
match cmd {
"pop-launcher" => {
let stdout = io::stdout();
block_on(Service::new(stdout.lock()).exec())
},
"desktop-entries" => block_on(plugins::desktop_entries::main()),
"pop-shell" => block_on(plugins::pop_shell::main()),
"find" => block_on(plugins::find::main()),

View file

@ -1,4 +1,4 @@
/usr/lib/pop-launcher/plugins/pop-launcher-plugins /usr/lib/pop-launcher/plugins/desktop_entries/desktop-entries
/usr/lib/pop-launcher/plugins/pop-launcher-plugins /usr/lib/pop-launcher/plugins/find/find
/usr/lib/pop-launcher/plugins/pop-launcher-plugins /usr/lib/pop-launcher/plugins/pop_shell/pop-shell
/usr/lib/pop-launcher/plugins/pop-launcher-plugins /usr/lib/pop-launcher/plugins/scripts/scripts
/usr/bin/pop-launcher /usr/lib/pop-launcher/plugins/desktop_entries/desktop-entries
/usr/bin/pop-launcher /usr/lib/pop-launcher/plugins/find/find
/usr/bin/pop-launcher /usr/lib/pop-launcher/plugins/pop_shell/pop-shell
/usr/bin/pop-launcher /usr/lib/pop-launcher/plugins/scripts/scripts

View file

@ -3,6 +3,7 @@ name = "pop-launcher-plugins"
version = "1.0.0"
authors = ["Michael Aaron Murphy <mmstick@pm.me>"]
edition = "2018"
publish = false
[dependencies]
flume = "0.10"

View file

@ -1,7 +1,7 @@
use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource};
use futures_lite::{AsyncWrite, StreamExt};
use pop_launcher::*;
use pop_launcher_plugins::*;
use crate::*;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
@ -172,7 +172,7 @@ impl<W: AsyncWrite + Unpin> DesktopEntryPlugin<W> {
|| strsim::damerau_levenshtein(&*query, &*search_interest) < 3;
if append {
let response = PluginResponse::Append(SearchMeta {
let response = PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: entry.name.clone(),
description: format!("{} - {}", path_string(&entry.src), entry.description),

View file

@ -1,6 +1,6 @@
use futures_lite::*;
use pop_launcher::*;
use pop_launcher_plugins::send;
use crate::send;
use smol::process::{ChildStdout, Command, Stdio};
use std::borrow::Cow;
use std::cell::Cell;
@ -121,7 +121,7 @@ impl SearchContext {
let path = PathBuf::from(line);
let response = PluginResponse::Append(SearchMeta {
let response = PluginResponse::Append(PluginSearchResult {
id,
description,
name,

View file

@ -1,3 +1,8 @@
pub mod desktop_entries;
pub mod find;
pub mod pop_shell;
pub mod scripts;
use futures_lite::{AsyncWrite, AsyncWriteExt};
use pop_launcher::PluginResponse;

View file

@ -1,4 +0,0 @@
pub mod desktop_entries;
pub mod find;
pub mod pop_shell;
pub mod scripts;

View file

@ -1,6 +1,6 @@
use futures_lite::{AsyncWrite, AsyncWriteExt, StreamExt};
use pop_launcher::*;
use pop_launcher_plugins::*;
use crate::*;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use zbus::Connection;
@ -106,7 +106,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
send(
&mut self.tx,
PluginResponse::Append(SearchMeta {
PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: item.name.clone(),
description: item.description.clone(),

View file

@ -1,5 +1,5 @@
use pop_launcher::*;
use pop_launcher_plugins::*;
use crate::*;
use flume::Sender;
use futures_lite::{AsyncBufReadExt, StreamExt};
@ -114,7 +114,7 @@ impl App {
if should_include {
send(
out,
PluginResponse::Append(SearchMeta {
PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: script.name.clone(),
description: script.description.clone(),

27
service/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "pop-launcher-service"
version = "1.0.0"
edition = "2018"
publish = false
[dependencies]
anyhow = "1"
async-io = "1"
async-oneshot = "0.5"
async-trait = "0.1"
flume = "=0.10.7" # Restrict version to build with 1.47.0
futures_codec = "0.4"
futures-lite = "1"
regex = "1.5"
ron = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_with = "1"
slab = "0.4"
smol = "1"
strsim = "0.10"
toml = "0.5"
tracing = "0.1"
tracing-subscriber = { version = "0.2", features = ["fmt"] }
gen-z = "0.1"
pop-launcher = { path = "../" }

View file

@ -1,12 +1,30 @@
use crate::*;
mod plugins;
use pop_launcher::*;
use crate::plugins::*;
use flume::{unbounded, Receiver, Sender};
use futures_lite::{future, StreamExt};
use regex::Regex;
use slab::Slab;
use std::io::Write;
pub type PluginKey = usize;
pub enum Event {
Request(Request),
Response((PluginKey, PluginResponse)),
PluginExit(PluginKey),
Help(async_oneshot::Sender<Slab<PluginHelp>>),
}
pub struct PluginHelp {
pub name: String,
pub description: String,
pub help: Option<String>,
}
pub struct Service<O> {
active_search: Vec<(PluginKey, SearchMeta)>,
active_search: Vec<(PluginKey, PluginSearchResult)>,
awaiting_results: usize,
last_query: String,
output: O,
@ -172,7 +190,7 @@ impl<O: Write> Service<O> {
}
}
fn append(&mut self, plugin: PluginKey, append: SearchMeta) {
fn append(&mut self, plugin: PluginKey, append: PluginSearchResult) {
self.active_search.push((plugin, append));
}
@ -303,7 +321,7 @@ impl<O: Write> Service<O> {
}
/// 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)> {
fn search_result(&mut self, id: usize) -> Option<(&mut PluginConnector, &mut PluginSearchResult)> {
let &mut Self {
ref mut active_search,
ref mut plugins,
@ -334,7 +352,7 @@ impl<O: Write> Service<O> {
*no_sort = false;
} else {
active_search.sort_by(|a, b| {
fn calculate_weight(meta: &SearchMeta, query: &str) -> usize {
fn calculate_weight(meta: &PluginSearchResult, query: &str) -> usize {
let mut weight = 0;
let name = meta.name.to_ascii_lowercase();

View file

@ -1,5 +1,5 @@
use crate::{Event, IconSource, Plugin, PluginConfig, PluginQuery, PluginResponse, SearchMeta};
use crate::*;
use pop_launcher::*;
use flume::Sender;
use slab::Slab;
use std::borrow::Cow;
@ -19,13 +19,6 @@ pub const CONFIG: PluginConfig = PluginConfig {
},
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>,
@ -78,7 +71,7 @@ impl Plugin for HelpPlugin {
if detail.help.is_some() {
let _ = self
.tx
.send_async(PluginResponse::Append(SearchMeta {
.send_async(PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: detail.name.clone(),
description: detail.description.clone(),

View file

@ -4,9 +4,9 @@ pub mod help;
pub use self::config::{PluginBinary, PluginConfig, PluginQuery};
pub use self::external::ExternalPlugin;
pub use self::help::{HelpPlugin, PluginHelp};
pub use self::help::HelpPlugin;
use crate::Request;
use crate::{PluginHelp, Request};
use async_trait::async_trait;
use flume::{Receiver, Sender};
use regex::Regex;

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