Skip to content

Commit fa085f9

Browse files
authored
fix: set locale with DBus to fix OpenRC support
This PR is intended to address #1955. At a high level, when using OpenRC with `openrc-settingsd`, the application would panic after trying to run the non-existent `localectl` command. This PR addresses that issue by setting the locale via D-Bus instead of through `localectl`. Additionally, there was a call to `localectl` to get the available locales for the system that was replaced with the more portable and POSIX compliant `locale` command. - [x] I have disclosed use of any AI generated code in my commit messages. - [x] I understand these changes in full and will be able to respond to review comments. - [x] My change is accurately described in the commit message. - [x] My contribution is tested and working as described. - [x] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. --- AI Disclosure: This code was generated with the assistance of AI (Claude 3.7 Sonnet) under human direction and supervision. All code has been reviewed and tested.
1 parent 78644a3 commit fa085f9

1 file changed

Lines changed: 243 additions & 34 deletions

File tree

cosmic-settings/src/pages/time/region.rs

Lines changed: 243 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use icu::{
2323
locale::Locale,
2424
};
2525
use locales_rs as locale;
26+
use regex::Regex;
2627
use slotmap::{DefaultKey, SlotMap};
2728

2829
static GNOME_LANGUAGE_SELECTOR: &str = "gnome-language-selector";
@@ -242,9 +243,7 @@ impl Page {
242243
let region = region.lang_code.clone();
243244

244245
return cosmic::task::future(async move {
245-
if let Ok(exit_status) = set_locale(lang, region.clone()).await
246-
&& exit_status.success()
247-
{
246+
if set_locale(lang, region.clone()).await.is_ok() {
248247
update_time_settings_after_region_change(region);
249248
}
250249

@@ -710,20 +709,26 @@ pub async fn page_reload() -> eyre::Result<PageRefresh> {
710709

711710
let mut available_languages_set = BTreeSet::new();
712711

713-
let output = tokio::process::Command::new("localectl")
714-
.arg("list-locales")
712+
// Use 'locale -a' instead of 'localectl list-locales' for OpenRC compatibility
713+
let output_result = tokio::process::Command::new("locale")
714+
.arg("-a")
715715
.output()
716-
.await
717-
.expect("Failed to run localectl");
716+
.await;
718717

719-
let output = String::from_utf8(output.stdout).unwrap_or_default();
720-
for line in output.lines() {
721-
if line == "C.UTF-8" {
722-
continue;
718+
let locale_list = match output_result {
719+
Ok(output) => {
720+
let output_str = String::from_utf8(output.stdout).unwrap_or_default();
721+
parse_locale_output(&output_str)
722+
}
723+
Err(why) => {
724+
tracing::error!(?why, "failed to list available locales using 'locale -a'");
725+
Vec::new()
723726
}
727+
};
724728

725-
if let Some(locale) = registry.locale(line) {
726-
available_languages_set.insert(localized_locale(&locale, line.to_owned()));
729+
for line in locale_list {
730+
if let Some(locale) = registry.locale(&line) {
731+
available_languages_set.insert(localized_locale(&locale, line));
727732
}
728733
}
729734

@@ -841,27 +846,28 @@ fn popover_menu_row(
841846
.apply(Element::from)
842847
}
843848

844-
pub async fn set_locale(
845-
lang: String,
846-
region: String,
847-
) -> Result<std::process::ExitStatus, std::io::Error> {
848-
eprintln!("setting locale lang={lang}, region={region}");
849-
tokio::process::Command::new("localectl")
850-
.arg("set-locale")
851-
.args(&[
852-
["LANG=", &lang].concat(),
853-
["LC_ADDRESS=", &region].concat(),
854-
["LC_IDENTIFICATION=", &region].concat(),
855-
["LC_MEASUREMENT=", &region].concat(),
856-
["LC_MONETARY=", &region].concat(),
857-
["LC_NAME=", &region].concat(),
858-
["LC_NUMERIC=", &region].concat(),
859-
["LC_PAPER=", &region].concat(),
860-
["LC_TELEPHONE=", &region].concat(),
861-
["LC_TIME=", &region].concat(),
862-
])
863-
.status()
849+
/// Sets the system locale using D-Bus instead of localectl for OpenRC compatibility.
850+
pub async fn set_locale(lang: String, region: String) -> eyre::Result<()> {
851+
tracing::debug!("setting locale lang={lang}, region={region}");
852+
853+
let conn = zbus::Connection::system()
864854
.await
855+
.wrap_err("failed to connect to system D-Bus")?;
856+
857+
let proxy = locale1::locale1Proxy::new(&conn)
858+
.await
859+
.wrap_err("failed to create locale1 D-Bus proxy")?;
860+
861+
let locale_settings = build_locale_settings(&lang, &region);
862+
let locale_strs: Vec<&str> = locale_settings.iter().map(|s| s.as_str()).collect();
863+
864+
proxy
865+
.set_locale(&locale_strs, true)
866+
.await
867+
.wrap_err("failed to set locale via D-Bus")?;
868+
869+
tracing::debug!("successfully set locale via D-Bus");
870+
Ok(())
865871
}
866872

867873
/// Sets the user's preferred language list via AccountsService D-Bus.
@@ -883,7 +889,7 @@ pub async fn set_user_language(language_list: String) -> eyre::Result<()> {
883889
.await
884890
.wrap_err("failed to set language via AccountsService")?;
885891

886-
eprintln!("set user language via AccountsService: {language_list}");
892+
tracing::debug!("set user language via AccountsService: {language_list}");
887893
Ok(())
888894
}
889895

@@ -1016,3 +1022,206 @@ fn strip_locale_suffix(locale: &str) -> String {
10161022
.unwrap_or(without_codeset)
10171023
.to_string()
10181024
}
1025+
1026+
/// Parses the output from `locale -a` command and returns a vector of locale strings.
1027+
/// Filters out pseudo-locales (C, POSIX) and accepts only allowed character encodings.
1028+
fn parse_locale_output(output: &str) -> Vec<String> {
1029+
// Regex to match pseudo-locales: C or POSIX, optionally followed by .anything
1030+
let pseudo_locale_re = Regex::new(r"^(C|POSIX)(\.|$)").unwrap();
1031+
1032+
// Regex to match UTF-8 encoded locales (case-insensitive)
1033+
// Supports optional modifiers after encoding (e.g., @euro, @valencia)
1034+
let utf8_encoding_re = Regex::new(r"(?i)\.(utf-?8)(@.*)?$").unwrap();
1035+
1036+
output
1037+
.lines()
1038+
.map(|line| line.trim())
1039+
.filter(|line| !pseudo_locale_re.is_match(line))
1040+
.filter(|line| utf8_encoding_re.is_match(line))
1041+
.map(|line| line.to_string())
1042+
.collect()
1043+
}
1044+
1045+
/// Builds the locale settings array for D-Bus SetLocale call.
1046+
/// Sets LANG to the language parameter and all LC_* variables to the region parameter.
1047+
fn build_locale_settings(lang: &str, region: &str) -> Vec<String> {
1048+
vec![
1049+
format!("LANG={}", lang),
1050+
format!("LC_ADDRESS={}", region),
1051+
format!("LC_IDENTIFICATION={}", region),
1052+
format!("LC_MEASUREMENT={}", region),
1053+
format!("LC_MONETARY={}", region),
1054+
format!("LC_NAME={}", region),
1055+
format!("LC_NUMERIC={}", region),
1056+
format!("LC_PAPER={}", region),
1057+
format!("LC_TELEPHONE={}", region),
1058+
format!("LC_TIME={}", region),
1059+
]
1060+
}
1061+
1062+
#[cfg(test)]
1063+
mod tests {
1064+
use super::*;
1065+
1066+
#[test]
1067+
fn test_parse_locale_output_handles_empty_input() {
1068+
let output = "";
1069+
let result = parse_locale_output(output);
1070+
assert_eq!(result.len(), 0);
1071+
}
1072+
1073+
#[test]
1074+
fn test_parse_locale_output_preserves_locale_strings() {
1075+
let output = "en_US.utf8\nde_DE.utf8\nfr_FR.utf8\n";
1076+
let result = parse_locale_output(output);
1077+
assert_eq!(result.len(), 3);
1078+
assert!(result.contains(&"en_US.utf8".to_string()));
1079+
}
1080+
1081+
#[test]
1082+
fn test_build_locale_settings_includes_all_lc_variables() {
1083+
let lang = "en_US.UTF-8";
1084+
let region = "de_DE.UTF-8";
1085+
let settings = build_locale_settings(lang, region);
1086+
1087+
assert_eq!(settings.len(), 10);
1088+
assert!(settings.contains(&format!("LANG={}", lang)));
1089+
assert!(settings.contains(&format!("LC_ADDRESS={}", region)));
1090+
assert!(settings.contains(&format!("LC_IDENTIFICATION={}", region)));
1091+
assert!(settings.contains(&format!("LC_MEASUREMENT={}", region)));
1092+
assert!(settings.contains(&format!("LC_MONETARY={}", region)));
1093+
assert!(settings.contains(&format!("LC_NAME={}", region)));
1094+
assert!(settings.contains(&format!("LC_NUMERIC={}", region)));
1095+
assert!(settings.contains(&format!("LC_PAPER={}", region)));
1096+
assert!(settings.contains(&format!("LC_TELEPHONE={}", region)));
1097+
assert!(settings.contains(&format!("LC_TIME={}", region)));
1098+
}
1099+
1100+
#[test]
1101+
fn test_build_locale_settings_uses_correct_values() {
1102+
let lang = "fr_FR.UTF-8";
1103+
let region = "en_GB.UTF-8";
1104+
let settings = build_locale_settings(lang, region);
1105+
1106+
// LANG should use the lang parameter
1107+
assert!(settings.iter().any(|s| s == "LANG=fr_FR.UTF-8"));
1108+
// LC_* variables should use the region parameter
1109+
assert!(settings.iter().any(|s| s == "LC_TIME=en_GB.UTF-8"));
1110+
}
1111+
1112+
#[test]
1113+
fn test_parse_locale_output_filters_pseudo_locales() {
1114+
let output = "C\nC.utf8\nC.UTF-8\nPOSIX\nen_US.utf8\nde_DE.UTF-8\n";
1115+
let result = parse_locale_output(output);
1116+
1117+
// Should filter out all C and POSIX variants
1118+
assert!(!result.contains(&"C".to_string()));
1119+
assert!(!result.contains(&"C.utf8".to_string()));
1120+
assert!(!result.contains(&"C.UTF-8".to_string()));
1121+
assert!(!result.contains(&"POSIX".to_string()));
1122+
1123+
// Should keep actual locales
1124+
assert!(result.contains(&"en_US.utf8".to_string()));
1125+
assert!(result.contains(&"de_DE.UTF-8".to_string()));
1126+
assert_eq!(result.len(), 2);
1127+
}
1128+
1129+
#[test]
1130+
fn test_parse_locale_output_accepts_only_utf8_locales() {
1131+
let output =
1132+
"en_US\nen_US.utf8\nen_US.UTF-8\nar_IN\nar_IN.utf8\nde_DE.iso88591\nfr_FR.UTF-8\n";
1133+
let result = parse_locale_output(output);
1134+
1135+
// Should accept UTF-8 variants
1136+
assert!(result.contains(&"en_US.utf8".to_string()));
1137+
assert!(result.contains(&"en_US.UTF-8".to_string()));
1138+
assert!(result.contains(&"ar_IN.utf8".to_string()));
1139+
assert!(result.contains(&"fr_FR.UTF-8".to_string()));
1140+
1141+
// Should filter out non-UTF-8 encoded locales
1142+
assert!(!result.contains(&"en_US".to_string()));
1143+
assert!(!result.contains(&"ar_IN".to_string()));
1144+
assert!(!result.contains(&"de_DE.iso88591".to_string()));
1145+
1146+
assert_eq!(result.len(), 4);
1147+
}
1148+
1149+
#[test]
1150+
fn test_parse_locale_output_filters_any_c_posix_variant() {
1151+
let output = "C\nC.iso88591\nC.anything\nPOSIX\nPOSIX.utf8\nen_US.utf8\n";
1152+
let result = parse_locale_output(output);
1153+
1154+
// Should filter out any C or POSIX variant regardless of encoding
1155+
assert!(!result.contains(&"C".to_string()));
1156+
assert!(!result.contains(&"C.iso88591".to_string()));
1157+
assert!(!result.contains(&"C.anything".to_string()));
1158+
assert!(!result.contains(&"POSIX".to_string()));
1159+
assert!(!result.contains(&"POSIX.utf8".to_string()));
1160+
1161+
// Should keep actual locales
1162+
assert!(result.contains(&"en_US.utf8".to_string()));
1163+
assert_eq!(result.len(), 1);
1164+
}
1165+
1166+
#[test]
1167+
fn test_parse_locale_output_handles_whitespace() {
1168+
let output = " en_US.utf8 \n\t de_DE.UTF-8\t\n fr_FR.utf8 \n";
1169+
let result = parse_locale_output(output);
1170+
1171+
// Should handle leading/trailing whitespace
1172+
assert!(result.contains(&"en_US.utf8".to_string()));
1173+
assert!(result.contains(&"de_DE.UTF-8".to_string()));
1174+
assert!(result.contains(&"fr_FR.utf8".to_string()));
1175+
assert_eq!(result.len(), 3);
1176+
}
1177+
1178+
#[test]
1179+
fn test_parse_locale_output_handles_empty_lines() {
1180+
let output = "en_US.utf8\n\n\nde_DE.UTF-8\n\n";
1181+
let result = parse_locale_output(output);
1182+
1183+
// Should skip empty lines
1184+
assert!(result.contains(&"en_US.utf8".to_string()));
1185+
assert!(result.contains(&"de_DE.UTF-8".to_string()));
1186+
assert_eq!(result.len(), 2);
1187+
}
1188+
1189+
#[test]
1190+
fn test_parse_locale_output_catalan_not_filtered_as_pseudo() {
1191+
let output = "C\nca_ES.UTF-8\nca_ES.utf8\ncs_CZ.UTF-8\nen_US.utf8\n";
1192+
let result = parse_locale_output(output);
1193+
1194+
// Should filter out C but not Catalan (ca_*) or Czech (cs_*)
1195+
assert!(!result.contains(&"C".to_string()));
1196+
assert!(result.contains(&"ca_ES.UTF-8".to_string()));
1197+
assert!(result.contains(&"ca_ES.utf8".to_string()));
1198+
assert!(result.contains(&"cs_CZ.UTF-8".to_string()));
1199+
assert_eq!(result.len(), 4);
1200+
}
1201+
1202+
#[test]
1203+
fn test_parse_locale_output_handles_locale_modifiers() {
1204+
let output = "en_US.UTF-8@euro\nca_ES.UTF-8@valencia\nde_DE.utf8\n";
1205+
let result = parse_locale_output(output);
1206+
1207+
// Locales with modifiers should be accepted
1208+
assert!(result.contains(&"en_US.UTF-8@euro".to_string()));
1209+
assert!(result.contains(&"ca_ES.UTF-8@valencia".to_string()));
1210+
assert!(result.contains(&"de_DE.utf8".to_string()));
1211+
assert_eq!(result.len(), 3);
1212+
}
1213+
1214+
#[test]
1215+
fn test_parse_locale_output_case_variations() {
1216+
let output = "en_US.UTF-8\nen_US.utf-8\nen_US.utf8\nen_US.UTF8\nde_DE.Utf8\n";
1217+
let result = parse_locale_output(output);
1218+
1219+
// All case variations should be accepted (case-insensitive regex)
1220+
assert!(result.contains(&"en_US.UTF-8".to_string()));
1221+
assert!(result.contains(&"en_US.utf-8".to_string()));
1222+
assert!(result.contains(&"en_US.utf8".to_string()));
1223+
assert!(result.contains(&"en_US.UTF8".to_string()));
1224+
assert!(result.contains(&"de_DE.Utf8".to_string()));
1225+
assert_eq!(result.len(), 5);
1226+
}
1227+
}

0 commit comments

Comments
 (0)