Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
179 changes: 172 additions & 7 deletions src/options/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,14 @@ impl FromOverride<StyleOverride> for Style {
}
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct IconStyleOverride {
pub glyph: Option<char>,
pub glyph: Option<String>,
pub style: Option<StyleOverride>,
}

impl FromOverride<char> for char {
fn from(value: char, _default: char) -> char {
impl FromOverride<String> for String {
fn from(value: String, _default: String) -> String {
value
}
}
Expand All @@ -226,7 +226,7 @@ impl FromOverride<IconStyleOverride> for IconStyle {
}
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct FileNameStyleOverride {
pub icon: Option<IconStyleOverride>,
pub filename: Option<StyleOverride>,
Expand Down Expand Up @@ -614,8 +614,32 @@ impl ThemeConfig {
#[must_use]
pub fn to_theme(&self) -> Option<UiStyles> {
let ui_styles_override: Option<UiStylesOverride> = {
let file = std::fs::File::open(&self.location).ok()?;
serde_norway::from_reader(&file).ok()
let file = match std::fs::File::open(&self.location) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Some(UiStyles::default());
}
Err(e) => {
eprintln!(
"Warning: Failed to open theme file {:?}: {}",
self.location, e
);
return Some(UiStyles::default());
}
};

match serde_norway::from_reader(&file) {
Ok(styles) => Some(styles),
Err(e) => {
eprintln!(
"Warning: Failed to parse theme file {:?}: {}",
self.location, e
);
eprintln!(" Using default theme instead.");
eprintln!(" Hint: Check your theme.yml for syntax errors or invalid emoji sequences.");
None
}
}
};
FromOverride::from(ui_styles_override, Some(UiStyles::default()))
}
Expand Down Expand Up @@ -666,4 +690,145 @@ mod tests {
assert_eq!(color_from_str(s), Some(Color::Fixed(*c)));
}
}

#[test]
fn test_single_codepoint_emoji_deserialize() {
let yaml = r"
filenames:
data:
icon:
glyph: πŸ’Ύ
";
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(result.is_ok(), "Single codepoint emoji should deserialize");
let styles = result.unwrap();
let filename_styles = styles.filenames.as_ref().unwrap();
let data_style = filename_styles.get("data").unwrap();
assert!(data_style.icon.is_some());
}

#[test]
fn test_variation_selector_emoji_deserialize() {
// Emojis with variation selectors: πŸ–ΌοΈ (U+1F5BC + U+FE0F), πŸ–₯️ (U+1F5A5 + U+FE0F)
let yaml = r"
filenames:
Pictures:
icon:
glyph: πŸ–ΌοΈ
Desktop:
icon:
glyph: πŸ–₯️
";
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(
result.is_ok(),
"Emoji with variation selector should deserialize: {:?}",
result.err()
);

if let Ok(styles) = result {
let filename_styles = styles.filenames.as_ref().unwrap();
assert!(filename_styles.contains_key("Pictures"));
assert!(filename_styles.contains_key("Desktop"));
let pictures = filename_styles.get("Pictures").unwrap();
assert!(pictures.icon.is_some());
}
}

#[test]
fn test_multi_codepoint_emoji_deserialize() {
// ZWJ sequences: πŸ‘¨β€πŸ’» (U+1F468 + U+200D + U+1F4BB)
let yaml = r"
filenames:
developer:
icon:
glyph: πŸ‘¨β€πŸ’»
";
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(
result.is_ok(),
"Multi-codepoint emoji should deserialize: {:?}",
result.err()
);
}

#[test]
fn test_flag_emoji_deserialize() {
// Regional indicator sequences: πŸ‡ΊπŸ‡Έ (U+1F1FA + U+1F1F8)
let yaml = r"
filenames:
usa:
icon:
glyph: πŸ‡ΊπŸ‡Έ
";
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(
result.is_ok(),
"Flag emoji should deserialize: {:?}",
result.err()
);
}

#[test]
fn test_theme_yml_with_mixed_emojis() {
let yaml = r"
filenames:
data: {icon: {glyph: πŸ’Ύ}}
Pictures: {icon: {glyph: πŸ–ΌοΈ}}
Desktop: {icon: {glyph: πŸ–₯️}}
README: {icon: {glyph: πŸ“–}}
extensions:
rs: {icon: {glyph: πŸ¦€}}
py: {icon: {glyph: 🐍}}
";
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(
result.is_ok(),
"Mixed emoji theme should deserialize successfully: {:?}",
result.err()
);

if let Ok(styles) = result {
assert!(styles.filenames.is_some());
assert!(styles.extensions.is_some());

let filenames = styles.filenames.as_ref().unwrap();
assert_eq!(filenames.len(), 4, "Should have 4 filename entries");

let extensions = styles.extensions.as_ref().unwrap();
assert_eq!(extensions.len(), 2, "Should have 2 extension entries");
}
}

#[test]
fn test_empty_glyph_string() {
let yaml = r#"
filenames:
empty:
icon:
glyph: ""
"#;
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(result.is_ok(), "Empty glyph string should deserialize");

if let Ok(styles) = result {
let filename_styles = styles.filenames.as_ref().unwrap();
let empty_style = filename_styles.get("empty").unwrap();
assert!(empty_style.icon.is_some());
assert_eq!(
empty_style.icon.as_ref().unwrap().glyph,
Some(String::new())
);
}
}

#[test]
fn test_error_handling_invalid_yaml() {
let yaml = r"
filenames:
broken: {icon: {glyph:
";
let result: Result<UiStylesOverride, _> = serde_norway::from_str(yaml);
assert!(result.is_err(), "Invalid YAML should produce an error");
}
}
8 changes: 2 additions & 6 deletions src/output/file_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,9 @@ impl<C: Colours> FileName<'_, '_, C> {
},
icon_override
.glyph
.unwrap_or_else(|| icon_for_file(self.file))
.to_string(),
),
None => (
iconify_style(self.style()),
icon_for_file(self.file).to_string(),
.unwrap_or_else(|| icon_for_file(self.file)),
),
None => (iconify_style(self.style()), icon_for_file(self.file)),
};

