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 | |
| 18 | use oxedyne_fe2o3_pearlite::raster; |
| 19 | use oxedyne_fe2o3_pearlite::register; |
| 20 | use oxedyne_fe2o3_pearlite::shell::{ |
| 21 | self, |
| 22 | Shell, |
| 23 | }; |
| 24 | |
| 25 | use oxedyne_fe2o3_austenite::emit::pearl::PearlDoc; |
| 26 | |
| 27 | use oxedyne_fe2o3_core::prelude::*; |
| 28 | |
| 29 | fn 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? |
| 49 | fn 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 | |
| 54 | fn 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. |
| 66 | fn 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")] |
| 78 | fn 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"))] |
| 83 | fn 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")] |
| 94 | fn 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"))] |
| 104 | fn 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). |
| 112 | fn 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 | |
| 128 | fn 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 | |
| 132 | fn has_flag(args: &[String], name: &str) -> bool { |
| 133 | args.iter().any(|a| a == name) |
| 134 | } |
| 135 | |
| 136 | fn 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 | |
| 150 | fn 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 | |
| 170 | fn 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 | } |