|
| 1 | +// SPDX-License-Identifier: GPL-3.0-only |
| 2 | +// Copyright © 2021 System76 |
| 3 | + |
| 4 | +use fork::{daemon, Fork}; |
| 5 | +use pop_launcher::{Indice, PluginResponse, PluginSearchResult}; |
| 6 | +use pop_launcher_toolkit::plugin_trait::{async_trait, PluginExt}; |
| 7 | +use std::io; |
| 8 | +use std::os::unix::process::CommandExt; |
| 9 | +use std::path::PathBuf; |
| 10 | +use std::process::{exit, Command}; |
| 11 | + |
| 12 | +// This example demonstrate how to write a pop-launcher plugin using the `PluginExt` helper trait. |
| 13 | +// We are going to build a plugin to display man pages descriptions and open them on activation. |
| 14 | +// To do that we will use `whatis`, a command that searches the manual page names and displays their descriptions. |
| 15 | + |
| 16 | +// For instance running `whatis git` would output the following : |
| 17 | +// ``` |
| 18 | +// git (1) - the stupid content tracker |
| 19 | +// Git (3pm) - Perl interface to the Git version control system |
| 20 | +// ``` |
| 21 | + |
| 22 | +// Run `whatis` and split the output line to get a man page name and its description |
| 23 | +fn run_whatis(arg: &str) -> io::Result<Vec<(String, String)>> { |
| 24 | + let output = Command::new("whatis").arg(arg).output()?.stdout; |
| 25 | + |
| 26 | + Ok(String::from_utf8_lossy(&output) |
| 27 | + .lines() |
| 28 | + .filter_map(|entry| entry.split_once('-')) |
| 29 | + .map(|(man_page, description)| { |
| 30 | + (man_page.trim().to_string(), description.trim().to_string()) |
| 31 | + }) |
| 32 | + .collect()) |
| 33 | +} |
| 34 | + |
| 35 | +// Open a new terminal and run `man` with the provided man page name |
| 36 | +fn open_man_page(arg: &str) -> io::Result<()> { |
| 37 | + // |
| 38 | + let (terminal, targ) = detect_terminal(); |
| 39 | + |
| 40 | + if let Ok(Fork::Child) = daemon(true, false) { |
| 41 | + Command::new(terminal).args(&[targ, "man", arg]).exec(); |
| 42 | + } |
| 43 | + |
| 44 | + exit(0); |
| 45 | +} |
| 46 | + |
| 47 | +// A helper function to detect the user default terminal. |
| 48 | +// If the terminal is not found, fallback to `gnome-termninal |
| 49 | +fn detect_terminal() -> (PathBuf, &'static str) { |
| 50 | + use std::fs::read_link; |
| 51 | + |
| 52 | + const SYMLINK: &str = "/usr/bin/x-terminal-emulator"; |
| 53 | + |
| 54 | + if let Ok(found) = read_link(SYMLINK) { |
| 55 | + return (read_link(&found).unwrap_or(found), "-e"); |
| 56 | + } |
| 57 | + |
| 58 | + (PathBuf::from("/usr/bin/gnome-terminal"), "--") |
| 59 | +} |
| 60 | + |
| 61 | +// Our plugin struct, holding the search results. |
| 62 | +#[derive(Default)] |
| 63 | +pub struct WhatIsPlugin { |
| 64 | + entries: Vec<(String, String)>, |
| 65 | +} |
| 66 | + |
| 67 | +// This is the main part of our plugin, defining how it will react to pop-launcher requests. |
| 68 | +#[async_trait] |
| 69 | +impl PluginExt for WhatIsPlugin { |
| 70 | + // Define the name of our plugin, this is mainly used to write log |
| 71 | + // emitted by tracing macros to `$HOME/.local/state/pop-launcher/wathis.log. |
| 72 | + fn name(&self) -> &str { |
| 73 | + "whatis" |
| 74 | + } |
| 75 | + |
| 76 | + // Define how the plugin will react to pop-launcher search requests. |
| 77 | + // Note that we need to send `PluginResponse::Finished` once we are done, |
| 78 | + // otherwise pop-launcher will not display our search results and wait forever. |
| 79 | + async fn search(&mut self, query: &str) { |
| 80 | + // pop-launcher will only dispatch query matching the regex defined in our `plugin.ron` |
| 81 | + // file, can safely strip it out. |
| 82 | + let query = query.strip_prefix("whatis "); |
| 83 | + |
| 84 | + if let Some(query) = query { |
| 85 | + // Whenever we get a new query, pass the query to the `whatis` helper function |
| 86 | + // and update our plugin entries with the result. |
| 87 | + match run_whatis(query) { |
| 88 | + Ok(entries) => self.entries = entries, |
| 89 | + // If we need to produce log, we use the tracing macros. |
| 90 | + Err(err) => tracing::error!("Error while running 'whatis' command: {err}"), |
| 91 | + } |
| 92 | + |
| 93 | + // Now we send our entries back to the launcher. We also need a way to find our entry on activation |
| 94 | + // requests, here we use the entry index as an idendifier. |
| 95 | + for (idx, (cmd, description)) in self.entries.iter().enumerate() { |
| 96 | + self.respond_with(PluginResponse::Append(PluginSearchResult { |
| 97 | + id: idx as u32, |
| 98 | + name: format!("{cmd} - {description}"), |
| 99 | + keywords: None, |
| 100 | + description: description.clone(), |
| 101 | + icon: None, |
| 102 | + exec: None, |
| 103 | + window: None, |
| 104 | + })) |
| 105 | + .await; |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + // Tell pop-launcher we are done with this search request. |
| 110 | + self.respond_with(PluginResponse::Finished).await; |
| 111 | + } |
| 112 | + |
| 113 | + // pop-launcher is asking for an entry activation. |
| 114 | + async fn activate(&mut self, id: Indice) { |
| 115 | + // First we try to find the requested entry in the plugin struct |
| 116 | + if let Some((command, _description)) = self.entries.get(id as usize) { |
| 117 | + // Open a new terminal with the requested man page and exit the plugin. |
| 118 | + if let Err(err) = open_man_page(command) { |
| 119 | + tracing::error!("Failed to open man page for '{command}': {err}") |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +// Now we just need to call the `run` function to start our plugin. |
| 126 | +// You can test it by writing request to its stdin. |
| 127 | +// For instance issuing a search request : `{ "Search": "whatis git" }`, |
| 128 | +// or activate one of the search results : `{ "Activate": 0 }` |
| 129 | +#[tokio::main(flavor = "current_thread")] |
| 130 | +async fn main() { |
| 131 | + WhatIsPlugin::default().run().await |
| 132 | +} |
0 commit comments