use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use crate::config::Config;
use crate::error::{Error, Result};
use crate::typst_inprocess::{spawn_thread, InprocessHandle};
pub fn typst_external_path() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join("typst");
if candidate.is_file() {
return Some(candidate);
}
let with_ext = dir.join("typst.exe");
if with_ext.is_file() {
return Some(with_ext);
}
}
None
}
pub fn engine_summary(cfg: &Config) -> String {
if cfg.typst_compile.use_inprocess_engine() {
let bundle = cfg.typst_compile.bundle_fonts;
let system = cfg.typst_compile.use_system_fonts;
let pkgs = cfg.typst_compile.packages_enabled;
let font_label = match (bundle, system) {
(true, true) => "bundled + system",
(true, false) => "bundled",
(false, true) => "system",
(false, false) => "NO FONTS",
};
format!(
"internal · fonts: {font_label} · @preview: {}",
if pkgs { "on" } else { "off" }
)
} else {
match typst_external_path() {
Some(p) => format!("external · {}", p.display()),
None => "external · `typst` NOT FOUND on PATH".to_owned(),
}
}
}
#[derive(Debug)]
pub struct CompileOutcome {
pub success: bool,
pub stderr: String,
pub stdout: String,
pub pdf_path: PathBuf,
}
pub enum CompileHandle {
External { child: Child, pdf_path: PathBuf },
Inprocess(InprocessHandle),
}
impl CompileHandle {
pub fn try_wait(&mut self) -> std::io::Result<Option<()>> {
match self {
Self::External { child, .. } => child.try_wait().map(|opt| opt.map(|_| ())),
Self::Inprocess(h) => h.try_wait_mut(),
}
}
pub fn kill(&mut self) {
match self {
Self::External { child, .. } => {
let _ = child.kill();
}
Self::Inprocess(h) => {
h.cancel();
}
}
}
}
pub fn spawn_with_config(cfg: &Config, typ_path: &Path) -> Result<CompileHandle> {
if cfg.typst_compile.use_inprocess_engine() {
let root = typ_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let settings =
crate::typst_world::WorldSettings::from_cfg(&cfg.typst_compile);
let handle = spawn_thread(&root, typ_path, settings)?;
return Ok(CompileHandle::Inprocess(handle));
}
spawn_external(typ_path)
}
fn spawn_external(typ_path: &Path) -> Result<CompileHandle> {
let pdf_path = typ_path.with_extension("pdf");
let child = Command::new("typst")
.arg("compile")
.arg(typ_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Error::Store(
"`typst` not found in PATH — install Typst from typst.app/docs/install/ \
or set `typst_compile.engine = \"inprocess\"` in inkhaven.hjson"
.into(),
)
} else {
Error::Store(format!("spawn `typst compile`: {e}"))
}
})?;
Ok(CompileHandle::External { child, pdf_path })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn cfg_with(engine: &str) -> Config {
let mut cfg = Config::default();
cfg.typst_compile.engine = engine.to_owned();
cfg
}
#[test]
fn engine_summary_internal_default_flags() {
let cfg = cfg_with("inprocess");
let s = engine_summary(&cfg);
assert!(s.starts_with("internal"), "got: {s}");
assert!(s.contains("bundled + system"), "got: {s}");
assert!(s.contains("@preview: on"), "got: {s}");
}
#[test]
fn engine_summary_internal_hermetic() {
let mut cfg = cfg_with("inprocess");
cfg.typst_compile.use_system_fonts = false;
cfg.typst_compile.packages_enabled = false;
let s = engine_summary(&cfg);
assert!(s.contains("fonts: bundled"), "got: {s}");
assert!(s.contains("@preview: off"), "got: {s}");
}
#[test]
fn engine_summary_external_reports_path_or_missing() {
let cfg = cfg_with("external");
let s = engine_summary(&cfg);
assert!(s.starts_with("external"), "got: {s}");
assert!(
s.contains("/") || s.contains("NOT FOUND"),
"expected a concrete path or NOT FOUND marker, got: {s}",
);
}
}
pub fn finish(handle: CompileHandle) -> Result<CompileOutcome> {
match handle {
CompileHandle::External { child, pdf_path } => {
let output = child
.wait_with_output()
.map_err(|e| Error::Store(format!("wait_with_output on `typst compile`: {e}")))?;
Ok(CompileOutcome {
success: output.status.success(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
pdf_path,
})
}
CompileHandle::Inprocess(h) => Ok(h.into_outcome()),
}
}