Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "texforge"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
rust-version = "1.75"
description = "Self-contained LaTeX to PDF compiler CLI"
Expand Down Expand Up @@ -38,6 +38,9 @@ flate2 = "1.1"
tar = "0.4"
zip = { version = "2", default-features = false, features = ["deflate"] }

# File watching for --watch mode
notify = { version = "7", features = ["macos_fsevent"] }

# Mermaid diagram rendering
mermaid-rs-renderer = { version = "0.2", default-features = false }
resvg = "0.46"
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ sequenceDiagram
| `texforge new <name> -t <template>` | Create with specific template |
| `texforge init` | Initialize texforge in an existing LaTeX project |
| `texforge build` | Compile to PDF |
| `texforge build --watch` | Watch for changes and rebuild automatically |
| `texforge clean` | Remove build artifacts |
| `texforge fmt` | Format .tex files |
| `texforge fmt --check` | Check formatting without modifying |
Expand Down Expand Up @@ -198,6 +199,19 @@ Both rendered to PNG via pure Rust — no browser, no Node.js, no `dot` binary r

---

## Watch Mode

`texforge build --watch` watches for `.tex` file changes and rebuilds automatically:

```bash
texforge build --watch # rebuild after 10s of inactivity (default)
texforge build --watch --delay 5 # custom delay in seconds
```

The terminal stays open showing build output. Press `Ctrl+C` to stop.

---

## Linter

`texforge check` runs static analysis without compiling:
Expand Down
17 changes: 15 additions & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ enum Commands {
template: Option<String>,
},
/// Compile project to PDF
Build,
Build {
/// Watch for file changes and rebuild automatically
#[arg(long)]
watch: bool,
/// Debounce delay in seconds before rebuilding (default: 10)
#[arg(long, default_value = "10")]
delay: u64,
},
/// Format .tex files
Fmt {
/// Check formatting without modifying files
Expand Down Expand Up @@ -66,7 +73,13 @@ impl Cli {
Commands::Clean => commands::clean::execute(),
Commands::Init => commands::init::execute(),
Commands::New { name, template } => commands::new::execute(&name, template.as_deref()),
Commands::Build => commands::build::execute(),
Commands::Build { watch, delay } => {
if watch {
commands::build::watch(delay)
} else {
commands::build::execute()
}
}
Commands::Fmt { check } => commands::fmt::execute(check),
Commands::Check => commands::check::execute(),
Commands::Template { action } => match action {
Expand Down
83 changes: 76 additions & 7 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! `texforge build` command implementation.

use std::sync::mpsc;
use std::time::Duration;

use anyhow::Result;
use notify::{RecursiveMode, Watcher};

use crate::compiler;
use crate::diagrams;
Expand All @@ -9,24 +13,89 @@ use crate::domain::project::Project;
/// Compile project to PDF.
pub fn execute() -> Result<()> {
let project = Project::load()?;

println!("Building project: {}", project.config.documento.titulo);

std::fs::create_dir_all(project.root.join("build"))?;

// Pre-process embedded diagrams — works on copies in build/, originals untouched
diagrams::process(&project.root, &project.config.compilacion.entry)?;

// Compile from build/ — all assets are mirrored there, diagrams use relative paths
let build_dir = project.root.join("build");
let entry_filename = std::path::Path::new(&project.config.compilacion.entry)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or(project.config.compilacion.entry.clone());
compiler::compile(&build_dir, &entry_filename)?;

let pdf_name = std::path::Path::new(&project.config.compilacion.entry).with_extension("pdf");
println!("✅ build/{}", pdf_name.display());
Ok(())
}

/// Watch for .tex file changes and rebuild with debounce.
pub fn watch(delay_secs: u64) -> Result<()> {
let project = Project::load()?;
let debounce = Duration::from_secs(delay_secs);

println!(
"Watching project: {} ({}s debounce — Ctrl+C to stop)",
project.config.documento.titulo, delay_secs
);

// Initial build
run_build(&project);

let (tx, rx) = mpsc::channel();
let mut watcher = notify::recommended_watcher(move |res| {
if let Ok(event) = res {
let _ = tx.send(event);
}
})?;

watcher.watch(&project.root, RecursiveMode::Recursive)?;

let build_dir = project.root.join("build");
let mut pending = false;
let mut last_event = std::time::Instant::now();

loop {
match rx.recv_timeout(Duration::from_millis(500)) {
Ok(event) => {
// Only react to .tex file changes, ignore build/
let relevant = event.paths.iter().any(|p| {
!p.starts_with(&build_dir)
&& p.extension().and_then(|e| e.to_str()) == Some("tex")
});
if relevant {
pending = true;
last_event = std::time::Instant::now();
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(_) => break,
}

if pending && last_event.elapsed() >= debounce {
pending = false;
println!("\n--- rebuilding ---");
run_build(&project);
}
}

Ok(())
}

fn run_build(project: &Project) {
let _ = std::fs::create_dir_all(project.root.join("build"));
if let Err(e) = diagrams::process(&project.root, &project.config.compilacion.entry) {
eprintln!("Error: {}", e);
return;
}
let build_dir = project.root.join("build");
let entry_filename = std::path::Path::new(&project.config.compilacion.entry)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or(project.config.compilacion.entry.clone());
match compiler::compile(&build_dir, &entry_filename) {
Ok(()) => {
let pdf = std::path::Path::new(&project.config.compilacion.entry).with_extension("pdf");
println!("✅ build/{}", pdf.display());
}
Err(e) => eprintln!("Error: {}", e),
}
}
Loading