Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tinty list --json #94

Merged
merged 22 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ clap_complete = { version = "4.5.40" }
hex_color = "3.0.0"
serde = { version = "1.0.216", features = ["derive"] }
serde_yaml = "0.9.33"
serde_json = "1.0.135"
shell-words = "1.1.0"
strip-ansi-escapes = "0.2.0"
tinted-builder-rust = "0.12.1"
Expand All @@ -29,3 +30,4 @@ xdg = "2.5.2"
home = "0.5.11"
rand = "0.8.5"
regex = "1.7"
rayon = "1.10"
6 changes: 6 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ pub fn build_cli() -> Command {
.help("Lists availabile custom schemes")
.long("custom-schemes")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("json")
.long("json")
.help("Output as JSON")
.action(ArgAction::SetTrue),
))
.subcommand(
Command::new("config").about("Provides config related information")
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@
.get_one::<bool>("custom-schemes")
.map(|b| b.to_owned())
.unwrap_or(false);
let is_json = sub_matches
.get_one::<bool>("json")
.map(|b| b.to_owned())

Check warning on line 129 in src/main.rs

View workflow job for this annotation

GitHub Actions / lint / fmt

Diff in /home/runner/work/tinty/tinty/src/main.rs
.unwrap_or(false);


operations::list::list(&data_path, is_custom)?;
operations::list::list(&data_path, is_custom, is_json)?;
}
Some(("apply", sub_matches)) => {
if let Some(theme) = sub_matches.get_one::<String>("scheme_name") {
Expand Down
164 changes: 159 additions & 5 deletions src/operations/list.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
use crate::{
constants::{CUSTOM_SCHEMES_DIR_NAME, REPO_DIR, REPO_NAME, SCHEMES_REPO_NAME},
utils::get_all_scheme_names,
utils::{get_all_scheme_file_paths, get_all_scheme_names},
};
use anyhow::{anyhow, Result};
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use io::Write;
use rayon::prelude::*;
use serde::Serialize;
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use tinted_builder::{Color, Scheme, SchemeSystem, SchemeVariant};
use tinted_builder_rust::operation_build::utils::SchemeFile;

/// Lists available color schemes
///
/// Lists colorschemes file which is updated via scripts/install by getting a list of schemes
/// available in https://github.com/tinted-theming/schemes
pub fn list(data_path: &Path, is_custom: bool) -> Result<()> {
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 {
Expand All @@ -32,10 +42,154 @@ pub fn list(data_path: &Path, is_custom: bool) -> Result<()> {
_ => {}
}

let stdout = io::stdout();
let mut handle = stdout.lock();
if is_json {
let scheme_files = get_all_scheme_file_paths(&schemes_dir_path, None)?;
let json = as_json(scheme_files)?;
if let Err(_) = writeln!(handle, "{}", json) {}
return Ok(());
}

let scheme_vec = get_all_scheme_names(&schemes_dir_path, None)?;
for scheme in scheme_vec {
println!("{}", scheme);
if let Err(_) = writeln!(handle, "{}", scheme) {
break;
}
}

Ok(())
}

#[derive(Clone, Serialize)]
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();
return 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) | Scheme::Base24(s) => s
.palette
.into_iter()
.map(|(k, v)| (k, ColorOut::from_color(&v)))
.collect(),
_ => HashMap::new(),
},
};
}
}

impl ColorOut {
pub fn from_color(color: &Color) -> Self {
return 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) | Scheme::Base24(s) => (
s.palette.get("base05").context("no fg color")?.clone(),
s.palette.get("base00").context("no bg color")?.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;
return base.powf(2.4);
}

fn luminance_to_lstar(luminance: f32) -> f32 {
if luminance <= (216 as f32 / 24389 as f32) {
return luminance * (24389 as f32 / 27 as f32);
}

return luminance.powf(1 as f32 / 3 as f32) * 116 as f32 - 16 as f32;
}

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);
return (r * 0.2126) + (g * 0.7152) + (b * 0.0722);
}
}

