use clap::Args;
use espforge_options::catalog::{self, Match, ResolutionError, View};
use std::io::IsTerminal;
use std::process::ExitCode;
use std::process::{Command, Stdio};
#[derive(Args, Debug, Clone, Default)]
pub struct EspfArgs {
pub path: Vec<String>,
#[arg(long)]
pub no_pager: bool,
}
pub fn run(args: EspfArgs) -> ExitCode {
match execute(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(code) => ExitCode::from(code),
}
}
fn execute(args: &EspfArgs) -> Result<(), u8> {
let catalog = catalog::embedded();
let (path, view) = catalog::strip_view(args.path.clone());
let found = catalog::resolve(catalog, &path);
let rendered = match &found {
Ok(m) => render(m, view),
Err(e) => Err(crate::help_view::render_resolution_error(e, catalog)),
};
let Ok(text) = rendered else {
eprintln!("❌ {}", rendered.unwrap_err());
return Err(1);
};
if pages_of(&found, &view) && may_page(args) {
page(&text).map_err(|_| 2)
} else {
print!("{text}");
Ok(())
}
}
fn render(m: &Match, view: View) -> Result<String, String> {
let piped = !stdout_is_terminal();
match (m, view) {
(Match::Root(c), View::Default | View::Help) => Ok(crate::help_view::render_root(c, piped)),
(Match::Overview(c), View::Default | View::Help) => Ok(crate::help_view::render_overview(c)),
(Match::Overview(_), View::Examples) => Err(examples_needs_a_leaf()),
(Match::Section(s), View::Default | View::Help) => {
Ok(crate::help_view::render_section(s, piped))
}
(Match::Leaf(e), View::Default) => Ok(crate::help_view::render_leaf_list(e, piped)),
(Match::Leaf(e), View::Help) => Ok(crate::help_view::render_man(catalog::embedded(), e)),
(Match::Leaf(e), View::Examples) => Ok(crate::help_view::render_examples(e)),
(Match::Root(_) | Match::Section(_), View::Examples) => Err(examples_needs_a_leaf()),
}
}
fn examples_needs_a_leaf() -> String {
"examples works on an element, not a section.\n\
Pick one: `espforge espf <section> <element> examples`.\n"
.to_string()
}
fn pages_of(found: &Result<Match, ResolutionError>, view: &View) -> bool {
match found {
Ok(Match::Overview(_)) => true,
Ok(Match::Leaf(_)) if *view == View::Help => true,
_ => false,
}
}
fn may_page(args: &EspfArgs) -> bool {
if args.no_pager {
return false;
}
stdout_is_terminal()
}
fn stdout_is_terminal() -> bool {
std::io::stdout().is_terminal()
}
fn page(text: &str) -> std::io::Result<()> {
let pager = std::env::var("PAGER").unwrap_or_else(|_| "less -R".to_string());
let mut child = Command::new("sh")
.arg("-c")
.arg(&pager)
.stdin(Stdio::piped())
.spawn()?;
use std::io::Write;
child
.stdin
.take()
.expect("stdin was piped")
.write_all(text.as_bytes())?;
child.wait()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn exec(path: &[&str], no_pager: bool) -> Result<(), u8> {
execute(&EspfArgs {
path: path.iter().map(|s| s.to_string()).collect(),
no_pager,
})
}
#[test]
fn the_root_lists_everything() {
assert_eq!(exec(&[], false), Ok(()));
}
#[test]
fn every_path_in_the_grammar_works() {
assert_eq!(exec(&["overview"], true), Ok(()));
assert_eq!(exec(&["peripherals"], true), Ok(()));
assert_eq!(exec(&["peripherals", "help"], true), Ok(()));
assert_eq!(exec(&["peripherals", "i2c"], true), Ok(()));
assert_eq!(exec(&["peripherals", "i2c", "help"], true), Ok(()));
assert_eq!(exec(&["peripherals", "i2c", "examples"], true), Ok(()));
assert_eq!(exec(&["i2c"], true), Ok(()));
}
#[test]
fn unknown_names_are_user_errors() {
assert_eq!(exec(&["peripherals", "i21"], true), Err(1));
assert_eq!(exec(&["nosuch"], true), Err(1));
assert_eq!(exec(&["peripherals", "noleaf", "help"], true), Err(1));
}
#[test]
fn examples_outside_a_leaf_is_an_error_that_says_so() {
assert_eq!(exec(&["peripherals", "examples"], true), Err(1));
}
#[test]
fn overview_takes_no_further_arguments() {
assert_eq!(exec(&["overview", "peripherals"], true), Err(1));
assert_eq!(exec(&["overview", "a", "b", "c", "d"], true), Err(1));
}
#[test]
fn the_overview_sample_penetrates_check_validation() {
use espforge_core::answers::Overrides;
use espforge_core::pipeline;
let overview = catalog::embedded().overview;
let start = overview
.find("EXAMPLE\n")
.expect("overview has an EXAMPLE block")
+ "EXAMPLE\n".len();
let end = overview
.find("\nNEXT\n")
.expect("overview has a NEXT block");
let body = &overview[start..end];
let indent = body
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
let sample: String = body
.lines()
.map(|l| l.get(indent..).unwrap_or("").to_string())
.collect::<Vec<_>>()
.join("\n");
let checked = pipeline::validate(&sample, &Overrides::default())
.map_err(|invalid| super::super::report::invalid(&invalid, "overview-sample", &sample))
.expect("the overview sample must be valid ESPF");
assert_eq!(checked.fe.parsed.sections["devices"].len(), 0);
}
#[test]
fn every_routed_driver_has_a_devices_entry() {
use espforge_bindings::drivers::generated_registry;
let c = catalog::embedded();
for tag in generated_registry::TAGS {
let device = c
.leafs_by_tag(tag)
.into_iter()
.find(|e| e.section == "devices")
.unwrap_or_else(|| panic!("driver `{tag}` has no `devices` entry"));
assert_eq!(device.status.marker(), "", "`{tag}` builds, so no marker");
}
}
}