1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#![allow(clippy::module_name_repetitions)]
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use std::{collections::HashMap, env};
use syn::Ident;
#[allow(clippy::too_many_lines)]
pub fn preload_themes_impl(_input: TokenStream) -> TokenStream {
// eprintln!("\ncurrent_dir={:?}", env::current_dir());
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
// eprintln!("The project manifest directory is: {manifest_dir}");
let themes_dir = manifest_dir + "/themes/built_in";
let mut theme_indices = Vec::new();
let mut bg_to_names: HashMap<[u8; 3], Vec<String>> = HashMap::new();
#[allow(clippy::map_unwrap_or, clippy::unnecessary_map_or)]
for entry in std::fs::read_dir(themes_dir).unwrap() {
let path = entry.unwrap().path();
// Skip hidden files like .DS_Store and read only .toml files
if path.file_name().and_then(|n| n.to_str()).map_or(true, |n| {
n.starts_with('.')
|| !std::path::Path::new(n)
.extension()
.map(|ext| ext.eq_ignore_ascii_case("toml"))
.unwrap_or(false)
}) {
continue;
}
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Error reading {}", path.display()));
let value: toml::Value = toml::from_str(&content)
.unwrap_or_else(|_| panic!("Bad toml at path {}", path.display()));
let name = path.file_stem().unwrap().to_str().unwrap().to_string();
if let Some(bg_array) = value.get("backgrounds").and_then(|v| v.as_array()) {
let backgrounds: Vec<_> = bg_array
.iter()
.filter_map(|v| v.as_str())
.filter_map(hex_to_rgb)
.collect();
let bg_rgbs = backgrounds.iter().map(|[r, g, b]| {
quote! { [#r, #g, #b] }
});
let term_bg_luma = to_upper_camel_case(
value
.get("term_bg_luma")
.and_then(|v| v.as_str())
.unwrap_or("dark"),
);
let term_bg_luma_ident = Ident::new(&term_bg_luma, Span::call_site());
// let min_color_support_ident = Ident::new(&min_color_support, Span::call_site());
let theme_index = quote! {
#name => ThemeIndex {
name: #name,
bg_rgbs: &[#(#bg_rgbs),*],
term_bg_luma: TermBgLuma::#term_bg_luma_ident,
min_color_support: ColorSupport::TrueColor, // Will generate 256 if needed at run time
content: #content,
}
};
theme_indices.push(theme_index);
// Build bg->names mapping
for bg_rgb in backgrounds {
bg_to_names.entry(bg_rgb).or_default().push(name.clone());
}
}
}
// eprintln!("Building index by background...");
let bg_lookup_entries = bg_to_names.iter().map(|([r, g, b], names)| {
let hex = format!("{r:02x}{g:02x}{b:02x}");
quote! {
#hex => &[#(#names),*]
}
});
// eprintln!("Done!");
quote! {
/// Generated theme index and background lookup tables
///
/// This macro generates the `THEME_INDEX` and `BG_LOOKUP` static data structures
/// that contain preloaded theme definitions and background color mappings.
///
/// # Generated Structures
///
/// ## `ThemeIndex`
/// A struct containing theme metadata and content:
/// - `content`: The raw TOML content of the theme
/// - `term_bg_luma`: Background luminance requirement
/// - `min_color_support`: Minimum color support level
/// - `bg_rgbs`: RGB values for theme backgrounds
///
/// ## `THEME_INDEX`
/// A static HashMap mapping theme names to their `ThemeIndex` data
///
/// ## `BG_LOOKUP`
/// A static HashMap mapping background color hex values to theme names
#[derive(Debug)]
pub struct ThemeIndex {
/// The name of the theme
pub name: &'static str,
/// Array of RGB color values that represent the theme's background colors
pub bg_rgbs: &'static [[u8; 3]],
/// The background luminance requirement (light or dark) for this theme
pub term_bg_luma: TermBgLuma,
/// The minimum color support level required by this theme
pub min_color_support: ColorSupport,
/// The raw TOML content of the theme definition
pub content: &'static str,
}
impl ThemeIndex {
/// Checks if the given background color matches any of this theme's background colors
///
/// # Arguments
/// * `bg` - RGB color array to check against theme backgrounds
///
/// # Returns
/// `true` if the color matches any theme background, `false` otherwise
fn matches_background(&self, bg: [u8; 3]) -> bool {
// eprintln!("bg={bg:?}, self.bg_rgbs={:?}", self.bg_rgbs);
self.bg_rgbs.iter().any(|&theme_bg| {
bg == theme_bg
})
}
/// Gets a theme instance with the specified color support level
///
/// Loads the theme and converts its colors to match the specified color support level.
/// Colors are automatically downgraded if necessary (e.g., from TrueColor to Color256 or Basic).
///
/// # Arguments
/// * `color_support` - The target color support level
///
/// # Returns
/// A `Theme` instance with colors adjusted for the specified support level
///
/// # Panics
/// Panics if the theme cannot be loaded (should not happen for valid theme index entries)
fn get_theme_with_color_support(&self, color_support: ColorSupport) -> Theme {
let mut theme = Theme::get_builtin(self.name).expect("Could not get theme");
if color_support != ColorSupport::TrueColor {
theme.convert_to_color_support(color_support);
}
theme
}
}
static THEME_INDEX: phf::Map<&'static str, ThemeIndex> = phf::phf_map! {
#(#theme_indices),*
};
static BG_LOOKUP: phf::Map<&'static str, &'static [&'static str]> = phf::phf_map! {
#(#bg_lookup_entries),*
};
/// Converts RGB color values to a hexadecimal color string with '#' prefix
///
/// # Arguments
/// * `[r, g, b]` - An array reference containing RGB values (0-255)
///
/// # Returns
/// A string in the format "#rrggbb" where each component is represented as two lowercase hexadecimal digits
///
/// # Examples
/// ```
/// use thag_styling::styling::rgb_to_hex;
/// let hex = rgb_to_hex(&[255, 128, 0]);
/// assert_eq!(hex, "#ff8000");
/// ```
#[must_use]
pub fn rgb_to_hex(&[r, g, b]: &[u8; 3]) -> String {
format!("#{r:02x}{g:02x}{b:02x}")
}
/// Converts RGB color values to a hexadecimal color string without '#' prefix
///
/// # Arguments
/// * `[r, g, b]` - An array reference containing RGB values (0-255)
///
/// # Returns
/// A string in the format "rrggbb" where each component is represented as two lowercase hexadecimal digits
///
/// # Examples
/// ```
/// use thag_styling::styling::rgb_to_bare_hex;
/// let hex = rgb_to_bare_hex(&[255, 128, 0]);
/// assert_eq!(hex, "ff8000");
/// ```
#[must_use]
pub fn rgb_to_bare_hex(&[r, g, b]: &[u8; 3]) -> String {
format!("{r:02x}{g:02x}{b:02x}")
}
}
.into()
}
fn hex_to_rgb(hex: &str) -> Option<[u8; 3]> {
let hex = hex.trim_start_matches('#');
if hex.len() == 6 {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some([r, g, b])
} else {
None
}
}
fn to_upper_camel_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.extend(c.to_uppercase());
capitalize_next = false;
} else {
result.extend(c.to_lowercase());
}
}
result
}