use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use ashlar_native::open_files::{OpenFiles, Opened, from_args};
use ashlar_native::single_instance::Forwarded;
use ashlar_ui::wake::{self, Reason};
pub const OPEN: Reason = Reason("open");
pub(crate) struct Inbox {
files: OpenFiles,
focus: Arc<AtomicBool>,
waker: Arc<OnceLock<wake::Waker>>,
}
impl Default for Inbox {
fn default() -> Self {
let waker: Arc<OnceLock<wake::Waker>> = Arc::default();
let w = Arc::clone(&waker);
let files = OpenFiles::new(Arc::new(move || {
if let Some(w) = w.get() {
w.wake();
}
}));
Self { files, focus: Arc::default(), waker }
}
}
impl Inbox {
pub(crate) fn set_ctx(&self, ctx: &egui::Context) {
let _ = self.waker.set(wake::waker(ctx, OPEN));
}
pub(crate) fn forwarder(&self) -> impl Fn(Forwarded) + Send + 'static {
let sender = self.files.sender();
let focus = Arc::clone(&self.focus);
let waker = Arc::clone(&self.waker);
move |f: Forwarded| {
focus.store(true, Ordering::SeqCst);
let items = from_args(&f.args, &f.cwd);
if items.is_empty() {
if let Some(w) = waker.get() {
w.wake();
}
} else {
sender.send(items);
}
}
}
pub(crate) fn forward(&self, f: Forwarded) {
(self.forwarder())(f);
}
#[cfg(target_os = "macos")]
pub(crate) fn listen_os(&self) {
if let Err(e) = self.files.listen_os() {
log::warn!("open-document events are off: {e}");
}
}
pub(crate) fn drain(&self) -> (Vec<Opened>, bool) {
(self.files.drain(), self.focus.swap(false, Ordering::SeqCst))
}
}
pub(crate) fn known_extension(path: &Path, extensions: &[String]) -> bool {
let Some(ext) = path.extension().and_then(|e| e.to_str()) else { return false };
extensions.iter().any(|k| k.trim_start_matches('.').eq_ignore_ascii_case(ext))
}
pub(crate) fn split_drops(paths: Vec<PathBuf>, extensions: &[String]) -> (Option<PathBuf>, Vec<PathBuf>) {
let mut open = None;
let mut rest = Vec::new();
for p in paths {
if open.is_none() && known_extension(&p, extensions) {
open = Some(p);
} else {
rest.push(p);
}
}
(open, rest)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extensions_match_without_dot_and_case() {
let exts = vec!["note".to_string(), ".TXT".to_string()];
assert!(known_extension(Path::new("/a/b.NOTE"), &exts));
assert!(known_extension(Path::new("/a/b.txt"), &exts));
assert!(!known_extension(Path::new("/a/b.png"), &exts));
assert!(!known_extension(Path::new("/a/note"), &exts));
let (open, rest) = split_drops(vec!["/x.png".into(), "/y.note".into(), "/z.note".into()], &exts);
assert_eq!(open, Some(PathBuf::from("/y.note")));
assert_eq!(rest, vec![PathBuf::from("/x.png"), PathBuf::from("/z.note")]);
}
#[test]
fn a_forward_queues_its_files_and_asks_for_focus() {
let inbox = Inbox::default();
let cwd = std::env::temp_dir();
inbox.forward(Forwarded { cwd: cwd.clone(), args: vec!["--safe-mode".into(), "a.note".into()] });
let (items, focus) = inbox.drain();
assert_eq!(items, vec![Opened::File(cwd.join("a.note"))]);
assert!(focus);
assert_eq!(inbox.drain(), (vec![], false));
}
}