use std::{
fs,
path::{Path, PathBuf},
thread,
time::Duration,
};
use crate::{Content, Failure, Job, preview::Preview, read_input, render_job, report_result};
const POLL: Duration = Duration::from_millis(200);
pub fn run(job: &Job, preview_port: Option<u16>, json_diagnostics: bool) -> Result<(), Failure> {
check(job, preview_port.is_some())?;
let preview = preview_port
.map(|port| Preview::serve(port, job))
.transpose()?;
if !json_diagnostics {
eprintln!("chartlet: watching {}, Ctrl-C stops", job.input.display());
}
let mut last = read_input(&job.input);
render(job, &last, preview.as_deref(), json_diagnostics);
loop {
thread::sleep(POLL);
let mut current = read_input(&job.input);
if current == last {
continue;
}
loop {
thread::sleep(POLL);
let next = read_input(&job.input);
if next == current {
break;
}
current = next;
}
if current != last {
last = current;
render(job, &last, preview.as_deref(), json_diagnostics);
}
}
}
fn check(job: &Job, preview: bool) -> Result<(), Failure> {
if job.input == Path::new("-") {
return Err(
"--watch needs a specification file; it cannot watch standard input"
.to_owned()
.into(),
);
}
if preview && job.png {
return Err("--preview shows SVG or HTML; use --watch alone for a PNG"
.to_owned()
.into());
}
if job.output.is_none() && !preview {
return Err(
"--watch needs -o <path>; it does not write to standard output"
.to_owned()
.into(),
);
}
for (option, path) in [("-o", &job.output), ("--manifest", &job.manifest)] {
if path
.as_ref()
.is_some_and(|path| same_file(path, &job.input))
{
return Err(
format!("{option} must not be the specification file that is watched").into(),
);
}
}
Ok(())
}
fn same_file(a: &Path, b: &Path) -> bool {
match (fs::canonicalize(a), fs::canonicalize(b)) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
fn render(
job: &Job,
input: &Result<String, String>,
preview: Option<&Preview>,
json_diagnostics: bool,
) {
let mut warnings = Vec::new();
let result = input
.clone()
.map_err(Failure::from)
.and_then(|input| render_job(job, &input))
.and_then(|(content, rendered, manifest)| {
crate::report_warnings(
rendered,
&mut warnings,
json_diagnostics,
job.strict.as_ref(),
)?;
if let Some(output) = &job.output {
replace(output, content.bytes())?;
}
if let (Some(path), Some(manifest)) = (&job.manifest, manifest) {
replace(path, (manifest.to_json() + "\n").as_bytes())?;
}
Ok(content)
});
if let Some(preview) = preview {
preview.update(
match &result {
Ok(Content::Text(text)) => Ok(text.clone()),
Ok(Content::Png(_)) => unreachable!("the preview refuses PNG output"),
Err(failure) => Err(failure.to_string()),
},
&warnings,
);
}
report_result(result.as_ref().err(), &warnings, json_diagnostics);
if !json_diagnostics && result.is_ok() {
match &job.output {
Some(output) => eprintln!("chartlet: wrote {}", output.display()),
None => eprintln!("chartlet: rendered {}", job.input.display()),
}
}
}
fn replace(path: &Path, bytes: &[u8]) -> Result<(), String> {
let name = path
.file_name()
.ok_or_else(|| format!("{} is not a file path", path.display()))?;
let mut temporary = PathBuf::from(path);
temporary.set_file_name(format!(".{}.chartlet-tmp", name.to_string_lossy()));
fs::write(&temporary, bytes)
.and_then(|()| fs::rename(&temporary, path))
.map_err(|error| format!("could not write {}: {error}", path.display()))
}