| 34 |
34 |
|
//! four = "#ebcb8b"; five = "#b48ead"; six = "#88c0d0"
|
| 35 |
35 |
|
//! ```
|
| 36 |
36 |
|
|
|
37 |
+ |
// Color-space math: single-letter channel names (r/g/b/l/m/s) and the published
|
|
38 |
+ |
// high-precision OKLab/sRGB matrix constants are the domain vocabulary here.
|
|
39 |
+ |
#![allow(clippy::many_single_char_names, clippy::unreadable_literal)]
|
|
40 |
+ |
|
| 37 |
41 |
|
use serde::Serialize;
|
| 38 |
42 |
|
use std::collections::{BTreeMap, HashMap};
|
| 39 |
43 |
|
use std::path::{Path, PathBuf};
|
| 554 |
558 |
|
.chars()
|
| 555 |
559 |
|
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|
| 556 |
560 |
|
{
|
| 557 |
|
- |
return Err(format!("Invalid theme ID: {}", id));
|
|
561 |
+ |
return Err(format!("Invalid theme ID: {id}"));
|
| 558 |
562 |
|
}
|
| 559 |
563 |
|
Ok(())
|
| 560 |
564 |
|
}
|
| 591 |
595 |
|
if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) {
|
| 592 |
596 |
|
for (key, val) in sect {
|
| 593 |
597 |
|
if let Some(color) = val.as_str() {
|
| 594 |
|
- |
colors.insert(format!("{}.{}", section, key), color.to_string());
|
|
598 |
+ |
colors.insert(format!("{section}.{key}"), color.to_string());
|
| 595 |
599 |
|
}
|
| 596 |
600 |
|
}
|
| 597 |
601 |
|
}
|
| 607 |
611 |
|
let mut seen: HashMap<String, ThemeMeta> = HashMap::new();
|
| 608 |
612 |
|
|
| 609 |
613 |
|
for (dir, is_custom) in dirs {
|
| 610 |
|
- |
let entries = match std::fs::read_dir(dir) {
|
| 611 |
|
- |
Ok(e) => e,
|
| 612 |
|
- |
Err(_) => continue,
|
|
614 |
+ |
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
615 |
+ |
continue;
|
| 613 |
616 |
|
};
|
| 614 |
617 |
|
|
| 615 |
618 |
|
for entry in entries {
|
| 616 |
|
- |
let entry = match entry {
|
| 617 |
|
- |
Ok(e) => e,
|
| 618 |
|
- |
Err(_) => continue,
|
|
619 |
+ |
let Ok(entry) = entry else {
|
|
620 |
+ |
continue;
|
| 619 |
621 |
|
};
|
| 620 |
622 |
|
let path = entry.path();
|
| 621 |
623 |
|
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
|
| 628 |
630 |
|
.unwrap_or_default()
|
| 629 |
631 |
|
.to_string();
|
| 630 |
632 |
|
|
| 631 |
|
- |
let content = match std::fs::read_to_string(&path) {
|
| 632 |
|
- |
Ok(c) => c,
|
| 633 |
|
- |
Err(_) => continue,
|
|
633 |
+ |
let Ok(content) = std::fs::read_to_string(&path) else {
|
|
634 |
+ |
continue;
|
| 634 |
635 |
|
};
|
| 635 |
636 |
|
let table: toml::Table = match content.parse() {
|
| 636 |
637 |
|
Ok(t) => t,
|
| 651 |
652 |
|
/// Checks directories in reverse order so the highest-priority directory wins.
|
| 652 |
653 |
|
/// Returns `(path, is_custom)` or `None` if not found.
|
| 653 |
654 |
|
pub fn find_theme_path(dirs: &[(PathBuf, bool)], id: &str) -> Option<(PathBuf, bool)> {
|
| 654 |
|
- |
let filename = format!("{}.toml", id);
|
|
655 |
+ |
let filename = format!("{id}.toml");
|
| 655 |
656 |
|
|
| 656 |
657 |
|
for (dir, is_custom) in dirs.iter().rev() {
|
| 657 |
658 |
|
let path = dir.join(&filename);
|
| 669 |
670 |
|
validate_theme_id(id)?;
|
| 670 |
671 |
|
let table: toml::Table = content
|
| 671 |
672 |
|
.parse()
|
| 672 |
|
- |
.map_err(|e| format!("Failed to parse theme '{}': {}", id, e))?;
|
|
673 |
+ |
.map_err(|e| format!("Failed to parse theme '{id}': {e}"))?;
|
| 673 |
674 |
|
let meta = parse_meta(id, &table, is_custom);
|
| 674 |
675 |
|
let colors = extract_colors(&table);
|
| 675 |
676 |
|
Ok(ThemeColors { meta, colors })
|
| 680 |
681 |
|
validate_theme_id(id)?;
|
| 681 |
682 |
|
|
| 682 |
683 |
|
let (path, is_custom) =
|
| 683 |
|
- |
find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
|
|
684 |
+ |
find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{id}' not found"))?;
|
| 684 |
685 |
|
|
| 685 |
686 |
|
let content = std::fs::read_to_string(&path)
|
| 686 |
687 |
|
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
|
| 708 |
709 |
|
let content = std::fs::read_to_string(source_path)
|
| 709 |
710 |
|
.map_err(|e| format!("Failed to read {}: {}", source_path.display(), e))?;
|
| 710 |
711 |
|
|
| 711 |
|
- |
let table: toml::Table = content
|
| 712 |
|
- |
.parse()
|
| 713 |
|
- |
.map_err(|e| format!("Invalid TOML: {}", e))?;
|
|
712 |
+ |
let table: toml::Table = content.parse().map_err(|e| format!("Invalid TOML: {e}"))?;
|
| 714 |
713 |
|
|
| 715 |
714 |
|
let has_colors = COLOR_SECTIONS
|
| 716 |
715 |
|
.iter()
|
| 732 |
731 |
|
std::fs::create_dir_all(custom_dir)
|
| 733 |
732 |
|
.map_err(|e| format!("Failed to create {}: {}", custom_dir.display(), e))?;
|
| 734 |
733 |
|
|
| 735 |
|
- |
let dest = custom_dir.join(format!("{}.toml", id));
|
| 736 |
|
- |
std::fs::copy(source_path, &dest).map_err(|e| format!("Failed to copy theme: {}", e))?;
|
|
734 |
+ |
let dest = custom_dir.join(format!("{id}.toml"));
|
|
735 |
+ |
std::fs::copy(source_path, &dest).map_err(|e| format!("Failed to copy theme: {e}"))?;
|
| 737 |
736 |
|
|
| 738 |
737 |
|
Ok(parse_meta(&id, &table, true))
|
| 739 |
738 |
|
}
|
| 745 |
744 |
|
pub fn delete_theme(custom_dir: &Path, id: &str) -> Result<(), String> {
|
| 746 |
745 |
|
validate_theme_id(id)?;
|
| 747 |
746 |
|
|
| 748 |
|
- |
let path = custom_dir.join(format!("{}.toml", id));
|
|
747 |
+ |
let path = custom_dir.join(format!("{id}.toml"));
|
| 749 |
748 |
|
if !path.is_file() {
|
| 750 |
|
- |
return Err(format!("Custom theme '{}' not found", id));
|
|
749 |
+ |
return Err(format!("Custom theme '{id}' not found"));
|
| 751 |
750 |
|
}
|
| 752 |
751 |
|
|
| 753 |
752 |
|
std::fs::remove_file(&path).map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
|
| 775 |
774 |
|
.and_then(|s| s.as_table())
|
| 776 |
775 |
|
.and_then(|s| s.get(key))
|
| 777 |
776 |
|
.and_then(|v| v.as_str())
|
| 778 |
|
- |
.map(|s| s.to_string())
|
|
777 |
+ |
.map(std::string::ToString::to_string)
|
| 779 |
778 |
|
}
|
| 780 |
779 |
|
|
| 781 |
780 |
|
/// Load just the preview swatches for a theme — for UI thumbnails.
|
| 783 |
782 |
|
validate_theme_id(id)?;
|
| 784 |
783 |
|
|
| 785 |
784 |
|
let (path, is_custom) =
|
| 786 |
|
- |
find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
|
|
785 |
+ |
find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{id}' not found"))?;
|
| 787 |
786 |
|
|
| 788 |
787 |
|
let content = std::fs::read_to_string(&path)
|
| 789 |
788 |
|
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
|
| 805 |
804 |
|
pub fn export_theme(dirs: &[(PathBuf, bool)], id: &str, dest_path: &Path) -> Result<(), String> {
|
| 806 |
805 |
|
validate_theme_id(id)?;
|
| 807 |
806 |
|
|
| 808 |
|
- |
let (source, _) =
|
| 809 |
|
- |
find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
|
|
807 |
+ |
let (source, _) = find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{id}' not found"))?;
|
| 810 |
808 |
|
|
| 811 |
|
- |
std::fs::copy(&source, dest_path).map_err(|e| format!("Failed to export theme: {}", e))?;
|
|
809 |
+ |
std::fs::copy(&source, dest_path).map_err(|e| format!("Failed to export theme: {e}"))?;
|
| 812 |
810 |
|
|
| 813 |
811 |
|
Ok(())
|
| 814 |
812 |
|
}
|