From 3f8253ad76bb6e7ca2032b0f2a3b8772da634140 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Wed, 18 May 2022 11:27:24 +0200 Subject: [PATCH] docs(toolkit): add a plugin example --- Cargo.lock | 2 + toolkit/Cargo.toml | 10 +- toolkit/examples/man-pages-plugin.rs | 132 +++++++++++++++++++++++++++ toolkit/examples/plugin.ron | 12 +++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 toolkit/examples/man-pages-plugin.rs create mode 100644 toolkit/examples/plugin.ron diff --git a/Cargo.lock b/Cargo.lock index 92800b0..64a2baa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,10 +1148,12 @@ version = "0.1.0" dependencies = [ "async-trait", "dirs 4.0.0", + "fork", "futures", "pop-launcher", "pop-launcher-plugins", "pop-launcher-service", + "tokio", "tracing", "tracing-subscriber", ] diff --git a/toolkit/Cargo.toml b/toolkit/Cargo.toml index c0b250a..65f5efc 100644 --- a/toolkit/Cargo.toml +++ b/toolkit/Cargo.toml @@ -14,4 +14,12 @@ async-trait = "0.1.53" tracing = "0.1.32" tracing-subscriber = { version = "0.3.9", default-features = false, features = ["std", "fmt", "env-filter"] } dirs = "4.0.0" -futures = "0.3.21" \ No newline at end of file +futures = "0.3.21" + +[dev-dependencies] +tokio = { version = "1", features = [ "rt" ] } +fork = "0.1.19" + +[[example]] +name = "man-pages-plugin" +path = "examples/man-pages-plugin.rs" \ No newline at end of file diff --git a/toolkit/examples/man-pages-plugin.rs b/toolkit/examples/man-pages-plugin.rs new file mode 100644 index 0000000..f348cf7 --- /dev/null +++ b/toolkit/examples/man-pages-plugin.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright © 2021 System76 + +use fork::{daemon, Fork}; +use pop_launcher::{Indice, PluginResponse, PluginSearchResult}; +use pop_launcher_toolkit::plugin_trait::{async_trait, PluginExt}; +use std::io; +use std::os::unix::process::CommandExt; +use std::path::PathBuf; +use std::process::{exit, Command}; + +// This example demonstrate how to write a pop-launcher plugin using the `PluginExt` helper trait. +// We are going to build a plugin to display man pages descriptions and open them on activation. +// To do that we will use `whatis`, a command that searches the manual page names and displays their descriptions. + +// For instance running `whatis git` would output the following : +// ``` +// git (1) - the stupid content tracker +// Git (3pm) - Perl interface to the Git version control system +// ``` + +// Run `whatis` and split the output line to get a man page name and its description +fn run_whatis(arg: &str) -> io::Result> { + let output = Command::new("whatis").arg(arg).output()?.stdout; + + Ok(String::from_utf8_lossy(&output) + .lines() + .filter_map(|entry| entry.split_once('-')) + .map(|(man_page, description)| { + (man_page.trim().to_string(), description.trim().to_string()) + }) + .collect()) +} + +// Open a new terminal and run `man` with the provided man page name +fn open_man_page(arg: &str) -> io::Result<()> { + // + let (terminal, targ) = detect_terminal(); + + if let Ok(Fork::Child) = daemon(true, false) { + Command::new(terminal).args(&[targ, "man", arg]).exec(); + } + + exit(0); +} + +// A helper function to detect the user default terminal. +// If the terminal is not found, fallback to `gnome-termninal +fn detect_terminal() -> (PathBuf, &'static str) { + use std::fs::read_link; + + const SYMLINK: &str = "/usr/bin/x-terminal-emulator"; + + if let Ok(found) = read_link(SYMLINK) { + return (read_link(&found).unwrap_or(found), "-e"); + } + + (PathBuf::from("/usr/bin/gnome-terminal"), "--") +} + +// Our plugin struct, holding the search results. +#[derive(Default)] +pub struct WhatIsPlugin { + entries: Vec<(String, String)>, +} + +// This is the main part of our plugin, defining how it will react to pop-launcher requests. +#[async_trait] +impl PluginExt for WhatIsPlugin { + // Define the name of our plugin, this is mainly used to write log + // emitted by tracing macros to `$HOME/.local/state/pop-launcher/wathis.log. + fn name(&self) -> &str { + "whatis" + } + + // Define how the plugin will react to pop-launcher search requests. + // Note that we need to send `PluginResponse::Finished` once we are done, + // otherwise pop-launcher will not display our search results and wait forever. + async fn search(&mut self, query: &str) { + // pop-launcher will only dispatch query matching the regex defined in our `plugin.ron` + // file, can safely strip it out. + let query = query.strip_prefix("whatis "); + + if let Some(query) = query { + // Whenever we get a new query, pass the query to the `whatis` helper function + // and update our plugin entries with the result. + match run_whatis(query) { + Ok(entries) => self.entries = entries, + // If we need to produce log, we use the tracing macros. + Err(err) => tracing::error!("Error while running 'whatis' command: {err}"), + } + + // Now we send our entries back to the launcher. We also need a way to find our entry on activation + // requests, here we use the entry index as an idendifier. + for (idx, (cmd, description)) in self.entries.iter().enumerate() { + self.respond_with(PluginResponse::Append(PluginSearchResult { + id: idx as u32, + name: format!("{cmd} - {description}"), + keywords: None, + description: description.clone(), + icon: None, + exec: None, + window: None, + })) + .await; + } + } + + // Tell pop-launcher we are done with this search request. + self.respond_with(PluginResponse::Finished).await; + } + + // pop-launcher is asking for an entry activation. + async fn activate(&mut self, id: Indice) { + // First we try to find the requested entry in the plugin struct + if let Some((command, _description)) = self.entries.get(id as usize) { + // Open a new terminal with the requested man page and exit the plugin. + if let Err(err) = open_man_page(command) { + tracing::error!("Failed to open man page for '{command}': {err}") + } + } + } +} + +// Now we just need to call the `run` function to start our plugin. +// You can test it by writing request to its stdin. +// For instance issuing a search request : `{ "Search": "whatis git" }`, +// or activate one of the search results : `{ "Activate": 0 }` +#[tokio::main(flavor = "current_thread")] +async fn main() { + WhatIsPlugin::default().run().await +} diff --git a/toolkit/examples/plugin.ron b/toolkit/examples/plugin.ron new file mode 100644 index 0000000..5a73a37 --- /dev/null +++ b/toolkit/examples/plugin.ron @@ -0,0 +1,12 @@ +( + name: "Find man pages", + description: "Syntax: { whatis }\nExample: whatis git", + query: ( + regex: "^(whatis ).+", + help: "whatis", + isolate: true, + no_sort: true, + ), + bin: (path: "man-pages-plugin"), + icon: Name("org.gnome.Documents-symbolic"), +)