#![allow(clippy::suboptimal_flops)]
use crate::{
constants::{CUSTOM_SCHEMES_DIR_NAME, REPO_DIR, REPO_NAME, SCHEMES_REPO_NAME},
utils::{get_all_scheme_file_paths, get_all_scheme_names},
};
use anyhow::{anyhow, Context, Result};
use io::Write;
use rayon::{iter::ParallelIterator, slice::ParallelSlice};
use serde::Serialize;
use std::{
collections::HashMap,
io,
path::Path,
sync::{Arc, Mutex},
};
use tinted_builder::{Color, Scheme, SchemeSystem, SchemeVariant};
use tinted_builder_rust::operation_build::utils::SchemeFile;
pub fn list(data_path: &Path, is_custom: bool, is_json: bool) -> Result<()> {
let schemes_dir_path = if is_custom {
data_path.join(CUSTOM_SCHEMES_DIR_NAME)
} else {
data_path.join(format!("{REPO_DIR}/{SCHEMES_REPO_NAME}"))
};
match (schemes_dir_path.exists(), is_custom) {
(false, true) => {
return Err(anyhow!(
"You don't have any local custom schemes at: {}",
schemes_dir_path.display(),
))
}
(false, false) => {
return Err(anyhow!(
"Schemes are missing, run install and then try again: `{REPO_NAME} install`",
))
}
_ => {}
}
let stdout = io::stdout();
if is_json {
let scheme_files = get_all_scheme_file_paths(&schemes_dir_path, None)?;
let json = as_json(scheme_files)?;
let mut handle = stdout.lock();
let _ = writeln!(handle, "{json}");
return Ok(());
}
let scheme_vec = get_all_scheme_names(&schemes_dir_path, None)?;
let mut handle = stdout.lock();
for scheme in scheme_vec {
if writeln!(handle, "{scheme}").is_err() {
break;
}
}
Ok(())
}
#[derive(Clone, Serialize)]
pub struct SchemeEntry {
id: String,
name: String,
author: String,
system: SchemeSystem,
variant: SchemeVariant,
slug: String,
palette: HashMap<String, ColorOut>,
lightness: Option<Lightness>,
}
#[derive(Clone, Serialize)]
struct ColorOut {
hex_str: String,
pub hex: (String, String, String),
pub rgb: (u8, u8, u8),
pub dec: (f32, f32, f32),
}
#[derive(Clone, Serialize)]
struct Lightness {
foreground: f32,
background: f32,
}
impl SchemeEntry {
pub fn from_scheme(scheme: &Scheme) -> Self {
let slug = scheme.get_scheme_slug();
let system = scheme.get_scheme_system();
Self {
id: format!("{system}-{slug}"),
name: scheme.get_scheme_name(),
system,
slug,
author: scheme.get_scheme_author(),
variant: scheme.get_scheme_variant(),
lightness: Lightness::from_color(scheme).ok(),
palette: match scheme.clone() {
Scheme::Base16(s) => s
.palette
.into_iter()
.map(|(k, v)| (k, ColorOut::from_color(&v)))
.collect(),
Scheme::Base24(s) => s
.palette
.into_iter()
.map(|(k, v)| (k, ColorOut::from_color(&v)))
.collect(),
Scheme::Tinted8(s) => {
let mut map = HashMap::new();
for (color_name, color_variant) in
tinted_builder::tinted8::Palette::get_color_list()
{
if let Some(color) = s.palette.get_color(&color_name, &color_variant) {
let key = format!("{color_name}-{color_variant}");
map.insert(key, ColorOut::from_color(color));
}
}
map
}
_ => HashMap::new(),
},
}
}
pub(crate) fn to_envs(&self) -> Vec<(String, String)> {
let basic_info = [
("TINTY_SCHEME_ID".to_string(), self.id.clone()),
("TINTY_SCHEME_NAME".to_string(), self.name.clone()),
("TINTY_SCHEME_SLUG".to_string(), self.slug.clone()),
("TINTY_SCHEME_SYSTEM".to_string(), self.system.to_string()),
("TINTY_SCHEME_VARIANT".to_string(), self.variant.to_string()),
]
.to_vec();
let lightness_info = self
.lightness
.clone()
.map(|l| {
[
(
"TINTY_SCHEME_LIGHTNESS_FOREGROUND".to_string(),
l.foreground.to_string(),
),
(
"TINTY_SCHEME_LIGHTNESS_BACKGROUND".to_string(),
l.background.to_string(),
),
]
.to_vec()
})
.unwrap_or_default();
let color_info: Vec<(String, String)> = self
.palette
.clone()
.iter()
.flat_map(|(color, color_out)| {
let c = color.to_uppercase();
[
(
format!("TINTY_SCHEME_PALETTE_{}_HEX_R", c.to_uppercase()),
color_out.hex.0.clone(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_HEX_G", c.to_uppercase()),
color_out.hex.1.clone(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_HEX_B", c.to_uppercase()),
color_out.hex.2.clone(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_RGB_R", c.to_uppercase()),
color_out.rgb.0.to_string(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_RGB_G", c.to_uppercase()),
color_out.rgb.1.to_string(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_RGB_B", c.to_uppercase()),
color_out.rgb.2.to_string(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_DEC_R", c.to_uppercase()),
color_out.dec.0.to_string(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_DEC_G", c.to_uppercase()),
color_out.dec.1.to_string(),
),
(
format!("TINTY_SCHEME_PALETTE_{}_DEC_B", c.to_uppercase()),
color_out.dec.2.to_string(),
),
]
})
.collect();
let mut envs: Vec<(String, String)> = Vec::new();
envs.extend(basic_info);
envs.extend(lightness_info);
envs.extend(color_info);
envs
}
}
impl ColorOut {
pub fn from_color(color: &Color) -> Self {
Self {
hex_str: format!("#{}{}{}", color.hex.0, color.hex.1, color.hex.2),
hex: color.hex.clone(),
rgb: color.rgb,
dec: color.dec,
}
}
}
impl Lightness {
pub fn from_color(scheme: &Scheme) -> Result<Self> {
let (fg, bg) = match scheme.clone() {
Scheme::Base16(s) => (
s.palette.get("base05").context("no fg color")?.clone(),
s.palette.get("base00").context("no bg color")?.clone(),
),
Scheme::Base24(s) => (
s.palette.get("base05").context("no fg color")?.clone(),
s.palette.get("base00").context("no bg color")?.clone(),
),
Scheme::Tinted8(s) => (
s.ui.global.foreground.normal.clone(),
s.ui.global.background.normal.clone(),
),
_ => return Err(anyhow!("no supported palette found")),
};
let fg_luminance = Self::luminance(&fg);
let bg_luminance = Self::luminance(&bg);
let foreground = Self::luminance_to_lstar(fg_luminance);
let background = Self::luminance_to_lstar(bg_luminance);
Ok(Self {
foreground,
background,
})
}
fn gamma_corrected_to_linear(channel: f32) -> f32 {
if channel <= 0.04045 {
return channel / 12.92;
}
let base = (channel + 0.055) / 1.055;
base.powf(2.4)
}
fn luminance_to_lstar(luminance: f32) -> f32 {
if luminance <= (216.0 / 24389.0) {
return luminance * (24389.0 / 27.0);
}
luminance.cbrt().mul_add(116.0, -16.0)
}
fn luminance(color: &Color) -> f32 {
let r = Self::gamma_corrected_to_linear(color.dec.0);
let g = Self::gamma_corrected_to_linear(color.dec.1);
let b = Self::gamma_corrected_to_linear(color.dec.2);
(r * 0.2126) + (g * 0.7152) + (b * 0.0722)
}
}
fn as_json(scheme_files: HashMap<String, SchemeFile>) -> Result<String> {
let mut keys: Vec<String> = scheme_files.keys().cloned().collect();
let mutex = Arc::new(Mutex::new(HashMap::new()));
let mut sorted_results: Vec<SchemeEntry> = Vec::new();
scheme_files
.into_iter()
.collect::<Vec<_>>()
.par_chunks(10)
.map(|chunk| {
chunk
.iter()
.filter_map(|(k, sf)| {
sf.get_scheme()
.ok()
.map(|scheme| (k.clone(), SchemeEntry::from_scheme(&scheme)))
})
.collect::<HashMap<String, SchemeEntry>>()
})
.for_each(|map| {
if let Ok(mut accum) = mutex.lock() {
accum.extend(map);
}
});
keys.sort();
let Ok(results) = mutex.lock() else {
return Err(anyhow!("Unable to unlock mutex"));
};
for k in keys {
if let Some(v) = results.get(&k) {
sorted_results.push(v.clone());
}
}
Ok(serde_json::to_string(&*sorted_results)?)
}