1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! Files a desktop hands the application without a picker.
//!
//! Two ways in, and both arrive as the same [`IncomingContent`] a share or a
//! picked file does, so an application collects one stream rather than growing
//! a desktop-shaped path of its own:
//!
//! * dropped on the window, which winit reports as `DragDropped`;
//! * named on the command line, which is what a desktop shell passes when a
//! document is opened with this application.
//!
//! The command-line half covers Linux and Windows, where opening a document
//! launches the program with its path in `argv`. A macOS `.app` bundle is told
//! through an Apple Event instead, which winit does not surface — so a Mac
//! delivers dropped files and not opened ones, and this says so rather than
//! implying coverage it does not have.
use cranpose_services::{media::uri_for_path, publish_incoming_content, IncomingContent};
use std::path::Path;
/// Publishes one file as incoming content.
///
/// The URI form is what the rest of the framework already understands: a
/// `file:` URI resolves through the content resolver, so a dropped file, a
/// picked file and a shared file reach the application as the same handle.
pub(crate) fn publish_file(path: &Path) {
let mut content = IncomingContent::from_uri(uri_for_path(path));
if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
content = content.with_name(name);
}
publish_incoming_content(content);
}
/// Publishes the documents this process was launched to open.
///
/// Called once as the shell starts, before the first composition, so the
/// backlog is waiting for the first collector rather than arriving before
/// anything can observe it.
pub(crate) fn publish_launch_documents() {
for path in launch_documents(std::env::args().skip(1)) {
publish_file(&path);
}
}
/// The arguments that name an existing file.
///
/// An option and its value are not documents, and neither is a path to
/// something that is not there: a desktop application is launched with all
/// three, and only the files are content.
fn launch_documents<I>(arguments: I) -> Vec<std::path::PathBuf>
where
I: IntoIterator<Item = String>,
{
arguments
.into_iter()
.filter(|argument| !argument.starts_with('-'))
.map(std::path::PathBuf::from)
.filter(|path| path.is_file())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[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());
}
}