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, out: smol::Unblock, } 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::(); #[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::(); 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, name: String, icon: Option, path: PathBuf, keywords: Vec, description: String, } async fn load_from(path: &Path, path_tx: &Sender, tx: Sender) { 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(); } } }