bits.push(style.paint(icon));
Expand Down
7 changes: 4 additions & 3 deletions src/output/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1125,8 +1125,8 @@ pub fn iconify_style(style: Style) -> Style {

/// Lookup the icon for a file based on the file's name, if the entry is a
/// directory, or by the lowercase file extension.
pub fn icon_for_file(file: &File<'_>) -> char {
if file.points_to_directory() {
pub fn icon_for_file(file: &File<'_>) -> String {
let icon_char = if file.points_to_directory() {
*DIRECTORY_ICONS.get(file.name.as_str()).unwrap_or_else(|| {
if file.is_empty_dir() {
&Icons::FOLDER_OPEN // ο„•
Expand All @@ -1140,5 +1140,6 @@ pub fn icon_for_file(file: &File<'_>) -> char {
*EXTENSION_ICONS.get(ext.as_str()).unwrap_or(&Icons::FILE) // ο…›
} else {
Icons::FILE_UNKNOW // σ°‘―
}
};
icon_char.to_string()
}
4 changes: 2 additions & 2 deletions src/theme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,14 +481,14 @@ impl FileNameColours for Theme {
fn style_override(&self, file: &File<'_>) -> Option<FileNameStyle> {
if let Some(ref name_overrides) = self.ui.filenames {
if let Some(file_override) = name_overrides.get(&file.name) {
return Some(*file_override);
return Some(file_override.clone());
}
}

if let Some(ref ext_overrides) = self.ui.extensions {
if let Some(ext) = file.ext.clone() {
if let Some(file_override) = ext_overrides.get(&ext) {
return Some(*file_override);
return Some(file_override.clone());
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/theme/ui_styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::default::Default;

#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct IconStyle {
pub glyph: Option<char>,
pub glyph: Option<String>,
pub style: Option<Style>,
}

#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct FileNameStyle {
pub icon: Option<IconStyle>,
pub filename: Option<Style>,
Expand Down
Loading