use std::path::PathBuf;
use std::process::{Child, Command};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
use crate::builder::{cargo_build, find_bin_name, kill_child};
use crate::events::CompilerEvent;
use crate::hud::{open_hud_window, DevStatus, HudState};
use crate::ui;
pub fn run(bin_override: Option<String>, release: bool, emit_events: bool) {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let bin_name = find_bin_name(&cwd, bin_override.as_deref()).unwrap_or_else(|| {
ui::error("could not determine binary name — add [[bin]] to Cargo.toml or use --bin");
});
let shared = Arc::new(Mutex::new(HudState::new(bin_name.clone())));
let shutdown = Arc::new(AtomicBool::new(false));
if emit_events {
CompilerEvent::dev_server_started(
bin_name.clone(),
vec![cwd.join("src"), cwd.join("Cargo.toml")],
)
.emit();
}
{
let shared = shared.clone();
let shutdown = shutdown.clone();
let cwd = cwd.clone();
let bin_name = bin_name.clone();
std::thread::spawn(move || {
background_loop(shared, shutdown, cwd, bin_name, release, emit_events)
});
}
{
let shared = shared.clone();
let shutdown = shutdown.clone();
gpui::Application::new().run(move |cx: &mut gpui::App| {
open_hud_window(shared, shutdown, cx);
});
}
shutdown.store(true, Ordering::Relaxed);
}
fn background_loop(
shared: Arc<Mutex<HudState>>,
shutdown: Arc<AtomicBool>,
cwd: PathBuf,
bin_name: String,
release: bool,
emit_events: bool,
) {
let (tx, rx) = std::sync::mpsc::channel::<PathBuf>();
let tx_notify = tx.clone();
let mut watcher = match recommended_watcher(move |res: notify::Result<Event>| {
if let Ok(ev) = res {
match ev.kind {
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) => {
if let Some(path) = ev.paths.into_iter().next() {
let _ = tx_notify.send(path);
}
}
_ => {}
}
}
}) {
Ok(w) => w,
Err(e) => {
eprintln!(" {} could not create file watcher: {e}", crate::ui::err());
return;
}
};
let src = cwd.join("src");
if src.exists() {
watcher.watch(&src, RecursiveMode::Recursive).ok();
}
watcher
.watch(&cwd.join("Cargo.toml"), RecursiveMode::NonRecursive)
.ok();
let mut child = do_build_launch(&shared, &cwd, &bin_name, release, None, emit_events);
loop {
if shutdown.load(Ordering::Relaxed) {
if let Some(mut c) = child {
kill_child(&mut c);
if emit_events {
CompilerEvent::process_exited(c.id(), None).emit();
}
}
break;
}
match rx.recv_timeout(Duration::from_millis(200)) {
Ok(changed_path) => {
if emit_events {
CompilerEvent::file_changed(changed_path).emit();
}
let t = Instant::now();
while t.elapsed() < Duration::from_millis(300) {
while rx.try_recv().is_ok() {}
std::thread::sleep(Duration::from_millis(30));
}
eprintln!(" {} change detected — rebuilding…", crate::ui::arrow());
child = do_build_launch(&shared, &cwd, &bin_name, release, child, emit_events);
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
if let Some(ref mut c) = child {
match c.try_wait() {
Ok(Some(status)) => {
let code = status.code();
eprintln!(" {} app exited ({code:?})", crate::ui::warn());
if emit_events {
CompilerEvent::process_exited(c.id(), code).emit();
}
if let Ok(mut s) = shared.lock() {
s.status = DevStatus::Exited { code };
}
child = None;
if code.is_some() {
break;
}
}
Ok(None) => {}
Err(_) => child = None,
}
}
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
}
fn locate_binary(cwd: &std::path::Path, profile: &str, bin_name: &str) -> PathBuf {
let workspace_root = std::process::Command::new("cargo")
.args(["locate-project", "--workspace", "--message-format", "plain"])
.current_dir(cwd)
.output()
.ok()
.and_then(|out| {
if out.status.success() {
let s = String::from_utf8(out.stdout).ok()?;
let p = PathBuf::from(s.trim());
p.parent().map(|d| d.to_path_buf())
} else {
None
}
});
let target_base = workspace_root.unwrap_or_else(|| cwd.to_path_buf());
target_base.join("target").join(profile).join(bin_name)
}
fn do_build_launch(
shared: &Arc<Mutex<HudState>>,
cwd: &PathBuf,
bin_name: &str,
release: bool,
old_child: Option<Child>,
emit_events: bool,
) -> Option<Child> {
if let Ok(mut s) = shared.lock() {
s.status = DevStatus::Building;
}
if let Some(mut c) = old_child {
let pid = c.id();
kill_child(&mut c);
if emit_events {
CompilerEvent::process_exited(pid, None).emit();
}
}
if emit_events {
CompilerEvent::compilation_started(vec![cwd.join("src")], Some("file_change".to_string()))
.emit();
}
let t0 = Instant::now();
let outcome = cargo_build(cwd, release, Some(shared.clone()));
let elapsed_ms = t0.elapsed().as_millis() as u64;
if outcome.success {
if emit_events {
let profile = if release { "release" } else { "debug" };
let output = locate_binary(cwd, profile, bin_name);
CompilerEvent::compilation_success(elapsed_ms, output).emit();
}
if let Ok(mut s) = shared.lock() {
s.status = DevStatus::Running { elapsed_ms };
}
let profile = if release { "release" } else { "debug" };
let bin_path = locate_binary(cwd, profile, bin_name);
eprintln!(
" {} built in {elapsed_ms} ms — launching {bin_name}",
crate::ui::ok()
);
match Command::new(&bin_path).current_dir(cwd).spawn() {
Ok(c) => {
if emit_events {
CompilerEvent::process_launched(c.id(), bin_path).emit();
}
Some(c)
}
Err(e) => {
eprintln!(" {} failed to launch {bin_name}: {e}", crate::ui::err());
if let Ok(mut s) = shared.lock() {
s.status = DevStatus::Failed {
errors: vec![crate::hud::BuildError {
level: "error".into(),
message: format!("Failed to launch binary: {e}"),
..Default::default()
}],
count: 1,
};
}
None
}
}
} else {
if emit_events {
CompilerEvent::compilation_error(elapsed_ms, outcome.errors.clone()).emit();
}
let count = outcome.errors.len();
eprintln!(" {} build failed — {count} error(s)", crate::ui::err());
if let Ok(mut s) = shared.lock() {
s.status = DevStatus::Failed {
errors: outcome.errors,
count,
};
}
None
}
}