kimun_core 0.2.6

Core library for the Kimün notes application
Documentation
use std::path::{Path, PathBuf};

pub fn path_to_string<P: AsRef<Path>>(path: P) -> String {
    path.as_ref()
        .to_path_buf()
        .into_os_string()
        .into_string()
        .unwrap_or_else(|os_string| os_string.to_string_lossy().into())
}

fn app_dir_name() -> &'static str {
    #[cfg(debug_assertions)]
    {
        "kimun_debug"
    }
    #[cfg(not(debug_assertions))]
    {
        "kimun"
    }
}

/// Returns the platform-specific directory where Kimün stores its log file.
///
/// Fallback chain per platform:
///   preferred dir → `current_dir()` → `temp_dir()` (always available)
/// Always returns an absolute path with the app-name suffix appended.
pub fn app_log_dir() -> PathBuf {
    let name = app_dir_name();
    let fallback = || std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
    #[cfg(target_os = "macos")]
    {
        std::env::var("HOME")
            .map(|h| {
                PathBuf::from(h)
                    .join("Library/Application Support")
                    .join(name)
            })
            .unwrap_or_else(|_| fallback().join(name))
    }
    #[cfg(target_os = "linux")]
    {
        std::env::var("XDG_DATA_HOME")
            .map(PathBuf::from)
            .unwrap_or_else(|_| {
                std::env::var("HOME")
                    .map(|h| PathBuf::from(h).join(".local/share"))
                    .unwrap_or_else(|_| fallback())
            })
            .join(name)
    }
    #[cfg(target_os = "windows")]
    {
        std::env::var("APPDATA")
            .map(|p| PathBuf::from(p).join(name))
            .unwrap_or_else(|_| fallback().join(name))
    }
    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
    {
        fallback().join(name)
    }
}

/// Creates the directory (and any missing parents) at `path` if it does not
/// already exist. Returns the canonicalized path on success.
pub fn ensure_dir_exists(path: &Path) -> std::io::Result<std::path::PathBuf> {
    if !path.exists() {
        std::fs::create_dir_all(path)?;
    }
    path.canonicalize()
}

// taken from https://github.com/YesSeri/diacritics/
// with modifications
pub fn remove_diacritics(string: &str) -> String {
    let chars = string.chars();
    chars.fold(String::with_capacity(string.len()), |mut acc, current| {
        escape_diacritic(&mut acc, current);
        acc
    })
}

