cranpose 0.1.162

Cranpose runtime and UI facade
Documentation
use std::{
    path::PathBuf,
    sync::{
        Mutex,
        atomic::{AtomicUsize, Ordering},
    },
};

use cranpose_services::{IncomingContentObserver, observe_incoming_content};
use winit::data_transfer::TransferType;

use super::*;

#[derive(Debug)]
struct StagedFileList {
    paths: Vec<PathBuf>,
    reads_before_ready: AtomicUsize,
}

impl StagedFileList {
    fn ready_after(reads_before_ready: usize, names: &[&str]) -> Arc<dyn TypedData> {
        Arc::new(Self {
            paths: names
                .iter()
                .map(|name| PathBuf::from("/drops").join(name))
                .collect(),
            reads_before_ready: AtomicUsize::new(reads_before_ready),
        })
    }
}

impl TypedData for StagedFileList {
    fn type_(&self) -> &dyn TransferType {
        &TypeHint::UriList
    }

    fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
        None
    }

    fn try_as_uris(&self) -> io::Result<Vec<String>> {
        Err(io::ErrorKind::Unsupported.into())
    }

    fn try_as_file_paths(&self) -> io::Result<Vec<PathBuf>> {
        let still_blocked = self
            .reads_before_ready
            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |left| {
                left.checked_sub(1)
            })
            .is_ok();
        if still_blocked {
            return Err(io::ErrorKind::WouldBlock.into());
        }
        Ok(self.paths.clone())
    }

    fn try_as_string(&self) -> io::Result<String> {
        Err(io::ErrorKind::InvalidData.into())
    }
}

#[derive(Debug)]
struct PlainText;

impl TypedData for PlainText {
    fn type_(&self) -> &dyn TransferType {
        &TypeHint::Plaintext
    }

    fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
        None
    }

    fn try_as_uris(&self) -> io::Result<Vec<String>> {
        Err(io::ErrorKind::InvalidData.into())
    }

    fn try_as_file_paths(&self) -> io::Result<Vec<PathBuf>> {
        Err(io::ErrorKind::InvalidData.into())
    }

    fn try_as_string(&self) -> io::Result<String> {
        Ok("plain".to_owned())
    }
}

fn published_names_matching(
    prefix: &'static str,
) -> (Arc<Mutex<Vec<String>>>, IncomingContentObserver) {
    let names = Arc::new(Mutex::new(Vec::new()));
    let sink = Arc::clone(&names);
    let observer = observe_incoming_content(move |item| {
        let name = item.display_name();
        if name.starts_with(prefix)
            && let Ok(mut names) = sink.lock()
        {
            names.push(name);
        }
    });
    (names, observer)
}

fn snapshot(names: &Mutex<Vec<String>>) -> Vec<String> {
    names.lock().map(|names| names.clone()).unwrap_or_default()
}

#[test]
fn a_dropped_file_list_publishes_every_file() {
    let (names, _observer) = published_names_matching("drop-every-");
    let mut drops = FileDrops::default();

    drops.receive(StagedFileList::ready_after(
        0,
        &["drop-every-a.txt", "drop-every-b.png"],
    ));

    assert_eq!(snapshot(&names), ["drop-every-a.txt", "drop-every-b.png"]);
    assert!(drops.waiting.is_empty());
}

#[test]
fn a_file_list_that_is_not_ready_is_read_again_on_the_next_transfer() {
    let (names, _observer) = published_names_matching("drop-later-");
    let mut drops = FileDrops::default();

    drops.receive(StagedFileList::ready_after(1, &["drop-later-a.txt"]));
    assert!(snapshot(&names).is_empty());
    assert_eq!(drops.waiting.len(), 1);

    drops.receive(StagedFileList::ready_after(0, &["drop-later-b.txt"]));
    assert_eq!(snapshot(&names), ["drop-later-a.txt", "drop-later-b.txt"]);
    assert!(drops.waiting.is_empty());
}

#[test]
fn data_that_is_not_a_file_list_is_discarded() {
    let mut drops = FileDrops::default();

    drops.receive(Arc::new(PlainText));

    assert!(drops.waiting.is_empty());
}

#[test]
fn an_option_is_not_a_document() {
    let arguments = ["--headless".to_owned(), "-v".to_owned()];
    assert!(launch_documents(arguments).is_empty());
}

#[test]
fn a_path_to_nothing_is_not_a_document() {
    let arguments = ["/cranpose/no/such/file.txt".to_owned()];
    assert!(launch_documents(arguments).is_empty());
}

#[test]
fn an_existing_file_named_on_the_command_line_is_a_document() {
    let directory = crate::test_scratch_dir("launch-document");
    let path = directory.join("cranpose-launch-document.txt");
    std::fs::write(&path, b"opened").expect("write");
    let found = launch_documents([path.to_string_lossy().into_owned()]);
    let _ = std::fs::remove_dir_all(&directory);
    assert_eq!(found, vec![path]);
}

#[test]
fn a_directory_is_not_a_document() {
    let directory = crate::test_scratch_dir("launch-directory");
    let arguments = [directory.to_string_lossy().into_owned()];
    let found = launch_documents(arguments);
    let _ = std::fs::remove_dir_all(&directory);
    assert!(found.is_empty());
}