Repository: lsd-rs/lsd (language: rust)

Issue:
Icon theme with overrides from config
- os: debian
- `lsd --version`: lsd 0.23.1
- `echo $TERM`: Windows Terminal
- `echo $LS_COLORS`: 

## Current behavior
Created icons.yaml in ~/config/lsd folder based on example in documentation. Docs says
>"The final set of icons used will be a combination of what is shipped with in lsd with overrides from config applied on top of it".

However this does not work for me. Icons from what is shipped is gone anď only this in icons.yaml is used.


Using icon theme. Config.yaml and icons.yaml has a default icon.

![image](https://user-images.githubusercontent.com/43099715/206930946-70bc4e37-f430-40ed-a275-7324ffe89532.png)


Removing icon theme and now config.yaml and icons.yaml have the icon that is shipped.

![image](https://user-images.githubusercontent.com/43099715/206931021-1a0498a6-0bf8-4053-853f-4a63fb8b42ef.png)




Discussion / hints:
icon theme is currently on master and not yet released, please try to build master or wait until we release 0.24.0
Using master though and rebuilt it today and same issue.
I found that if we set `extension` field in `icons.yaml`, the default theme for `extension` is unexpectedly ignored. The same thing happens when we set `name` field and ~`filetype`~ field.

Speaking on a code basis, I believe the following is the cause of this bug.
https://github.com/Peltoche/lsd/blob/da61909835b281d7f7950ec3f763cd077b5b8f02/src/theme.rs#L78-L86

The return value typed `D` has `Default` trait so Serde (Rust's (de)serialize library) will use default field if the field is not set. However in this case a user sets the field so the default field must be ignored.


## example

When you set `icons.yaml` like:
```yaml
extension:
    rs: 🦀 
```

lsd constructs `IconTheme` struct like:
```yaml
name:
    a.out: \u{f489}
    api: \u{f98c}
    # and the other default values...
extension:
    rs: 🦀
    # ↑ only!
filetype:
    dir: \u{f115}
    file: \u{f016}
    # and the other default entries...
```

but we want like this:
```yaml
name:
    a.out: \u{f489}
    api: \u{f98c}
    # and the other default entries...
extension:
    rs: 🦀
    1: \u{f02d}
    2: \u{f02d}
    # and default entries other than those set in the configuration file...
filetype:
    dir: \u{f115}
    file: \u{f016}
    # and the other default values...
```

### Edit
I found that `filetype` can marge `icons.yaml` with the default values because `IconTheme.filetype` is a struct with Default tarit (the others are HashMap<String, String>). 
@sudame would you like to take a shot at creating a fix for this?
@meain OK, I'll try this.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 922c2a9dc..1f2e9e180 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   [`LS_COLORS`](README.md#Colors) can be used to customize.
 - Handle dereference (-L) with broken symlink from [r3dArch](https://github.com/r3dArch)
 - Avoid using Clap's deprecated structs and functions [sudame](https://github.com/sudame)
+- Icon theme with overrides from config [sudame](https://github.com/sudame)
 
 ## [0.23.1] - 2022-09-13
 
diff --git a/src/theme/icon.rs b/src/theme/icon.rs
index 1c65b2127..28eac3ab9 100644
--- a/src/theme/icon.rs
+++ b/src/theme/icon.rs
@@ -1,12 +1,48 @@
 use serde::Deserialize;
 use std::collections::HashMap;
 
+enum ByFilename {
+    Name,
+    Extension,
+}
+
+fn deserialize_by_filename<'de, D>(
+    deserializer: D,
+    by: ByFilename,
+) -> Result<HashMap<String, String>, D::Error>
+where
+    D: serde::de::Deserializer<'de>,
+{
+    let default = match by {
+        ByFilename::Name => IconTheme::get_default_icons_by_name(),
+        ByFilename::Extension => IconTheme::get_default_icons_by_extension(),
+    };
+    HashMap::<_, _>::deserialize(deserializer)
+        .map(|input| default.into_iter().chain(input.into_iter()).collect())
+}
+
+fn deserialize_by_name<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
+where
+    D: serde::de::Deserializer<'de>,
+{
+    deserialize_by_filename(deserializer, ByFilename::Name)
+}
+
+fn deserialize_by_extension<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
+where
+    D: serde::de::Deserializer<'de>,
+{
+    deserialize_by_filename(deserializer, ByFilename::Extension)
+}
+
 #[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
 pub struct IconTheme {
+    #[serde(deserialize_with = "deserialize_by_name")]
     pub name: HashMap<String, String>,
+    #[serde(deserialize_with = "deserialize_by_extension")]
     pub extension: HashMap<String, String>,
     pub filetype: ByType,
 }
@@ -653,4 +689,36 @@ filetype:
         let empty: IconTheme = Theme::with_yaml("filetype:\n  dir: ").unwrap();
         assert_eq!(empty.filetype.dir, "");
     }
+
+    #[test]
+    fn test_custom_icon_by_name() {
+        // When a user sets to use 📦-icon for a cargo.toml file,
+        let theme: IconTheme = Theme::with_yaml("name:\n  cargo.toml: 📦").unwrap();
+        // 📦-icon should be used for a cargo.toml file.
+        assert_eq!(theme.name.get("cargo.toml").unwrap(), "📦");
+    }
+
+    #[test]
+    fn test_default_icon_by_name_with_custom_entry() {
+        // When a user sets to use 📦-icon for a cargo.toml file,
+        let theme: IconTheme = Theme::with_yaml("name:\n  cargo.toml: 📦").unwrap();
+        // the default icon  should be used for a cargo.lock file.
+        assert_eq!(theme.name.get("cargo.lock").unwrap(), "\u{e7a8}");
+    }
+
+    #[test]
+    fn test_custom_icon_by_extension() {
+        // When a user sets to use 🦀-icon for *.rs files,
+        let theme: IconTheme = Theme::with_yaml("extension:\n  rs: 🦀").unwrap();
+        // 🦀-icon should be used for *.rs files.
+        assert_eq!(theme.extension.get("rs").unwrap(), "🦀");
+    }
+
+    #[test]
+    fn test_default_icon_by_extension_with_custom_entry() {
+        // When a user sets to use 🦀-icon for *.rs files,
+        let theme: IconTheme = Theme::with_yaml("extension:\n  rs: 🦀").unwrap();
+        // the default icon  should be used for *.go files.
+        assert_eq!(theme.extension.get("go").unwrap(), "\u{e627}");
+    }
 }


Test patch: