Compare commits

...

2 Commits

Author SHA256 Message Date
kira 0de2f1ce10 Work on locale stage. 2026-05-03 00:56:20 +02:00
kira b8c2630d3e Small fixes and cleanups. 2026-05-01 23:32:25 +02:00
13 changed files with 418 additions and 57 deletions
+1 -1
View File
@@ -12,6 +12,6 @@ toml = "1"
[profile.release] [profile.release]
#lto = true #lto = true
codegen-units = 16 codegen-units = 8
opt-level = 3 opt-level = 3
strip = true strip = true
+5
View File
@@ -1,3 +1,8 @@
# kira-installer # kira-installer
Universal GNU Linux installer. Universal GNU Linux installer.
It supposed to be simple, self contained, and reqire only minimal base system.
In order to test you need to have kira_config.toml in the same directory as executable file.
In order to make debug biuld just run ```cargo build```
+13 -13
View File
@@ -19,13 +19,13 @@
Modify it to your liking. Modify it to your liking.
*/ */
use iced::widget::container; use iced::{Color, Theme, color};
use iced::{Border, Color, Theme, color, Background};
// Function returns theme to be used by installer // Function returns theme to be used by installer
pub fn main_theme() -> Theme { pub fn main_theme() -> Theme {
let mut pl = Theme::Dracula.palette(); let mut pl = Theme::Dracula.palette();
pl.primary = color!(0xFFD700); pl.primary = color!(0xFFD700);
return Theme::custom("Kira Theme", pl); return Theme::custom("Kira Theme", pl);
} }
@@ -38,18 +38,18 @@ pub fn change_lightnes(c: &Color, factor: f32) -> Color {
res res
} }
pub fn text_with_border(_theme: &Theme) -> container::Style { // pub fn text_with_border(_theme: &Theme) -> container::Style {
container::Style { // container::Style {
border: Border { // border: Border {
color: Color::from_rgb8(120, 98, 10), // color: Color::from_rgb8(120, 98, 10),
width: 2.0, // width: 2.0,
radius: 7.into(), // radius: 7.into(),
}, // },
background: Some(Background::Color(change_lightnes(&_theme.palette().background, 0.3))), // background: Some(Background::Color(change_lightnes(&_theme.palette().background, 0.3))),
..container::Style::default() // ..container::Style::default()
} // }
} // }
pub fn get_spiner_bytes() -> Vec<u8>{ pub fn get_spiner_bytes() -> Vec<u8>{
return include_bytes!("media/spiner.apng").to_vec(); return include_bytes!("media/spiner.apng").to_vec();
+3 -1
View File
@@ -20,6 +20,8 @@
"network.button.check": "Check Network", "network.button.check": "Check Network",
"timezone.selected_timezone": "Selected timezone:", "timezone.selected_timezone": "Selected timezone:",
"timezone.select_timezone": "Please select preffered time zone", "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"
} }
+47 -14
View File
@@ -32,9 +32,9 @@ use crate::stages::welcome;
use crate::stages::welcome::WelcomeStage; use crate::stages::welcome::WelcomeStage;
rust_i18n::i18n!("src/locales", fallback = "en"); rust_i18n::i18n!("src/locales", fallback = "en");
mod kira_theming;
mod stage; mod stage;
mod stages; mod stages;
mod theme;
enum Views { enum Views {
Start, Start,
@@ -42,6 +42,7 @@ enum Views {
License(license::LicenseStage), License(license::LicenseStage),
Network(stages::network::NetworkStage), Network(stages::network::NetworkStage),
TimeZone(stages::timezone::TimeZoneStage), TimeZone(stages::timezone::TimeZoneStage),
Locale(stages::locale::LocaleStage),
} }
enum Message { enum Message {
@@ -50,6 +51,7 @@ enum Message {
License(license::Message), License(license::Message),
Network(stages::network::Message), Network(stages::network::Message),
TimeZone(stages::timezone::Message), TimeZone(stages::timezone::Message),
Locale(stages::locale::Message),
} }
struct KiraState { struct KiraState {
@@ -107,6 +109,7 @@ fn view(k_state: &KiraState) -> Element<'_, Message> {
Views::License(license_stage) => license_stage.view().map(Message::License), Views::License(license_stage) => license_stage.view().map(Message::License),
Views::Network(network_stage) => network_stage.view().map(Message::Network), Views::Network(network_stage) => network_stage.view().map(Message::Network),
Views::TimeZone(timezone_stage) => timezone_stage.view().map(Message::TimeZone), Views::TimeZone(timezone_stage) => timezone_stage.view().map(Message::TimeZone),
Views::Locale(locale_stage) => locale_stage.view().map(Message::Locale),
} }
} }
@@ -126,14 +129,19 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
}, },
Message::Welcome(wlc_msg) => { Message::Welcome(wlc_msg) => {
if let Views::Welcome(wlc_view) = &mut k_state.current_view { if let Views::Welcome(wlc_view) = &mut k_state.current_view {
let action = wlc_view.update(wlc_msg); match wlc_view.update(wlc_msg) {
if let StageAction::Next(welcome_res) = action { StageAction::Next(welcome_res) => {
k_state.config.config_trail.push(welcome_res); k_state.config.config_trail.push(welcome_res);
k_state.current_view = Views::License(license::LicenseStage {}); k_state.current_view = Views::License(license::LicenseStage {});
Task::none()
}
StageAction::Abort => iced::exit(),
_ => Task::none(),
} }
} else {
Task::none()
} }
Task::none() },
}
Message::License(license_message) => { Message::License(license_message) => {
if let Views::License(license_view) = &mut k_state.current_view { if let Views::License(license_view) = &mut k_state.current_view {
let action = license_view.update(license_message); let action = license_view.update(license_message);
@@ -144,14 +152,18 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
Views::Network(network::NetworkStage::new(&k_state.toml_config)); Views::Network(network::NetworkStage::new(&k_state.toml_config));
Task::done(Message::Network(network::Message::CheckNetwork)) Task::done(Message::Network(network::Message::CheckNetwork))
} }
StageAction::Abort(_) => iced::exit(), StageAction::Abort => iced::exit(),
StageAction::Back => iced::exit(), StageAction::Back => {
k_state.current_view = Views::Welcome(WelcomeStage::new());
k_state.config.config_trail.pop();
Task::none()
}
StageAction::None => Task::none(), StageAction::None => Task::none(),
} }
} else { } else {
Task::none() Task::none()
} }
} },
Message::Network(network_message) => { Message::Network(network_message) => {
if let Views::Network(network_view) = &mut k_state.current_view { if let Views::Network(network_view) = &mut k_state.current_view {
let update_result = network_view.update(network_message); let update_result = network_view.update(network_message);
@@ -166,6 +178,7 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
} }
StageAction::Back => { StageAction::Back => {
k_state.current_view = Views::License(license::LicenseStage {}); k_state.current_view = Views::License(license::LicenseStage {});
k_state.config.config_trail.pop();
Task::none() Task::none()
} }
_ => Task::none(), _ => Task::none(),
@@ -174,16 +187,17 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
} else { } else {
Task::none() Task::none()
} }
} },
Message::TimeZone(timezone_message) => { Message::TimeZone(timezone_message) => {
if let Views::TimeZone(timezone_view) = &mut k_state.current_view { if let Views::TimeZone(timezone_view) = &mut k_state.current_view {
let action = timezone_view.update(timezone_message); let action = timezone_view.update(timezone_message);
match action { match action {
StageAction::Next(tz_res) => { StageAction::Next(tz_res) => {
k_state.config.config_trail.push(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::Abort => iced::exit(),
StageAction::Back => { StageAction::Back => {
k_state.config.config_trail.pop(); k_state.config.config_trail.pop();
k_state.current_view = k_state.current_view =
@@ -195,6 +209,25 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
} else { } else {
Task::none() 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()
}
} }
} }
} }
@@ -218,7 +251,7 @@ pub fn main() -> ExitCode {
..Default::default() ..Default::default()
}) })
.centered() .centered()
.theme(theme::main_theme()) .theme(kira_theming::main_theme())
.run(); .run();
match iced_result { match iced_result {
+8 -1
View File
@@ -45,7 +45,7 @@ pub enum StageAction {
Back, Back,
None, None,
Next(StageResult), Next(StageResult),
Abort(StageResult), Abort,
} }
@@ -53,4 +53,11 @@ pub struct KiraConfig {
pub config_trail: Vec<StageResult>, pub config_trail: Vec<StageResult>,
} }
impl KiraConfig {
pub fn get_stage(&self, name: &str) -> Option<StageResult> {
self.config_trail.iter().find(|v| v.name == name)
.and_then(|v| Some(v.clone()))
}
}
+1 -6
View File
@@ -62,12 +62,7 @@ impl LicenseStage {
match message { match message {
Message::Accept => stage::StageAction::Next(Self::accepted()), Message::Accept => stage::StageAction::Next(Self::accepted()),
Message::Back => stage::StageAction::Back, Message::Back => stage::StageAction::Back,
Message::Decline => stage::StageAction::Abort(StageResult { Message::Decline => stage::StageAction::Abort,
name: "license".into(),
config: None,
resuts: None,
error: Some("Declined.".to_string()),
}),
} }
} }
+318
View File
@@ -0,0 +1,318 @@
// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation>
// 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 <https://www.gnu.org/licenses/>.
/*
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<Vec<(String, String)>> {
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<HashMap<String, String>, 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<LocaleData>,
all_locale: Option<LocaleData>,
locales: Vec<LocaleData>,
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<LocaleData> = 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()
}
}
+1
View File
@@ -19,3 +19,4 @@ pub mod welcome;
pub mod license; pub mod license;
pub mod network; pub mod network;
pub mod timezone; pub mod timezone;
pub mod locale;
+1 -1
View File
@@ -111,7 +111,7 @@ impl NetworkStage {
} }
let spinner_frames = let spinner_frames =
apng::Frames::from_bytes(crate::theme::get_spiner_bytes()).unwrap(); apng::Frames::from_bytes(crate::kira_theming::get_spiner_bytes()).unwrap();
Self { Self {
internet_active: false, internet_active: false,
+13 -8
View File
@@ -110,9 +110,14 @@ impl TimeZoneStage {
time_zones_map.insert(tz_data.region.clone(), vec![tz_data.zone.clone()]); time_zones_map.insert(tz_data.region.clone(), vec![tz_data.zone.clone()]);
} }
} }
//let time_zones: HashMap<String, TimeZoneData> = 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<String> = time_zones_map.keys().cloned().collect();
regions.sort();
Self { Self {
regions: time_zones_map.keys().cloned().collect(), regions: regions,
zones: Vec::new(), zones: Vec::new(),
time_zones: Ok(time_zones_map), time_zones: Ok(time_zones_map),
region_id: None, region_id: None,
@@ -187,12 +192,12 @@ impl TimeZoneStage {
StageAction::None StageAction::None
} }
Message::SelectZone(scroll_list::ListViewMessage::Select(zone_id)) => { 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() }; let zone = TimeZoneData {
self.selected_zone_text = format!( region: self.regions[self.region_id.unwrap()].clone(),
"{} {}", zone: self.zones[zone_id].clone(),
t!("timezone.selected_timezone"), };
zone.to_string() self.selected_zone_text =
); format!("{} {}", t!("timezone.selected_timezone"), zone.to_string());
self.zone_id = Some(zone_id); self.zone_id = Some(zone_id);
self.selected_zone = Some(zone); self.selected_zone = Some(zone);
StageAction::None StageAction::None
+1 -1
View File
@@ -22,7 +22,7 @@ fn unselected_button_style(theme: &Theme, status: button::Status) -> button::Sty
..button::primary(theme, status) ..button::primary(theme, status)
}, },
button::Status::Active => button::Style { button::Status::Active => button::Style {
background: Some(iced::Background::Color(crate::theme::change_lightnes( background: Some(iced::Background::Color(crate::kira_theming::change_lightnes(
&theme.palette().primary, &theme.palette().primary,
-0.1, -0.1,
))), ))),
+3 -8
View File
@@ -20,7 +20,7 @@
*/ */
use crate::{stage::{ConfigValue, StageAction, StageResult}, theme}; use crate::{stage::{ConfigValue, StageAction, StageResult}, kira_theming};
use iced::{Alignment, widget}; use iced::{Alignment, widget};
use rust_i18n::t; use rust_i18n::t;
use std::collections::HashMap; use std::collections::HashMap;
@@ -111,12 +111,7 @@ impl WelcomeStage {
self.locale = Some(loc); self.locale = Some(loc);
StageAction::None StageAction::None
} }
Message::Exit => StageAction::Abort(StageResult { Message::Exit => StageAction::Abort,
name: "welcome".to_string(),
config: None,
resuts: None,
error: None,
}),
Message::Next => StageAction::Next(self.gen_result()), Message::Next => StageAction::Next(self.gen_result()),
} }
} }
@@ -127,7 +122,7 @@ impl WelcomeStage {
// Embed the image bytes into the executable // Embed the image bytes into the executable
let welcom_logo_handle = let welcom_logo_handle =
widget::image::Handle::from_bytes(theme::get_logo_bytes()); widget::image::Handle::from_bytes(kira_theming::get_logo_bytes());
widget::column![ widget::column![
widget::container( widget::container(