1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
use std::path::PathBuf;
use anyhow::{Context, Result};
use caixa_core::LAYOUT_DIR_LIB;
use caixa_fmt::{FmtConfig, format_source};
use clap::Args;
/// Format caixa.lisp (or any .lisp file) via caixa-fmt.
///
/// Behavior mirrors `cargo fmt`: in-place rewrite by default, `--check` for
/// a non-zero exit if the file isn't already formatted.
#[derive(Args)]
pub struct Fmt {
/// Paths to format. Defaults to `./caixa.lisp` + every `.lisp` under `lib/`.
#[arg(value_name = "PATH")]
pub paths: Vec<PathBuf>,
/// Check only — exit 0 if already-formatted, 1 otherwise. Don't write.
#[arg(long)]
pub check: bool,
/// Print the formatted output to stdout instead of writing it.
#[arg(long)]
pub stdout: bool,
/// Override the line width (default 100).
#[arg(long)]
pub line_width: Option<usize>,
}
impl Fmt {
pub fn run(self) -> Result<()> {
let cfg = FmtConfig {
line_width: self.line_width.unwrap_or(100),
..FmtConfig::default()
};
let targets = self.resolve_targets()?;
let mut any_changed = false;
for path in &targets {
let src = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
let formatted = format_source(&src, &cfg)
.with_context(|| format!("formatting {}", path.display()))?;
// `--stdout` is a filter: it must emit the document every time,
// including when it was already formatted. The unchanged-file
// skip below used to run first, so `feira fmt --stdout` on a
// clean file printed nothing and exited 0 — which silently
// truncates the buffer of any editor wired to capture stdout.
if self.stdout {
print!("{formatted}");
any_changed |= formatted != src;
continue;
}
if formatted == src {
continue;
}
any_changed = true;
if self.check {
eprintln!("would reformat {}", path.display());
} else {
std::fs::write(path, &formatted)
.with_context(|| format!("writing {}", path.display()))?;
eprintln!("reformatted {}", path.display());
}
}
if self.check && any_changed {
std::process::exit(1);
}
Ok(())
}
fn resolve_targets(&self) -> Result<Vec<PathBuf>> {
if !self.paths.is_empty() {
return Ok(self.paths.clone());
}
let mut out = Vec::new();
let root = PathBuf::from(".");
let manifest = root.join("caixa.lisp");
if manifest.exists() {
out.push(manifest);
}
if let Ok(dir) = std::fs::read_dir(root.join(LAYOUT_DIR_LIB)) {
for entry in dir.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "lisp") {
out.push(p);
}
}
}
Ok(out)
}
}