fn as_json(scheme_files: HashMap<String, (PathBuf, SchemeFile)>) -> Result<String> {
let mut keys: Vec<String> = scheme_files.keys().cloned().collect();
// Create a thread-safe HashMap to collect results
let locked_results = Arc::new(Mutex::new(HashMap::new()));
let mut sorted_results: Vec<SchemeEntry> = Vec::new();
// We could be parsing hundreds of files. Parallelize with 10 files each arm.
keys.par_chunks(10).try_for_each(|chunk| -> Result<()> {
for key in chunk {
if let Some((_, scheme_file)) = scheme_files.get(key) {
let scheme = scheme_file.get_scheme()?;
let mut results_lock = locked_results.lock().unwrap();
results_lock.insert(key.clone(), SchemeEntry::from_scheme(&scheme));
}
}
Ok(())
})?;
keys.sort();
let results = locked_results.lock().unwrap();
for k in keys {
if let Some(v) = results.get(&k) {
sorted_results.push(v.clone());
}
}

return Ok(serde_json::to_string_pretty(&*sorted_results)?);
}
30 changes: 23 additions & 7 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ use anyhow::{anyhow, Context, Error, Result};
use home::home_dir;
use rand::Rng;
use regex::bytes::Regex;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
use tinted_builder::SchemeSystem;
use tinted_builder_rust::operation_build::utils::SchemeFile;

/// Ensures that a directory exists, creating it if it does not.
pub fn ensure_directory_exists<P: AsRef<Path>>(dir_path: P) -> Result<()> {
Expand Down Expand Up @@ -345,7 +347,7 @@ pub fn git_is_working_dir_clean(target_dir: &Path) -> Result<bool> {
.with_context(|| format!("Failed to execute process in {}", target_dir.display()))?;

// With the --quiet flag, it will return a 0 exit-code if no files has changed.
Ok(status.success())
Ok(!status.success())
bezhermoso marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn create_theme_filename_without_extension(item: &ConfigItem) -> Result<String> {
Expand All @@ -360,15 +362,27 @@ pub fn get_all_scheme_names(
schemes_path: &Path,
scheme_systems_option: Option<SchemeSystem>,
) -> Result<Vec<String>> {
let file_paths = get_all_scheme_file_paths(schemes_path, scheme_systems_option)?;
let mut scheme_vec: Vec<String> = file_paths.into_keys().collect();
scheme_vec.sort();

Ok(scheme_vec)
}

pub fn get_all_scheme_file_paths(
schemes_path: &Path,
scheme_systems_option: Option<SchemeSystem>,
) -> Result<HashMap<String, (PathBuf, SchemeFile)>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this return value is redundant (let me know if I'm missing something). SchemeFile is an enum which contains Yaml(PathBuf) or Yml(PathBuf), so this return value could be:

Suggested change
) -> Result<HashMap<String, (PathBuf, SchemeFile)>> {
) -> Result<HashMap<String, SchemeFile>> {

if !schemes_path.exists() {
return Err(anyhow!(
"Schemes do not exist, run install and try again: `{} install`",
REPO_NAME
));
}

let mut scheme_files: HashMap<String, (PathBuf, SchemeFile)> = HashMap::new();

// For each supported scheme system, add schemes to vec
let mut scheme_vec: Vec<String> = Vec::new();
let scheme_systems = scheme_systems_option
.map(|s| vec![s])
.unwrap_or(SchemeSystem::variants().to_vec());
Expand All @@ -387,7 +401,7 @@ pub fn get_all_scheme_names(
.unwrap_or_default();

if extension == SCHEME_EXTENSION {
scheme_vec.push(format!(
let name = format!(
"{}-{}",
scheme_system.as_str(),
file.unwrap()
Expand All @@ -396,15 +410,17 @@ pub fn get_all_scheme_names(
.unwrap_or_default()
.to_str()
.unwrap_or_default()
));
);

let scheme_file = SchemeFile::new(file_path.as_path())?;
scheme_files.insert(name.clone(), (file_path.clone(), scheme_file));
}
}
}

scheme_vec.sort();

Ok(scheme_vec)
Ok(scheme_files)
}

pub fn replace_tilde_slash_with_home(path_str: &str) -> Result<PathBuf> {
let trimmed_path_str = path_str.trim();
if trimmed_path_str.starts_with("~/") {
Expand Down
Loading