pop-launcher/plugins/src/scripts/mod.rs
2021-08-16 13:26:52 +02:00

208 lines
6.5 KiB
Rust

use crate::*;
use pop_launcher::*;
use flume::Sender;
use futures_lite::{AsyncBufReadExt, StreamExt};
use std::os::unix::process::CommandExt;
use std::{
io,
path::{Path, PathBuf},
process::{Command, Stdio},
};
const LOCAL_PATH: &str = ".local/share/pop-launcher/scripts";
const SYSTEM_ADMIN_PATH: &str = "/etc/pop-launcher/scripts";
const DISTRIBUTION_PATH: &str = "/usr/lib/pop-launcher/scripts";
pub async fn main() {
let mut requests = json_input_stream(async_stdin());
let mut app = App::new();
app.reload().await;
while let Some(result) = requests.next().await {
match result {
Ok(response) => match response {
Request::Activate(id) => app.activate(id).await,
Request::Search(query) => app.search(&query).await,
Request::Exit => break,
_ => (),
},
Err(why) => {
tracing::error!("malformed JSON input: {}", why);
}
}
}
}
pub struct App {
scripts: Vec<ScriptInfo>,
out: smol::Unblock<io::Stdout>,
}
impl App {
fn new() -> Self {
App {
scripts: Vec::with_capacity(16),
out: async_stdout(),
}
}
async fn activate(&mut self, id: u32) {
use fork::{daemon, Fork};
if let Ok(Fork::Child) = daemon(true, true) {
if let Some(script) = self.scripts.get(id as usize) {
let interpreter = script.interpreter.as_deref().unwrap_or("sh");
let why = dbg!(Command::new(interpreter).arg(script.path.as_os_str()))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.exec();
tracing::error!("failed to exec: {}", why);
std::process::exit(1);
}
}
send(&mut self.out, PluginResponse::Close).await;
}
async fn reload(&mut self) {
let (path_tx, path_rx) = flume::unbounded::<PathBuf>();
#[allow(deprecated)]
let _ = path_tx.send(
std::env::home_dir()
.expect("user does not have home dir")
.join(LOCAL_PATH),
);
let _ = path_tx.send(Path::new(SYSTEM_ADMIN_PATH).to_owned());
let _ = path_tx.send(Path::new(DISTRIBUTION_PATH).to_owned());
let (tx, rx) = flume::unbounded::<ScriptInfo>();
let script_sender = async move {
while let Ok(path) = path_rx.recv_async().await {
load_from(&path, &path_tx, tx.clone()).await;
}
};
let script_receiver = async {
while let Ok(script) = rx.recv_async().await {
tracing::debug!("appending script: {:?}", script);
self.scripts.push(script);
}
};
futures_lite::future::zip(script_sender, script_receiver).await;
}
async fn search(&mut self, query: &str) {
let &mut Self {
ref scripts,
ref mut out,
..
} = self;
for (id, script) in scripts.iter().enumerate() {
let should_include = script.name.to_ascii_lowercase().contains(query)
|| script.description.to_ascii_lowercase().contains(query)
|| script.keywords.iter().any(|k| k.contains(query));
if should_include {
send(
out,
PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: script.name.clone(),
description: script.description.clone(),
icon: script
.icon
.as_ref()
.map(|icon| IconSource::Name(icon.clone().into())),
keywords: Some(script.keywords.clone()),
..Default::default()
}),
)
.await;
}
}
send(out, PluginResponse::Finished).await;
}
}
#[derive(Debug, Default)]
struct ScriptInfo {
interpreter: Option<String>,
name: String,
icon: Option<String>,
path: PathBuf,
keywords: Vec<String>,
description: String,
}
async fn load_from(path: &Path, path_tx: &Sender<PathBuf>, tx: Sender<ScriptInfo>) {
if let Ok(directory) = path.read_dir() {
for entry in directory.filter_map(Result::ok) {
let tx = tx.clone();
let path = entry.path();
if path.is_dir() {
path_tx.send_async(path);
continue;
}
smol::spawn(async move {
let mut file = match smol::fs::File::open(&path).await {
Ok(file) => smol::io::BufReader::new(file).lines(),
Err(why) => {
tracing::error!("cannot open script at {}: {}", path.display(), why);
return;
}
};
let mut info = ScriptInfo {
path,
..Default::default()
};
let mut first = true;
while let Some(Ok(line)) = file.next().await {
if !line.starts_with('#') {
break;
}
let line = line[1..].trim();
if first {
first = false;
if let Some(interpreter) = line.strip_prefix('!') {
info.interpreter = Some(interpreter.to_owned());
continue;
}
}
if let Some(stripped) = line.strip_prefix("name:") {
info.name = stripped.trim_start().to_owned();
} else if let Some(stripped) = line.strip_prefix("description:") {
info.description = stripped.trim_start().to_owned();
} else if let Some(stripped) = line.strip_prefix("icon:") {
info.icon = Some(stripped.trim_start().to_owned());
} else if let Some(stripped) = line.strip_prefix("keywords:") {
info.keywords =
stripped.trim_start().split(' ').map(String::from).collect();
}
}
let _ = tx.send_async(info).await;
})
.detach();
}
}
}