feat: add support for external plugins and enhance plugin management
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -2219,6 +2219,7 @@ dependencies = [
|
||||
"k-launcher-config",
|
||||
"k-launcher-kernel",
|
||||
"k-launcher-os-bridge",
|
||||
"k-launcher-plugin-host",
|
||||
"k-launcher-ui",
|
||||
"k-launcher-ui-egui",
|
||||
"plugin-apps",
|
||||
@@ -2256,6 +2257,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k-launcher-plugin-host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"k-launcher-kernel",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k-launcher-ui"
|
||||
version = "0.1.0"
|
||||
@@ -2534,6 +2547,17 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.7.11"
|
||||
@@ -4344,8 +4368,13 @@ version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"crates/k-launcher-config",
|
||||
"crates/k-launcher-kernel",
|
||||
"crates/k-launcher-os-bridge",
|
||||
"crates/k-launcher-plugin-host",
|
||||
"crates/k-launcher-ui",
|
||||
"crates/plugins/plugin-apps",
|
||||
"crates/plugins/plugin-calc",
|
||||
@@ -19,6 +20,7 @@ dirs = "5.0"
|
||||
futures = "0.3"
|
||||
iced = { version = "0.14", features = ["image", "svg", "tokio", "tiny-skia"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
tracing = "0.1"
|
||||
|
||||
13
README.md
13
README.md
@@ -33,6 +33,19 @@ cargo build --release
|
||||
| `>` prefix | Shell | `> echo hello` |
|
||||
| `/` or `~/` | Files | `~/Documents` |
|
||||
|
||||
## External Plugins
|
||||
|
||||
Drop in community plugins — any language, no recompilation. Plugins are executables that communicate over stdin/stdout JSON:
|
||||
|
||||
```toml
|
||||
# ~/.config/k-launcher/config.toml
|
||||
[[plugins.external]]
|
||||
name = "my-plugin"
|
||||
path = "/usr/lib/k-launcher/plugins/my-plugin"
|
||||
```
|
||||
|
||||
See [Plugin Development](docs/plugin-development.md) for the full protocol.
|
||||
|
||||
## Docs
|
||||
|
||||
- [Installation](docs/install.md)
|
||||
|
||||
@@ -76,6 +76,14 @@ impl Default for SearchCfg {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct ExternalPluginCfg {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct PluginsCfg {
|
||||
@@ -83,6 +91,7 @@ pub struct PluginsCfg {
|
||||
pub cmd: bool,
|
||||
pub files: bool,
|
||||
pub apps: bool,
|
||||
pub external: Vec<ExternalPluginCfg>,
|
||||
}
|
||||
|
||||
impl Default for PluginsCfg {
|
||||
@@ -92,6 +101,7 @@ impl Default for PluginsCfg {
|
||||
cmd: true,
|
||||
files: true,
|
||||
apps: true,
|
||||
external: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use futures::future::join_all;
|
||||
|
||||
pub type PluginName = &'static str;
|
||||
|
||||
// --- Newtypes ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
@@ -86,7 +84,7 @@ impl std::fmt::Debug for SearchResult {
|
||||
|
||||
#[async_trait]
|
||||
pub trait Plugin: Send + Sync {
|
||||
fn name(&self) -> PluginName;
|
||||
fn name(&self) -> &str;
|
||||
async fn search(&self, query: &str) -> Vec<SearchResult>;
|
||||
}
|
||||
|
||||
@@ -144,7 +142,7 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for MockPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
fn name(&self) -> &str {
|
||||
"mock"
|
||||
}
|
||||
|
||||
|
||||
16
crates/k-launcher-plugin-host/Cargo.toml
Normal file
16
crates/k-launcher-plugin-host/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "k-launcher-plugin-host"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "k_launcher_plugin_host"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["process", "io-util", "sync"] }
|
||||
tracing = { workspace = true }
|
||||
201
crates/k-launcher-plugin-host/src/lib.rs
Normal file
201
crates/k-launcher-plugin-host/src/lib.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use async_trait::async_trait;
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, ResultId, ResultTitle, Score, SearchResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
|
||||
use tokio::process::{ChildStdin, ChildStdout, Command};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
// --- Protocol types ---
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Query {
|
||||
query: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExternalResult {
|
||||
id: String,
|
||||
title: String,
|
||||
score: u32,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
icon: Option<String>,
|
||||
action: ExternalAction,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ExternalAction {
|
||||
SpawnProcess { cmd: String },
|
||||
CopyToClipboard { text: String },
|
||||
OpenPath { path: String },
|
||||
}
|
||||
|
||||
// --- Process I/O handle ---
|
||||
|
||||
struct ProcessIo {
|
||||
stdin: BufWriter<ChildStdin>,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
}
|
||||
|
||||
async fn do_search(
|
||||
io: &mut ProcessIo,
|
||||
query: &str,
|
||||
) -> Result<Vec<ExternalResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let line = serde_json::to_string(&Query { query: query.to_string() })?;
|
||||
io.stdin.write_all(line.as_bytes()).await?;
|
||||
io.stdin.write_all(b"\n").await?;
|
||||
io.stdin.flush().await?;
|
||||
let mut response = String::new();
|
||||
io.stdout.read_line(&mut response).await?;
|
||||
Ok(serde_json::from_str(&response)?)
|
||||
}
|
||||
|
||||
// --- ExternalPlugin ---
|
||||
|
||||
pub struct ExternalPlugin {
|
||||
name: String,
|
||||
path: String,
|
||||
args: Vec<String>,
|
||||
inner: Mutex<Option<ProcessIo>>,
|
||||
}
|
||||
|
||||
impl ExternalPlugin {
|
||||
pub fn new(name: impl Into<String>, path: impl Into<String>, args: Vec<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
path: path.into(),
|
||||
args,
|
||||
inner: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn(&self) -> std::io::Result<ProcessIo> {
|
||||
let mut child = Command::new(&self.path)
|
||||
.args(&self.args)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
let stdin = BufWriter::new(child.stdin.take().unwrap());
|
||||
let stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
Ok(ProcessIo { stdin, stdout })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for ExternalPlugin {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||
let mut guard = self.inner.lock().await;
|
||||
|
||||
if guard.is_none() {
|
||||
match self.spawn().await {
|
||||
Ok(io) => *guard = Some(io),
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to spawn plugin {}: {e}", self.name);
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = match guard.as_mut() {
|
||||
Some(io) => do_search(io, query).await,
|
||||
None => unreachable!(),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(results) => results
|
||||
.into_iter()
|
||||
.map(|r| SearchResult {
|
||||
id: ResultId::new(r.id),
|
||||
title: ResultTitle::new(r.title),
|
||||
description: r.description,
|
||||
icon: r.icon,
|
||||
score: Score::new(r.score),
|
||||
action: match r.action {
|
||||
ExternalAction::SpawnProcess { cmd } => LaunchAction::SpawnProcess(cmd),
|
||||
ExternalAction::CopyToClipboard { text } => {
|
||||
LaunchAction::CopyToClipboard(text)
|
||||
}
|
||||
ExternalAction::OpenPath { path } => LaunchAction::OpenPath(path),
|
||||
},
|
||||
on_select: None,
|
||||
})
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
tracing::warn!("plugin {} error: {e}", self.name);
|
||||
*guard = None;
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn query_serializes_correctly() {
|
||||
let q = Query { query: "firefox".to_string() };
|
||||
assert_eq!(serde_json::to_string(&q).unwrap(), r#"{"query":"firefox"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_spawn_action() {
|
||||
let json = r#"[{"id":"1","title":"Firefox","score":80,"action":{"type":"SpawnProcess","cmd":"firefox"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].id, "1");
|
||||
assert_eq!(results[0].title, "Firefox");
|
||||
assert_eq!(results[0].score, 80);
|
||||
assert!(matches!(&results[0].action, ExternalAction::SpawnProcess { cmd } if cmd == "firefox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_copy_action() {
|
||||
let json = r#"[{"id":"c","title":"= 4","score":90,"action":{"type":"CopyToClipboard","text":"4"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(&results[0].action, ExternalAction::CopyToClipboard { text } if text == "4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_open_path_action() {
|
||||
let json = r#"[{"id":"f","title":"/home/user","score":50,"action":{"type":"OpenPath","path":"/home/user"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(&results[0].action, ExternalAction::OpenPath { path } if path == "/home/user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_optional_fields() {
|
||||
let json = r#"[{"id":"x","title":"X","score":10,"description":"desc","icon":"/icon.png","action":{"type":"SpawnProcess","cmd":"x"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(results[0].description.as_deref(), Some("desc"));
|
||||
assert_eq!(results[0].icon.as_deref(), Some("/icon.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_missing_optional_fields() {
|
||||
let json = r#"[{"id":"x","title":"X","score":10,"action":{"type":"SpawnProcess","cmd":"x"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert!(results[0].description.is_none());
|
||||
assert!(results[0].icon.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_json_is_err() {
|
||||
assert!(serde_json::from_str::<Vec<ExternalResult>>("not json").is_err());
|
||||
}
|
||||
|
||||
// Unused import suppression for Arc (used only in production code path)
|
||||
fn _assert_send_sync() {
|
||||
fn check<T: Send + Sync>() {}
|
||||
check::<ExternalPlugin>();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ egui = ["dep:k-launcher-ui-egui"]
|
||||
iced = { workspace = true }
|
||||
k-launcher-config = { path = "../k-launcher-config" }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
k-launcher-plugin-host = { path = "../k-launcher-plugin-host" }
|
||||
k-launcher-os-bridge = { path = "../k-launcher-os-bridge" }
|
||||
k-launcher-ui = { path = "../k-launcher-ui" }
|
||||
k-launcher-ui-egui = { path = "../k-launcher-ui-egui", optional = true }
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use k_launcher_kernel::Kernel;
|
||||
use k_launcher_os_bridge::UnixAppLauncher;
|
||||
use k_launcher_plugin_host::ExternalPlugin;
|
||||
use plugin_apps::{AppsPlugin, frecency::FrecencyStore};
|
||||
#[cfg(target_os = "linux")]
|
||||
use plugin_apps::linux::FsDesktopEntrySource;
|
||||
@@ -21,6 +22,9 @@ fn main() -> iced::Result {
|
||||
if cfg.plugins.apps {
|
||||
plugins.push(Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)));
|
||||
}
|
||||
for ext in &cfg.plugins.external {
|
||||
plugins.push(Arc::new(ExternalPlugin::new(&ext.name, &ext.path, ext.args.clone())));
|
||||
}
|
||||
|
||||
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> =
|
||||
Arc::new(Kernel::new(plugins, cfg.search.max_results));
|
||||
|
||||
@@ -5,7 +5,7 @@ pub mod linux;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult};
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, ResultId, ResultTitle, Score, SearchResult};
|
||||
|
||||
use crate::frecency::FrecencyStore;
|
||||
|
||||
@@ -144,7 +144,7 @@ pub(crate) fn humanize_category(s: &str) -> String {
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for AppsPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
fn name(&self) -> &str {
|
||||
"apps"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult};
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, ResultId, ResultTitle, Score, SearchResult};
|
||||
|
||||
pub struct CalcPlugin;
|
||||
|
||||
@@ -26,7 +26,7 @@ fn should_eval(query: &str) -> bool {
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for CalcPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
fn name(&self) -> &str {
|
||||
"calc"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult};
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, ResultId, ResultTitle, Score, SearchResult};
|
||||
|
||||
pub struct CmdPlugin;
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Default for CmdPlugin {
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for CmdPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
fn name(&self) -> &str {
|
||||
"cmd"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ mod platform;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult};
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, ResultId, ResultTitle, Score, SearchResult};
|
||||
|
||||
pub struct FilesPlugin;
|
||||
|
||||
@@ -32,7 +32,7 @@ fn expand_query(query: &str) -> Option<String> {
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for FilesPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
fn name(&self) -> &str {
|
||||
"files"
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ calc = true # math expression evaluator
|
||||
cmd = true # shell command runner (> prefix)
|
||||
files = true # filesystem browser (/ or ~/ prefix)
|
||||
apps = true # XDG application launcher
|
||||
|
||||
# External (dynamic) plugins — repeat block for each plugin
|
||||
[[plugins.external]]
|
||||
name = "my-plugin" # display name / identifier
|
||||
path = "/path/to/my-plugin" # path to executable
|
||||
args = [] # optional extra arguments
|
||||
```
|
||||
|
||||
## RGBA Format
|
||||
|
||||
@@ -1,10 +1,102 @@
|
||||
# Plugin Development
|
||||
|
||||
Plugins are Rust crates that implement the `Plugin` trait from `k-launcher-kernel`. They run concurrently — the kernel fans out every query to all enabled plugins and merges results by score.
|
||||
Plugins are queried concurrently — the kernel fans out every search to all enabled plugins and merges results by score.
|
||||
|
||||
> Note: plugins are compiled into the binary at build time. There is no dynamic loading support yet.
|
||||
There are two kinds of plugins:
|
||||
|
||||
## Step-by-Step
|
||||
- **External plugins** — executables that speak a JSON protocol over stdin/stdout. Any language, no compilation required. Recommended for community plugins.
|
||||
- **Built-in plugins** — Rust crates compiled into the binary. For performance-critical or tightly integrated plugins.
|
||||
|
||||
---
|
||||
|
||||
## External Plugins
|
||||
|
||||
An external plugin is any executable that:
|
||||
|
||||
1. Reads a JSON object from stdin (one line per query)
|
||||
2. Writes a JSON array of results to stdout (one line per response)
|
||||
|
||||
### Protocol
|
||||
|
||||
**Input** (one line, newline-terminated):
|
||||
```json
|
||||
{"query": "firefox"}
|
||||
```
|
||||
|
||||
**Output** (one line, newline-terminated):
|
||||
```json
|
||||
[{"id":"app-firefox","title":"Firefox","score":80,"description":"Web Browser","action":{"type":"SpawnProcess","cmd":"firefox"}}]
|
||||
```
|
||||
|
||||
The process is kept alive between queries — do **not** exit after each response.
|
||||
|
||||
### Action types
|
||||
|
||||
| `"type"` | Extra fields | Behavior |
|
||||
|----------|-------------|---------|
|
||||
| `SpawnProcess` | `"cmd"` | Launch process directly |
|
||||
| `CopyToClipboard` | `"text"` | Copy text to clipboard |
|
||||
| `OpenPath` | `"path"` | Open file/dir with xdg-open |
|
||||
|
||||
### Optional result fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `description` | `string` | Secondary line shown below title |
|
||||
| `icon` | `string` | Icon path (future use) |
|
||||
|
||||
### Enabling an external plugin
|
||||
|
||||
In `~/.config/k-launcher/config.toml`:
|
||||
|
||||
```toml
|
||||
[[plugins.external]]
|
||||
name = "my-plugin"
|
||||
path = "/usr/lib/k-launcher/plugins/my-plugin"
|
||||
args = [] # optional
|
||||
```
|
||||
|
||||
Multiple `[[plugins.external]]` blocks are supported.
|
||||
|
||||
### Example: shell plugin
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# A plugin that greets the user.
|
||||
while IFS= read -r line; do
|
||||
query=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin)['query'])")
|
||||
if [[ "$query" == hello* ]]; then
|
||||
echo '[{"id":"greet","title":"Hello, World!","score":80,"action":{"type":"CopyToClipboard","text":"Hello, World!"}}]'
|
||||
else
|
||||
echo '[]'
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Example: Python plugin
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import sys, json
|
||||
|
||||
for line in sys.stdin:
|
||||
query = json.loads(line)["query"]
|
||||
results = []
|
||||
if query.startswith("hello"):
|
||||
results.append({
|
||||
"id": "greet",
|
||||
"title": "Hello, World!",
|
||||
"score": 80,
|
||||
"action": {"type": "CopyToClipboard", "text": "Hello, World!"},
|
||||
})
|
||||
print(json.dumps(results), flush=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Built-in Plugins (compiled-in)
|
||||
|
||||
Built-in plugins implement the `Plugin` trait from `k-launcher-kernel` as Rust crates compiled into the binary.
|
||||
|
||||
### 1. Create a new crate in the workspace
|
||||
|
||||
@@ -38,9 +130,7 @@ async-trait = "0.1"
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
use k_launcher_kernel::{
|
||||
LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult,
|
||||
};
|
||||
use k_launcher_kernel::{LaunchAction, Plugin, ResultId, ResultTitle, Score, SearchResult};
|
||||
|
||||
pub struct HelloPlugin;
|
||||
|
||||
@@ -52,7 +142,7 @@ impl HelloPlugin {
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for HelloPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
fn name(&self) -> &str {
|
||||
"hello"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user