Add Pallet Watch (#25)
closes #18 Reviewed-on: http://192.168.1.227:3000/sfrembling/pallet/pulls/25 Co-authored-by: godsfryingpan <sfrembling@gmail.com> Co-committed-by: godsfryingpan <sfrembling@gmail.com>
This commit was merged in pull request #25.
This commit is contained in:
164
src/app.rs
164
src/app.rs
@@ -2,6 +2,7 @@ use std::{
|
||||
env::set_current_dir,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
};
|
||||
|
||||
use clap::CommandFactory;
|
||||
@@ -60,6 +61,14 @@ enum Subcommand {
|
||||
/// The package to be added
|
||||
package: String,
|
||||
},
|
||||
/// Watches for source code changes and triggers a recompilation && re-run on change
|
||||
Watch {
|
||||
/// The build mode to use
|
||||
mode: Option<String>,
|
||||
/// Arguments to pass to the project binary
|
||||
#[arg(long, short)]
|
||||
args: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
@@ -186,10 +195,165 @@ impl App {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Subcommand::Watch { mode, args } => {
|
||||
if let Err(e) = watch(&mode, args) {
|
||||
eprintln!("Error running watch: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn watch(mode: &Option<String>, args: Option<Vec<String>>) -> std::io::Result<()> {
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
|
||||
// enter alternate screen
|
||||
write!(stdout, "\x1b[?1049h")?;
|
||||
stdout.flush()?;
|
||||
|
||||
// set up ctrl+c handler BEFORE doing anything else
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = running.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
running_clone.store(false, Ordering::SeqCst);
|
||||
})
|
||||
.map_err(|e| std::io::Error::other(format!("{e}")))?;
|
||||
|
||||
let result = watch_inner(mode, args, running);
|
||||
|
||||
// exit alternate screen — guaranteed to run now
|
||||
write!(stdout, "\x1b[?1049l")?;
|
||||
stdout.flush()?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn watch_inner(
|
||||
mode: &Option<String>,
|
||||
args: Option<Vec<String>>,
|
||||
running: Arc<std::sync::atomic::AtomicBool>,
|
||||
) -> std::io::Result<()> {
|
||||
use notify::{Event, RecursiveMode, Watcher, recommended_watcher};
|
||||
use std::io::Write;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
let clear = || {
|
||||
let mut stdout = std::io::stdout();
|
||||
write!(stdout, "\x1b[2J\x1b[H").ok();
|
||||
stdout.flush().ok();
|
||||
};
|
||||
|
||||
let print_header = |label: &str| {
|
||||
println!(
|
||||
" {} 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 mut watcher = recommended_watcher(tx).map_err(|e| std::io::Error::other(format!("{e}")))?;
|
||||
watcher
|
||||
.watch(Path::new("src"), RecursiveMode::Recursive)
|
||||
.map_err(|e| std::io::Error::other(format!("{e}")))?;
|
||||
|
||||
let mut last_build = Instant::now()
|
||||
.checked_sub(Duration::from_secs(1))
|
||||
.unwrap_or(Instant::now());
|
||||
|
||||
while running.load(Ordering::SeqCst) {
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(Ok(event)) => {
|
||||
let is_relevant = matches!(
|
||||
event.kind,
|
||||
notify::EventKind::Create(_)
|
||||
| notify::EventKind::Modify(_)
|
||||
| notify::EventKind::Remove(_)
|
||||
);
|
||||
let affects_source = event.paths.iter().any(|p| {
|
||||
matches!(
|
||||
p.extension().and_then(|e| e.to_str()),
|
||||
Some("c") | Some("h")
|
||||
)
|
||||
});
|
||||
let debounced = last_build.elapsed() > Duration::from_millis(500);
|
||||
|
||||
if is_relevant && affects_source && debounced {
|
||||
last_build = Instant::now();
|
||||
|
||||
// kill whatever is currently running
|
||||
kill_child(&mut child);
|
||||
|
||||
clear();
|
||||
print_header("rebuilding");
|
||||
|
||||
match build(mode, false) {
|
||||
Ok(_) => {
|
||||
child = run_child(mode, &args).ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" {} {e}", "Error".red().bold());
|
||||
// child stays None until next successful build
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => eprintln!(" {} watch error: {e}", "Error".red().bold()),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => continue, // check running flag and loop
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
|
||||
kill_child(&mut child);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_package(package: &str) -> std::io::Result<()> {
|
||||
let status = Command::new("pkg-config")
|
||||
.arg("--exists")
|
||||
|
||||
Reference in New Issue
Block a user