mod app_server;
mod broadcast_server;
mod build;
mod spinner;
mod watch;
use std::path::Path;
use clap::Args;
use console::style;
use app_server::AppServer;
use broadcast_server::{Event, EventBus};
use build::{BuildKind, BuildTask};
use spinner::Spinner;
use watch::SourceWatcher;
use crate::cargo::{BuildFlags, BuildOpts};
#[derive(Args)]
pub struct DevCommand {
#[command(flatten)]
build: BuildFlags,
}
impl DevCommand {
pub async fn run(self) {
let opts: BuildOpts = self.build.into();
let listener = broadcast_server::bind().await;
let dev_url = format!("http://{}", listener.local_addr().unwrap());
let events = EventBus::new();
tokio::spawn(broadcast_server::run(listener, events.clone()));
eprintln!();
eprint!(
" {} {}",
style("topcoat").cyan().bold(),
style("dev server").dim()
);
if let Some(profile) = opts.profile.as_deref().filter(|&p| p != "dev") {
eprint!(" {}", style(format!("[{profile}]")).yellow());
}
eprintln!();
let mut watcher = SourceWatcher::start().await;
eprintln!(" {}", style("watching for file changes...").dim());
eprintln!();
let mut build: Option<BuildTask> = Some(BuildTask::spawn(BuildKind::Initial, opts.clone()));
let mut server: Option<AppServer> = None;
events.publish(Event::Rebuilding);
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
eprintln!();
let spinner = Spinner::new("shutting down...");
if let Some(build) = build.take() {
build.cancel().await;
}
if let Some(server) = server.take() {
server.shutdown().await;
}
drop(spinner);
eprintln!();
break;
}
exe = async { build.as_mut().unwrap().finished().await }, if build.is_some() => {
build = None;
if let Some(exe) = exe {
if let Some(old) = server.take() {
old.shutdown().await;
}
server = start_app(&exe, &dev_url, &events);
} else {
events.publish(Event::BuildFailed);
report_waiting(server.is_some());
}
}
status = async { server.as_mut().unwrap().exited().await }, if server.is_some() => {
server = None;
events.publish(Event::AppExited);
let status = status.map_or_else(|error| error.to_string(), |status| status.to_string());
eprintln!(
" {}",
style(format!("application exited ({status})")).red().bold()
);
eprintln!();
report_waiting(false);
}
() = watcher.changed() => {
if let Some(stale) = build.take() {
stale.cancel().await;
}
build = Some(BuildTask::spawn(BuildKind::Rebuild, opts.clone()));
events.publish(Event::Rebuilding);
}
}
}
}
}
fn start_app(exe: &Path, dev_url: &str, events: &EventBus) -> Option<AppServer> {
match AppServer::spawn(exe, dev_url) {
Ok(server) => Some(server),
Err(error) => {
events.publish(Event::AppExited);
eprintln!(
" {}",
style(format!("failed to start application: {error}"))
.red()
.bold()
);
eprintln!();
report_waiting(false);
None
}
}
}
fn report_waiting(server_running: bool) {
let message = if server_running {
"previous build still running; waiting for changes..."
} else {
"waiting for changes..."
};
eprintln!(" {}", style(message).dim());
eprintln!();
}