fn escape_diacritic(acc: &mut String, current: char) {
    match current {
        'A' | '' | '' | 'À' | 'Á' | 'Â' | '' | '' | '' | '' | 'Ã' | 'Ā' | 'Ă' | ''
        | '' | '' | '' | 'Ȧ' | 'Ǡ' | 'Ä' | 'Ǟ' | '' | 'Å' | 'Ǻ' | 'Ǎ' | 'Ȁ' | 'Ȃ' | ''
        | '' | '' | '' | 'Ą' | 'Ⱥ' | '' => acc.push('A'),
        '' => acc.push_str("AA"),
        'Æ' | 'Ǽ' | 'Ǣ' => acc.push('A'),
        '' => acc.push_str("AO"),
        '' => acc.push_str("AU"),
        '' | '' => acc.push_str("AV"),
        '' => acc.push_str("AY"),
        'B' | '' | '' | '' | '' | '' | 'Ƀ' | 'Ƃ' | 'Ɓ' => acc.push('B'),
        'C' | '' | '' | 'Ć' | 'Ĉ' | 'Ċ' | 'Č' | 'Ç' | '' | 'Ƈ' | 'Ȼ' | '' => {
            acc.push('C')
        }
        'D' | '' | '' | '' | 'Ď' | '' | '' | '' | '' | 'Đ' | 'Ƌ' | 'Ɗ' | 'Ɖ' | '' => {
            acc.push('D')
        }
        'DZ' | 'DŽ' => acc.push_str("DZ"),
        'Dz' | 'Dž' => acc.push_str("Dz"),
        'E' | '' | '' | 'È' | 'É' | 'Ê' | '' | '' | '' | '' | '' | 'Ē' | '' | ''
        | 'Ĕ' | 'Ė' | 'Ë' | '' | 'Ě' | 'Ȅ' | 'Ȇ' | '' | '' | 'Ȩ' | '' | 'Ę' | '' | ''
        | 'Ɛ' | 'Ǝ' => acc.push('E'),
        'F' | '' | '' | '' | 'Ƒ' | '' => acc.push('F'),
        'G' | '' | '' | 'Ǵ' | 'Ĝ' | '' | 'Ğ' | 'Ġ' | 'Ǧ' | 'Ģ' | 'Ǥ' | 'Ɠ' | '' | ''
        | '' => acc.push('G'),
        'H' | '' | '' | 'Ĥ' | '' | '' | 'Ȟ' | '' | '' | '' | 'Ħ' | '' | '' | '' => {
            acc.push('H')
        }
        'I' | '' | '' | 'Ì' | 'Í' | 'Î' | 'Ĩ' | 'Ī' | 'Ĭ' | 'İ' | 'Ï' | '' | '' | 'Ǐ'
        | 'Ȉ' | 'Ȋ' | '' | 'Į' | '' | 'Ɨ' => acc.push('I'),
        'J' | '' | '' | 'Ĵ' | 'Ɉ' => acc.push('J'),
        'K' | '' | '' | '' | 'Ǩ' | '' | 'Ķ' | '' | 'Ƙ' | '' | '' | '' | '' | '' => {
            acc.push('K')
        }
        'L' | '' | '' | 'Ŀ' | 'Ĺ' | 'Ľ' | '' | '' | 'Ļ' | '' | '' | 'Ł' | 'Ƚ' | ''
        | '' | '' | '' | '' => acc.push('L'),
        'LJ' => acc.push_str("LJ"),
        'Lj' => acc.push_str("Lj"),
        'M' | '' | '' | '' | '' | '' | '' | 'Ɯ' => acc.push('M'),
        'N' | '' | '' | 'Ǹ' | 'Ń' | 'Ñ' | '' | 'Ň' | '' | 'Ņ' | '' | '' | 'Ƞ' | 'Ɲ'
        | '' | '' => acc.push('N'),
        'NJ' => acc.push_str("NJ"),
        'Nj' => acc.push_str("Nj"),
        'O' | '' | '' | 'Ò' | 'Ó' | 'Ô' | '' | '' | '' | '' | 'Õ' | '' | 'Ȭ' | ''
        | 'Ō' | '' | '' | 'Ŏ' | 'Ȯ' | 'Ȱ' | 'Ö' | 'Ȫ' | '' | 'Ő' | 'Ǒ' | 'Ȍ' | 'Ȏ' | 'Ơ'
        | '' | '' | '' | '' | '' | '' | '' | 'Ǫ' | 'Ǭ' | 'Ø' | 'Ǿ' | 'Ɔ' | 'Ɵ' | ''
        | '' => acc.push('O'),
        'Ƣ' => acc.push_str("OI"),
        '' => acc.push_str("OO"),
        'Ȣ' => acc.push_str("OU"),
        '\u{008C}' | 'Œ' => acc.push_str("OE"),
        '\u{009C}' | 'œ' => acc.push_str("oe"),
        'P' | '' | '' | '' | '' | 'Ƥ' | '' | '' | '' | '' => acc.push('P'),
        'Q' | '' | '' | '' | '' | 'Ɋ' => acc.push('Q'),
        'R' | '' | '' | 'Ŕ' | '' | 'Ř' | 'Ȑ' | 'Ȓ' | '' | '' | 'Ŗ' | '' | 'Ɍ' | ''
        | '' | '' | '' => acc.push('R'),
        'S' | '' | '' | '' | 'Ś' | '' | 'Ŝ' | '' | 'Š' | '' | '' | '' | 'Ș' | 'Ş'
        | '' | '' | '' => acc.push('S'),
        'T' | '' | '' | '' | 'Ť' | '' | 'Ț' | 'Ţ' | '' | '' | 'Ŧ' | 'Ƭ' | 'Ʈ' | 'Ⱦ'
        | '' => acc.push('T'),
        '' => acc.push_str("TZ"),
        'U' | '' | '' | 'Ù' | 'Ú' | 'Û' | 'Ũ' | '' | 'Ū' | '' | 'Ŭ' | 'Ü' | 'Ǜ' | 'Ǘ'
        | 'Ǖ' | 'Ǚ' | '' | 'Ů' | 'Ű' | 'Ǔ' | 'Ȕ' | 'Ȗ' | 'Ư' | '' | '' | '' | '' | ''
        | '' | '' | 'Ų' | '' | '' | 'Ʉ' => acc.push('U'),
        'V' | '' | '' | '' | '' | 'Ʋ' | '' | 'Ʌ' => acc.push('V'),
        '' => acc.push_str("VY"),
        'W' | '' | '' | '' | '' | 'Ŵ' | '' | '' | '' | '' => acc.push('W'),
        'X' | '' | '' | '' | '' => acc.push('X'),
        'Y' | '' | '' | '' | 'Ý' | 'Ŷ' | '' | 'Ȳ' | '' | 'Ÿ' | '' | '' | 'Ƴ' | 'Ɏ'
        | '' => acc.push('Y'),
        'Z' | '' | '' | 'Ź' | '' | 'Ż' | 'Ž' | '' | '' | 'Ƶ' | 'Ȥ' | 'Ɀ' | '' | '' => {
            acc.push('Z')
        }
        'a' | '' | '' | '' | 'à' | 'á' | 'â' | '' | '' | '' | '' | 'ã' | 'ā' | 'ă'
        | '' | '' | '' | '' | 'ȧ' | 'ǡ' | 'ä' | 'ǟ' | '' | 'å' | 'ǻ' | 'ǎ' | 'ȁ' | 'ȃ'
        | '' | '' | '' | '' | 'ą' | '' | 'ɐ' => acc.push('a'),
        '' => acc.push_str("aa"),
        'æ' | 'ǽ' | 'ǣ' => acc.push('a'),
        '' => acc.push_str("ao"),
        '' => acc.push_str("au"),
        '' | '' => acc.push_str("av"),
        '' => acc.push_str("ay"),
        'b' | '' | '' | '' | '' | '' | 'ƀ' | 'ƃ' | 'ɓ' | 'þ' => acc.push('b'),
        'c' | '' | '' | 'ć' | 'ĉ' | 'ċ' | 'č' | 'ç' | '' | 'ƈ' | 'ȼ' | '' | '' => {
            acc.push('c')
        }
        'd' | '' | '' | '' | 'ď' | '' | '' | '' | '' | 'đ' | 'ƌ' | 'ɖ' | 'ɗ' | '' => {
            acc.push('d')
        }
        'dz' | 'dž' => acc.push_str("dz"),
        'e' | '' | '' | 'è' | 'é' | 'ê' | '' | 'ế' | '' | '' | '' | 'ē' | '' | ''
        | 'ĕ' | 'ė' | 'ë' | '' | 'ě' | 'ȅ' | 'ȇ' | '' | '' | 'ȩ' | '' | 'ę' | '' | ''
        | 'ɇ' | 'ɛ' | 'ǝ' => acc.push('e'),
        'f' | '' | '' | '' | 'ƒ' | '' => acc.push('f'),
        'g' | '' | '' | 'ǵ' | 'ĝ' | '' | 'ğ' | 'ġ' | 'ǧ' | 'ģ' | 'ǥ' | 'ɠ' | '' | ''
        | '' => acc.push('g'),
        'h' | '' | '' | 'ĥ' | '' | '' | 'ȟ' | '' | '' | '' | '' | 'ħ' | '' | ''
        | 'ɥ' => acc.push('h'),
        'ƕ' => acc.push_str("hv"),
        'i' | '' | '' | 'ì' | 'í' | 'î' | 'ĩ' | 'ī' | 'ĭ' | 'ï' | '' | '' | 'ǐ' | 'ȉ'
        | 'ȋ' | '' | 'į' | '' | 'ɨ' | 'ı' => acc.push('i'),
        'j' | '' | '' | 'ĵ' | 'ǰ' | 'ɉ' => acc.push('j'),
        'k' | '' | '' | '' | 'ǩ' | '' | 'ķ' | '' | 'ƙ' | '' | '' | '' | '' | '' => {
            acc.push('k')
        }
        'l' | '' | '' | 'ŀ' | 'ĺ' | 'ľ' | '' | '' | 'ļ' | '' | '' | 'ſ' | 'ł' | 'ƚ'
        | 'ɫ' | '' | '' | '' | '' => acc.push('l'),
        'lj' => acc.push_str("lj"),
        'm' | '' | '' | 'ḿ' | '' | '' | 'ɱ' | 'ɯ' => acc.push('m'),
        'n' | '' | '' | 'ǹ' | 'ń' | 'ñ' | '' | 'ň' | '' | 'ņ' | '' | '' | 'ƞ' | 'ɲ'
        | 'ʼn' | '' | '' => acc.push('n'),
        'nj' => acc.push_str("nj"),
        'o' | '' | '' | 'ò' | 'ó' | 'ô' | '' | '' | '' | '' | 'õ' | '' | 'ȭ' | ''
        | 'ō' | '' | '' | 'ŏ' | 'ȯ' | 'ȱ' | 'ö' | 'ȫ' | '' | 'ő' | 'ǒ' | 'ȍ' | 'ȏ' | 'ơ'
        | '' | '' | '' | '' | '' | '' | '' | 'ǫ' | 'ǭ' | 'ø' | 'ǿ' | 'ɔ' | '' | ''
        | 'ɵ' => acc.push('o'),
        'ƣ' => acc.push_str("oi"),
        'ȣ' => acc.push_str("ou"),
        '' => acc.push_str("oo"),
        'p' | '' | '' | '' | '' | 'ƥ' | '' | '' | '' | '' => acc.push('p'),
        'q' | '' | '' | 'ɋ' | '' | '' => acc.push('q'),
        'r' | '' | '' | 'ŕ' | '' | 'ř' | 'ȑ' | 'ȓ' | '' | '' | 'ŗ' | '' | 'ɍ' | 'ɽ'
        | '' | '' | '' => acc.push('r'),
        's' | '' | '' | 'ß' | 'ś' | '' | 'ŝ' | '' | 'š' | '' | '' | '' | 'ș' | 'ş'
        | 'ȿ' | '' | '' | '' => acc.push('s'),
        't' | '' | '' | '' | '' | 'ť' | '' | 'ț' | 'ţ' | '' | '' | 'ŧ' | 'ƭ' | 'ʈ'
        | '' | '' => acc.push('t'),
        '' => acc.push_str("tz"),
        'u' | '' | '' | 'ù' | 'ú' | 'û' | 'ũ' | '' | 'ū' | '' | 'ŭ' | 'ü' | 'ǜ' | 'ǘ'
        | 'ǖ' | 'ǚ' | '' | 'ů' | 'ű' | 'ǔ' | 'ȕ' | 'ȗ' | 'ư' | '' | '' | '' | '' | ''
        | '' | '' | 'ų' | '' | '' | 'ʉ' => acc.push('u'),
        'v' | '' | '' | '' | 'ṿ' | 'ʋ' | '' | 'ʌ' => acc.push('v'),
        '' => acc.push_str("vy"),
        'w' | '' | '' | '' | '' | 'ŵ' | '' | '' | '' | '' | '' => {
            acc.push('w')
        }
        'x' | '' | '' | '' | '' => acc.push('x'),
        'y' | '' | '' | '' | 'ý' | 'ŷ' | '' | 'ȳ' | '' | 'ÿ' | '' | '' | '' | 'ƴ'
        | 'ɏ' | 'ỿ' => acc.push('y'),
        'z' | '' | '' | 'ź' | '' | 'ż' | 'ž' | '' | '' | 'ƶ' | 'ȥ' | 'ɀ' | '' | '' => {
            acc.push('z')
        }
        '\u{0300}'..='\u{036F}' | '\u{1AB0}'..='\u{1AFF}' | '\u{1DC0}'..='\u{1DFF}' => {}
        _ => acc.push(current),
    }
}
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn app_log_dir_is_non_empty_and_has_correct_suffix() {
        let dir = app_log_dir();
        assert!(!dir.as_os_str().is_empty(), "log dir must not be empty");
        let name = dir
            .file_name()
            .and_then(|n| n.to_str())
            .expect("log dir must have a file name component");
        // In test builds cfg(debug_assertions) is active, so the suffix is kimun_debug.
        // The path must also be absolute — fallback chain ends at temp_dir(), never a relative dot.
        assert!(dir.is_absolute(), "log dir must be an absolute path");
        assert_eq!(name, "kimun_debug");
    }

    #[test]
    fn test_uppercase() {
        assert_eq!(remove_diacritics("TÅRÖÄÆØ"), String::from("TAROAAO"))
    }
    #[test]
    fn test_lowercase() {
        assert_eq!(remove_diacritics("čďêƒíó"), String::from("cdefio"))
    }
    #[test]
    fn test_real_diacritics() {
        // this is not a traditional é, but a combination of e and \u{300}
        assert_eq!(remove_diacritics("é"), String::from("e"));
        assert_eq!(remove_diacritics("e\u{300}"), String::from("e"));
    }
}