refinements based on feedback

This commit is contained in:
2026-03-23 16:50:24 -05:00
parent 03bc56007f
commit b541865ff1

View File

@@ -205,29 +205,91 @@ impl App {
} }
fn watch(mode: &Option<String>, args: Option<Vec<String>>) -> std::io::Result<()> { fn watch(mode: &Option<String>, args: Option<Vec<String>>) -> std::io::Result<()> {
use std::io::Write;
// save terminal state
let mut stdout = std::io::stdout();
// enter alternate screen — this is a separate terminal buffer,
// exiting it restores everything exactly as it was
write!(stdout, "\x1b[?1049h")?;
stdout.flush()?;
// make sure we always restore the terminal, even on panic
let result = watch_inner(mode, args);
// exit alternate screen
write!(stdout, "\x1b[?1049l")?;
stdout.flush()?;
result
}
fn watch_inner(mode: &Option<String>, args: Option<Vec<String>>) -> std::io::Result<()> {
use notify::{Event, RecursiveMode, Watcher, recommended_watcher}; use notify::{Event, RecursiveMode, Watcher, recommended_watcher};
use std::io::Write;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
println!( let clear = || {
" {} project for changes (ctrl+c to stop)", let mut stdout = std::io::stdout();
"Watching".green().bold() write!(stdout, "\x1b[2J\x1b[H").ok();
); stdout.flush().ok();
};
// do an initial build+run before watching let print_header = |label: &str| {
build(mode, false)?; println!(
run(mode, args.clone())?; " {} project (ctrl+c to stop) — {label}",
"Watching".green().bold()
);
println!("{}", "".repeat(50).dimmed());
};
// track the running child process
let mut child: Option<std::process::Child> = None;
let kill_child = |child: &mut Option<std::process::Child>| {
if let Some(c) = child {
c.kill().ok();
c.wait().ok();
}
*child = None;
};
let run_child = |mode: &Option<String>,
args: &Option<Vec<String>>|
-> std::io::Result<std::process::Child> {
let conf = get_config().ok_or(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no Pallet.toml found",
))?;
let build_config = conf.get_or_default(mode).ok_or(std::io::Error::new(
std::io::ErrorKind::NotFound,
"build layout not found",
))?;
let mut command = Command::new(format!("target/{}/{}", build_config.name, conf.name));
if let Some(a) = args {
for arg in a {
command.arg(arg);
}
}
command
.spawn()
.map_err(|e| std::io::Error::other(format!("{e}")))
};
// initial build + run
clear();
print_header("starting");
if build(mode, false).is_ok() {
child = run_child(mode, &args).ok();
}
let (tx, rx) = mpsc::channel::<notify::Result<Event>>(); let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
let mut watcher = recommended_watcher(tx).map_err(|e| std::io::Error::other(format!("{e}")))?; let mut watcher = recommended_watcher(tx).map_err(|e| std::io::Error::other(format!("{e}")))?;
watcher watcher
.watch(Path::new("src"), RecursiveMode::Recursive) .watch(Path::new("src"), RecursiveMode::Recursive)
.map_err(|e| std::io::Error::other(format!("{e}")))?; .map_err(|e| std::io::Error::other(format!("{e}")))?;
// debounce — ignore events within 500ms of the last one
// this prevents double-triggers from editors that write files in multiple steps
let mut last_build = Instant::now() let mut last_build = Instant::now()
.checked_sub(Duration::from_secs(1)) .checked_sub(Duration::from_secs(1))
.unwrap_or(Instant::now()); .unwrap_or(Instant::now());
@@ -235,37 +297,37 @@ fn watch(mode: &Option<String>, args: Option<Vec<String>>) -> std::io::Result<()
loop { loop {
match rx.recv() { match rx.recv() {
Ok(Ok(event)) => { Ok(Ok(event)) => {
// only care about actual file modifications
let is_relevant = matches!( let is_relevant = matches!(
event.kind, event.kind,
notify::EventKind::Create(_) notify::EventKind::Create(_)
| notify::EventKind::Modify(_) | notify::EventKind::Modify(_)
| notify::EventKind::Remove(_) | notify::EventKind::Remove(_)
); );
// only care about .c and .h files
let affects_source = event.paths.iter().any(|p| { let affects_source = event.paths.iter().any(|p| {
matches!( matches!(
p.extension().and_then(|e| e.to_str()), p.extension().and_then(|e| e.to_str()),
Some("c") | Some("h") Some("c") | Some("h")
) )
}); });
let debounced = last_build.elapsed() > Duration::from_millis(500); let debounced = last_build.elapsed() > Duration::from_millis(500);
if is_relevant && affects_source && debounced { if is_relevant && affects_source && debounced {
last_build = Instant::now(); last_build = Instant::now();
println!(
"\n {} changes detected, rebuilding...", // kill whatever is currently running
"Watch".green().bold() kill_child(&mut child);
);
if let Err(e) = build(mode, false) { clear();
eprintln!(" {} {e}", "Error".red().bold()); print_header("rebuilding");
// don't exit — keep watching even if build fails
continue; match build(mode, false) {
} Ok(_) => {
if let Err(e) = run(mode, args.clone()) { child = run_child(mode, &args).ok();
eprintln!(" {} {e}", "Error".red().bold()); }
Err(e) => {
eprintln!(" {} {e}", "Error".red().bold());
// child stays None until next successful build
}
} }
} }
} }
@@ -277,6 +339,7 @@ fn watch(mode: &Option<String>, args: Option<Vec<String>>) -> std::io::Result<()
} }
} }
kill_child(&mut child);
Ok(()) Ok(())
} }