Skip to content

Commit

Permalink
Merge pull request #33 from l4l/locales
Browse files Browse the repository at this point in the history
  • Loading branch information
l4l authored Jan 10, 2021
2 parents 62c4aff + 03896ae commit 39c828d
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- Support localization (#33)
- Search by keywords in apps mode (#20)
- Magic separators support: `!!` for args, `#` for envs and `~` for workdir (#19)
- Display full path for ambiguous binapps (0b47575)
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ itertools = "0.9.0"
euclid = "0.22.1"
nom = { version = "6.0.1", default-features = false, features = ["std", "regexp"] }
regex = "1.4.2"
libc = "0.2.81"

[profile.release]
lto = true
Expand Down
21 changes: 16 additions & 5 deletions src/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use xdg::BaseDirectories;

use crate::icon::Icon;

mod locale;

pub static XDG_DIRS: OnceCell<BaseDirectories> = OnceCell::new();

pub struct Entry {
Expand Down Expand Up @@ -83,8 +85,19 @@ fn traverse_dir_entry(mut entries: &mut Vec<Entry>, dir_entry: DirEntry) {
return;
}
};

let main_section = entry.section("Desktop Entry");
match (main_section.attr("Name"), main_section.attr("Exec")) {
let locale = locale::Locale::current();

let localized_entry = |attr_name: &str| {
locale
.keys()
.filter_map(|key| main_section.attr_with_param(attr_name, key))
.next()
.or_else(|| main_section.attr(attr_name))
};

match (localized_entry("Name"), main_section.attr("Exec")) {
(Some(n), Some(e)) => {
entries.push(Entry {
name: n.to_owned(),
Expand All @@ -96,14 +109,12 @@ fn traverse_dir_entry(mut entries: &mut Vec<Entry>, dir_entry: DirEntry) {
.to_owned(),
path: dir_entry_path,
exec: e.to_owned(),
// TODO: use `attr_with_param` with locale first
name_with_keywords: n.to_owned()
+ main_section.attr("Keywords").unwrap_or_default(),
name_with_keywords: n.to_owned() + localized_entry("Keywords").unwrap_or_default(),
is_terminal: main_section
.attr("Terminal")
.map(|s| s == "true")
.unwrap_or(false),
icon: main_section.attr("Icon").and_then(|name| {
icon: localized_entry("Icon").and_then(|name| {
let icon_path = Path::new(name);

if icon_path.is_absolute() {
Expand Down
135 changes: 135 additions & 0 deletions src/desktop/locale.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use std::ffi::CStr;

use once_cell::sync::OnceCell;
use regex::Regex;

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct Locale<'a> {
lang: Option<&'a str>,
country: Option<&'a str>,
modifier: Option<&'a str>,
}

const LOCALE_REGEX: &str = r#"(?x)
^
([[:alpha:]]+) # lang
(?:_([[:alpha:]]+))? # country
(?:\.[^@]*)? # encoding
(?:@(.*))? # modifier
$"#;

impl<'a> Locale<'a> {
fn from_caputres(s: &'a str, captures: regex::Captures<'_>) -> Self {
Self {
lang: captures.get(1).map(|m| &s[m.range()]),
country: captures.get(2).map(|m| &s[m.range()]),
modifier: captures.get(3).map(|m| &s[m.range()]),
}
}
}

impl Locale<'static> {
pub fn current<'a>() -> &'a Self {
static LOCALE: OnceCell<Option<Locale<'static>>> = OnceCell::new();
LOCALE
.get_or_init(|| {
let s = unsafe {
let ptr = libc::setlocale(libc::LC_MESSAGES, b"\0".as_ptr().cast());
if ptr.is_null() {
return None;
}
CStr::from_ptr(ptr)
}
.to_str()
.ok()?;

let re = Regex::new(LOCALE_REGEX).unwrap();

let c = re.captures(s)?;

Some(Self::from_caputres(s, c))
})
.as_ref()
.unwrap_or(&Self {
lang: None,
country: None,
modifier: None,
})
}

pub fn keys(&self) -> impl Iterator<Item = impl AsRef<str>> + '_ {
static LOCALE_ITERS: OnceCell<Vec<String>> = OnceCell::new();
LOCALE_ITERS
.get_or_init(|| {
let mut v = vec![];
if let Some(((l, c), m)) = self.lang.zip(self.country).zip(self.modifier) {
v.push(format!("{}_{}@{}", l, c, m));
}
if let Some((l, c)) = self.lang.zip(self.country) {
v.push(format!("{}_{}", l, c));
}
if let Some((l, m)) = self.lang.zip(self.modifier) {
v.push(format!("{}@{}", l, m));
}
if let Some(l) = self.lang {
v.push(l.to_string());
}

v
})
.clone()
.into_iter()
}
}

#[cfg(test)]
mod tests {
use super::*;

use test_case::test_case;

#[test]
fn regex_compiles() {
let _ = Regex::new(LOCALE_REGEX).unwrap();
}

#[test]
fn regex_doesnt_match_empty() {
let re = Regex::new(LOCALE_REGEX).unwrap();
assert!(re.captures("").is_none());
}

impl Locale<'static> {
fn new(
lang: impl Into<Option<&'static str>>,
country: impl Into<Option<&'static str>>,
modifier: impl Into<Option<&'static str>>,
) -> Self {
Self {
lang: lang.into(),
country: country.into(),
modifier: modifier.into(),
}
}
}

#[test_case("qw", Locale::new("qw", None, None); "lang")]
#[test_case("qw_ER", Locale::new("qw", "ER", None); "lang, country")]
#[test_case("qw_ER.ty", Locale::new("qw", "ER", None); "lang, country, encoding")]
#[test_case(
"qw_ER.ty@ui",
Locale::new("qw", "ER", "ui");
"lang, country, encoding, modifier"
)]
#[test_case("qw@ui", Locale::new("qw", None, "ui"); "lang, modifier")]
fn regex_compiles(s: &str, x: Locale<'static>) {
let re = Regex::new(LOCALE_REGEX).unwrap();
let c = re.captures(s).unwrap();

let m = c.get(0).unwrap();
assert_eq!(m.start(), 0);
assert_eq!(m.end(), s.len());

assert_eq!(Locale::from_caputres(s, c), x);
}
}

0 comments on commit 39c828d

Please sign in to comment.