diff --git a/Cargo.lock b/Cargo.lock index a09f4eb6..0b0e216e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,7 +376,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -578,7 +578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "humantime" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1458,6 +1458,7 @@ dependencies = [ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.33 (registry+https://github.com/rust-lang/crates.io-index)", "jod-thread 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1476,6 +1477,7 @@ dependencies = [ "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2194,7 +2196,7 @@ dependencies = [ "checksum http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" -"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" +"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum hyper 0.12.33 (registry+https://github.com/rust-lang/crates.io-index)" = "7cb44cbce9d8ee4fb36e4c0ad7b794ac44ebaad924b9c8291a63215bb44c2c8f" "checksum hyper-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" diff --git a/Cargo.toml b/Cargo.toml index e775753e..d9530ff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,8 +37,8 @@ csv = "1.0" env_logger = "0.6" failure = "0.1.3" futures = "0.1" +humantime = "1.3.0" hyper = "0.12" -reqwest = "0.9.20" jod-thread = "0.1.0" log = "0.4" maplit = "1.0.1" @@ -48,9 +48,11 @@ rbx_dom_weak = "1.9.0" rbx_reflection = "3.1.388" rbx_xml = "0.11.0" regex = "1.0" +reqwest = "0.9.20" ritz = "0.1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +termcolor = "1.0.5" uuid = { version = "0.7", features = ["v4", "serde"] } [target.'cfg(windows)'.dependencies] diff --git a/assets/icon-32.png b/assets/icon-32.png new file mode 100644 index 00000000..d3beb2f8 Binary files /dev/null and b/assets/icon-32.png differ diff --git a/assets/index.css b/assets/index.css index 7621a0d9..c0a1e687 100644 --- a/assets/index.css +++ b/assets/index.css @@ -1,11 +1,17 @@ * { margin: 0; padding: 0; + border: none; + text-decoration: inherit; + color: inherit; font: inherit; + box-sizing: inherit; } html { + box-sizing: border-box; font-family: sans-serif; + text-decoration: none; height: 100%; } @@ -17,27 +23,54 @@ body { } .main { + flex: 0 0; + display: flex; + flex-direction: column; padding: 1rem; - text-align: center; margin: 0 auto; width: 100%; - max-width: 60rem; + max-width: 50rem; background-color: #efefef; border: 1px solid #666; border-radius: 4px; } -.title { - font-size: 2rem; +.header { + flex: 0 0; + display: flex; + justify-content: space-around; + align-items: center; + margin-bottom: 1rem; +} + +.main-logo { + flex: 0 0; + width: 20rem; +} + +.stats { + flex: 0 0 20rem; + padding: 0 1rem; +} + +.stat { + display: block; +} + +.stat-name { + display: inline; font-weight: bold; } -.subtitle { - font-size: 1.5rem; - font-weight: bold; +.button-list { + flex: 0 0; + display: flex; + justify-content: space-around; } -.docs { - font-size: 1.3rem; - font-weight: bold; +.button { + display: inline-block; + border: 1px solid #666; + padding: 0.3em 1em; + margin: 0 0.2rem; } \ No newline at end of file diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 99b33c90..aeded2cd 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,7 +1,13 @@ -use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use std::{ + collections::HashMap, + io::{self, Write}, + path::PathBuf, + sync::Arc, +}; use failure::Fail; use rbx_dom_weak::RbxInstanceProperties; +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; use crate::{ imfs::{Imfs, RealFetcher, WatchMode}, @@ -46,7 +52,7 @@ pub fn serve(options: &ServeOptions) -> Result<(), ServeError> { }) .unwrap_or(DEFAULT_PORT); - println!("Rojo server listening on port {}", port); + let _ = show_start_message(port); let mut tree = RojoTree::new(InstancePropertiesWithMeta { properties: RbxInstanceProperties { @@ -75,20 +81,36 @@ pub fn serve(options: &ServeOptions) -> Result<(), ServeError> { server.start(port); - // let receiver = imfs.change_receiver(); + Ok(()) +} - // while let Ok(change) = receiver.recv() { - // imfs.commit_change(&change) - // .expect("Failed to commit Imfs change"); +fn show_start_message(port: u16) -> io::Result<()> { + let mut writer = BufferWriter::stderr(ColorChoice::Auto); + let mut buffer = writer.buffer(); - // use notify::DebouncedEvent; - // if let DebouncedEvent::Write(path) = change { - // let contents = imfs.get_contents(path) - // .expect("Failed to read changed path"); + writeln!(&mut buffer, "Rojo server listening:")?; - // println!("{:?}", std::str::from_utf8(contents)); - // } - // } + write!(&mut buffer, " Address: ")?; + buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?; + writeln!(&mut buffer, "localhost")?; + + buffer.set_color(&ColorSpec::new())?; + write!(&mut buffer, " Port: ")?; + buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?; + writeln!(&mut buffer, "{}", port)?; + + writeln!(&mut buffer, "")?; + + buffer.set_color(&ColorSpec::new())?; + write!(&mut buffer, "Visit ")?; + + buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?; + write!(&mut buffer, "http://localhost:{}/", port)?; + + buffer.set_color(&ColorSpec::new())?; + writeln!(&mut buffer, " in your browser for more information.")?; + + writer.print(&buffer)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index f190593c..a8999ccc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ // Recursion limit bump is to support Ritz, a JSX-like proc macro used for // Rojo's web UI currently. -#![recursion_limit = "128"] +#![recursion_limit = "1024"] #[macro_use] mod impl_from; diff --git a/src/serve_session.rs b/src/serve_session.rs index c1e37aeb..e978daf6 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -1,6 +1,7 @@ use std::{ collections::HashSet, sync::{Arc, Mutex, MutexGuard}, + time::Instant, }; use crate::{ @@ -20,6 +21,10 @@ use crate::{ /// future. `ServeSession` would be roughly the right interface to expose for /// those cases. pub struct ServeSession { + /// When the serve session was started. Used only for user-facing + /// diagnostics. + start_time: Instant, + /// The root project for the serve session, if there was one defined. /// /// This will be defined if a folder with a `default.project.json` file was @@ -66,6 +71,8 @@ pub struct ServeSession { /// that handles ServeSession. impl ServeSession { pub fn new(imfs: Imfs, tree: RojoTree, root_project: Option) -> Self { + let start_time = Instant::now(); + let session_id = SessionId::new(); let message_queue = MessageQueue::new(); @@ -80,6 +87,7 @@ impl ServeSession { ); Self { + start_time, session_id, root_project, tree, @@ -107,6 +115,16 @@ impl ServeSession { self.session_id } + pub fn project_name(&self) -> Option<&str> { + self.root_project + .as_ref() + .map(|project| project.name.as_str()) + } + + pub fn start_time(&self) -> Instant { + self.start_time + } + pub fn serve_place_ids(&self) -> Option<&HashSet> { self.root_project .as_ref() diff --git a/src/web/ui.rs b/src/web/ui.rs index 45945fe4..fcb6b5ce 100644 --- a/src/web/ui.rs +++ b/src/web/ui.rs @@ -1,6 +1,6 @@ //! Defines the HTTP-based UI. These endpoints generally return HTML and SVG. -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use futures::{future, Future}; use hyper::{header, service::Service, Body, Method, Request, Response, StatusCode}; @@ -15,10 +15,11 @@ use crate::{ }, }; +static LOGO: &[u8] = include_bytes!("../../assets/logo-512.png"); +static ICON: &[u8] = include_bytes!("../../assets/icon-32.png"); static HOME_CSS: &str = include_str!("../../assets/index.css"); pub struct UiService { - #[allow(unused)] // TODO: Fill out interface service serve_session: Arc>, } @@ -31,6 +32,8 @@ impl Service for UiService { fn call(&mut self, request: Request) -> Self::Future { let response = match (request.method(), request.uri().path()) { (&Method::GET, "/") => self.handle_home(), + (&Method::GET, "/logo.png") => self.handle_logo(), + (&Method::GET, "/icon.png") => self.handle_icon(), (&Method::GET, "/visualize/rbx") => self.handle_visualize_rbx(), (&Method::GET, "/visualize/imfs") => self.handle_visualize_imfs(), (_method, path) => { @@ -50,28 +53,74 @@ impl UiService { UiService { serve_session } } + fn handle_logo(&self) -> Response { + Response::builder() + .header(header::CONTENT_TYPE, "image/png") + .body(Body::from(LOGO)) + .unwrap() + } + + fn handle_icon(&self) -> Response { + Response::builder() + .header(header::CONTENT_TYPE, "image/png") + .body(Body::from(ICON)) + .unwrap() + } + fn handle_home(&self) -> Response { + let project_name = self.serve_session.project_name().unwrap_or(""); + let uptime = { + let elapsed = self.serve_session.start_time().elapsed(); + + // Round off all of our sub-second precision to make timestamps + // nicer. + let just_nanos = Duration::from_nanos(elapsed.subsec_nanos() as u64); + let elapsed = elapsed - just_nanos; + + humantime::format_duration(elapsed).to_string() + }; + let page = html! { - "Rojo" + "Rojo Live Server" + -
-

- "Rojo Live Sync is up and running!" -

-

- "Version " { SERVER_VERSION } -

- - "Rojo Documentation" - -
+
+
+ +
+ + "Server Version: " + { SERVER_VERSION } + + + "Project: " + { project_name } + + + "Server Uptime: " + { uptime.to_string() } + +
+
+ +
};