From 2a0fafc5bf0df948fc5d4438d4ae3523ad01eb86 Mon Sep 17 00:00:00 2001 From: Tim-Paik Date: Tue, 5 Apr 2022 18:11:29 +0800 Subject: [PATCH] update dev mode --- Cargo.lock | 21 ++++ Cargo.toml | 4 + neutauri_bundler/Cargo.toml | 5 +- neutauri_bundler/src/dev.rs | 220 +++++++++++++++++++++++++++++++++++ neutauri_bundler/src/main.rs | 88 +++++++++++++- 5 files changed, 329 insertions(+), 9 deletions(-) create mode 100644 neutauri_bundler/src/dev.rs diff --git a/Cargo.lock b/Cargo.lock index f99d896..ac0887c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -777,6 +777,26 @@ dependencies = [ "syn", ] +[[package]] +name = "gumdrop" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" +dependencies = [ + "gumdrop_derive", +] + +[[package]] +name = "gumdrop_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "heck" version = "0.3.3" @@ -1031,6 +1051,7 @@ version = "0.1.0" dependencies = [ "bincode", "brotli", + "gumdrop", "image", "new_mime_guess", "serde", diff --git a/Cargo.toml b/Cargo.toml index e21b0dc..2fb435c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,7 @@ members = [ "neutauri_runtime", "neutauri_bundler", ] + +[neutauri_bundler.profile.release.package.wry] +debug = true +debug-assertions = true diff --git a/neutauri_bundler/Cargo.toml b/neutauri_bundler/Cargo.toml index 9e49aaa..fc69c82 100644 --- a/neutauri_bundler/Cargo.toml +++ b/neutauri_bundler/Cargo.toml @@ -6,12 +6,9 @@ version = "0.1.0" [dependencies] bincode = "1.3" brotli = "3.3" +gumdrop = "0.8" image = "0.23" new_mime_guess = "4.0" serde = {version = "1.0", features = ["derive"]} toml = "0.5" wry = "0.12" - -[profile.release.package.wry] -debug = true -debug-assertions = true \ No newline at end of file diff --git a/neutauri_bundler/src/dev.rs b/neutauri_bundler/src/dev.rs new file mode 100644 index 0000000..71980f1 --- /dev/null +++ b/neutauri_bundler/src/dev.rs @@ -0,0 +1,220 @@ +use std::{fs, io::Read, path::PathBuf}; + +use wry::{ + application::{ + dpi::{PhysicalSize, Size}, + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{Fullscreen, Icon, Window, WindowBuilder}, + }, + webview::{RpcRequest, WebContext, WebViewBuilder}, +}; + +use crate::data; + +const PROTOCOL_PREFIX: &str = "{PROTOCOL}://"; +const PROTOCOL: &str = "dev"; + +fn custom_protocol_uri>(protocol: T, path: T) -> String { + PROTOCOL_PREFIX.replacen("{PROTOCOL}", &protocol.into(), 1) + &path.into() +} +fn custom_protocol_uri_to_path>(protocol: T, uri: T) -> wry::Result { + let prefix = PROTOCOL_PREFIX.replacen("{PROTOCOL}", &protocol.into(), 1); + let uri = uri.into(); + let path = uri.strip_prefix(&prefix); + match path { + Some(str) => Ok(str.to_string()), + None => Err(wry::Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + prefix + " is not found in " + &uri, + ))), + } +} + +pub fn dev(config_path: String) -> wry::Result<()> { + let config_path = std::path::Path::new(&config_path).canonicalize()?; + let config: data::Config = toml::from_str(fs::read_to_string(&config_path)?.as_str()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + let source = config.source.clone().canonicalize()?; + + let event_loop = EventLoop::new(); + + let window_builder = WindowBuilder::new() + .with_always_on_top(config.window_attr()?.always_on_top) + .with_decorations(config.window_attr()?.decorations) + .with_resizable(config.window_attr()?.resizable) + .with_title(config.window_attr()?.title.clone()) + .with_maximized(config.window_attr()?.maximized) + .with_transparent(config.window_attr()?.transparent) + .with_visible(config.window_attr()?.visible); + let window_builder = match config.window_attr()?.fullscreen { + true => window_builder.with_fullscreen(Some(Fullscreen::Borderless(None))), + false => window_builder, + }; + let window_builder = match config.window_attr()?.window_icon { + Some(ref icon) => window_builder.with_window_icon(Some(Icon::from_rgba( + icon.rgba.clone(), + icon.width, + icon.height, + )?)), + None => window_builder, + }; + let monitor_size = event_loop + .primary_monitor() + .unwrap_or_else(|| { + event_loop + .available_monitors() + .next() + .expect("no monitor found") + }) + .size(); + let window_builder = match config.window_attr()?.inner_size { + Some(size) => window_builder.with_inner_size(get_size(size, monitor_size)), + None => window_builder, + }; + let window_builder = match config.window_attr()?.max_inner_size { + Some(size) => window_builder.with_max_inner_size(get_size(size, monitor_size)), + None => window_builder, + }; + let window_builder = match config.window_attr()?.min_inner_size { + Some(size) => window_builder.with_min_inner_size(get_size(size, monitor_size)), + None => window_builder, + }; + let window = window_builder.build(&event_loop)?; + + let webview_builder = WebViewBuilder::new(window)?; + let url = config.webview_attr()?.url.clone(); + let webview_builder = match url { + Some(url) => { + if url.starts_with('/') { + webview_builder.with_url(&custom_protocol_uri(PROTOCOL, &url))? + } else { + webview_builder.with_url(&url)? + } + } + None => webview_builder.with_url(&custom_protocol_uri(PROTOCOL, "/index.html"))?, + }; + let html = config.webview_attr()?.html.clone(); + let webview_builder = match html { + Some(html) => webview_builder.with_html(&html)?, + None => webview_builder, + }; + let initialization_script = config.webview_attr()?.initialization_script.clone(); + let webview_builder = match initialization_script { + Some(script) => webview_builder.with_initialization_script(&script), + None => webview_builder, + }; + let webview_builder = match config.window_attr()?.visible { + true => webview_builder.with_visible(true), + false => webview_builder + .with_visible(false) + .with_initialization_script( + r#"window.addEventListener('load', function(event) { rpc.call('show_window'); });"#, + ), + }; + let path = std::env::current_exe()?; + let path = path.file_stem().unwrap_or_else(|| "neutauri_app".as_ref()); + let mut web_context = if cfg!(target_os = "windows") { + let config_path = match std::env::var("APPDATA") { + Ok(dir) => PathBuf::from(dir), + Err(_) => PathBuf::from("."), + } + .join(path); + WebContext::new(Some(config_path)) + } else if cfg!(target_os = "linux") { + let config_path = match std::env::var("XDG_CONFIG_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => match std::env::var("HOME") { + Ok(dir) => PathBuf::from(dir).join(".config"), + Err(_) => PathBuf::from("."), + }, + } + .join(path); + WebContext::new(Some(config_path)) + } else if cfg!(target_os = "macos") { + let config_path = match std::env::var("HOME") { + Ok(dir) => PathBuf::from(dir).join("Library/Application Support/"), + Err(_) => PathBuf::from("."), + } + .join(path); + WebContext::new(Some(config_path)) + } else { + WebContext::new(None) + }; + let webview = webview_builder + .with_visible(config.window_attr()?.visible) + .with_transparent(config.window_attr()?.transparent) + .with_web_context(&mut web_context) + .with_custom_protocol(PROTOCOL.to_string(), move |request| { + let path = custom_protocol_uri_to_path(PROTOCOL, request.uri())?; + let mut local_path = source.clone(); + local_path.push(path.strip_prefix("/").unwrap_or_else(|| &path)); + let mut data = Vec::new(); + let mut mime: String = "application/octet-stream".to_string(); + match fs::File::open(&local_path) { + Ok(mut f) => { + mime = new_mime_guess::from_path(&local_path) + .first_or_octet_stream() + .to_string(); + f.read_to_end(&mut data)?; + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound && config.webview_attr()?.spa { + let mut index_path = source.clone(); + index_path.push("index.html"); + mime = new_mime_guess::from_path(&index_path) + .first_or_octet_stream() + .to_string(); + let mut f = fs::File::open(index_path)?; + f.read_to_end(&mut data)?; + } + } + } + wry::http::ResponseBuilder::new().mimetype(&mime).body(data) + }) + .with_rpc_handler(|window: &Window, req: RpcRequest| { + match req.method.as_str() { + "show_window" => window.set_visible(true), + "ping" => println!("recived a ping"), + _ => (), + }; + None + }) + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => webview.focus(), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} + +fn get_size(size: data::WindowSize, monitor_size: PhysicalSize) -> Size { + let (width, height) = match size { + data::WindowSize::Large => ( + monitor_size.width as f64 * 0.7, + monitor_size.height as f64 * 0.7, + ), + data::WindowSize::Medium => ( + monitor_size.width as f64 * 0.6, + monitor_size.height as f64 * 0.6, + ), + data::WindowSize::Small => ( + monitor_size.width as f64 * 0.5, + monitor_size.height as f64 * 0.5, + ), + data::WindowSize::Fixed { width, height } => (width, height), + data::WindowSize::Scale { factor } => ( + monitor_size.width as f64 * factor, + monitor_size.height as f64 * factor, + ), + }; + Size::Physical(PhysicalSize::new(width as u32, height as u32)) +} diff --git a/neutauri_bundler/src/main.rs b/neutauri_bundler/src/main.rs index d0f4014..23ff064 100644 --- a/neutauri_bundler/src/main.rs +++ b/neutauri_bundler/src/main.rs @@ -1,10 +1,88 @@ +use gumdrop::Options; + mod bundle; +mod dev; mod data; -fn main() -> std::io::Result<()> { - let arg = std::env::args() - .nth(1) - .unwrap_or_else(|| "neutauri.toml".into()); - bundle::bundle(arg)?; +#[derive(Debug, Options)] +struct Args { + #[options(help = "print help information")] + help: bool, + #[options(help = "print version information")] + version: bool, + + #[options(command)] + command: Option, +} + +#[derive(Debug, Clone, Options)] +enum Command { + #[options(help = "pack a neutauri project")] + Bundle(BundleOpts), + #[options(help = "run the project in the current directory in development mode")] + Dev(DevOpts), +} + +#[derive(Debug, Clone, Options)] +struct BundleOpts { + #[options(help = "print help information")] + help: bool, + #[options(help = "path to the config file [default: neutauri.toml]")] + config: Option, +} + +#[derive(Debug, Clone, Options)] +struct DevOpts { + #[options(help = "print help information")] + help: bool, + #[options(help = "path to the config file [default: neutauri.toml]")] + config: Option, +} + +fn print_help_and_exit(args: Args) { + if args.command.is_some() { + Args::parse_args_default_or_exit(); + std::process::exit(0); + } + eprintln!( + "Usage: {:?} [SUBCOMMAND] [OPTIONS]", + std::env::args() + .into_iter() + .nth(0) + .unwrap_or_else(|| "neutauri_bundler".to_string()) + ); + eprintln!(); + eprintln!("{}", args.self_usage()); + eprintln!(); + eprintln!("Available commands:"); + eprintln!("{}", args.self_command_list().unwrap()); + std::process::exit(0); +} + +fn main() -> wry::Result<()> { + let args = std::env::args().collect::>(); + let args = Args::parse_args(&args[1..], gumdrop::ParsingStyle::default()).unwrap_or_else(|e| { + eprintln!("{}: {}", args[0], e); + std::process::exit(2); + }); + match args.command.clone() { + Some(command) => match command { + Command::Bundle(opts) => { + if opts.help_requested() { + print_help_and_exit(args); + } + let config_path = opts.config.unwrap_or_else(|| "neutauri.toml".to_string()); + bundle::bundle(config_path)?; + } + Command::Dev(opts) => { + if opts.help_requested() { + print_help_and_exit(args); + } + let config_path = opts.config.unwrap_or_else(|| "neutauri.toml".to_string()); + dev::dev(config_path)?; + }, + }, + None => print_help_and_exit(args), + } Ok(()) }