glyphweave 0.3.0

Shape-constrained SVG word clouds, built for speed. Fast Rust CLI + library.
Documentation
use crate::cli::args::{CliAlgorithm, PaletteKind};
use glyphweave::core::error::GlyphWeaveError;
use serde::Deserialize;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Default, Deserialize)]
pub struct FileConfig {
	pub canvas_size: Option<[usize; 2]>,
	pub canvas_margin: Option<usize>,
	pub word_size_range: Option<[usize; 2]>,
	pub colors: Option<Vec<String>>,
	pub rotations: Option<Vec<u16>>,
	pub text_size: Option<String>,
	pub algorithm: Option<String>,
	pub font: Option<PathBuf>,
	pub seed: Option<u64>,
	pub ratio: Option<f32>,
	pub max_tries: Option<usize>,
	pub no_progress: Option<bool>,
	pub palette: Option<String>,
	pub palette_base: Option<String>,
	pub palette_size: Option<usize>,
}

impl FileConfig {
	fn merge_from(&mut self, other: FileConfig) {
		if other.canvas_size.is_some() {
			self.canvas_size = other.canvas_size;
		}
		if other.canvas_margin.is_some() {
			self.canvas_margin = other.canvas_margin;
		}
		if other.word_size_range.is_some() {
			self.word_size_range = other.word_size_range;
		}
		if other.colors.is_some() {
			self.colors = other.colors;
		}
		if other.rotations.is_some() {
			self.rotations = other.rotations;
		}
		if other.text_size.is_some() {
			self.text_size = other.text_size;
		}
		if other.algorithm.is_some() {
			self.algorithm = other.algorithm;
		}
		if other.font.is_some() {
			self.font = other.font;
		}
		if other.seed.is_some() {
			self.seed = other.seed;
		}
		if other.ratio.is_some() {
			self.ratio = other.ratio;
		}
		if other.max_tries.is_some() {
			self.max_tries = other.max_tries;
		}
		if other.no_progress.is_some() {
			self.no_progress = other.no_progress;
		}
		if other.palette.is_some() {
			self.palette = other.palette;
		}
		if other.palette_base.is_some() {
			self.palette_base = other.palette_base;
		}
		if other.palette_size.is_some() {
			self.palette_size = other.palette_size;
		}
	}

	pub fn canvas_size_tuple(&self) -> Option<(usize, usize)> {
		self.canvas_size.map(|size| (size[0], size[1]))
	}

	pub fn word_size_tuple(&self) -> Option<(usize, usize)> {
		self.word_size_range.map(|size| (size[0], size[1]))
	}

	pub fn algorithm_enum(&self) -> Result<Option<CliAlgorithm>, GlyphWeaveError> {
		let Some(text) = self.algorithm.as_deref() else {
			return Ok(None);
		};

		CliAlgorithm::parse_text(text).map(Some).ok_or_else(|| {
			GlyphWeaveError::InvalidConfig(format!("invalid algorithm '{text}' in config"))
		})
	}

	pub fn palette_enum(&self) -> Result<Option<PaletteKind>, GlyphWeaveError> {
		let Some(text) = self.palette.as_deref() else {
			return Ok(None);
		};

		PaletteKind::parse_text(text).map(Some).ok_or_else(|| {
			GlyphWeaveError::InvalidConfig(format!("invalid palette '{text}' in config"))
		})
	}
}

pub fn load_merged_config(explicit_path: Option<&Path>) -> Result<FileConfig, GlyphWeaveError> {
	let mut merged = FileConfig::default();

	if let Some(path) = user_config_path()
		&& path.exists()
	{
		let file = load_config_file(&path)?;
		merged.merge_from(file);
	}

	let project = PathBuf::from(".glyphweave.toml");
	if project.exists() {
		let file = load_config_file(&project)?;
		merged.merge_from(file);
	}

	if let Some(path) = explicit_path {
		let file = load_config_file(path)?;
		merged.merge_from(file);
	}

	Ok(merged)
}

fn load_config_file(path: &Path) -> Result<FileConfig, GlyphWeaveError> {
	let content = std::fs::read_to_string(path).map_err(|err| {
		GlyphWeaveError::InvalidConfig(format!("failed to read config '{}': {err}", path.display()))
	})?;

	toml::from_str::<FileConfig>(&content).map_err(|err| {
		GlyphWeaveError::InvalidConfig(format!(
			"failed to parse config '{}': {err}",
			path.display()
		))
	})
}

fn user_config_path() -> Option<PathBuf> {
	if let Ok(xdg_home) = std::env::var("XDG_CONFIG_HOME") {
		let mut path = PathBuf::from(xdg_home);
		path.push("glyphweave");
		path.push("config.toml");
		return Some(path);
	}

	let Ok(home) = std::env::var("HOME") else {
		return None;
	};

	let mut path = PathBuf::from(home);
	path.push(".config");
	path.push("glyphweave");
	path.push("config.toml");
	Some(path)
}