use crate::config;
use crate::config::Config;
use crate::loader::FortuneFile;
use crate::log::ConsoleLog;
use anyhow::{Context, Result};
use fs2::FileExt;
use rand::seq::IndexedRandom;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Write;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::{fs, io};
pub fn random_quote(quotes: &[String]) -> &str {
let mut rng = rand::rng();
quotes.choose(&mut rng).map(|s| s.as_str()).unwrap()
}
pub fn print_random_from_files(paths: &[&Path]) -> Result<(), String> {
let mut all_quotes: Vec<String> = Vec::new();
let mut origin_by_index: Vec<PathBuf> = Vec::new();
for path in paths {
match FortuneFile::from_file(path) {
Ok(f) => {
for q in &f.quotes {
all_quotes.push(q.clone());
origin_by_index.push(path.to_path_buf());
}
}
Err(e) => {
ConsoleLog::warn(format!("Could not load file {}: {e}", path.display()));
}
}
}
if all_quotes.is_empty() {
ConsoleLog::ko("No quotes found in any of the fortune files.");
return Err("No quotes found.".into());
}
let mut last_quote = None;
for p in paths {
if let Ok(q) = load_last_cache(p) {
last_quote = Some(q);
break; }
}
let quote = if let Some(last) = last_quote {
let filtered: Vec<&String> = all_quotes.iter().filter(|q| *q != &last).collect();
if filtered.is_empty() {
all_quotes.choose(&mut rand::rng()).unwrap().clone()
} else {
filtered.choose(&mut rand::rng()).unwrap().to_string()
}
} else {
all_quotes.choose(&mut rand::rng()).unwrap().clone()
};
let idx = all_quotes
.iter()
.position(|q| q == "e)
.expect("internal mismatch");
let origin = &origin_by_index[idx];
println!("{quote}");
if let Err(e) = save_last_cache(origin.as_path(), "e) {
ConsoleLog::warn(format!("Could not update cache: {e}"));
}
Ok(())
}
pub fn get_cache_path(dat_path: &Path) -> PathBuf {
let mut base = config::app_dir();
base.push("cache");
if let Err(e) = fs::create_dir_all(&base) {
ConsoleLog::warn(format!("Unable to create cache directory: {e}"));
}
let name = dat_path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string()
+ ".cache";
base.push(name);
base
}
pub fn read_last_cache(path: &Path) -> Option<String> {
match fs::read_to_string(path) {
Ok(content) => {
ConsoleLog::info(format!("Loaded cached quote from '{}'.", path.display()));
Some(content)
}
Err(_) => None,
}
}
pub fn write_last_cache(path: &Path, quote: &str) {
if let Err(e) = fs::write(path, quote) {
ConsoleLog::warn(format!(
"Failed to write cache file '{}': {e}",
path.display()
));
}
}
pub fn random_nonrepeating(quotes: &[String], last: Option<String>) -> &str {
let mut rng = rand::rng();
let filtered: Vec<&String> = quotes
.iter()
.filter(|q| Some(q.as_str()) != last.as_deref())
.collect();
if filtered.is_empty() {
quotes.choose(&mut rng).unwrap()
} else {
filtered.choose(&mut rng).unwrap()
}
}
fn get_cache_dir() -> PathBuf {
let mut p = config::app_dir();
p.push("rfortune");
p.push("cache");
p
}
pub fn clear_cache_dir() -> io::Result<()> {
let dir = get_cache_dir();
if dir.exists() {
fs::remove_dir_all(&dir)?;
ConsoleLog::ok(format!("Cache directory cleared: {}", dir.display()));
} else {
ConsoleLog::info("Cache directory is already empty.");
}
Ok(())
}
fn ensure_cache_dir(store: &Path) -> Result<()> {
if let Some(parent) = store.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create cache dir: {}", parent.display()))?;
}
Ok(())
}
fn open_and_lock(store: &Path, exclusive: bool) -> Result<File> {
let file = if exclusive {
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(store)
.with_context(|| format!("open cache file: {}", store.display()))?
} else {
OpenOptions::new()
.read(true)
.open(store)
.with_context(|| format!("open cache file (read): {}", store.display()))?
};
if exclusive {
file.lock_exclusive()
.with_context(|| format!("lock cache (exclusive): {}", store.display()))?;
} else {
file.lock_shared()
.with_context(|| format!("lock cache (shared): {}", store.display()))?;
}
Ok(file)
}
pub fn save_last_cache(path: &Path, quote: &str) -> Result<()> {
let store = get_cache_path(path);
ensure_cache_dir(&store)?;
let mut file = open_and_lock(&store, true)?;
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
file.write_all(quote.as_bytes())?;
file.sync_all()?;
file.unlock().ok();
drop(file);
Ok(())
}
pub fn load_last_cache(path: &Path) -> Result<String> {
let store = get_cache_path(path);
if let Some(parent) = store.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create cache directory: {}", parent.display()))?;
}
if !store.exists() {
return Err(anyhow::anyhow!("no cache"));
}
let mut file = open_and_lock(&store, false)?;
let mut data = String::new();
file.seek(SeekFrom::Start(0))?;
file.read_to_string(&mut data).ok();
let _ = file.unlock();
Ok(data)
}
pub fn ensure_app_initialized() -> io::Result<()> {
let dir = config::app_dir();
if dir.exists() {
return Ok(()); }
if atty::is(atty::Stream::Stdin) {
print!("Configuration directory not found. Initialize rFortune now? [Y/n]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
if answer == "n" || answer == "no" {
ConsoleLog::warn("Initialization aborted by user.");
std::process::exit(0);
}
} else {
ConsoleLog::info("Configuration directory missing — initializing automatically.");
}
ConsoleLog::info("Initializing rFortune environment...");
config::init_config_file()?;
ConsoleLog::ok("rFortune initialized successfully.");
Ok(())
}
pub fn get_fortune_sources(cli_files: Option<Vec<String>>, config: &Config) -> Vec<String> {
if let Some(files) = cli_files
&& !files.is_empty()
{
return files;
}
if !config.fortune_files.is_empty() {
return config.fortune_files.clone();
}
if let Some(df) = &config.default_file {
return vec![df.clone()];
}
vec!["/usr/local/share/rfortune/fortunes".into()]
}
pub fn resolve_fortune_sources(cli_files: Option<Vec<String>>, config: &Config) -> Vec<String> {
if let Some(files) = cli_files
&& !files.is_empty()
{
return files;
}
if !config.fortune_files.is_empty() {
return config.fortune_files.clone();
}
if let Some(default) = &config.default_file {
return vec![default.clone()];
}
vec![]
}