feat: Support context options
This commit is contained in:
parent
a852584a0d
commit
dae8108cb1
8 changed files with 220 additions and 37 deletions
48
README.md
48
README.md
|
|
@ -38,8 +38,12 @@ If you are writing a frontend, you are sending these events to the pop-launcher
|
|||
pub enum Request {
|
||||
/// Activate on the selected item
|
||||
Activate(Indice),
|
||||
/// Activate a context item on an item.
|
||||
ActivateContext { id: Indice, context: Indice },
|
||||
/// Perform a tab completion from the selected item
|
||||
Complete(Indice),
|
||||
/// Request for any context options this result may have.
|
||||
Context(Indice),
|
||||
/// Request to end the service
|
||||
Exit,
|
||||
/// Requests to cancel any active searches
|
||||
|
|
@ -54,7 +58,9 @@ pub enum Request {
|
|||
#### JSON Equivalent
|
||||
|
||||
- `{ "Activate": number }`
|
||||
- `{ "ActivateContext": { "id": number, "context": id }}`
|
||||
- `{ "Complete": number }`
|
||||
- `{ "Context": number }`
|
||||
- `"Exit"`
|
||||
- `"Interrupt"`
|
||||
- `{ "Quit": number }`
|
||||
|
|
@ -72,8 +78,16 @@ pub enum PluginResponse {
|
|||
Clear,
|
||||
/// Close the launcher
|
||||
Close,
|
||||
// Notifies that a .desktop entry should be launched by the frontend
|
||||
DesktopEntry(PathBuf),
|
||||
// Additional options for launching a certain item
|
||||
Context {
|
||||
id: Indice,
|
||||
options: Vec<ContextOption>,
|
||||
},
|
||||
// Notifies that a .desktop entry should be launched by the frontend.
|
||||
DesktopEntry {
|
||||
path: PathBuf,
|
||||
gpu_preference: GpuPreference,
|
||||
},
|
||||
/// Update the text in the launcher
|
||||
Fill(String),
|
||||
/// Indicoates that a plugin is finished with its queries
|
||||
|
|
@ -86,7 +100,8 @@ pub enum PluginResponse {
|
|||
- `{ "Append": PluginSearchResult }`,
|
||||
- `"Clear"`,
|
||||
- `"Close"`,
|
||||
- `{ "DesktopEntry": string }`
|
||||
- `{ "Context": { "id": number, "options": Array<ContextOption> }}`
|
||||
- `{ "DesktopEntry": { "path": string, "gpu_preference": GpuPreference }}`
|
||||
- `{ "Fill": string }`
|
||||
- `"Finished"`
|
||||
|
||||
|
|
@ -104,6 +119,21 @@ Where `PluginSearchResult` is:
|
|||
}
|
||||
```
|
||||
|
||||
`ContextOption` is:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
```
|
||||
|
||||
`GpuPreference` is:
|
||||
|
||||
```ts
|
||||
"Default" | "NonDefault"
|
||||
```
|
||||
|
||||
And `IconSource` is either:
|
||||
|
||||
- `{ "Name": string }`, where the name is a system icon, or an icon referred to by path
|
||||
|
|
@ -117,8 +147,16 @@ Those implementing frontends should listen for these events:
|
|||
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
|
||||
DesktopEntry(PathBuf),
|
||||
// Additional options for launching a certain item
|
||||
Context {
|
||||
id: Indice,
|
||||
options: Vec<ContextOption>,
|
||||
},
|
||||
// Notifies that a .desktop entry should be launched by the frontend.
|
||||
DesktopEntry {
|
||||
path: PathBuf,
|
||||
gpu_preference: GpuPreference,
|
||||
},
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl PartialEq for Item {
|
|||
}
|
||||
|
||||
pub async fn main() {
|
||||
let mut app = DesktopEntryPlugin::new(async_stdout());
|
||||
let mut app = App::new(async_stdout());
|
||||
app.reload().await;
|
||||
|
||||
let mut requests = json_input_stream(async_stdin());
|
||||
|
|
@ -45,6 +45,10 @@ pub async fn main() {
|
|||
tracing::debug!("received request: {:?}", request);
|
||||
match request {
|
||||
Request::Activate(id) => app.activate(id).await,
|
||||
Request::ActivateContext { id, context } => {
|
||||
app.activate_context(id, context).await
|
||||
}
|
||||
Request::Context(id) => app.context(id).await,
|
||||
Request::Search(query) => app.search(&query).await,
|
||||
Request::Exit => break,
|
||||
_ => (),
|
||||
|
|
@ -58,13 +62,13 @@ pub async fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
struct DesktopEntryPlugin<W> {
|
||||
struct App<W> {
|
||||
entries: Vec<Item>,
|
||||
locale: Option<String>,
|
||||
tx: W,
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> DesktopEntryPlugin<W> {
|
||||
impl<W: AsyncWrite + Unpin> App<W> {
|
||||
fn new(tx: W) -> Self {
|
||||
let lang = std::env::var("LANG").ok();
|
||||
|
||||
|
|
@ -138,9 +142,55 @@ impl<W: AsyncWrite + Unpin> DesktopEntryPlugin<W> {
|
|||
}
|
||||
|
||||
async fn activate(&mut self, id: u32) {
|
||||
tracing::debug!("activate {} from {:?}", id, self.entries);
|
||||
if let Some(entry) = self.entries.get(id as usize) {
|
||||
let response = PluginResponse::DesktopEntry(entry.path.clone());
|
||||
let response = PluginResponse::DesktopEntry {
|
||||
path: entry.path.clone(),
|
||||
gpu_preference: if entry.prefers_non_default_gpu {
|
||||
GpuPreference::NonDefault
|
||||
} else {
|
||||
GpuPreference::Default
|
||||
},
|
||||
};
|
||||
|
||||
send(&mut self.tx, response).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn activate_context(&mut self, id: u32, context: u32) {
|
||||
if let Some(entry) = self.entries.get(id as usize) {
|
||||
let response = match context {
|
||||
0 => PluginResponse::DesktopEntry {
|
||||
path: entry.path.clone(),
|
||||
gpu_preference: if !entry.prefers_non_default_gpu {
|
||||
GpuPreference::NonDefault
|
||||
} else {
|
||||
GpuPreference::Default
|
||||
},
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
|
||||
send(&mut self.tx, response).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn context(&mut self, id: u32) {
|
||||
if let Some(entry) = self.entries.get(id as usize) {
|
||||
let option = ContextOption {
|
||||
id: 0,
|
||||
name: (if entry.prefers_non_default_gpu {
|
||||
"Launch Using Integrated Graphics Card"
|
||||
} else {
|
||||
"Launch Using Discrete Graphics Card"
|
||||
})
|
||||
.to_owned(),
|
||||
};
|
||||
|
||||
let response = PluginResponse::Context {
|
||||
id,
|
||||
options: vec![option],
|
||||
};
|
||||
|
||||
send(&mut self.tx, response).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ pub async fn main() {
|
|||
match request {
|
||||
Ok(request) => match request {
|
||||
Request::Activate(id) => app.activate(id).await,
|
||||
Request::Complete(_) | Request::Interrupt => (),
|
||||
Request::Quit(_id) => (),
|
||||
Request::Search(query) => app.search(&query).await,
|
||||
Request::Exit => break,
|
||||
_ => (),
|
||||
},
|
||||
Err(why) => {
|
||||
tracing::error!("malformed JSON request: {}", why);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use postage::prelude::*;
|
|||
use regex::Regex;
|
||||
use slab::Slab;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
collections::{HashMap, HashSet},
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
|
|
@ -34,6 +34,7 @@ pub async fn main() {
|
|||
|
||||
pub struct Service<O> {
|
||||
active_search: Vec<(PluginKey, PluginSearchResult)>,
|
||||
associated_list: HashMap<Indice, Indice>,
|
||||
awaiting_results: HashSet<PluginKey>,
|
||||
last_query: String,
|
||||
output: O,
|
||||
|
|
@ -46,6 +47,7 @@ impl<O: Write> Service<O> {
|
|||
pub fn new(output: O) -> Self {
|
||||
Self {
|
||||
active_search: Vec::new(),
|
||||
associated_list: HashMap::new(),
|
||||
awaiting_results: HashSet::new(),
|
||||
last_query: String::new(),
|
||||
output,
|
||||
|
|
@ -105,7 +107,11 @@ impl<O: Write> Service<O> {
|
|||
Request::Search(query) => self.search(query).await,
|
||||
Request::Interrupt => self.interrupt().await,
|
||||
Request::Activate(id) => self.activate(id).await,
|
||||
Request::ActivateContext { id, context } => {
|
||||
self.activate_context(id, context).await
|
||||
}
|
||||
Request::Complete(id) => self.complete(id).await,
|
||||
Request::Context(id) => self.context(id).await,
|
||||
Request::Quit(id) => self.quit(id).await,
|
||||
|
||||
// When requested to exit, the service will forward that
|
||||
|
|
@ -125,10 +131,17 @@ impl<O: Write> Service<O> {
|
|||
PluginResponse::Append(item) => self.append(plugin, item),
|
||||
PluginResponse::Clear => self.clear(),
|
||||
PluginResponse::Close => self.close(),
|
||||
PluginResponse::Context { id, options } => self.context_response(id, options),
|
||||
PluginResponse::Fill(text) => self.fill(text),
|
||||
PluginResponse::Finished => self.finished(plugin).await,
|
||||
PluginResponse::DesktopEntry(path) => {
|
||||
self.respond(&Response::DesktopEntry(path));
|
||||
PluginResponse::DesktopEntry {
|
||||
path,
|
||||
gpu_preference,
|
||||
} => {
|
||||
self.respond(&Response::DesktopEntry {
|
||||
path,
|
||||
gpu_preference,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -185,12 +198,24 @@ impl<O: Write> Service<O> {
|
|||
));
|
||||
}
|
||||
|
||||
async fn activate(&mut self, id: u32) {
|
||||
async fn activate(&mut self, id: Indice) {
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
let _ = plugin.sender_exec().send(Request::Activate(meta.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn activate_context(&mut self, id: Indice, context: Indice) {
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
let _ = plugin
|
||||
.sender_exec()
|
||||
.send(Request::ActivateContext {
|
||||
id: meta.id,
|
||||
context,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn append(&mut self, plugin: PluginKey, append: PluginSearchResult) {
|
||||
self.active_search.push((plugin, append));
|
||||
}
|
||||
|
|
@ -203,12 +228,25 @@ impl<O: Write> Service<O> {
|
|||
self.respond(&Response::Close);
|
||||
}
|
||||
|
||||
async fn complete(&mut self, id: u32) {
|
||||
fn context_response(&mut self, id: Indice, options: Vec<ContextOption>) {
|
||||
if let Some(id) = self.associated_list.get(&id) {
|
||||
let id = *id;
|
||||
self.respond(&Response::Context { id, options });
|
||||
}
|
||||
}
|
||||
|
||||
async fn complete(&mut self, id: Indice) {
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
let _ = plugin.sender_exec().send(Request::Complete(meta.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn context(&mut self, id: Indice) {
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
let _ = plugin.sender_exec().send(Request::Context(meta.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn fill(&mut self, text: String) {
|
||||
self.respond(&Response::Fill(text));
|
||||
}
|
||||
|
|
@ -222,7 +260,7 @@ impl<O: Write> Service<O> {
|
|||
}
|
||||
|
||||
let search_list = self.sort();
|
||||
self.respond(&Response::Update(search_list))
|
||||
self.respond(&Response::Update(search_list));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +272,7 @@ impl<O: Write> Service<O> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn quit(&mut self, id: u32) {
|
||||
async fn quit(&mut self, id: Indice) {
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
let _ = plugin.sender_exec().send(Request::Quit(meta.id)).await;
|
||||
}
|
||||
|
|
@ -342,6 +380,7 @@ impl<O: Write> Service<O> {
|
|||
fn sort(&mut self) -> Vec<SearchResult> {
|
||||
let &mut Self {
|
||||
ref mut active_search,
|
||||
ref mut associated_list,
|
||||
ref mut no_sort,
|
||||
ref last_query,
|
||||
ref plugins,
|
||||
|
|
@ -423,21 +462,25 @@ impl<O: Write> Service<O> {
|
|||
|
||||
let mut windows = Vec::with_capacity(take);
|
||||
let mut non_windows = Vec::with_capacity(take);
|
||||
associated_list.clear();
|
||||
|
||||
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,
|
||||
.map(|(id, (plugin, meta))| {
|
||||
associated_list.insert(meta.id, id as u32);
|
||||
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 {
|
||||
|
|
|
|||
16
service/src/plugins/external/mod.rs
vendored
16
service/src/plugins/external/mod.rs
vendored
|
|
@ -9,7 +9,7 @@ use std::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::{Event, Plugin, PluginResponse, Request};
|
||||
use crate::{Event, Indice, Plugin, PluginResponse, Request};
|
||||
use async_oneshot::oneshot;
|
||||
use futures_lite::{AsyncWriteExt, FutureExt, StreamExt};
|
||||
use postage::mpsc::Sender;
|
||||
|
|
@ -168,14 +168,22 @@ impl ExternalPlugin {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl Plugin for ExternalPlugin {
|
||||
async fn activate(&mut self, id: u32) {
|
||||
async fn activate(&mut self, id: Indice) {
|
||||
let _ = self.query(&Request::Activate(id)).await;
|
||||
}
|
||||
|
||||
async fn complete(&mut self, id: u32) {
|
||||
async fn activate_context(&mut self, id: Indice, context: Indice) {
|
||||
let _ = self.query(&Request::ActivateContext { id, context }).await;
|
||||
}
|
||||
|
||||
async fn complete(&mut self, id: Indice) {
|
||||
let _ = self.query(&Request::Complete(id)).await;
|
||||
}
|
||||
|
||||
async fn context(&mut self, id: Indice) {
|
||||
let _ = self.query(&Request::Context(id)).await;
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
if let Some((_, _, mut trigger)) = self.process.take() {
|
||||
let _ = trigger.send(());
|
||||
|
|
@ -200,7 +208,7 @@ impl Plugin for ExternalPlugin {
|
|||
.await;
|
||||
}
|
||||
}
|
||||
async fn quit(&mut self, id: u32) {
|
||||
async fn quit(&mut self, id: Indice) {
|
||||
let _ = self.query(&Request::Quit(id)).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,10 +58,14 @@ impl Plugin for HelpPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
async fn activate_context(&mut self, _: u32, _: u32) {}
|
||||
|
||||
async fn complete(&mut self, id: u32) {
|
||||
self.activate(id).await
|
||||
}
|
||||
|
||||
async fn context(&mut self, _: u32) {}
|
||||
|
||||
fn exit(&mut self) {}
|
||||
|
||||
async fn interrupt(&mut self) {}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub use self::config::{PluginBinary, PluginConfig, PluginQuery};
|
|||
pub use self::external::ExternalPlugin;
|
||||
pub use self::help::HelpPlugin;
|
||||
|
||||
use crate::{PluginHelp, Request};
|
||||
use crate::{Indice, PluginHelp, Request};
|
||||
use async_trait::async_trait;
|
||||
use postage::mpsc::{Receiver, Sender};
|
||||
use postage::prelude::*;
|
||||
|
|
@ -18,9 +18,13 @@ where
|
|||
Self: Sized + Send,
|
||||
{
|
||||
/// Activate the selected ID from this plugin
|
||||
async fn activate(&mut self, id: u32);
|
||||
async fn activate(&mut self, id: Indice);
|
||||
|
||||
async fn complete(&mut self, id: u32);
|
||||
async fn activate_context(&mut self, id: Indice, context: Indice);
|
||||
|
||||
async fn complete(&mut self, id: Indice);
|
||||
|
||||
async fn context(&mut self, id: Indice);
|
||||
|
||||
fn exit(&mut self);
|
||||
|
||||
|
|
@ -30,7 +34,7 @@ where
|
|||
|
||||
async fn search(&mut self, query: &str);
|
||||
|
||||
async fn quit(&mut self, id: u32);
|
||||
async fn quit(&mut self, id: Indice);
|
||||
|
||||
async fn run(&mut self, mut rx: Receiver<Request>) {
|
||||
while let Some(request) = rx.recv().await {
|
||||
|
|
@ -44,7 +48,11 @@ where
|
|||
Request::Search(query) => self.search(&query).await,
|
||||
Request::Interrupt => self.interrupt().await,
|
||||
Request::Activate(id) => self.activate(id).await,
|
||||
Request::ActivateContext { id, context } => {
|
||||
self.activate_context(id, context).await
|
||||
}
|
||||
Request::Complete(id) => self.complete(id).await,
|
||||
Request::Context(id) => self.context(id).await,
|
||||
Request::Quit(id) => self.quit(id).await,
|
||||
Request::Exit => {
|
||||
self.exit();
|
||||
|
|
|
|||
36
src/lib.rs
36
src/lib.rs
|
|
@ -41,6 +41,18 @@ pub type Generation = u32;
|
|||
/// u32 value defining the indice of a slot.
|
||||
pub type Indice = u32;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ContextOption {
|
||||
pub id: Indice,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum GpuPreference {
|
||||
Default,
|
||||
NonDefault,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub enum IconSource {
|
||||
// Locate by name or path.
|
||||
|
|
@ -60,8 +72,16 @@ pub enum PluginResponse {
|
|||
Clear,
|
||||
/// Close the launcher.
|
||||
Close,
|
||||
// Additional options for launching a certain item
|
||||
Context {
|
||||
id: Indice,
|
||||
options: Vec<ContextOption>,
|
||||
},
|
||||
// Notifies that a .desktop entry should be launched by the frontend.
|
||||
DesktopEntry(PathBuf),
|
||||
DesktopEntry {
|
||||
path: PathBuf,
|
||||
gpu_preference: GpuPreference,
|
||||
},
|
||||
/// Update the text in the launcher.
|
||||
Fill(String),
|
||||
/// Indicoates that a plugin is finished with its queries.
|
||||
|
|
@ -92,8 +112,12 @@ pub struct PluginSearchResult {
|
|||
pub enum Request {
|
||||
/// Activate on the selected item.
|
||||
Activate(Indice),
|
||||
/// Activate a context item on an item.
|
||||
ActivateContext { id: Indice, context: Indice },
|
||||
/// Perform a tab completion from the selected item.
|
||||
Complete(Indice),
|
||||
/// Request for any context options this result may have.
|
||||
Context(Indice),
|
||||
/// Request to end the service.
|
||||
Exit,
|
||||
/// Requests to cancel any active searches.
|
||||
|
|
@ -109,8 +133,16 @@ pub enum Request {
|
|||
pub enum Response {
|
||||
// An operation was performed and the frontend may choose to exit its process.
|
||||
Close,
|
||||
// Additional options for launching a certain item
|
||||
Context {
|
||||
id: Indice,
|
||||
options: Vec<ContextOption>,
|
||||
},
|
||||
// Notifies that a .desktop entry should be launched by the frontend.
|
||||
DesktopEntry(PathBuf),
|
||||
DesktopEntry {
|
||||
path: PathBuf,
|
||||
gpu_preference: GpuPreference,
|
||||
},
|
||||
// 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue