Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_pearlite/src/bin/pearlite.rs

7.7 KiB, 37 runs

created by r1870400018:58212, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! `pearlite` -- the native `.prl` reader's command line, and its desktop launcher.
2//!
3//! Run with no argument, or with a document path and no subcommand, it is a desktop application: a
4//! path opens straight into the native window, and no argument brings up the open-file dialog, so a
5//! double-click or a file association needs nothing more (with the `gui` feature). The subcommands:
6//!
7//! - `render <DOC.prl> [OUT_DIR]` -- per-page SVG, via `PearlDoc::render_page` alone (Phase 0's own
8//! round trip, exposed here as a convenience alongside the other two).
9//! - `png <DOC.prl> [OUT_DIR] [--dpi N]` -- per-page PNG at a chosen DPI (Phase 1, [`raster`]).
10//! - `serve <DOC.prl> [--port N] [--no-open]` -- a loopback browser-shell app serving the existing
11//! `pearl-reader` web assets against this one document (Phase 2, [`shell`]).
12//! - `open <DOC.prl>` -- a true native window rendering the document with the CPU rasteriser, no webview
13//! (Phase 3, [`window`](oxedyne_fe2o3_pearlite::window); behind the default-off `gui` feature). With no
14//! path the open-file dialog asks for one.
15//! - `register` -- associates `.prl` documents with this binary for the current user
16//! ([`register`](oxedyne_fe2o3_pearlite::register)).
17
18use oxedyne_fe2o3_pearlite::raster;
19use oxedyne_fe2o3_pearlite::register;
20use oxedyne_fe2o3_pearlite::shell::{
21 self,
22 Shell,
23};
24
25use oxedyne_fe2o3_austenite::emit::pearl::PearlDoc;
26
27use oxedyne_fe2o3_core::prelude::*;
28
29fn main() -> Outcome<()> {
30 let args: Vec<String> = std::env::args().skip(1).collect();
31 let cmd = match args.first() {
32 Some(c) => c.as_str(),
33 None => return cmd_launch(None),
34 };
35 match cmd {
36 "render" => cmd_render(&args[1..]),
37 "png" => cmd_png(&args[1..]),
38 "serve" => cmd_serve(&args[1..]),
39 "open" => cmd_open(&args[1..]),
40 "register" => cmd_register(),
41 "help" | "--help" | "-h" => usage(),
42 // A desktop passes the document itself, with no subcommand, when a `.prl` is opened.
43 c if is_document(c) => cmd_launch(Some(c)),
44 _ => usage(),
45 }
46}
47
48/// Does the argument name a document rather than a subcommand: an existing file, or a `.prl` path?
49fn is_document(arg: &str) -> bool {
50 let p = std::path::Path::new(arg);
51 p.is_file() || p.extension().map(|e| e.eq_ignore_ascii_case("prl")).unwrap_or(false)
52}
53
54fn usage() -> Outcome<()> {
55 println!("usage:");
56 println!(" pearlite render <DOC.prl> [OUT_DIR]");
57 println!(" pearlite png <DOC.prl> [OUT_DIR] [--dpi N]");
58 println!(" pearlite serve <DOC.prl> [--port N] [--no-open]");
59 println!(" pearlite open [DOC.prl] (native window; build with --features gui)");
60 println!(" pearlite register (associate .prl documents with this reader)");
61 println!(" pearlite [DOC.prl] (desktop launch: the window, or the open dialog)");
62 Ok(())
63}
64
65/// Writes the current user's `.prl` association and application entry, pointing at this executable.
66fn cmd_register() -> Outcome<()> {
67 let steps = res!(register::plan());
68 for line in res!(register::execute(&steps)) {
69 println!("pearlite register: {}", line);
70 }
71 println!("pearlite register: .prl documents now open in {}", res!(register::handler_exe()).display());
72 Ok(())
73}
74
75/// The desktop launch: the document when one is named, otherwise the open-file dialog. Failures are shown
76/// on screen, since a double-click has no terminal.
77#[cfg(feature = "gui")]
78fn cmd_launch(path: Option<&str>) -> Outcome<()> {
79 oxedyne_fe2o3_pearlite::launch::run(path.map(std::path::PathBuf::from))
80}
81
82#[cfg(not(feature = "gui"))]
83fn cmd_launch(path: Option<&str>) -> Outcome<()> {
84 match path {
85 None => usage(),
86 Some(_) => cmd_open(&[]),
87 }
88}
89
90/// Opens the document in a native window, or asks for one when none is named. The reader lives behind
91/// the `gui` feature so the other subcommands pull in neither winit nor softbuffer; without the feature
92/// this arm says how to get it.
93#[cfg(feature = "gui")]
94fn cmd_open(args: &[String]) -> Outcome<()> {
95 use oxedyne_fe2o3_pearlite::launch;
96
97 match args.first() {
98 Some(s) => launch::open_path(std::path::Path::new(s)),
99 None => launch::run(None),
100 }
101}
102
103#[cfg(not(feature = "gui"))]
104fn cmd_open(_args: &[String]) -> Outcome<()> {
105 Err(err!(
106 "This build has no native window: rebuild with `--features gui` to use `pearlite open`.";
107 Input, Unimplemented))
108}
109
110/// The document path and the directory to write pages into, from a subcommand's own positional and
111/// optional arguments (the pattern `render` and `png` share).
112fn doc_and_out_dir(args: &[String]) -> Outcome<(String, String)> {
113 let source = match args.first() {
114 Some(s) => s.clone(),
115 None => return Err(err!("A .prl path is required."; Input, Missing)),
116 };
117 let out_dir = match args.get(1).filter(|a| !a.starts_with("--")) {
118 Some(s) => s.clone(),
119 None => std::path::Path::new(&source)
120 .parent()
121 .map(|p| p.to_string_lossy().into_owned())
122 .filter(|s| !s.is_empty())
123 .unwrap_or_else(|| ".".to_string()),
124 };
125 Ok((source, out_dir))
126}
127
128fn flag_value(args: &[String], name: &str) -> Option<String> {
129 args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned()
130}
131
132fn has_flag(args: &[String], name: &str) -> bool {
133 args.iter().any(|a| a == name)
134}
135
136fn cmd_render(args: &[String]) -> Outcome<()> {
137 let (source, out_dir) = res!(doc_and_out_dir(args));
138 let doc = res!(PearlDoc::read_file(&source));
139 let pages = res!(doc.page_count());
140 res!(std::fs::create_dir_all(&out_dir), IO, File, Write);
141 for i in 0..pages {
142 let svg = res!(doc.render_page(i));
143 let path = fmt!("{}/pearl-page-{:03}.svg", out_dir, i + 1);
144 res!(std::fs::write(&path, &svg), IO, File, Write);
145 }
146 println!("pearlite render: {} -> {} page(s); written to {}/", source, pages, out_dir);
147 Ok(())
148}
149
150fn cmd_png(args: &[String]) -> Outcome<()> {
151 let (source, out_dir) = res!(doc_and_out_dir(args));
152 let dpi = match flag_value(args, "--dpi") {
153 Some(s) => match s.parse::<f32>() {
154 Ok(v) => v,
155 Err(e) => return Err(err!("'{}' is not a valid --dpi value: {}.", s, e; Input, Invalid)),
156 },
157 None => raster::DEFAULT_DPI,
158 };
159 let doc = res!(PearlDoc::read_file(&source));
160 let pngs = res!(raster::render_all_pages_to_png(&doc, dpi));
161 res!(std::fs::create_dir_all(&out_dir), IO, File, Write);
162 for (i, png) in pngs.iter().enumerate() {
163 let path = fmt!("{}/pearl-page-{:03}.png", out_dir, i + 1);
164 res!(std::fs::write(&path, png), IO, File, Write);
165 }
166 println!("pearlite png: {} -> {} page(s) at {} dpi; written to {}/", source, pngs.len(), dpi, out_dir);
167 Ok(())
168}
169
170fn cmd_serve(args: &[String]) -> Outcome<()> {
171 let source = match args.first() {
172 Some(s) => s.clone(),
173 None => return Err(err!("A .prl path is required."; Input, Missing)),
174 };
175 let port = match flag_value(args, "--port") {
176 Some(s) => match s.parse::<u16>() {
177 Ok(v) => v,
178 Err(e) => return Err(err!("'{}' is not a valid --port value: {}.", s, e; Input, Invalid)),
179 },
180 None => 0,
181 };
182 let open = !has_flag(args, "--no-open");
183
184 let doc_bytes = res!(std::fs::read(&source), IO, File, Read);
185 // The document's own file name is the path the served index.html's `?doc=` query names, so the
186 // reader's own status line shows it exactly as `render_page`'s in-browser sibling would.
187 let doc_name = std::path::Path::new(&source)
188 .file_name()
189 .map(|n| n.to_string_lossy().into_owned())
190 .unwrap_or_else(|| "opened.prl".to_string());
191
192 let listener = res!(Shell::bind(port));
193 let bound_port = res!(listener.local_addr(), IO, Network).port();
194 let url = fmt!("http://127.0.0.1:{}/?doc={}", bound_port, doc_name);
195 println!("pearlite serve: {} at {}", source, url);
196
197 if open {
198 if let Err(e) = shell::open_browser(&url) {
199 println!("pearlite serve: could not open a browser automatically ({}); open {} by hand.",
200 e, url);
201 }
202 }
203
204 let shell = Shell::new(doc_name, doc_bytes);
205 shell.serve(listener)
206}