211 lines
6.4 KiB
Rust
211 lines
6.4 KiB
Rust
use futures_lite::*;
|
|
use pop_launcher::*;
|
|
use crate::send;
|
|
use smol::process::{ChildStdout, Command, Stdio};
|
|
use std::borrow::Cow;
|
|
use std::cell::Cell;
|
|
use std::io;
|
|
use std::path::{Path, PathBuf};
|
|
use std::rc::Rc;
|
|
|
|
enum Event {
|
|
Activate(u32),
|
|
Search(String),
|
|
}
|
|
|
|
pub async fn main() {
|
|
let (event_tx, event_rx) = flume::unbounded::<Event>();
|
|
|
|
// Channel for cancelling searches that are in progress.
|
|
let (interrupt_tx, interrupt_rx) = flume::bounded::<()>(0);
|
|
|
|
// Indicates if a search is being performed in the background.
|
|
let active = Rc::new(Cell::new(false));
|
|
|
|
let mut app = SearchContext {
|
|
search_results: Vec::with_capacity(128),
|
|
active: active.clone(),
|
|
interrupt_rx,
|
|
out: async_stdout(),
|
|
};
|
|
|
|
// Manages the external process, tracks search results, and executes activate requests
|
|
let search_handler = async move {
|
|
while let Ok(search) = event_rx.recv_async().await {
|
|
match search {
|
|
Event::Activate(id) => {
|
|
if let Some(selection) = app.search_results.get(id as usize) {
|
|
let path = selection.clone();
|
|
let handle = smol::spawn(async move {
|
|
xdg_open(&path).await;
|
|
});
|
|
|
|
handle.detach();
|
|
|
|
send(&mut app.out, PluginResponse::Close).await;
|
|
}
|
|
}
|
|
|
|
Event::Search(search) => app.search(search).await,
|
|
}
|
|
}
|
|
};
|
|
|
|
// Forwards requests to the search handler, and performs an interrupt as necessary.
|
|
let request_handler = async move {
|
|
let interrupt = || async {
|
|
if active.get() && !interrupt_tx.is_full() {
|
|
tracing::debug!("sending interrupt");
|
|
let _ = interrupt_tx.send_async(()).await;
|
|
}
|
|
};
|
|
|
|
let mut requests = json_input_stream(async_stdin());
|
|
|
|
while let Some(result) = requests.next().await {
|
|
match result {
|
|
Ok(request) => match request {
|
|
// Launch the default application with the selected file
|
|
Request::Activate(id) => {
|
|
event_tx.send_async(Event::Activate(id)).await?;
|
|
}
|
|
|
|
// Interrupt any active searches being performed
|
|
Request::Interrupt => interrupt().await,
|
|
|
|
// Schedule a new search process to be launched
|
|
Request::Search(query) => {
|
|
interrupt().await;
|
|
|
|
let query = match query.find(' ') {
|
|
Some(pos) => query[pos..].trim_start(),
|
|
None => &query,
|
|
};
|
|
|
|
event_tx.send_async(Event::Search(query.to_owned())).await?;
|
|
active.set(true);
|
|
}
|
|
|
|
_ => (),
|
|
},
|
|
|
|
Err(why) => {
|
|
tracing::error!("malformed JSON input: {}", why);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok::<(), flume::SendError<Event>>(())
|
|
};
|
|
|
|
let _ = future::zip(request_handler, search_handler).await;
|
|
}
|
|
|
|
/// Maintains state for search requests
|
|
struct SearchContext {
|
|
pub active: Rc<Cell<bool>>,
|
|
pub interrupt_rx: flume::Receiver<()>,
|
|
pub out: smol::Unblock<io::Stdout>,
|
|
pub search_results: Vec<PathBuf>,
|
|
}
|
|
|
|
impl SearchContext {
|
|
/// Appends a new search result to the context.
|
|
async fn append(&mut self, id: u32, line: String) {
|
|
let name = line
|
|
.rfind('/')
|
|
.map(|pos| line[pos + 1..].to_owned())
|
|
.unwrap_or_else(|| line.clone());
|
|
|
|
let description = ["~/", line.as_str()].concat();
|
|
|
|
let path = PathBuf::from(line);
|
|
|
|
let response = PluginResponse::Append(PluginSearchResult {
|
|
id,
|
|
description,
|
|
name,
|
|
icon: Some(IconSource::Mime(if path.is_dir() {
|
|
Cow::Borrowed("inode/directory")
|
|
} else if let Some(guess) = new_mime_guess::from_path(&path).first() {
|
|
Cow::Owned(guess.essence_str().to_owned())
|
|
} else {
|
|
Cow::Borrowed("text/plain")
|
|
})),
|
|
..Default::default()
|
|
});
|
|
|
|
send(&mut self.out, response).await;
|
|
self.search_results.push(path);
|
|
}
|
|
|
|
/// Submits the query to `fdfind` and actively monitors the search results while handling interrupts.
|
|
async fn search(&mut self, search: String) {
|
|
tracing::debug!("searching for {}", search);
|
|
|
|
let mut stdout = match query(&search).await {
|
|
Ok(stdout) => futures_lite::io::BufReader::new(stdout).lines(),
|
|
Err(why) => {
|
|
tracing::error!("failed to spawn fdfind process: {}", why);
|
|
self.active.set(false);
|
|
return;
|
|
}
|
|
};
|
|
|
|
self.search_results.clear();
|
|
let mut id = 0;
|
|
let mut append;
|
|
|
|
'stream: loop {
|
|
let interrupt = async {
|
|
let _ = self.interrupt_rx.recv_async().await;
|
|
None
|
|
};
|
|
|
|
match interrupt.or(stdout.next()).await {
|
|
Some(result) => match result {
|
|
Ok(line) => append = line,
|
|
Err(why) => {
|
|
tracing::error!("error on stdout line read: {}", why);
|
|
break 'stream;
|
|
}
|
|
},
|
|
|
|
None => break 'stream,
|
|
}
|
|
|
|
self.append(id, append).await;
|
|
|
|
id += 1;
|
|
|
|
if id == 10 {
|
|
break 'stream;
|
|
}
|
|
}
|
|
|
|
send(&mut self.out, PluginResponse::Finished).await;
|
|
}
|
|
}
|
|
|
|
/// Submits the search query to `fdfind`, and returns its stdout pipe.
|
|
async fn query(arg: &str) -> io::Result<ChildStdout> {
|
|
let mut child = Command::new("fdfind")
|
|
.arg(arg)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null())
|
|
.spawn()?;
|
|
|
|
match child.stdout.take() {
|
|
Some(stdout) => Ok(stdout),
|
|
None => Err(io::Error::new(
|
|
io::ErrorKind::BrokenPipe,
|
|
"stdout pipe is missing",
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Launches a file with its default appplication via `xdg-open`.
|
|
async fn xdg_open(file: &Path) {
|
|
let _ = Command::new("xdg-open").arg(file).spawn();
|
|
}
|