use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex, RwLock},
time::Instant,
};
use crate::{
anki_api::get_anki_deck_name,
card_wrapper::{CardInfo, CardModificationStatus, TFiles},
output::{OutputManager, OutputMessage},
output_console::OutputConsole,
};
mod anki_api;
mod auto_number;
mod card_wrapper;
mod cards_cache;
mod compile;
mod config;
mod generator;
mod output;
mod output_console;
mod parse_file;
mod typst_as_library;
mod utils;
fn main() -> anyhow::Result<()> {
let cfg = config::get();
let _cfg_guard = config::ConfigGuard;
let output = OutputConsole::new();
if cfg.auto_number_file.is_some() {
return auto_number::run_auto_number(output);
}
run(output);
Ok(())
}
fn run(output: impl OutputManager + 'static) {
let output = Arc::new(output);
let cfg = config::get();
if cfg.dry_run {
output.send(OutputMessage::DbgShowConfig(cfg.clone()));
}
parse_file::check_ankiconf_exists();
let ankiconf_hash = parse_file::get_ankiconf_hash();
let mut cards_cache_manager =
cards_cache::CardsCacheManager::init(ankiconf_hash, output.as_ref());
let typ_files = walkdir::WalkDir::new(&cfg.path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|s| s.to_str())
.map(|s| s.eq_ignore_ascii_case("typ"))
.unwrap_or(false)
})
.map(|e| e.path().to_path_buf())
.filter(|p| {
let s = p.file_name().unwrap_or_default().to_string_lossy();
!(s == "ankiconf.typ" || s.starts_with("temporal-"))
})
.collect::<Vec<std::path::PathBuf>>();
let mut i = 0;
let mut cards: Vec<CardInfo> = Vec::new();
let files: TFiles = Arc::new(RwLock::new(HashMap::new()));
let mut deck_names: HashSet<String> = HashSet::new();
let mut files_lock = files.write().unwrap();
for filepath in &typ_files {
let p = cfg.path_relative_to_root(filepath);
if cfg.is_file_excluded(p.as_ref()) {
if cfg.dry_run {
output.send(OutputMessage::ParsingError(
format!("Skipping excluded file: {}", p).to_string(),
));
}
continue;
}
let file;
if let Ok(content) = std::fs::read_to_string(filepath) {
file = match parse_file::parse_cards_from_file_content(
filepath,
content,
&mut cards_cache_manager,
output.clone(),
&mut i,
&mut deck_names,
&mut cards,
) {
Ok(f) => f,
Err(e) => {
output.send(OutputMessage::ParsingError(e));
continue;
}
};
} else {
output.send(OutputMessage::ParsingError(format!(
"Warning: Failed to read file {:?}",
filepath.to_string_lossy()
)));
continue;
}
if file.total_cards == 0 {
continue;
}
files_lock.insert(filepath.clone(), file);
}
if cards.is_empty() {
output.send(OutputMessage::ParsingError(
"No cards found, aborting.".to_string(),
));
return output.fail();
}
if !anki_api::check_anki_running() {
output.send(OutputMessage::NoAnkiConnection);
if !cfg.dry_run {
return output.fail();
}
}
for deck_name in &deck_names {
if cfg.dry_run {
output.send(OutputMessage::DbgCreateDeck(deck_name.to_string()));
} else {
let _ = anki_api::create_deck(&get_anki_deck_name(deck_name));
}
}
if cfg.check_duplicates {
let mut seen_ids = HashSet::new();
let mut e = false;
for card in &cards {
if seen_ids.contains(&card.card_id) {
output.send(OutputMessage::ParsingError(format!(
"Warning: Duplicate card ID found: {} ({})",
card.card_id,
card.source_file.to_string_lossy()
)));
e = true;
} else {
seen_ids.insert(card.card_id.clone());
}
}
if e && !cfg.dry_run {
output.send(OutputMessage::ParsingError(
"Error: Duplicate card IDs found, aborting.".to_string(),
));
return output.fail();
}
}
cards_cache_manager.detect_configuration_change(output.as_ref());
for card in &mut cards {
card.set_status(&cards_cache_manager);
card.anki_deck_name = Some(anki_api::get_anki_deck_name(&card.deck_name));
}
for card in &cards {
if let Some(file_stats) = files_lock.get_mut(&card.source_file) {
match card.modification_status {
CardModificationStatus::Unchanged => {
file_stats.unchanged_cards.0 += 1;
}
CardModificationStatus::Updated => {
file_stats.updated_cards.0 += 1;
}
CardModificationStatus::New => {
file_stats.new_cards.0 += 1;
}
CardModificationStatus::Unknown => {}
}
}
}
drop(files_lock);
output.send(OutputMessage::ListTypstFiles(files.clone()));
let cards_cache_manager = Arc::new(Mutex::new(cards_cache_manager));
let now = Instant::now();
compile::compile_cards_concurrent(
&cards,
output.clone(),
cards_cache_manager.clone(),
files.clone(),
);
let elapsed = now.elapsed();
let cards_cache_manager = match Arc::try_unwrap(cards_cache_manager) {
Ok(mutex) => mutex.into_inner().unwrap(),
Err(_) => panic!("Failed to unwrap Arc for CardsCacheManager"),
};
output.send(OutputMessage::DbgCompilationDone {
files: files.clone(),
});
let compiled_count = cards
.iter()
.filter(|c| c.modification_status != CardModificationStatus::Unchanged)
.count();
println!(
"Compiled {} cards in {:.2?} ({:.2} cards/sec)",
compiled_count,
elapsed,
compiled_count as f64 / elapsed.as_secs_f64()
);
if !cfg.dry_run {
cards_cache_manager.save_cache(output.as_ref());
}
output.send(OutputMessage::DbgDone);
if cfg.keep_terminal_open {
println!("Press Enter to exit...");
let mut input = String::new();
let _ = std::io::stdin().read_line(&mut input);
}
}