From 0de2f1ce1007a81f1829bcb7e880dc7a8c426567a5d2c9334ce656c377f0eb83 Mon Sep 17 00:00:00 2001 From: Kira Date: Sun, 3 May 2026 00:56:20 +0200 Subject: [PATCH] Work on locale stage. --- Cargo.toml | 2 +- src/locales/en.json | 4 +- src/main.rs | 31 +++- src/stage.rs | 7 + src/stages/locale/mod.rs | 318 +++++++++++++++++++++++++++++++++++++ src/stages/mod.rs | 3 +- src/stages/timezone/mod.rs | 23 +-- 7 files changed, 372 insertions(+), 16 deletions(-) create mode 100644 src/stages/locale/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 0db4d35..44ec775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,6 @@ toml = "1" [profile.release] #lto = true -codegen-units = 16 +codegen-units = 8 opt-level = 3 strip = true diff --git a/src/locales/en.json b/src/locales/en.json index 67f3f27..af03a01 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -20,6 +20,8 @@ "network.button.check": "Check Network", "timezone.selected_timezone": "Selected timezone:", "timezone.select_timezone": "Please select preffered time zone", - "timezone.init_error": "Erro getting time zones data from system." + "timezone.init_error": "Error getting time zones data from system!", + "locale.select_language_locale": "Language locale set to", + "locale.select_formats_locale": "Formats locale set to" } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b5a100e..ce6b419 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,7 @@ enum Views { License(license::LicenseStage), Network(stages::network::NetworkStage), TimeZone(stages::timezone::TimeZoneStage), + Locale(stages::locale::LocaleStage), } enum Message { @@ -50,6 +51,7 @@ enum Message { License(license::Message), Network(stages::network::Message), TimeZone(stages::timezone::Message), + Locale(stages::locale::Message), } struct KiraState { @@ -107,6 +109,7 @@ fn view(k_state: &KiraState) -> Element<'_, Message> { Views::License(license_stage) => license_stage.view().map(Message::License), Views::Network(network_stage) => network_stage.view().map(Message::Network), Views::TimeZone(timezone_stage) => timezone_stage.view().map(Message::TimeZone), + Views::Locale(locale_stage) => locale_stage.view().map(Message::Locale), } } @@ -138,7 +141,7 @@ fn update(k_state: &mut KiraState, message: Message) -> Task { } else { Task::none() } - } + }, Message::License(license_message) => { if let Views::License(license_view) = &mut k_state.current_view { let action = license_view.update(license_message); @@ -160,7 +163,7 @@ fn update(k_state: &mut KiraState, message: Message) -> Task { } else { Task::none() } - } + }, Message::Network(network_message) => { if let Views::Network(network_view) = &mut k_state.current_view { let update_result = network_view.update(network_message); @@ -184,14 +187,15 @@ fn update(k_state: &mut KiraState, message: Message) -> Task { } else { Task::none() } - } + }, Message::TimeZone(timezone_message) => { if let Views::TimeZone(timezone_view) = &mut k_state.current_view { let action = timezone_view.update(timezone_message); match action { StageAction::Next(tz_res) => { k_state.config.config_trail.push(tz_res); - iced::exit() + k_state.current_view = Views::Locale(stages::locale::LocaleStage::new(&k_state.config)); + Task::none() } StageAction::Abort => iced::exit(), StageAction::Back => { @@ -205,6 +209,25 @@ fn update(k_state: &mut KiraState, message: Message) -> Task { } else { Task::none() } + }, + Message::Locale(locale_msg) => { + if let Views::Locale(locale_view) = &mut k_state.current_view { + match locale_view.update(locale_msg) { + StageAction::Next(locale_res) => { + k_state.config.config_trail.push(locale_res); + iced::exit() + } + StageAction::Back => { + k_state.config.config_trail.pop(); + k_state.current_view = + Views::TimeZone(stages::timezone::TimeZoneStage::new()); + Task::none() + } + _ => Task::none(), + } + } else { + Task::none() + } } } } diff --git a/src/stage.rs b/src/stage.rs index f8da219..8bdccab 100644 --- a/src/stage.rs +++ b/src/stage.rs @@ -53,4 +53,11 @@ pub struct KiraConfig { pub config_trail: Vec, } +impl KiraConfig { + pub fn get_stage(&self, name: &str) -> Option { + self.config_trail.iter().find(|v| v.name == name) + .and_then(|v| Some(v.clone())) + } + +} diff --git a/src/stages/locale/mod.rs b/src/stages/locale/mod.rs new file mode 100644 index 0000000..268a0fe --- /dev/null +++ b/src/stages/locale/mod.rs @@ -0,0 +1,318 @@ +// +// Copyright (C) <2026> + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* + This is Locales stage, used to choose global and numbers locale +*/ + +use crate::{ + kira_theming, + stage::{ConfigValue, KiraConfig, StageAction, StageResult}, +}; +use iced::{Alignment, widget}; +use rust_i18n::t; +use std::collections::HashMap; + +const LOCALES_GEN_FILE_NAME: &str = "/etc/locale.gen"; +const LOCALES_GEN_START_LINE: usize = 17; + +fn default_locale() -> (String, String) { + ("C.UTF-8".to_string(), "UTF-8".to_string()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LocaleData { + code: String, + description: String, +} + +impl std::fmt::Display for LocaleData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.description, self.code) + } +} + +fn get_locales_codes_list_blocking() -> std::io::Result> { + use std::fs::File; + use std::io::{BufReader, Read}; + + let mut file_content = String::new(); + { + let locgen_file = File::open(LOCALES_GEN_FILE_NAME)?; + let mut reader = BufReader::new(locgen_file); + reader.read_to_string(&mut file_content)?; + } + + let mut res: Vec<(String, String)> = file_content + .split('\n') + .skip(LOCALES_GEN_START_LINE) + .filter_map(|l| { + let trimmed = l.trim_start_matches('#').trim(); + if trimmed.len() > 0 { + trimmed.split_once(' ') + } else { + None + } + }) + .map(|(loc, charset)| (loc.to_string(), charset.to_string())) + .filter(|(_, chrs)| chrs == "UTF-8") // yeld only UTF-8 locales + .collect(); + + res.push(("C.UTF-8".into(), "UTF-8".into())); + + Ok(res) +} + +fn get_locales_description_blocking() -> Result, String> { + use std::process::Command; + + match Command::new("grep") + .args(["-r", "title ", "/usr/share/i18n/locales/"]) + .output() + { + Ok(cmd_output) => { + if cmd_output.status.success() { + match String::from_utf8(cmd_output.stdout) { + Ok(raw_str_list) => Ok(raw_str_list + .split("\n") + .filter_map(|s| s.trim().split_once(":title")) + .filter_map(|(code_part, description_part)| { + code_part.rsplit_once("/").and_then(|(_, loc_code)| { + Some(( + loc_code.trim().to_string(), + description_part.trim().trim_matches('\"').to_string(), + )) + }) + }) + .collect()), + Err(ex) => Err(format!( + "Exception while converting locales description to UTF8 {}", + ex + )), + } + } else { + Err(format!( + "Error getting locales description list: {}", + String::from_utf8(cmd_output.stderr).unwrap_or("UNKNOWN".into()) + )) + } + } + Err(ex) => Err(format!( + "Exception while trying to get locales description {}", + ex + )), + } +} + +#[derive(Debug, Clone)] +pub struct LocaleStage { + lang_locale: Option, + all_locale: Option, + locales: Vec, + lang_locale_text: String, + all_locale_text: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + SelectLangLocale(LocaleData), + SelectAllLocale(LocaleData), + Next, + Back, +} + +impl LocaleStage { + pub fn new(kira_config: &KiraConfig) -> Self { + // get seelctet language code from welcome stage + let maybe_lang = kira_config + .get_stage("welcome") + .and_then(|v| v.config.and_then(|v| v.get("loc_code").cloned())); + + let loc_codes = get_locales_codes_list_blocking().unwrap(); + + // if we get lang code from wellcome stage, try to fing match with system locales + let lang_match = if let Some(ConfigValue::String(lang)) = maybe_lang { + let lang = lang.replace("-", "_").to_lowercase(); + loc_codes + .iter() + .find(|(code, _)| code.to_lowercase().starts_with(&lang)) + .cloned() + .unwrap_or_else(default_locale) + } else { + default_locale() + }; + + let raw_loc_descr = get_locales_description_blocking().unwrap(); + + println!("{:?}", raw_loc_descr); + + let locales: Vec = loc_codes + .iter() + .filter_map(|(code, _)| { + code.split_once('.') + .and_then(|(key_code, _)| Some((key_code, code))) + }) + .filter_map(|(key_code, code)| { + raw_loc_descr + .get(key_code) + .and_then(|descr| Some((code.clone(), descr.clone()))) + }) + .map(|(code, descr)| LocaleData { + code: code, + description: descr, + }) + .collect(); + + println!("{:?}", locales); + + let locale = locales.iter().find(|l| l.code == lang_match.0).cloned(); + + let lang_locale_text = locale + .as_ref() + .and_then(|l| { + Some(format!( + "{}: {}", + t!("locale.select_language_locale"), + l.code + )) + }) + .unwrap_or_else(|| String::new()); + let all_locale_text = locale + .as_ref() + .and_then(|l| { + Some(format!( + "{}: {}", + t!("locale.select_formats_locale"), + l.code + )) + }) + .unwrap_or_else(|| String::new()); + Self { + lang_locale: locale.clone(), + all_locale: locale, + locales: locales, + lang_locale_text: lang_locale_text, + all_locale_text: all_locale_text, + } + } + + fn gen_result(&self) -> StageResult { + if let Some(lang_locale) = &self.lang_locale + && let Some(all_locale) = &self.all_locale + { + StageResult { + name: "locale".to_string(), + config: Some(HashMap::from([ + ( + "lang_locale".to_string(), + ConfigValue::String(lang_locale.code.clone()), + ), + ( + "all_locale".to_string(), + ConfigValue::String(all_locale.code.clone()), + ), + ])), + resuts: None, + error: None, + } + } else { + StageResult { + name: "locale".to_string(), + config: Some(HashMap::from([ + ( + "lang_locale".to_string(), + ConfigValue::String(default_locale().0), + ), + ( + "all_locale".to_string(), + ConfigValue::String(default_locale().0), + ), + ])), + resuts: None, + error: None, + } + } + } + + pub fn update(&mut self, message: Message) -> StageAction { + match message { + Message::SelectLangLocale(loc) => { + self.lang_locale_text = + format!("{}: {}", t!("locale.select_language_locale"), loc.code); + self.lang_locale = Some(loc); + StageAction::None + } + Message::SelectAllLocale(loc) => { + self.all_locale_text = + format!("{}: {}", t!("locale.select_formats_locale"), loc.code); + self.all_locale = Some(loc); + StageAction::None + } + Message::Back => StageAction::Back, + Message::Next => StageAction::Next(self.gen_result()), + } + } + + pub fn view(&self) -> iced::Element<'_, Message> { + let next_button = widget::button(widget::text(t!("button.next"))).on_press(Message::Next); + let back_button = widget::button(widget::text(t!("button.back"))).on_press(Message::Back); + + // Embed the image bytes into the executable + let welcome_logo_handle = widget::image::Handle::from_bytes(kira_theming::get_logo_bytes()); + + widget::column![ + widget::container( + widget::column![ + widget::image(welcome_logo_handle) + .width(iced::Pixels(128.0)) + .height(iced::Pixels(128.0)), + widget::column![ + widget::text(self.lang_locale_text.as_str()), + widget::pick_list( + self.locales.clone(), + self.lang_locale.clone(), + Message::SelectLangLocale, + ), + widget::text(self.all_locale_text.as_str()), + widget::pick_list( + self.locales.clone(), + self.all_locale.clone(), + Message::SelectAllLocale, + ) + ] + ] + .padding(10) + .spacing(10) + .align_x(Alignment::Center) + ) + .height(iced::Length::Fill) + .width(iced::Length::Fill) + .align_x(Alignment::Center) + .align_y(Alignment::Center), + widget::row![ + back_button, + widget::space::horizontal(), // Pushes the right button to the far right + next_button + ] + .width(iced::Length::Fill) + .align_y(Alignment::End) + .padding(10), + ] + .align_x(Alignment::Center) + .spacing(10) + .into() + } +} diff --git a/src/stages/mod.rs b/src/stages/mod.rs index 56dcaf8..f96d128 100644 --- a/src/stages/mod.rs +++ b/src/stages/mod.rs @@ -18,4 +18,5 @@ pub mod welcome; pub mod license; pub mod network; -pub mod timezone; \ No newline at end of file +pub mod timezone; +pub mod locale; diff --git a/src/stages/timezone/mod.rs b/src/stages/timezone/mod.rs index 48599e2..7090ec2 100644 --- a/src/stages/timezone/mod.rs +++ b/src/stages/timezone/mod.rs @@ -110,9 +110,14 @@ impl TimeZoneStage { time_zones_map.insert(tz_data.region.clone(), vec![tz_data.zone.clone()]); } } - //let time_zones: HashMap = HashMap::from_iter(tzs.iter().map(|tz| (tz.region.clone(), tz.clone()))); + // sort zones alphabetically + time_zones_map.values_mut().for_each(|v| v.sort()); + // sort region names alphabetically + let mut regions: Vec = time_zones_map.keys().cloned().collect(); + regions.sort(); + Self { - regions: time_zones_map.keys().cloned().collect(), + regions: regions, zones: Vec::new(), time_zones: Ok(time_zones_map), region_id: None, @@ -187,14 +192,14 @@ impl TimeZoneStage { StageAction::None } Message::SelectZone(scroll_list::ListViewMessage::Select(zone_id)) => { - let zone = TimeZoneData { region: self.regions[self.region_id.unwrap()].clone(), zone: self.zones[zone_id].clone() }; - self.selected_zone_text = format!( - "{} {}", - t!("timezone.selected_timezone"), - zone.to_string() - ); + let zone = TimeZoneData { + region: self.regions[self.region_id.unwrap()].clone(), + zone: self.zones[zone_id].clone(), + }; + self.selected_zone_text = + format!("{} {}", t!("timezone.selected_timezone"), zone.to_string()); self.zone_id = Some(zone_id); - self.selected_zone = Some(zone); + self.selected_zone = Some(zone); StageAction::None } Message::Next => StageAction::Next(self.gen_result()),