mod steps;
use std::path::{Path, PathBuf};
use super::detect::{contains_nerd_font, default_font_dirs};
use crate::i18n::translate_active;
use crate::runtime::{Command, Task};
pub const RELEASE: &str = "v3.5.1";
pub const FAMILY: &str = "Symbols Nerd Font Mono";
pub const FONT_FILE: &str = "SymbolsNerdFontMono-Regular.ttf";
const LINUX_FOLDER: &str = "QuvytaNerdFont";
const RELEASE_URL: &str = "https://github.com/ryanoasis/nerd-fonts/releases/download";
const TAR_XZ: (&str, &str) =
("NerdFontsSymbolsOnly.tar.xz", "01172f37db8543edb102e5cb5c64101c9f4686630804d49b419aa07b23a69996");
const ZIP: (&str, &str) =
("NerdFontsSymbolsOnly.zip", "fdca3682534f6f65e1ccb2345b0362ccf67d9b8eca7c8025330946e93e2473bc");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Os {
Linux,
Mac,
Windows,
}
impl Os {
fn current() -> Self {
if cfg!(windows) {
Self::Windows
} else if cfg!(target_os = "macos") {
Self::Mac
} else {
Self::Linux
}
}
}
fn process_env(name: &str) -> Option<String> {
std::env::var(name).ok()
}
#[must_use]
pub fn installed() -> bool {
installed_in(&default_font_dirs(process_env))
}
#[must_use]
pub fn installed_in(dirs: &[PathBuf]) -> bool {
dirs.iter().any(|dir| contains_nerd_font(dir, 3))
}
#[must_use]
pub fn target_dir() -> Option<PathBuf> {
target_dir_for(Os::current(), process_env)
}
fn target_dir_for(os: Os, env: impl Fn(&str) -> Option<String>) -> Option<PathBuf> {
let absolute = |name: &str| env(name).map(PathBuf::from).filter(|path| path.is_absolute());
match os {
Os::Linux => absolute("XDG_DATA_HOME")
.or_else(|| absolute("HOME").map(|home| home.join(".local").join("share")))
.map(|data| data.join("fonts").join(LINUX_FOLDER)),
Os::Mac => absolute("HOME").map(|home| home.join("Library").join("Fonts")),
Os::Windows => absolute("LOCALAPPDATA").map(|local| local.join("Microsoft").join("Windows").join("Fonts")),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Archive {
url: String,
sha256: String,
}
impl Archive {
#[must_use]
pub fn release() -> Self {
release_for(Os::current())
}
#[must_use]
pub fn new(url: impl Into<String>, sha256: &str) -> Self {
Self { url: url.into(), sha256: sha256.trim().to_ascii_lowercase() }
}
#[must_use]
pub fn url(&self) -> &str {
&self.url
}
#[must_use]
pub fn sha256(&self) -> &str {
&self.sha256
}
}
fn release_for(os: Os) -> Archive {
let (name, sha256) = if os == Os::Windows { ZIP } else { TAR_XZ };
Archive::new(format!("{RELEASE_URL}/{RELEASE}/{name}"), sha256)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Install {
archive: Archive,
target: Option<PathBuf>,
register: bool,
staging: PathBuf,
}
impl Default for Install {
fn default() -> Self {
Self::new()
}
}
impl Install {
#[must_use]
pub fn new() -> Self {
Self { archive: Archive::release(), target: target_dir(), register: true, staging: std::env::temp_dir() }
}
#[must_use]
pub fn archive(mut self, archive: Archive) -> Self {
self.archive = archive;
self
}
#[must_use]
pub fn target(mut self, dir: impl Into<PathBuf>) -> Self {
self.target = Some(dir.into());
self
}
#[must_use]
pub fn register(mut self, register: bool) -> Self {
self.register = register;
self
}
#[must_use]
pub fn target_dir(&self) -> Option<&Path> {
self.target.as_deref()
}
pub fn run(
self,
cancel: &dyn Fn() -> bool,
on_progress: &mut dyn FnMut(Progress),
) -> Result<PathBuf, InstallError> {
let result = steps::run(&self, Os::current(), cancel, on_progress);
match &result {
Ok(path) => on_progress(Progress::Done { path: path.clone() }),
Err(InstallError::Cancelled) => {}
Err(error) => on_progress(Progress::Failed(error.clone())),
}
result
}
#[must_use]
pub fn task<Msg: Send + 'static>(self, on_progress: impl Fn(Progress) -> Msg + Send + Sync + 'static) -> Task<Msg> {
let texts = steps::Texts::capture();
Task::new(translate_active("quvyta.nerd-font.task", &[]), move |cx| {
let mut noted = false;
let mut report = |progress: Progress| {
match &progress {
Progress::Downloading { fraction } => {
if !noted {
noted = true;
cx.note(texts.get("quvyta.nerd-font.downloading", ""));
}
if let Some(fraction) = fraction {
cx.progress(fraction * 0.9);
}
}
Progress::Verifying => cx.note(texts.get("quvyta.nerd-font.verifying", "")),
Progress::Installing => cx.note(texts.get("quvyta.nerd-font.installing", "")),
Progress::Done { .. } | Progress::Failed(_) => return,
}
cx.send(on_progress(progress));
};
let result = self.run(&|| cx.is_cancelled(), &mut report);
match result {
Ok(path) => {
cx.progress(1.0);
Ok(on_progress(Progress::Done { path }))
}
Err(error) => {
let (key, detail) = error.key_and_detail();
let reason = texts.get(key, detail);
if error != InstallError::Cancelled {
cx.send(on_progress(Progress::Failed(error)));
}
Err(reason)
}
}
})
}
}
#[must_use]
pub fn install<Msg: Send + 'static>(on_progress: impl Fn(Progress) -> Msg + Send + Sync + 'static) -> Command<Msg> {
Command::task(Install::new().task(on_progress))
}
#[derive(Debug, Clone, PartialEq)]
pub enum Progress {
Downloading {
fraction: Option<f32>,
},
Verifying,
Installing,
Done {
path: PathBuf,
},
Failed(InstallError),
}
impl Progress {
#[must_use]
pub fn text(&self) -> String {
match self {
Self::Downloading { fraction: None } => translate_active("quvyta.nerd-font.downloading", &[]),
Self::Downloading { fraction: Some(fraction) } => {
#[allow(clippy::cast_possible_truncation)]
let percent = (fraction.clamp(0.0, 1.0) * 100.0).round() as i32;
translate_active("quvyta.nerd-font.downloading-share", &[("percent", percent.into())])
}
Self::Verifying => translate_active("quvyta.nerd-font.verifying", &[]),
Self::Installing => translate_active("quvyta.nerd-font.installing", &[]),
Self::Done { path } => {
translate_active("quvyta.nerd-font.done", &[("path", path.display().to_string().into())])
}
Self::Failed(error) => error.text(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstallError {
NoFolder,
MissingTool(String),
Download(String),
Verify(String),
Checksum {
expected: String,
actual: String,
},
Extract(String),
Copy(String),
Register(String),
Cancelled,
}
impl InstallError {
fn key_and_detail(&self) -> (&'static str, &str) {
match self {
Self::NoFolder => ("quvyta.nerd-font.error-folder", ""),
Self::MissingTool(tool) => ("quvyta.nerd-font.error-tool", tool),
Self::Download(detail) => ("quvyta.nerd-font.error-download", detail),
Self::Verify(detail) => ("quvyta.nerd-font.error-verify", detail),
Self::Checksum { .. } => ("quvyta.nerd-font.error-checksum", ""),
Self::Extract(detail) => ("quvyta.nerd-font.error-extract", detail),
Self::Copy(detail) => ("quvyta.nerd-font.error-copy", detail),
Self::Register(detail) => ("quvyta.nerd-font.error-register", detail),
Self::Cancelled => ("quvyta.nerd-font.cancelled", ""),
}
}
#[must_use]
pub fn text(&self) -> String {
let (key, detail) = self.key_and_detail();
translate_active(key, &[("detail", detail.into())])
}
}
#[must_use]
pub fn after_install_text() -> String {
translate_active("quvyta.nerd-font.after-install", &[])
}
#[must_use]
pub fn status_text(installed: bool) -> String {
let key = if installed { "quvyta.nerd-font.found" } else { "quvyta.nerd-font.missing" };
translate_active(key, &[])
}
#[cfg(test)]
mod tests;