use crate::config::{Config, THEME_FILE_NAME};
use crate::terminal::style::Style;
use crate::theme::model::{Mode, ParsedRc};
use crate::theme::parse::{
DropReason, parse_palette_str_with_diagnostics, parse_rc_str_with_diagnostics,
};
use crate::theme::resolve::PALETTE_EXTENSION;
use crate::walk;
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub fn run(config: &Config, explicit_path: Option<PathBuf>) -> Result<ExitCode> {
let style = Style::detect();
let stdout = std::io::stdout();
let mut out = stdout.lock();
let issues = check(config, explicit_path, &mut out, style)?;
Ok(if issues == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
fn check<W: Write>(
config: &Config,
explicit_path: Option<PathBuf>,
out: &mut W,
style: Style,
) -> Result<usize> {
let path = match explicit_path {
Some(p) => p,
None => {
let cwd = std::env::current_dir()?;
match walk::find_nearest(&cwd, THEME_FILE_NAME) {
Some(p) => p,
None => {
writeln!(
out,
"No {} found while walking up from {}",
THEME_FILE_NAME,
cwd.display()
)?;
return Ok(1);
}
}
}
};
let content =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let (rc, diags) = parse_rc_str_with_diagnostics(&content);
writeln!(out, "Checking {}", path.display())?;
writeln!(out)?;
let mut issues = 0usize;
write_diagnostics(out, &diags, " ", style)?;
issues += diags.len();
writeln!(out)?;
writeln!(out, " Palette(s):")?;
let mut audited_palettes: HashSet<PathBuf> = HashSet::new();
issues += report_mode(
&rc,
Mode::Dark,
"dark ",
config,
out,
&mut audited_palettes,
style,
)?;
issues += report_mode(
&rc,
Mode::Light,
"light",
config,
out,
&mut audited_palettes,
style,
)?;
writeln!(out)?;
if issues == 0 {
writeln!(out, " {}", style.green("No issues found."))?;
} else {
let summary = format!(
"Found {issues} issue{}.",
if issues == 1 { "" } else { "s" }
);
writeln!(out, " {}", style.red(&summary))?;
}
Ok(issues)
}
fn write_diagnostics<W: Write>(
out: &mut W,
diags: &[(usize, DropReason)],
indent: &str,
style: Style,
) -> Result<()> {
if diags.is_empty() {
writeln!(out, "{indent}{}", style.green("No parsing errors."))?;
return Ok(());
}
writeln!(out, "{indent}Parsing errors:")?;
for (line, reason) in diags {
let msg = format!("line {line}: {}", format_reason(reason));
writeln!(out, "{indent} {}", style.red(&msg))?;
}
Ok(())
}
fn report_mode<W: Write>(
rc: &ParsedRc,
mode: Mode,
label: &str,
config: &Config,
out: &mut W,
audited_palettes: &mut HashSet<PathBuf>,
style: Style,
) -> Result<usize> {
let Some(name) = rc.parent_for(mode) else {
writeln!(out, " {label}: no parent palette (rc's own keys only)")?;
return Ok(0);
};
let palette_path =
config
.base_theme_dir
.join(format!("{}.{}", name.as_str(), PALETTE_EXTENSION));
if !palette_path.exists() {
writeln!(
out,
" {label}: extends {name} -> {} {}",
palette_path.display(),
style.red("(NOT FOUND)")
)?;
return Ok(1);
}
writeln!(
out,
" {label}: extends {name} -> {}",
palette_path.display()
)?;
if !audited_palettes.insert(palette_path.clone()) {
writeln!(out, " (same palette as above; not re-audited)")?;
return Ok(0);
}
audit_palette(&palette_path, out, style)
}
fn audit_palette<W: Write>(path: &Path, out: &mut W, style: Style) -> Result<usize> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
let msg = format!("Could not read palette {}: {e}", path.display());
writeln!(out, " {}", style.red(&msg))?;
return Ok(1);
}
};
let (_palette, diags) = parse_palette_str_with_diagnostics(&content);
write_diagnostics(out, &diags, " ", style)?;
Ok(diags.len())
}
fn format_reason(r: &DropReason) -> String {
match r {
DropReason::MalformedLine => "malformed line (expected 'key = value')".to_string(),
DropReason::UnknownSection(name) => format!("unknown section [{name}]"),
DropReason::UnknownKey(key) => format!("unknown key '{key}'"),
DropReason::InvalidColor { key, value } => {
format!("invalid color '{value}' for key '{key}' (expected #rrggbb)")
}
DropReason::InvalidExtendsName { key, value, error } => {
format!("invalid theme name '{value}' for key '{key}': {error}")
}
}
}