Compare commits

...
41 Commits
Author SHA256 Message Date
kira b5b5bd764a More work on installation stage 2026-10-01 17:10:26 +02:00
kira 3b086605cf Work on making partitions start-end calculate properly. Specifically 2mb at start and -33 sectors GPT reserved at the end. 2026-09-28 00:13:12 +02:00
kira fa5c3d3aec Refactor of stages results handling 2026-09-25 17:33:10 +02:00
kira dda9288e23 Sawing progress 2026-09-10 14:34:13 +02:00
kira f404ff385a Big refactor of stage result, to be more easy instead of generic. 2026-06-08 00:56:37 +02:00
kira c56da21eab Adding initial stuff for install stage. 2026-06-06 00:28:29 +02:00
kira ed6ffcdee1 Changes to partition stage in respect to intall stage. 2026-06-06 00:27:55 +02:00
kira 4d8c9540d0 Added function to KiraSize to format size with B suffix. 2026-06-06 00:24:36 +02:00
kira 56e87a62cf Adding records for install stage. 2026-06-06 00:23:09 +02:00
kira 9769ae409e Functions for creating partitions and filesystems. 2026-06-06 00:21:34 +02:00
kira 3abbbe9abd Addin saving of previos stages for easy going back. 2026-05-30 22:12:08 +02:00
kira b2f510e679 Security stage (user login) mostly completed. 2026-05-30 17:18:54 +02:00
kira a503b2a36f Fixes and initial work on security stage. 2026-05-30 00:33:34 +02:00
kira f0a8e704c0 Using secure boot status to determin its awailability for installation. 2026-05-28 00:43:31 +02:00
kira 187129d314 Added secure boot status to sysinfo. 2026-05-28 00:42:31 +02:00
kira b0ea2e0ee3 Properly hadnle swap partition size calculation with regart to zram option. 2026-05-26 23:46:36 +02:00
kira 4fb49797cb Made option (reuse network settings) on by default. 2026-05-26 23:45:38 +02:00
kira bb3f5ed1f0 Work on Partition Stage, added custom swap size selection. 2026-05-26 22:54:19 +02:00
kira 078589332c Adding build.rs for config file copy and changing render backend to tiny-skia. 2026-05-26 18:26:56 +02:00
kira 6b4550dc8e Changing println to proper log macros and small fixes. 2026-05-26 17:56:02 +02:00
kira e0c6112422 Adding simple logger implementation to use with log crate. 2026-05-26 17:47:20 +02:00
kira 66f7851574 Minor changes and fixes. 2026-05-25 13:30:16 +02:00
kira b1f3644092 Adding KiraSize struct to handle power of 2 sizes. 2026-05-25 13:29:42 +02:00
kira 302423d98f Adding symple system info module. 2026-05-25 13:28:53 +02:00
kira 9743b010d1 More work on partition stuff #2, added BlkSize struct to handle partition sizes nicely. 2026-05-23 14:56:21 +02:00
kira c6e7fa6069 More work on partition stuff #2, things looks much better now. 2026-05-21 08:39:11 +02:00
kira c0ae014eef align_part function now passing tests. #2 2026-05-20 20:04:45 +02:00
kira edfd57d5d5 Working on partitions stuff related to issue #2 2026-05-19 23:52:20 +02:00
kira cf916eb508 Refactoring for StageResult related to issue #1 2026-05-16 21:10:50 +02:00
kira 4000e0cfac More work on partition stage. 2026-05-13 00:24:05 +02:00
kira f851c6fef0 More work on partition stage. 2026-05-11 23:01:54 +02:00
kira 2684c12e73 Initial work on partition stage. 2026-05-09 21:50:46 +02:00
kira 75ef50bed1 Most work on keyboard stage is done. 2026-05-08 00:24:22 +02:00
kira 9081e301d4 More work on keyboard stage, now compiling. 2026-05-06 23:21:12 +02:00
kira 8b3f22909e Working on keyboard stage, closures! 2026-05-06 20:39:26 +02:00
kira 7365f95d20 Work on scroll list style. Tranfer scroll list to src root. 2026-05-04 21:21:41 +02:00
kira d4c7e59cd6 Work on scroll list style. 2026-05-03 23:13:25 +02:00
kira cf7b9ace08 Finished locales stage, and work on theming. 2026-05-03 18:07:06 +02:00
kira a1263e7056 Fixing locale detection in locales stage. 2026-05-03 12:32:07 +02:00
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
29 changed files with 4326 additions and 1883 deletions
Generated
+575 -1436
View File
File diff suppressed because it is too large Load Diff
+26 -4
View File
@@ -3,15 +3,37 @@ name = "kira-installer"
version = "0.1.0"
edition = "2024"
# Reference the build script
# build = "build.rs"
[dependencies]
blocking = "1.6"
iced = { version = "0.14", features = ["smol", "image"] }
iced = { version = "0.14", default-features = false, features = ["crisp", "linux-theme-detection", "tiny-skia", "x11", "smol", "image"] }
#iced = { git = "https://github.com/iced-rs/iced", branch = "vello_cpu", default-features = false, features = ["crisp", "linux-theme-detection", "vello-cpu", "x11", "smol", "image"]}
iced_moving_picture = "0"
log = "0.4"
rust-i18n = "3"
toml = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
time-format = "1.2"
[dev-dependencies]
rand = "0.10"
[profile.dev]
opt-level = 3
codegen-units = 16
[profile.release]
#lto = true
codegen-units = 16
lto = true
debug = false
incremental = false
codegen-units = 1
# We need level 3 for tiny-skia otherwise render is lagging
# opt-level = "s"
opt-level = 3
strip = true
[registry]
global-min-publish-age = "14 days"
+6 -1
View File
@@ -1,3 +1,8 @@
# 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```
+23
View File
@@ -0,0 +1,23 @@
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
let profile = env::var("PROFILE").unwrap();
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let target_dir = PathBuf::from(&manifest_dir).join("target").join(&profile);
// Ensure target directory exists
if !target_dir.exists() {
fs::create_dir_all(&target_dir).ok();
}
// Copy the file (e.g., config.json) to the target directory
let source = PathBuf::from(&manifest_dir).join("src").join("kira_config.toml");
let dest = target_dir.join("kira_config.toml");
fs::copy(&source, &dest).expect("Failed to copy kira_config.toml");
// Optional: Tell Cargo to re-run this script if the source file changes
println!("cargo:rerun-if-changed=src/kira_config.toml");
}
+36
View File
@@ -0,0 +1,36 @@
use iced::widget::{self, Row, container, text};
use iced::{Alignment, Color, Element, Length};
// Assuming your Item struct or type implements Display or has a text representation
pub fn color_bar<Message: 'static>(
maybe_items: Option<Vec<(String, u16, Color)>>,
bar_height: u32,
) -> Element<'static, Message> {
if let Some(items) = maybe_items {
let mut c_b = Row::new().height(Length::Fill);
for (bar_text, bar_fill, bar_color) in items {
c_b = c_b.push(
container(text(bar_text).color(Color::BLACK).size(iced::Pixels(14.0)))
.height(Length::Fill)
.width(Length::FillPortion(bar_fill))
.style(move |_| container::background(bar_color))
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.padding(2),
);
}
container(c_b.spacing(2))
.style(container::rounded_box)
.padding(3)
.height(bar_height)
.into()
}
else {
widget::container(widget::space())
.height(bar_height)
.width(Length::Fill)
.into()
}
}
+4
View File
@@ -1,3 +1,7 @@
[network]
reqire_network = true
[partition]
min_disk_size_mb = 8192
efi_size_mb = 128
+576
View File
@@ -0,0 +1,576 @@
// <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/>.
/*
Functions for working with disk layout
*/
use crate::kira_size::KiraSize;
use log;
use std::collections::HashMap;
///
/// size - device size in bytes
/// sector size - default 4096
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlkDev {
pub name: String,
pub size: KiraSize,
pub sector_size: KiraSize,
}
impl std::fmt::Display for BlkDev {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", &self.name, self.size)
}
}
impl BlkDev {
/// Create new BlkDev struct with specifyed name and size
pub fn new(name: String, size: KiraSize) -> Self {
Self {
name: name,
size: size,
sector_size: KiraSize::new_b(4096),
}
}
/// Create new BlkDev struct with name and size parsed from hashmap
/// that contains String key val pairs with "NAME" and "SIZE" keys
pub fn from_hash_map(data: &HashMap<String, String>) -> Option<Self> {
Some(Self {
name: data.get("NAME")?.clone(),
size: KiraSize::from_str_bytes(data.get("SIZE")?)?,
sector_size: KiraSize::new_b(4096),
})
}
/// returns dev name in a "/dev/name" form
pub fn full_name(&self) -> String {
format!("/dev/{}", self.name)
}
/// this is blocking function
/// Lists block devices in system thru "lsbk" command
pub fn list_sys_blk_dev() -> Result<Vec<Self>, String> {
use std::process::Command;
// lsblk -d -b -P -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT
match Command::new("lsblk")
.args(["-d", "-b", "-P", "-o", "NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT"])
.output()
{
Ok(cmd_output) => {
if cmd_output.status.success() {
match String::from_utf8(cmd_output.stdout) {
Ok(raw_str_list) => Ok(raw_str_list
.lines()
.map(|l| {
l.split_whitespace()
.filter_map(|pair| {
pair.split_once("=").and_then(|kv| {
Some((
kv.0.trim_matches('"').to_string(),
kv.1.trim_matches('"').to_string(),
))
})
})
.collect::<HashMap<String, String>>()
})
.filter(|mp| mp.get("MOUNTPOINT").is_some_and(|v| v.is_empty()))
.filter_map(|mp| Self::from_hash_map(&mp))
.collect()),
Err(ex) => Err(format!(
"Exception while converting disks info to UTF8 {}",
ex
)),
}
} else {
Err(format!(
"Error getting disk devices list: {}",
String::from_utf8(cmd_output.stderr).unwrap_or("UNKNOWN".into())
))
}
}
Err(ex) => Err(ex.to_string()),
}
}
pub fn from_sys(name: &str) -> Option<Self> {
Self::list_sys_blk_dev()
.ok()?
.iter()
.find(|v| v.name == name)
.cloned()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BcachefsProp {
pub password: Option<String>,
pub comp_alg: Option<String>,
}
impl BcachefsProp {
pub fn new(password: Option<String>, comp_alg: Option<String>) -> Self {
Self {
password: password,
comp_alg: comp_alg,
}
}
pub fn default() -> Self {
Self {
password: None,
comp_alg: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FSType {
VFAT,
XFS,
LUKS,
SWAP,
EXT4,
BTRFS,
BCACHEFS(BcachefsProp),
UNKNOWN,
}
impl std::fmt::Display for FSType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::VFAT => write!(f, "vfat"),
Self::XFS => write!(f, "xfs"),
Self::LUKS => write!(f, "luks"),
Self::SWAP => write!(f, "swap"),
Self::EXT4 => write!(f, "ext4"),
Self::BTRFS => write!(f, "btrfs"),
Self::BCACHEFS(_) => write!(f, "bcachefs"),
Self::UNKNOWN => write!(f, "unknown"),
}
}
}
impl FSType {
pub fn from_str(s: &str) -> Self {
let s_low = s.to_lowercase();
match s_low.as_str() {
"vfat" => Self::VFAT,
"xfs" => Self::XFS,
"crypto_luks" => Self::LUKS,
"swap" => Self::SWAP,
"ext4" => Self::EXT4,
"btrfs" => Self::BTRFS,
"bcachefs" => Self::BCACHEFS(BcachefsProp::default()),
_ => Self::UNKNOWN,
}
}
/// return fs type that can be used with parted utility
pub fn to_parted_str(&self) -> &str {
match self {
Self::VFAT => "fat32",
Self::XFS => "xfs",
Self::LUKS => "luks",
Self::SWAP => "linux-swap",
Self::EXT4 => "ext4",
Self::BTRFS => "btrfs",
// workaroud as parted do not support bcachefs partition type
Self::BCACHEFS(_) => "xfs",
// lets do something generic here instead of panic
Self::UNKNOWN => "linux-swap",
}
}
}
// fn part_type_to_color(t: &str) -> Color {
// match t {
// "vfat" => Color::from_rgb8(204, 121, 167),
// "xfs" => Color::from_rgb8(0, 158, 115),
// "crypto_LUKS" => Color::from_rgb8(240, 228, 66),
// "swap" => Color::from_rgb8(213, 94, 0),
// "ext4" => Color::from_rgb8(0, 114, 178),
// "btrfs" => Color::from_rgb8(86, 180, 233),
// _ => Color::from_rgb8(230, 159, 0),
// }
// }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartRole {
EFI,
ROOT,
SWAP,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartInfo {
pub name: String,
pub size: KiraSize,
pub start: KiraSize,
pub fs_type: FSType,
pub gpt_label: Option<String>,
pub fs_label: Option<String>,
pub mount_point: Option<String>,
pub role: Option<PartRole>,
}
impl std::fmt::Display for PartInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", &self.name, &self.fs_type, self.size)
}
}
impl PartInfo {
///
/// calculate par end byte as 0 starting bytes index (for parted and such)
///
pub fn part_end(&self) -> KiraSize {
// important to shift here by 1 because disk index is starting from 0, and part end pointing to last byte of the partition.
(self.start + self.size) - KiraSize::new_b(1)
}
///
/// Returns zero based byte index of start of next (right after this one) partition
///
pub fn next_part_start(&self) -> KiraSize {
// important to shift here by 1 because disk index is starting from 0, and part end pointing to last byte of the partition.
self.start + self.size
}
pub fn from_hash_map(data: &HashMap<String, String>) -> Option<Self> {
Some(Self {
name: data.get("NAME")?.clone(),
size: KiraSize::from_str_bytes(data.get("SIZE")?)?,
start: KiraSize::new_b(0),
fs_type: FSType::from_str(data.get("FSTYPE")?),
gpt_label: data
.get("PARTLABEL")
.and_then(|v| if v.is_empty() { None } else { Some(v.clone()) }),
fs_label: data
.get("LABEL")
.and_then(|v| if v.is_empty() { None } else { Some(v.clone()) }),
mount_point: data
.get("MOUNTPOINT")
.and_then(|v| if v.is_empty() { None } else { Some(v.clone()) }),
role: None,
})
}
pub fn partlabel_dev(&self) -> Option<String> {
self.gpt_label
.as_ref()
.and_then(|l| Some(format!("/dev/disk/by-partlabel/{}", l)))
}
}
/// getting list of device partitions
fn get_dev_part_blocking(dev_name: &str) -> Result<Vec<PartInfo>, String> {
use std::process::Command;
//"lsblk -o NAME,SIZE,FSTYPE -l -n /dev/nvme0n1"
// lsblk -o NAME,SIZE,FSTYPE,TYPE,PARTLABEL,LABEL,PARTFLAGS -b -P
// -x NAME - sorting by name
match Command::new("lsblk")
.args([
"-x",
"NAME",
"-o",
"NAME,SIZE,FSTYPE,TYPE,PARTLABEL,LABEL,MOUNTPOINT,PARTFLAGS",
"-b",
"-P",
format!("/dev/{}", dev_name).as_str(),
])
.output()
{
Ok(cmd_output) => {
if cmd_output.status.success() {
match String::from_utf8(cmd_output.stdout) {
Ok(raw_str_list) => Ok(raw_str_list
.lines()
.map(|l| {
l.split_whitespace()
.filter_map(|pair| {
pair.split_once("=").and_then(|kv| {
Some((
kv.0.trim_matches('"').to_string(),
kv.1.trim_matches('"').to_string(),
))
})
})
.collect::<HashMap<String, String>>()
})
.filter(|mp| mp.get("TYPE").is_some_and(|v| v == "part"))
.filter_map(|mp| PartInfo::from_hash_map(&mp))
.collect()),
Err(ex) => Err(format!(
"Exception while converting partition info to UTF8 {}",
ex
)),
}
} else {
Err(format!(
"Error getting device partitions list: {}",
String::from_utf8(cmd_output.stderr).unwrap_or("UNKNOWN".into())
))
}
}
Err(ex) => Err(ex.to_string()),
}
}
///
/// size - size in bytes
/// start - start byte of partition, inclusive index.
/// returns tuple of aligned size, start index, and end index
/// return none if aligment is not possibe (size of partition is less than align)
pub fn align_part(size: u64, start: u64, align: u64) -> Option<(u64, u64, u64)> {
if size < align {
log::debug!("if size < align !!!!!!!");
return None;
}
let start_mod = start % align;
// increasing start if not align
let align_start = if start_mod > 0 {
start + (align - start_mod)
} else {
start
};
// decreasing end if not align
let part_end = align_start + size;
let end_mod = part_end % align;
let align_end = part_end - end_mod - 1;
let align_size = align_end - align_start + 1;
if align_size < align {
log::warn!(
"!!! align_size < align !!! align_size: {} align: {}",
align_size,
align
);
return None;
}
log::debug!(
"align_size: {}, align_start:{}, align_end: {} end_mod: {}",
align_size,
align_start,
align_end,
end_mod
);
Some((align_size, align_start, align_end))
}
#[derive(Debug, Clone, PartialEq)]
pub struct PartLayout {
pub dev: BlkDev,
pub empty_space: KiraSize,
pub part_list: Vec<PartInfo>,
}
///
/// GPT requires 33x512 sectors at the very end of the disk (16896 bytes):
/// 32 sectors - backup Partition Entry Array
/// 1 sector - backup GPT header
///
impl PartLayout {
pub fn new(dev: BlkDev) -> Self {
// 16896 - reserved space at the dev end for gpt
let usable_space = dev.size - KiraSize::new_b(16896);
Self {
empty_space: usable_space,
dev: dev,
part_list: Vec::new(),
}
}
pub fn read_from_disk(dev: &BlkDev) -> Result<Self, String> {
let part_list = get_dev_part_blocking(&dev.name)?;
let parts_size = part_list
.iter()
.fold(KiraSize::new_b(0), |acc, part| acc + part.size);
Ok(Self {
dev: dev.clone(),
empty_space: dev.size - parts_size,
part_list: part_list,
})
}
// pub fn new_table(mut self) -> Self {
// self.empty_space = self.dev.size;
// self.part_list = Vec::new();
// self
// }
pub fn add_part(
mut self,
part_size: KiraSize,
fs_type: FSType,
gpt_label: Option<String>,
fs_label: Option<String>,
mount_point: Option<String>,
role: Option<PartRole>,
) -> Self {
let part_start = if self.part_list.is_empty() {
// need to substract 2 mb offset from disk start.
self.empty_space = self.empty_space - KiraSize::new_mb(2);
KiraSize::new_mb(2)
} else {
// last part end + 1
self.part_list.last().unwrap().next_part_start()
};
let (a_size, a_start, _) =
align_part(part_size.b(), part_start.b(), self.dev.sector_size.b()).unwrap();
log::debug!(
"PartLayout.add_part gpt_label{:?}, size_asked:{}, size_al:{}",
gpt_label,
part_size,
a_size
);
let part_name = format!("{}p{}", self.dev.name, self.part_list.len() + 1);
self.part_list.push(PartInfo {
name: part_name,
size: KiraSize::new_b(a_size),
start: KiraSize::new_b(a_start),
fs_type: fs_type,
gpt_label: gpt_label,
fs_label: fs_label,
mount_point: mount_point,
role: role,
});
self.empty_space = self.empty_space - KiraSize::new_b(a_size);
self
}
///
/// Use this to add partition of (disk_empty_space - reserve_space)
/// Specifically to leave some space at the end for SWAP and such
///
pub fn add_part_reserve(
self,
reserve_space: KiraSize,
fs_type: FSType,
gpt_label: Option<String>,
fs_label: Option<String>,
mount_point: Option<String>,
role: Option<PartRole>,
) -> Self {
let p_size = self.empty_space - reserve_space;
self.add_part(p_size, fs_type, gpt_label, fs_label, mount_point, role)
}
pub fn gen_kira_layout(
dev: BlkDev,
efi_size: KiraSize,
swap_size: KiraSize,
maybe_password: Option<String>,
maybe_compr: Option<String>,
) -> Self {
let mut res: PartLayout = PartLayout::new(dev);
// UEFI partition
res = res
.add_part(
efi_size,
FSType::VFAT,
Some("bee-efi".into()),
Some("EFI".into()),
Some("/beefi".into()),
Some(PartRole::EFI),
)
.add_part_reserve(
swap_size,
FSType::BCACHEFS(BcachefsProp::new(maybe_password, maybe_compr)),
Some("bee-root".into()),
None,
Some("/".into()),
Some(PartRole::ROOT),
);
// swap
if swap_size.b() > 0 {
res = res.add_part(
swap_size,
FSType::SWAP,
Some("bee-swap".into()),
None,
Some("SWAP".into()),
Some(PartRole::SWAP),
);
}
res
}
}
#[cfg(test)]
mod tests {
use rand::RngExt;
use super::*;
#[test]
fn test_align_part_fn_joe() {
//START (S),SIZE (Z),ALIGNMENT (A),ALIGNED_START (Salign​),ALIGNED_END (Ealign​),Effektive Größe (Zalign​)
let joes_test_data: Vec<(u64, u64, u64, u64, u64, u64)> = vec![
(0, 10000, 4096, 0, 8191, 8192),
(5000, 20000, 4096, 8192, 24575, 16384),
(4096, 4096, 4096, 4096, 8191, 4096),
(1000000, 5000000, 1048576, 1048576, 5242879, 4194304),
(2097152, 1050000, 1048576, 2097152, 3145727, 1048576),
];
for test_data in joes_test_data {
let res = align_part(test_data.1, test_data.0, test_data.2).unwrap();
let test_res = (test_data.5, test_data.3, test_data.4);
assert_eq!(res, test_res);
}
}
#[test]
fn test_align_part_fn_rand() {
for ((size, start, align), test_res) in rand::rng()
.random_iter::<u32>()
.filter(|n| *n > 0)
.take(100)
.flat_map(|align| {
(0..u64::MAX)
.step_by(align as usize)
.take(1000)
.map(move |start_align| {
let mut rng = rand::rng();
let size_align = align as u64 * rng.random_range(1..u16::MAX) as u64;
let end_align = start_align + size_align - 1;
let start = start_align.saturating_sub(rng.random_range(0..align) as u64);
let size = size_align.saturating_add(rng.random_range(0..align) as u64);
(
(size, start, align as u64),
(size_align, start_align.clone(), end_align),
)
})
})
{
let res = align_part(size, start, align).unwrap();
assert_eq!(res, test_res);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
use log::{Level, Metadata, Record, SetLoggerError, LevelFilter};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::Mutex;
use std::sync::LazyLock;
use time_format;
struct KiraLogger {
file: Option<Mutex<File>>,
}
impl KiraLogger {
pub fn open(file_name: Option<&str>) -> std::io::Result<Self> {
match file_name {
Some(f_n) => Ok(Self {
file: Some(Mutex::new(
OpenOptions::new().append(true).create(true).open(f_n)?,
)),
}),
None => Ok(Self {
file: None,
}),
}
}
}
impl log::Log for KiraLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
(metadata.target().starts_with("kira_installer") && metadata.level() <= Level::Debug) || (metadata.level() <= Level::Error)
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let ts = time_format::now().unwrap();
let local_time = time_format::strftime_local("%Y-%m-%dT%H:%M:%S", ts).unwrap();
if let Some(m_file) = &self.file {
let mut write_lock = m_file.lock().unwrap();
let _ = writeln!(&mut *write_lock, "{} {}: {}", local_time, record.level(), record.args());
}
println!("{} {} {}: {}", local_time,record.metadata().target(), record.level(), record.args());
}
}
fn flush(&self) {
if let Some(m_file) = &self.file {
let mut write_lock = m_file.lock().unwrap();
let _ = (&mut *write_lock).flush();
}
}
}
static LOGGER: LazyLock<KiraLogger> = LazyLock::new(|| {
let ts = time_format::now().unwrap();
// Local time with timezone name
let local_time = time_format::strftime_local("%Y-%m-%d_%H-%M-%S", ts).unwrap();
KiraLogger::open(Some(format!("installation_{}.log", local_time).as_str())).unwrap()
});
pub fn init() -> Result<(), SetLoggerError> {
log::set_logger(&*LOGGER)
.map(|()| log::set_max_level(LevelFilter::Debug))
}
+112
View File
@@ -0,0 +1,112 @@
// <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/>.
/*
Scroll List iced widget implementation
*/
use std::fmt::Display;
use iced::widget::{Column, button, container, scrollable, text};
use iced::{Alignment, Color, Element, Length, Theme};
#[derive(Debug, Clone)]
pub enum ListViewMessage {
Select(usize),
}
fn unselected_button_style(theme: &Theme, status: button::Status) -> button::Style {
use iced::{Border, border::Radius};
let palette = theme.palette();
let mut res_stile = button::Style {
background: Some(iced::Background::Color(Color::TRANSPARENT)),
text_color: palette.text,
..button::primary(theme, button::Status::Active)
};
match status {
button::Status::Hovered => {
res_stile.border = Border {
color: Color::from_rgb(0.8, 0.3, 0.3),
width: 1.0,
radius: Radius::new(5.0),
};
},
_ => (),
}
return res_stile;
}
fn selected_button_style(theme: &Theme, status: button::Status) -> button::Style {
use iced::{Border, border::Radius};
//let palette = theme.palette();
let mut res_stile = button::primary(theme, button::Status::Active);
match status {
button::Status::Hovered => {
res_stile.border = Border {
color: Color::from_rgb(0.8, 0.3, 0.3),
width: 1.0,
radius: Radius::new(5.0),
};
},
_ => (),
}
return res_stile;
}
// Assuming your Item struct or type implements Display or has a text representation
pub fn list_view<T: Display>(
items: &Vec<T>,
selected_id: Option<usize>,
container_height: Length,
) -> Element<'_, ListViewMessage> {
let mut column = Column::new()
.spacing(2)
.align_x(Alignment::Center)
.width(Length::Fill);
for (index, value) in items.iter().enumerate() {
// Create a row for each item, possibly with buttons or other controls
let list_item = if let Some(id) = selected_id
&& id == index
{
button(text(value.to_string())).style(selected_button_style)
} else {
button(text(value.to_string())).style(unselected_button_style)
};
let list_item = list_item
.width(Length::Fill)
.on_press(ListViewMessage::Select(index));
column = column.push(list_item);
}
// Wrap the column in a scrollable widget
container(scrollable(column).width(Length::Fill).spacing(5))
.style(container::bordered_box)
.height(container_height)
.padding(5)
.into()
}
+131
View File
@@ -0,0 +1,131 @@
// <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/>.
/*
KiraSize struct implementation for work with base2 sizes.
*/
use std::ops::{Add, Sub};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct KiraSize {
size_bytes: u64,
}
impl std::fmt::Display for KiraSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.size_bytes < 1000 {
write!(f, "{}B", self.size_bytes)
} else {
let mut s = self.size_bytes as f64;
let mut idx = 0;
for i in 0..5 {
s = s / 1024.0;
idx = i;
if s < 1000.0 {
break;
}
}
write!(f, "{:.2}{}", s, Self::SUFFIXS[idx])
}
}
}
impl KiraSize {
pub const SUFFIXS: [&str; 5] = ["KB", "MB", "GB", "TB", "PB"];
/// returns size in bytes suffixed with "B" character
pub fn to_parted_bytes_str(&self)-> String {
format!("{}B", self.size_bytes)
}
pub fn new_b(size:u64) -> Self {
Self { size_bytes: size }
}
pub fn new_kb(size:u64) -> Self {
Self { size_bytes: size * 1024 }
}
pub fn new_mb(size:u64) -> Self {
Self { size_bytes: size * 1024u64.pow(2) }
}
pub fn new_gb(size:u64) -> Self {
Self { size_bytes: size * 1024u64.pow(3) }
}
pub fn new_tb(size:u64) -> Self {
Self { size_bytes: size * 1024u64.pow(4) }
}
pub fn new_pb(size:u64) -> Self {
Self { size_bytes: size * 1024u64.pow(5) }
}
pub fn b(&self) -> u64 {
self.size_bytes
}
pub fn kb(&self) -> u64 {
self.size_bytes / 1024u64
}
pub fn mb(&self) -> u64 {
self.size_bytes / 1024u64.pow(2)
}
pub fn gb(&self) -> u64 {
self.size_bytes / 1024u64.pow(3)
}
pub fn tb(&self) -> u64 {
self.size_bytes / 1024u64.pow(4)
}
pub fn pb(&self) -> u64 {
self.size_bytes / 1024u64.pow(5)
}
pub fn from_str_bytes(s: &str) -> Option<Self> {
Some(Self::new_b(u64::from_str_radix(s, 10).ok()?))
}
pub fn div_c(self, other: u64) -> Self {
Self { size_bytes: self.size_bytes / other }
}
///
/// Panics if overflow accurs.
///
pub fn mul_c(self, other: u64) -> Self {
Self { size_bytes: self.size_bytes.strict_mul(other) }
}
}
impl Add for KiraSize {
type Output = KiraSize;
fn add(self, other: KiraSize) -> Self::Output {
KiraSize {
size_bytes: self.size_bytes + other.size_bytes,
}
}
}
impl Sub for KiraSize {
type Output = KiraSize;
/// Panics if overflow accors.
fn sub(self, other: KiraSize) -> Self::Output {
KiraSize {
size_bytes: self.size_bytes.strict_sub(other.size_bytes),
}
}
}
+131
View File
@@ -0,0 +1,131 @@
// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation> <Kira>
// 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 file contains functions to query system information.
*/
use std::collections::HashMap;
use std::fs;
///
/// vis_vram_total - size of vram in bytes
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuInfo {
pub id: u8,
pub name: String,
pub vis_vram_total: u64,
}
/// See /proc/meminfo output format and units (kB)
pub fn get_meminfo() -> HashMap<String, u64> {
let mem_info_str = fs::read_to_string("/proc/meminfo").unwrap();
mem_info_str
.lines()
.filter_map(|l| {
l.split_once(':').and_then(|(k, v)| {
u64::from_str_radix(v.trim_end_matches("kB").trim(), 10)
.ok()
.and_then(|v_num| Some((k.to_string(), v_num)))
})
})
.collect()
}
// cat /sys/class/drm/card1/device/mem_info_vis_vram_total
pub fn get_gpus_list() -> Vec<GpuInfo> {
let drm_devs_dirs: Vec<(String, u8)> = fs::read_dir("/sys/class/drm/")
.unwrap()
.filter_map(|maybe_entry| {
maybe_entry.ok().and_then(|entry| {
entry.file_type().ok().and_then(|ft| {
if ft.is_symlink() || ft.is_dir() {
entry.file_name().into_string().ok()
} else {
None
}
})
})
})
.filter(|f_name| f_name.len() == 5 && f_name.starts_with("card"))
.filter_map(|dir_name| {
dir_name.split_once("card").and_then(|name_touple| {
u8::from_str_radix(name_touple.1, 10)
.ok()
.and_then(|gpu_id| Some((dir_name.clone(), gpu_id)))
})
})
.collect();
let res: Vec<GpuInfo> = drm_devs_dirs
.iter()
.filter_map(|(d, id)| {
fs::read_to_string(format!(
"/sys/class/drm/{}/device/mem_info_vis_vram_total",
d
))
.ok()
.and_then(|mem_str| {
u64::from_str_radix(mem_str.trim(), 10)
.ok()
.and_then(|mem_size| {
Some(GpuInfo {
id: *id,
name: d.clone(),
vis_vram_total: mem_size,
})
})
})
})
.collect();
res
}
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct SecureBootStatus {
pub installed: bool,
pub guid: String,
pub setup_mode: bool,
pub secure_boot: bool,
pub vendors: Vec<String>,
pub firmware_quirks: Vec<String>,
}
pub fn get_secure_boot_status() -> Result<SecureBootStatus, String> {
use std::process::Command;
//sbctl --json status
match Command::new("sbctl").args(["--json", "status"]).output() {
Ok(cmd_output) => {
if cmd_output.status.success() {
match serde_json::from_slice(&cmd_output.stdout) {
Ok(res) => Ok(res),
Err(ex) => Err(ex.to_string()),
}
} else {
Err(format!(
"Error getting disk devices list: {}",
String::from_utf8(cmd_output.stderr).unwrap_or("UNKNOWN".into())
))
}
}
Err(ex) => Err(ex.to_string()),
}
}
+332
View File
@@ -0,0 +1,332 @@
// <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/>.
/*
Functions for creating with disk layout
*/
use crate::kira_disk_layout::*;
use crate::kira_size::KiraSize;
use std::fs;
use std::process::Command;
#[derive(Debug)]
pub struct CommandFailed {
stderr: Vec<u8>,
cmd: String,
}
impl std::fmt::Display for CommandFailed {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Command failed! Command: {}; Stderr: {}",
self.cmd,
String::from_utf8(self.stderr.clone())
.unwrap_or_else(|_| { "STDEER IS NOT VALID UTF-8".to_string() })
)
}
}
impl CommandFailed {
pub fn new(cmd: &str, args: &Vec<&str>, stderr: Vec<u8>) -> Self {
Self {
stderr: stderr,
cmd: format!("{} {}", cmd, args.join(" ")),
}
}
}
#[derive(Debug)]
pub enum ExecCommandError {
IoError(std::io::Error),
FromUtf8Error(std::string::FromUtf8Error),
CommandFailed(CommandFailed),
Other(String),
}
// 2. Implement From for each wrapped error type
impl From<std::io::Error> for ExecCommandError {
fn from(err: std::io::Error) -> Self {
ExecCommandError::IoError(err)
}
}
impl From<std::string::FromUtf8Error> for ExecCommandError {
fn from(err: std::string::FromUtf8Error) -> Self {
ExecCommandError::FromUtf8Error(err)
}
}
impl From<CommandFailed> for ExecCommandError {
fn from(err: CommandFailed) -> Self {
ExecCommandError::CommandFailed(err)
}
}
impl From<String> for ExecCommandError {
fn from(err: String) -> Self {
ExecCommandError::Other(err)
}
}
impl std::fmt::Display for ExecCommandError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ExecCommandError::IoError(e) => write!(f, "IO Error: {}", e),
ExecCommandError::FromUtf8Error(e) => {
write!(f, "IO streem to String parse Error: {}", e)
}
ExecCommandError::CommandFailed(ex) => {
write!(f, "{}", ex)
}
ExecCommandError::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
// Implement std::error::Error
impl std::error::Error for ExecCommandError {}
/// exec given sommand with arguments, capturing stderr in case or failure
fn exec_command(cmd: &str, args: &Vec<&str>) -> Result<(), ExecCommandError> {
let out = Command::new(cmd).args(args).output()?;
if out.status.success() {
Ok(())
} else {
Err(CommandFailed::new(cmd, args, out.stderr).into())
}
}
/// exec given sommand with arguments, returning stdout parsed to UTF-8 string
/// capturing stderr in case or failure
fn exec_command_out(cmd: &str, args: &Vec<&str>) -> Result<String, ExecCommandError> {
let out = Command::new(cmd).args(args).output()?;
if out.status.success() {
Ok(String::from_utf8(out.stdout)?)
} else {
Err(CommandFailed::new(cmd, args, out.stderr).into())
}
}
/// sync system time with chrony
/// dinitctl start chrony
pub fn dinit_sync_time_chrony() -> Result<(), ExecCommandError> {
exec_command("dinitctl", &vec!["start", "chrony"])
}
/// Install base system
/// basestrap /mnt base base-devel dinit elogind-dinit
pub fn basestrap_dinit(root_dir: &str) -> Result<(), ExecCommandError> {
exec_command(
"basestrap",
&vec![root_dir, "base", "base-devel", "dinit", "elogind-dinit"],
)
}
/// Executes parted command with given arguments
fn exec_parted(args: &Vec<&str>) -> Result<String, ExecCommandError> {
exec_command_out("parted", args)
}
fn parted_get_last_part_num(dev: &BlkDev) -> Result<u32, ExecCommandError> {
// parted -m /dev/loop0 print
let res = exec_parted(&vec!["-m", dev.full_name().as_str(), "print"])?;
match res.lines().last().and_then(|last_line| {
last_line
.split_once(':')
.and_then(|(first_part, _)| u32::from_str_radix(first_part, 10).ok())
}) {
Some(n) => Ok(n),
None => Err("Error parsing parted output".to_string().into()),
}
}
/// init gpt partition table
/// parted --script $install_device mklabel gpt
/// Its blocking function
pub fn parted_init_gpt_part_table(dev: &BlkDev) -> Result<(), ExecCommandError> {
let dev_full_name = dev.full_name();
let args = vec!["--script", &dev_full_name, "mklabel", "gpt"];
exec_command("parted", &args)
}
const LINUX_ROOT_X86_64_TYPE: &str = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709";
/// creating partition from PartInfo specification
/// It assumes that we add partitions sequentially to empty space (for part attributes settings)
/// Otherwise can produce bonkers layout
pub fn parted_mkpart(dev: &BlkDev, part: &PartInfo) -> Result<(), ExecCommandError> {
// important to shift here by 1 because disk index is starting from 0, and part end pointing to last byte of the partition.
let part_end = part.part_end();
// gpt label should be in double qutes
let gpt_label = if let Some(s) = &part.gpt_label {
format!("\"{}\"", s)
} else {
"\"\"".to_string()
};
log::debug!(
"Creatin partition dev:{}, label:{}, fs_type:{}, start:{}, end:{}",
dev.full_name(),
gpt_label,
part.fs_type.to_parted_str(),
part.start.to_parted_bytes_str(),
part_end.to_parted_bytes_str()
);
// creating partition
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"mkpart",
gpt_label.as_str(),
part.fs_type.to_parted_str(),
part.start.to_parted_bytes_str().as_str(),
part_end.to_parted_bytes_str().as_str(),
])?;
// set partition options depending on its role
if let Some(role) = &part.role {
// getting part number of last created partition (hopefully)
let part_id = parted_get_last_part_num(dev)?;
match role {
PartRole::EFI => {
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"set",
&part_id.to_string(),
"esp",
"on",
])?;
}
// parted --script $install_device type 3 4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
PartRole::ROOT => {
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"type",
&part_id.to_string(),
LINUX_ROOT_X86_64_TYPE,
])?;
}
_ => (),
}
};
Ok(())
}
/// creating single drive bcachefs volume
/// compression: lz4, zstd
pub fn bcachefs_format(
dev_name: &str,
compression: Option<String>,
password: Option<String>,
) -> Result<(), ExecCommandError> {
// sudo bcachefs format --compression=lz4 --background_compression=lz4 --encrypted /dev/loop0
// --data_checksum=none - do not make sense on single dev volumes
// sudo bcachefs format --force --compression=lz4 --background_compression=lz4 --encrypted --passphrase_file=./test.pass /dev/loop0
let mut arg = vec!["format", "--force"];
// if compression algorythm specified
let maybe_comp_cmd = compression.and_then(|alg| Some(format!("--compression={}", alg)));
if let Some(comp_cmd) = &maybe_comp_cmd {
arg.push(comp_cmd);
}
// if we want encrypted volume
if let Some(passw) = password {
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &passw)?;
arg.push("--encrypted");
arg.push("--passphrase_file=/tmp/bzzpsspass.txt");
}
// add dev name at the end
arg.push(dev_name);
let out = exec_command("bcachefs", &arg);
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
out
}
/// unlok encrypted bcache volume
pub fn bcachefs_unlock(dev_name: &str, pass: &str) -> Result<(), ExecCommandError> {
// bcachefs unlock --file=/tmp/bzzpsspass.txt /dev/loop0
let arg = vec!["unlock", "--file=/tmp/bzzpsspass.txt ", dev_name];
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &pass)?;
let out = exec_command("bcachefs", &arg);
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
out
}
/// create subvolume
pub fn bcachefs_create_subvolume(vol_path: &str) -> Result<(), ExecCommandError> {
// sudo bcachefs subvolume create /mnt/test/nyan
exec_command("bcachefs", &vec!["subvolume", "create", vol_path])
}
/// mount bcachefs
pub fn bcachefs_mount(dev_name: &str, mount_point: &str) -> Result<(), ExecCommandError> {
// sudo mount -t bcachefs /dev/loop0 /mnt/test
exec_command("mount", &vec!["-t", "bcachefs", dev_name, mount_point])
}
pub fn mount_fs(dev_name: &str, mount_point: &str) -> Result<(), ExecCommandError> {
// sudo mount -t bcachefs /dev/loop0 /mnt/test
exec_command("mount", &vec![dev_name, mount_point])
}
pub fn swapon(dev_name: &str) -> Result<(), ExecCommandError> {
exec_command("swapon", &vec![dev_name])
}
pub fn fatlabel(dev: &str, label: &str) -> Result<(), ExecCommandError> {
//fatlabel /dev/sda1 ESP
exec_command("fatlabel", &vec![dev, label])
}
pub fn mkfs_fat32(dev: &str, maybe_label: Option<&str>) -> Result<(), ExecCommandError> {
//mkfs.fat -F 32 /dev/sda1
exec_command("mkfs.fat", &vec!["-F", "32", dev])?;
// if need to set label
if let Some(label) = maybe_label {
fatlabel(dev, label)
} else {
Ok(())
}
}
/// mkfs.fat -F 32 /dev/sda1
/// fatlabel /dev/sda1 ESP
pub fn mkfs_esp(dev: &str) -> Result<(), ExecCommandError> {
mkfs_fat32(dev, Some("ESP"))
}
pub fn mkswap(dev: &str) -> Result<(), ExecCommandError> {
//mkswap -L SWAP /dev/sda4
exec_command("mkswap", &vec!["-L", "bee-swap", dev])
}
+23 -8
View File
@@ -19,13 +19,14 @@
Modify it to your liking.
*/
use iced::{Color, Theme, color};
use iced::widget::container;
use iced::{Border, Color, Theme, color, Background};
// Function returns theme to be used by installer
pub fn main_theme() -> Theme {
let mut pl = Theme::Dracula.palette();
pl.primary = color!(0xFFD700);
return Theme::custom("Kira Theme", pl);
}
@@ -38,19 +39,33 @@ pub fn change_lightnes(c: &Color, factor: f32) -> Color {
res
}
pub fn text_with_border(_theme: &Theme) -> container::Style {
pub fn dialog_background(_theme: &Theme) -> container::Style {
let mut back = _theme.palette().background.clone();
back.a = 0.8;
container::Style {
border: Border {
color: Color::from_rgb8(120, 98, 10),
width: 2.0,
radius: 7.into(),
},
background: Some(Background::Color(change_lightnes(&_theme.palette().background, 0.3))),
background: Some(iced::Background::Color(back)),
..container::Style::default()
}
}
pub fn warning_dialog(_theme: &Theme) -> container::Style {
container::bordered_box(_theme).background(color!(0xDB5375))
}
// pub fn text_with_border(_theme: &Theme) -> container::Style {
// container::Style {
// border: Border {
// color: Color::from_rgb8(120, 98, 10),
// width: 2.0,
// radius: 7.into(),
// },
// background: Some(Background::Color(change_lightnes(&_theme.palette().background, 0.3))),
// ..container::Style::default()
// }
// }
pub fn get_spiner_bytes() -> Vec<u8>{
return include_bytes!("media/spiner.apng").to_vec();
}
+34 -3
View File
@@ -4,11 +4,14 @@
"button.next": "Next",
"button.back": "Back",
"button.cancel": "Cancel",
"button.finish": "Finish",
"button.exit": "Exit",
"button.yes": "Yes",
"button.no": "No",
"wellcome.text": "Welcome to Kira Installer!",
"wellcome.choose_language": "Please select language to use during istalation!",
"license.license": "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.\n\nThis 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.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.",
"license.accept_text": "By pressing 'Accept' button you agreening to terms described above:",
"license.accept_text": "By pressing 'Accept' button you agreening to terms described below:",
"license.button.accept": "Accept",
"license.button.decline": "Decline",
"network.reuse_checkbox_caption": "Reuse settings?",
@@ -20,6 +23,34 @@
"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": "The system language will be set to",
"locale.select_formats_locale": "The numbers and dates locale will be set to",
"keyboard.select_model": "Select keyboard model",
"keyboard.select_layout": "Select keyboard layout",
"keyboard.select_variant": "Select keyboard variant",
"keyboard.init_error": "Error getting keyboard layouts information from the system!",
"partition.no_swap": "No swap partition",
"partition.swap_no_hibernate": "Swap without hibernate",
"partition.swap_hibernate": "Swap with hibernation",
"partition.swap_custom": "Custom swap size",
"partition.select_device_placeholder": "Select Device",
"partition.select_device": "Select device for system installation",
"partition.select_swapmode_placeholder": "Select Swap Mode",
"partition.select_swapmode": "Select swap partition mode",
"partition.use_zram": "Use zram technology",
"partition.use_secure_boot": "Setup secure boot",
"partition.encrypt_drive": "Encrypt drive",
"partition.current_dev_layout": "Current device layout",
"partition.current_after_install": "After installation device will look like this",
"security.user_name": "User login name",
"security.user_full_name": "User full name/description",
"security.user_password": "User password",
"security.reuse_user_password": "Reuse user password for root user",
"security.root_password": "Root password",
"security.pass_low": "low security",
"security.pass_middle": "normal security",
"security.pass_hight": "hight security",
"install.caption": "Now we will beee install Bzz Linux to your PC",
"install.warning": "We a ready to start installation, this action will destroy all data on select drive!\n\nDo you want to proceed?"
}
+204 -79
View File
@@ -21,6 +21,7 @@
*/
use iced::widget;
use log;
use std::process::ExitCode;
use iced::{Element, Task};
@@ -32,9 +33,16 @@ use crate::stages::welcome;
use crate::stages::welcome::WelcomeStage;
rust_i18n::i18n!("src/locales", fallback = "en");
mod kira_color_bar;
mod kira_disk_layout;
mod kira_system;
mod kira_logger;
mod kira_scroll_list;
mod kira_size;
mod kira_sysinfo;
mod kira_theming;
mod stage;
mod stages;
mod theme;
enum Views {
Start,
@@ -42,6 +50,11 @@ enum Views {
License(license::LicenseStage),
Network(stages::network::NetworkStage),
TimeZone(stages::timezone::TimeZoneStage),
Locale(stages::locale::LocaleStage),
Keyboard(stages::keyboard::KeyboardStage),
Partition(stages::partition::PartitionStage),
Security(stages::security::SecurityStage),
Install(stages::install::InstallStage),
}
enum Message {
@@ -50,56 +63,47 @@ enum Message {
License(license::Message),
Network(stages::network::Message),
TimeZone(stages::timezone::Message),
Locale(stages::locale::Message),
Keyboard(stages::keyboard::Message),
Partition(stages::partition::Message),
Security(stages::security::Message),
Install(stages::install::Message),
}
struct KiraState {
current_view: Views,
toml_config: toml::Table,
config: KiraConfig,
k_config: KiraConfig,
stages_save: Vec<Views>,
}
const CONFIG_PATH_STR: &str = "kira_config.toml";
// const CONFIG_PATH_STR: &str = "kira_config.toml";
fn load_kira_config(config_path: &str) -> Result<toml::Table, Box<dyn std::error::Error>> {
use std::fs;
use toml::Table;
// 1. Read the file content into a String
let content = fs::read_to_string(config_path)?;
// fn load_kira_config(config_path: &str) -> Result<toml::Table, Box<dyn std::error::Error>> {
// use std::fs;
// use toml::Table;
// // 1. Read the file content into a String
// let content = fs::read_to_string(config_path)?;
// 2. Parse the string into a Table
let table: Table = content.parse()?;
// // 2. Parse the string into a Table
// let table: Table = content.parse()?;
println!("{:?}", table);
Ok(table)
}
// log::info!("{:?}", table);
// Ok(table)
// }
impl KiraState {
fn boot() -> (Self, Task<Message>) {
(
Self {
current_view: Views::Start,
toml_config: toml::Table::new(),
config: KiraConfig {
config_trail: Vec::new(),
},
k_config: KiraConfig ::default(),
stages_save: Vec::new(),
},
Task::done(Message::Start),
)
}
}
// impl KiraState {
// fn new(toml_config: toml::Table) -> Self {
// Self {
// current_view: Views::Start,
// toml_config: toml_config,
// config: KiraConfig { config_trail: Vec::new() }
// }
// }
// }
fn view(k_state: &KiraState) -> Element<'_, Message> {
match &k_state.current_view {
Views::Start => Element::new(widget::space()),
@@ -107,45 +111,58 @@ 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),
Views::Keyboard(keyboard_stage) => keyboard_stage.view().map(Message::Keyboard),
Views::Partition(partition_stage) => partition_stage.view().map(Message::Partition),
Views::Security(security_stage) => security_stage.view().map(Message::Security),
Views::Install(install_stage) => install_stage.view().map(Message::Install),
}
}
fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
match message {
Message::Start => match load_kira_config(CONFIG_PATH_STR) {
Ok(conf) => {
println!("Config loaded!");
k_state.toml_config = conf;
k_state.current_view = Views::Welcome(WelcomeStage::new());
Message::Start => {
log::info!("Starting!");
k_state.current_view = Views::Welcome(WelcomeStage::new(k_state.k_config.clone()));
Task::none()
}
Err(ex) => {
println!("Error reading config {}: {}", CONFIG_PATH_STR, ex);
iced::exit()
}
},
Message::Welcome(wlc_msg) => {
if let Views::Welcome(wlc_view) = &mut k_state.current_view {
let action = wlc_view.update(wlc_msg);
if let StageAction::Next(welcome_res) = action {
k_state.config.config_trail.push(welcome_res);
k_state.current_view = Views::License(license::LicenseStage {});
match wlc_view.update(wlc_msg) {
StageAction::Next(welcome_res) => {
k_state.k_config = welcome_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::License(license::LicenseStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Abort => iced::exit(),
_ => Task::none(),
}
} else {
Task::none()
}
Task::none()
}
Message::License(license_message) => {
if let Views::License(license_view) = &mut k_state.current_view {
let action = license_view.update(license_message);
match action {
StageAction::Next(license_res) => {
k_state.config.config_trail.push(license_res);
k_state.current_view =
Views::Network(network::NetworkStage::new(&k_state.toml_config));
k_state.k_config = license_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::Network(network::NetworkStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::done(Message::Network(network::Message::CheckNetwork))
}
StageAction::Abort(_) => iced::exit(),
StageAction::Back => iced::exit(),
StageAction::Abort => iced::exit(),
StageAction::Back => {
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
StageAction::None => Task::none(),
}
} else {
@@ -159,13 +176,16 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
network::UpdateResult::Task(t) => t.map(Message::Network),
network::UpdateResult::StageAction(action) => match action {
StageAction::Next(network_res) => {
k_state.config.config_trail.push(network_res);
k_state.current_view =
Views::TimeZone(stages::timezone::TimeZoneStage::new());
k_state.k_config = network_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::TimeZone(stages::timezone::TimeZoneStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Back => {
k_state.current_view = Views::License(license::LicenseStage {});
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
_ => Task::none(),
@@ -180,15 +200,18 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
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.k_config = tz_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::Locale(stages::locale::LocaleStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Abort(_) => iced::exit(),
StageAction::Abort => iced::exit(),
StageAction::Back => {
k_state.config.config_trail.pop();
k_state.current_view =
Views::Network(network::NetworkStage::new(&k_state.toml_config));
Task::done(Message::Network(network::Message::CheckNetwork))
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
StageAction::None => Task::none(),
}
@@ -196,18 +219,124 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
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.k_config = locale_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::Keyboard(stages::keyboard::KeyboardStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Back => {
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
_ => Task::none(),
}
} else {
Task::none()
}
}
Message::Keyboard(keyboard_msg) => {
if let Views::Keyboard(keyboard_view) = &mut k_state.current_view {
match keyboard_view.update(keyboard_msg) {
StageAction::Next(keyboard_res) => {
k_state.k_config = keyboard_res;
match stages::partition::PartitionStage::new(k_state.k_config.clone()) {
Ok(part_stage) => {
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::Partition(part_stage),
);
k_state.stages_save.push(s_state);
}
Err(ex) => {
log::error!("Error, cannot init partition stage: {}", ex);
}
}
Task::none()
}
StageAction::Back => {
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
_ => Task::none(),
}
} else {
Task::none()
}
}
Message::Partition(partition_msg) => {
if let Views::Partition(partition_view) = &mut k_state.current_view {
match partition_view.update(partition_msg) {
StageAction::Next(partition_res) => {
k_state.k_config = partition_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::Security(stages::security::SecurityStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Back => {
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
_ => Task::none(),
}
} else {
Task::none()
}
}
Message::Security(security_msg) => {
if let Views::Security(security_view) = &mut k_state.current_view {
match security_view.update(security_msg) {
StageAction::Next(security_res) => {
k_state.k_config = security_res;
let s_state = std::mem::replace(
&mut k_state.current_view,
Views::Install(stages::install::InstallStage::new(k_state.k_config.clone())),
);
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Back => {
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
_ => Task::none(),
}
} else {
Task::none()
}
}
Message::Install(install_msg) => {
if let Views::Install(install_view) = &mut k_state.current_view {
let update_result = install_view.update(install_msg);
match update_result {
stages::install::Update::Task(t) => t.map(Message::Install),
stages::install::Update::StgAct(stage_action) => match stage_action {
StageAction::Back => {
k_state.current_view = k_state.stages_save.pop().unwrap();
Task::none()
}
_ => Task::none(),
},
}
} else {
Task::none()
}
}
}
}
// pub fn main_interface() -> iced::Result {
// iced::run(WellcomeStage::update, WellcomeStage::view)
// }
pub fn main() -> ExitCode {
// let app_init = || {
// let mut k_state = KiraState::default()
// k_state.current_view = Views::Welcome(WelcomeStage::new());
// k_stateapp_init
// };
// Init logger
kira_logger::init().unwrap();
let iced_result = iced::application(KiraState::boot, update, view)
.window(iced::window::Settings {
@@ -218,19 +347,15 @@ pub fn main() -> ExitCode {
..Default::default()
})
.centered()
.theme(theme::main_theme())
.theme(kira_theming::main_theme())
.default_font(iced::Font::MONOSPACE)
.run();
match iced_result {
Ok(()) => ExitCode::SUCCESS,
Err(_) => ExitCode::from(42), // Custom error code
Err(ex) => {
log::error!("ICED Error: {}", ex);
ExitCode::from(42)
} // Custom error code
}
}
// fn main() {
// println!("Hello, world!");
// println!("{}", t!("wellcome.text"));
// // Initialize the state
// let _res = main_interface();
// }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

+43 -39
View File
@@ -1,56 +1,60 @@
// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation>
// <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 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 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 file contains basic enums and structs to be used with stages.
*/
This file contains basic enums and structs to be used with stages.
*/
use crate::stages;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum ConfigValue {
String(String),
I64(i64),
U64(u64),
F64(f64),
Bool(bool),
Vector(Vec<ConfigValue>),
Dictionary(HashMap<String, ConfigValue>)
pub struct KiraConfig {
pub res_welcome: Option<stages::welcome::WelcomeStageResult>,
pub res_license: bool,
pub res_network: Option<stages::network::NetworkStageResult>,
pub res_timezone: Option<stages::timezone::TimeZoneStageResult>,
pub res_locale: Option<stages::locale::LocaleStageResult>,
pub res_keyboard: Option<stages::keyboard::KeyboardStageResult>,
pub res_partition: Option<stages::partition::PartitionStageResult>,
pub res_security: Option<stages::security::SecurityStageResult>,
pub res_install: Option<stages::install::InstallStageResult>,
}
#[derive(Debug, Clone)]
pub struct StageResult {
pub name: String,
pub config: Option<HashMap<String, ConfigValue>>,
pub resuts: Option<HashMap<String, ConfigValue>>,
pub error: Option<String>
impl Default for KiraConfig {
fn default() -> Self {
Self {
res_welcome: None,
res_license: false,
res_network: None,
res_timezone: None,
res_locale: None,
res_keyboard: None,
res_partition: None,
res_security: None,
res_install: None,
}
}
}
#[derive(Debug, Clone)]
pub enum StageAction {
Back,
None,
Next(StageResult),
Abort(StageResult),
Next(KiraConfig),
Abort,
}
pub struct KiraConfig {
pub config_trail: Vec<StageResult>,
}
+370
View File
@@ -0,0 +1,370 @@
// <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/>.
/*
Actual installation script.
*/
use std::path::Path;
use crate::{
kira_disk_layout::{self, BlkDev, FSType, PartLayout, PartRole},
kira_size::KiraSize,
kira_system::{self, ExecCommandError},
kira_theming,
stage::{KiraConfig, StageAction},
stages::partition::PartitionStageResult,
};
use iced::{
Alignment, Task,
futures::{self, SinkExt, StreamExt, TryStreamExt, channel::mpsc::SendError},
widget,
};
use iced::{Color, Length, futures::channel::mpsc::Sender, widget::container};
use log;
use rust_i18n::t;
const EFI_BOOT_MOUNT_POINT: &str = "/beefi";
const MNT_DIR: &str = "/mnt/inst_root";
const ROOT_PARTLABEL_DEV: &str = "/dev/disk/by-partlabel/bee-root";
/// Creates new GPT table and partitions from PartLayout
pub fn create_partitions(part_layout: &PartLayout) -> Result<(), ExecCommandError> {
log::info!("Creating GPT part table...");
kira_system::parted_init_gpt_part_table(&part_layout.dev)?;
log::info!("Start to create partitions...");
for part in &part_layout.part_list {
log::info!("Creating partition: {}", part);
kira_system::parted_mkpart(&part_layout.dev, part)?;
}
Ok(())
}
/// inits bcachefs volume for installation to use
/// Formats device to bcachefs
/// Unlock it if encryption used
/// Mount to specified dir
/// creates subvolumes bzz/root bzz/home
pub fn bcachefs_init(
dev_name: &str,
mnt_dir: &str,
password: Option<String>,
) -> Result<(), ExecCommandError> {
// if encryption used - unlock drive
if let Some(pass) = password {
kira_system::bcachefs_unlock(dev_name, pass.as_str())?;
}
//mount drive
kira_system::bcachefs_mount(dev_name, mnt_dir)?;
// Creating bzz-snaps directory for root snapshoots
std::fs::create_dir(format!("{}{}", mnt_dir, "/bzz-snaps"))?;
// Creating boot directory for efi mounting
std::fs::create_dir_all(format!("{}{}", mnt_dir, EFI_BOOT_MOUNT_POINT))?;
// create subvolumes
for vol_name in ["/home", "/usr", "/var", "/var/tmp"] {
let vol_path = format!("{}{}", mnt_dir, vol_name);
kira_system::bcachefs_create_subvolume(vol_path.as_str())?;
}
Ok(())
}
pub fn init_disk(part_layout: &PartLayout, mnt_dir: &str) -> Result<(), ExecCommandError> {
let mnt_path = Path::new(mnt_dir);
log::info!("Cheking is MNT path exists: {}", mnt_dir);
if !mnt_path.exists() {
log::info!("MNT path do not exist, creating it...");
std::fs::create_dir_all(mnt_path)?;
}
// use parted to create partition gpt layout
create_partitions(part_layout)?;
// format partitions
for part in &part_layout.part_list {
match part.role {
Some(PartRole::EFI) => {
if let Some(part_l) = part.partlabel_dev() {
kira_system::mkfs_esp(&part_l)?;
}
}
Some(PartRole::ROOT) => {
if let Some(part_l) = part.partlabel_dev() {
if let FSType::BCACHEFS(bc_par) = &part.fs_type {
log::info!("Formating root pertition.");
kira_system::bcachefs_format(&part_l, bc_par.password.clone(), bc_par.password.clone())?;
// init root
log::info!("Mount and init root partition.");
bcachefs_init(ROOT_PARTLABEL_DEV, mnt_dir, bc_par.password.clone())?;
} else {
return Err(ExecCommandError::Other(
"ROOT partition is not BCACHEFS type!".into(),
));
}
}
}
Some(PartRole::SWAP) => {
if let Some(part_l) = part.partlabel_dev() {
kira_system::mkswap(&part_l)?;
}
}
_ => (),
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallationState {
UserConfirmDialog,
Installing,
Finish,
Error,
}
#[derive(Debug, Clone)]
pub struct InstallStageResult {
pub sucess: Result<(), String>,
}
#[derive(Debug, Clone)]
pub struct InstallStage {
state: InstallationState,
progress: f32,
progress_message: String,
modal_backgroud: Color,
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
pub enum Message {
UserConfirms,
UserDenies,
Sucess,
Cancel,
Finish,
UpdateProgress(Result<(f32, String), String>),
Failure(String),
}
#[derive(Debug)]
pub enum Update {
StgAct(StageAction),
Task(Task<Message>),
}
async fn init_disk_async(part_layout: PartLayout, mnt_dir: String) -> Result<(), ExecCommandError> {
use blocking;
blocking::unblock(move || init_disk(&part_layout, &mnt_dir)).await
}
fn do_install(
k_config: KiraConfig,
) -> impl futures::Stream<Item = Result<(f32, String), ExecCommandError>> {
iced::stream::channel(
100,
|mut output: Sender<Result<(f32, String), ExecCommandError>>| async move {
//sleep(Duration::from_millis(10)).await;
let disk_layout = k_config.res_partition.unwrap().part_layout.clone();
// creating gpt partition table
let res = init_disk_async(disk_layout, MNT_DIR.into()).await;
let res_err = res.is_err();
output
.send(res.map(|_| (1.1f32, "Formatting bcachefs".into())))
.await
.unwrap();
if res_err {
return;
}
// Check if send fails (receiver dropped/cancelled)
// if output.send(res).await.is_err() {
// return; // Exit early if UI is gone
// }
// Optional: Send completion or error state here
// output.send(Message::WorkFailed).await.ok();
},
)
}
fn check_kconfig(k_config: &KiraConfig) -> Result<(), String> {
if k_config.res_welcome.is_none(){ Err("Missing wellcome stage result!".into())}
else if k_config.res_license == false { Err("License is NOT accepted!".into())}
else if k_config.res_locale.is_none(){ Err("Missing locale stage result!".into())}
else if k_config.res_network.is_none(){ Err("Missing network stage result!".into())}
else if k_config.res_keyboard.is_none(){ Err("Missing keyboard stage result!".into())}
else if k_config.res_timezone.is_none(){ Err("Missing timezone stage result!".into())}
else if k_config.res_partition.is_none(){ Err("Missing partition stage result!".into())}
else if k_config.res_security.is_none(){ Err("Missing security stage result!".into())}
else {
Ok(())
}
}
impl InstallStage {
pub fn new(k_config: KiraConfig) -> Self {
let mut modal_backgroud = kira_theming::main_theme().palette().background;
modal_backgroud.a = 0.5;
Self {
modal_backgroud: modal_backgroud,
state: InstallationState::UserConfirmDialog,
progress: 0.0,
progress_message: String::new(),
k_config: k_config,
}
}
fn gen_result(&mut self) -> KiraConfig {
self.k_config.res_install = Some(InstallStageResult { sucess: Ok(()) });
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> Update {
match message {
Message::UserDenies => Update::StgAct(StageAction::Back),
Message::UserConfirms => {
log::info!("Cheking config...");
match check_kconfig(&self.k_config) {
Ok(()) => log::info!("Config looks good."),
Err(ex) => {
log::error!("{}", ex);
self.state = InstallationState::Error;
return Update::StgAct(StageAction::None);
}
}
// starting installation process here
log::info!("Starting disk partitioning.");
self.state = InstallationState::Installing;
Update::Task(Task::run(
do_install(self.k_config.clone()).map_err(|ex| ex.to_string()),
Message::UpdateProgress,
))
//Update::Task(Task::perform(, Message::PartitioningFinish))
}
Message::UpdateProgress(maybe_msg) => {
match maybe_msg {
Ok((progress, msg)) => {
self.progress = progress;
self.progress_message = msg;
Update::StgAct(StageAction::None)
}
Err(ex) => {
log::error!("{}", &ex);
self.state = InstallationState::Error;
Update::StgAct(StageAction::None)
}
}
//Update::StgAct(StageAction::None)
}
Message::Failure(ex) => {
self.state = InstallationState::Error;
Update::StgAct(StageAction::Abort)
}
Message::Cancel => Update::StgAct(StageAction::Abort),
Message::Sucess => Update::StgAct(StageAction::None),
Message::Finish => Update::StgAct(StageAction::None),
}
}
pub fn view(&self) -> iced::Element<'_, Message> {
let action_button = if self.state == InstallationState::Finish {
widget::button(widget::text(t!("button.finish"))).on_press(Message::Finish)
} else {
widget::button(widget::text(t!("button.cancel"))).on_press(Message::Cancel)
};
// Embed the image bytes into the executable
let logo_handle = widget::image::Handle::from_bytes(kira_theming::get_logo_bytes());
let main_content = widget::column![
widget::container(
widget::column![
widget::image(logo_handle)
.width(iced::Pixels(128.0))
.height(iced::Pixels(128.0)),
widget::text(t!("install.caption")),
widget::progress_bar(0.0..=100.0, self.progress),
widget::text(self.progress_message.clone()),
]
.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::container(action_button)
.width(iced::Length::Fill)
.align_y(Alignment::End)
.padding(10),
]
.align_x(Alignment::Center)
.spacing(10);
let page_stack = widget::Stack::with_capacity(2).push(main_content);
if self.state == InstallationState::UserConfirmDialog {
page_stack
.push(widget::opaque(
container(
container(
widget::column![
widget::text(t!("install.warning")).align_x(Alignment::Center),
widget::row![
widget::button(widget::text(t!("button.no")))
.on_press(Message::UserDenies),
widget::space::horizontal(), // Pushes the right button to the far right
widget::button(widget::text(t!("button.yes")))
.on_press(Message::UserConfirms)
]
.padding(10)
]
.spacing(10)
.padding(10)
.width(512)
.align_x(Alignment::Center),
)
.style(kira_theming::warning_dialog),
)
.style(container::bordered_box)
.style(kira_theming::dialog_background)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill),
))
.into()
} else {
page_stack.into()
}
}
}
+404
View File
@@ -0,0 +1,404 @@
// <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 Keyboar stage, used to select Keyboard Layouts
*/
use crate::kira_scroll_list::{self, ListViewMessage};
use crate::stage::{KiraConfig, StageAction};
use iced::{Alignment, Length, widget};
use log;
use rust_i18n::t;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Layout {
layout: String,
description: String,
}
impl std::fmt::Display for Layout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", &self.layout, &self.description)
}
}
impl Layout {
pub fn from_str_touple(v: &(&str, &str)) -> Self {
Self {
layout: v.0.trim().to_string(),
description: v.1.trim().to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Variant {
variant: String,
description: String,
}
impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", &self.variant, &self.description)
}
}
impl Variant {
pub fn from_str_touple(v: &(&str, &str)) -> Self {
Self {
variant: v.0.trim().to_string(),
description: v.1.trim().to_string(),
}
}
}
impl Default for Variant {
fn default() -> Self {
Self {
variant: "default".into(),
description: "Default variant".into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyboarLayout {
layout: Layout,
variant: Variant,
}
impl KeyboarLayout {
pub fn to_result(&self) -> String {
format!("{}||{}", self.layout.layout, self.variant.variant)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyboarModel {
model: String,
description: String,
}
impl std::fmt::Display for KeyboarModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", &self.model, &self.description)
}
}
impl KeyboarModel {
pub fn from_str_touple(v: &(&str, &str)) -> Self {
Self {
model: v.0.trim().to_string(),
description: v.1.trim().to_string(),
}
}
}
impl std::fmt::Display for KeyboarLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", &self.layout.layout, &self.variant.variant)
}
}
#[derive(Debug, Clone)]
pub struct KeyboardStage {
models: Vec<KeyboarModel>,
model: Option<KeyboarModel>,
layouts: Vec<Layout>,
layout: Option<usize>,
variants_map: HashMap<String, Vec<Variant>>,
variants: Vec<Variant>,
variant: Option<usize>,
keyboard_layout: Option<KeyboarLayout>,
// selected_kbd_layout_id: Option<usize>,
k_config: KiraConfig,
}
#[derive(Debug, Clone, PartialEq)]
pub struct KeyboardStageResult {
pub keyboard_model: KeyboarModel,
pub keyboard_layout: KeyboarLayout,
}
#[derive(Debug, Clone)]
pub enum Message {
SelectKeyboardModel(KeyboarModel),
SelectLayout(kira_scroll_list::ListViewMessage),
SelectVariant(kira_scroll_list::ListViewMessage),
// AddLayout(usize),
// RemoveLayout(usize),
// SelectAddedLayout(kira_scroll_list::ListViewMessage),
Next,
Back,
}
const XKB_RULES_FILE_NAME: &str = "/usr/share/X11/xkb/rules/evdev.lst";
/// Getting list of awailable keyboard layouts from from xkb/rules/evdev.lst file.
fn get_layouts_blocking() -> std::io::Result<(
Vec<KeyboarModel>,
Vec<Layout>,
HashMap<String, Vec<Variant>>,
)> {
use std::fs::File;
use std::io::{BufReader, Read};
let mut file_content = String::new();
{
let locgen_file = File::open(XKB_RULES_FILE_NAME)?;
let mut reader = BufReader::new(locgen_file);
reader.read_to_string(&mut file_content)?;
}
let raw_sections: Vec<String> = file_content.split("! ").map(str::to_string).collect();
// Parsing models data
let models: Vec<KeyboarModel> = raw_sections
.iter()
.find(|s| s.starts_with("model"))
.and_then(|s| {
Some(
s.lines()
.skip(1)
.filter_map(|s| s.trim().split_once(char::is_whitespace))
.map(|s_spl| KeyboarModel::from_str_touple(&s_spl))
.collect(),
)
})
.unwrap_or_else(Vec::new);
// Parsing layots data
let layouts: Vec<Layout> = raw_sections
.iter()
.find(|s| s.starts_with("layout"))
.and_then(|s| {
Some(
s.lines()
.skip(1)
.filter_map(|s| s.trim().split_once(char::is_whitespace))
.map(|s_spl| Layout::from_str_touple(&s_spl))
.collect(),
)
})
.unwrap_or_else(Vec::new);
// parsing variants data to HashMap
let mut variants: HashMap<String, Vec<Variant>> = layouts
.iter()
.map(|l| (l.layout.clone(), vec![Variant::default()]))
.collect();
raw_sections
.iter()
.find(|s| s.starts_with("variant"))
.and_then(|s| {
Some(
s.lines()
.skip(1)
.filter_map(|s| s.trim().split_once(char::is_whitespace))
.filter_map(|(vrnt, l_plus_descr)| {
l_plus_descr
.trim()
.split_once(": ")
.and_then(|(loc, descr)| {
Some((loc.trim(), Variant::from_str_touple(&(vrnt, descr))))
})
})
.for_each(|v| {
let maybe_key = variants
.get_mut(v.0)
.and_then(|vrn_vec| Some(vrn_vec.push(v.1.clone())));
if maybe_key.is_none() {
variants.insert(v.0.to_string(), vec![v.1.clone()]);
}
}),
)
});
Ok((models, layouts, variants))
}
impl KeyboardStage {
pub fn new(k_config: KiraConfig) -> Self {
match get_layouts_blocking() {
Ok((models, layouts, variants_map)) => {
let def_layout = layouts
.iter()
.enumerate()
.find_map(|(idx, l)| (l.layout == "us").then_some(idx));
let variants = variants_map["us"].clone();
let def_model = models[0].clone();
let keyboard_layout = def_layout.and_then(|li_d| {
Some(KeyboarLayout {
layout: layouts[li_d].clone(),
variant: variants[0].clone(),
})
});
Self {
layouts: layouts,
variants_map: variants_map,
variants: variants,
model: Some(def_model),
models: models,
layout: def_layout,
variant: Some(0),
keyboard_layout: keyboard_layout,
k_config: k_config,
}
}
Err(ex) => {
log::error!(
"Exception while trying to get kayboard layouts data from system {}",
ex
);
Self {
layouts: Vec::new(),
variants_map: HashMap::new(),
variants: Vec::new(),
model: None,
models: Vec::new(),
layout: None,
variant: None,
keyboard_layout: None,
k_config: k_config,
}
}
}
}
fn gen_result(&mut self) -> KiraConfig {
self.k_config.res_keyboard = Some(KeyboardStageResult {
keyboard_model: self.model.clone().expect("Keyboard model must be choosed!"),
keyboard_layout: self
.keyboard_layout
.clone()
.expect("Keyboard layout must be choosed!"),
});
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> StageAction {
match message {
Message::SelectLayout(ListViewMessage::Select(l_id)) => {
if let Some(current_l_id) = self.layout
&& current_l_id != l_id
{
self.layout = Some(l_id);
self.variants = self.variants_map[self.layouts[l_id].layout.as_str()].clone();
self.variant = Some(0);
self.keyboard_layout = Some(KeyboarLayout {
layout: self.layouts[l_id].clone(),
variant: self.variants[0].clone(),
});
}
StageAction::None
}
Message::SelectVariant(ListViewMessage::Select(v_id)) => {
self.variant = Some(v_id);
self.keyboard_layout = Some(KeyboarLayout {
layout: self.layouts[self.layout.unwrap()].clone(),
variant: self.variants[v_id].clone(),
});
StageAction::None
}
Message::SelectKeyboardModel(k_m) => {
self.model = Some(k_m);
StageAction::None
}
Message::Next => StageAction::Next(self.gen_result()),
Message::Back => StageAction::Back,
}
}
pub fn view(&self) -> iced::Element<'_, Message> {
let next_button = if self.model.is_some() && self.keyboard_layout.is_some() {
widget::button(widget::text(t!("button.next"))).on_press(Message::Next)
} else {
widget::button(widget::text(t!("button.next")))
};
let main_content = if self.layouts.len() > 0 {
widget::container(
widget::column![
widget::column![
widget::text(t!("keyboard.select_model")),
widget::pick_list(
self.models.clone(),
self.model.clone(),
Message::SelectKeyboardModel,
)
],
widget::row![
widget::column![
widget::text(t!("keyboard.select_layout")),
crate::kira_scroll_list::list_view(
&self.layouts,
self.layout,
Length::Shrink
)
.map(Message::SelectLayout)
],
widget::column![
widget::text(t!("keyboard.select_variant")),
crate::kira_scroll_list::list_view(
&self.variants,
self.variant,
Length::FillPortion(1)
)
.map(Message::SelectVariant)
],
]
.spacing(10)
.height(Length::Shrink),
]
.spacing(10)
.align_x(Alignment::Center),
)
} else {
widget::container(widget::text(t!("keyboard.init_error")))
};
let back_button = widget::button(widget::text(t!("button.back"))).on_press(Message::Back);
widget::column![
main_content
.padding(iced::Padding {
top: 10.0,
right: 10.0,
bottom: 0.0,
left: 10.0,
})
.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()
}
}
+41 -43
View File
@@ -1,36 +1,34 @@
// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation>
// <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 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 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 License stage. Used to inform user about program legal status.
*/
This is License stage. Used to inform user about program legal status.
*/
use iced::{Alignment, widget};
use rust_i18n::t;
use std::collections::HashMap;
use crate::stage;
use crate::stage::StageResult;
use crate::stage::KiraConfig;
#[derive(Debug, Clone)]
pub struct LicenseStage {}
pub struct LicenseStage {
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
pub enum Message {
@@ -46,28 +44,20 @@ pub enum Message {
//const LICENSE_TEXT: &str = include_str!("gpl-3.0.txt");
impl LicenseStage {
fn accepted() -> StageResult {
StageResult {
name: "license".into(),
config: Some(HashMap::from([(
"accepted".to_string(),
stage::ConfigValue::Bool(true),
)])),
resuts: None,
error: None,
}
pub fn new(k_config: KiraConfig) -> Self {
Self { k_config: k_config }
}
fn accepted(&mut self) -> KiraConfig {
self.k_config.res_license = true;
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> stage::StageAction {
match message {
Message::Accept => stage::StageAction::Next(Self::accepted()),
Message::Accept => stage::StageAction::Next(self.accepted()),
Message::Back => stage::StageAction::Back,
Message::Decline => stage::StageAction::Abort(StageResult {
name: "license".into(),
config: None,
resuts: None,
error: Some("Declined.".to_string()),
}),
Message::Decline => stage::StageAction::Abort,
}
}
@@ -79,12 +69,20 @@ impl LicenseStage {
widget::button(widget::text(t!("license.button.decline"))).on_press(Message::Decline);
widget::column![
widget::container(widget::column![
widget::text(t!("license.accept_text")),
widget::container(widget::text(t!("license.license")))
.padding(10)
.style(widget::container::bordered_box)
].align_x(Alignment::Center).padding(20).spacing(10))
widget::container(
widget::column![
widget::text(t!("license.accept_text")).font(iced::Font {
weight: iced::font::Weight::Bold,
..iced::Font::DEFAULT
}),
widget::container(widget::text(t!("license.license")))
.padding(10)
.style(widget::container::bordered_box)
]
.align_x(Alignment::Center)
.padding(20)
.spacing(15)
)
.height(iced::Length::Fill)
.width(iced::Length::Fill)
.align_x(Alignment::Center)
+316
View File
@@ -0,0 +1,316 @@
// <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::{KiraConfig, StageAction},
};
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.code, self.description)
}
}
impl LocaleData {
pub fn default() -> Self {
Self {
code: "C.UTF-8".into(),
description: "C locale".into(),
}
}
}
/// gets locales list from system "/etc/locale.gen" file
/// Filters out non UTF-8 locales
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)
}
/// Gets locales description from "/usr/share/i18n/locales/" directory.
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>,
formats_locale: Option<LocaleData>,
locales: Vec<LocaleData>,
lang_locale_text: String,
all_locale_text: String,
k_config: KiraConfig,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LocaleStageResult {
pub lang_locale: LocaleData,
pub formats_locale: LocaleData,
}
#[derive(Debug, Clone)]
pub enum Message {
SelectLangLocale(LocaleData),
SelectFormatsLocale(LocaleData),
Next,
Back,
}
impl LocaleStage {
pub fn new(k_config: KiraConfig) -> Self {
// get seelctet language code from welcome stage
let maybe_lang = k_config
.res_welcome
.as_ref()
.and_then(|rw| Some(rw.loc_code.clone()));
let raw_loc_codes = get_locales_codes_list_blocking().unwrap();
let raw_loc_descr = get_locales_description_blocking().unwrap();
let locales: Vec<LocaleData> = raw_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();
// if we get lang code from wellcome stage, try to fing match with system locales
let lang_match = if let Some(lang) = maybe_lang
&& lang != "en"
{
let lang = lang.replace("-", "_").to_lowercase();
locales
.iter()
.find(|l| l.code.to_lowercase().starts_with(&lang))
.cloned()
.unwrap_or_else(LocaleData::default)
} else {
LocaleData::default()
};
let lang_locale_text = format!(
"{}: {}",
t!("locale.select_language_locale"),
lang_match.code
);
let all_locale_text = format!(
"{}: {}",
t!("locale.select_formats_locale"),
lang_match.code
);
Self {
lang_locale: Some(lang_match.clone()),
formats_locale: Some(lang_match),
locales: locales,
lang_locale_text: lang_locale_text,
all_locale_text: all_locale_text,
k_config: k_config,
}
}
fn gen_result(&mut self) -> KiraConfig {
self.k_config.res_locale = if let Some(lang_locale) = &self.lang_locale
&& let Some(formats_locale) = &self.formats_locale
{
Some(LocaleStageResult {
lang_locale: lang_locale.clone(),
formats_locale: formats_locale.clone(),
})
} else {
let loc = LocaleData::default();
Some(LocaleStageResult {
lang_locale: loc.clone(),
formats_locale: loc,
})
};
self.k_config.clone()
}
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::SelectFormatsLocale(loc) => {
self.all_locale_text =
format!("{}: {}", t!("locale.select_formats_locale"), loc.code);
self.formats_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::container(
widget::column![
widget::text(self.lang_locale_text.as_str()),
widget::pick_list(
self.locales.clone(),
self.lang_locale.clone(),
Message::SelectLangLocale,
)
]
.spacing(5)
)
.style(widget::container::bordered_box)
.padding(5),
widget::container(
widget::column![
widget::text(self.all_locale_text.as_str()),
widget::pick_list(
self.locales.clone(),
self.formats_locale.clone(),
Message::SelectFormatsLocale,
)
]
.spacing(5)
)
.style(widget::container::bordered_box)
.padding(5)
]
.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()
}
}
+6 -1
View File
@@ -18,4 +18,9 @@
pub mod welcome;
pub mod license;
pub mod network;
pub mod timezone;
pub mod timezone;
pub mod locale;
pub mod keyboard;
pub mod partition;
pub mod security;
pub mod install;
+43 -50
View File
@@ -21,16 +21,14 @@
use iced::{Alignment, Task, widget};
use iced_moving_picture::widget::apng;
use log;
use rust_i18n::t;
use std::collections::HashMap;
use toml::Table;
use crate::stage;
use crate::stage::StageResult;
use crate::stage::{self, KiraConfig};
#[derive(Debug, Clone)]
const REQIRE_NETWORK: bool = false;
#[derive(PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum State {
Cheking,
Normal,
@@ -43,14 +41,21 @@ pub enum CheckResult {
CheckError,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NetworkStageResult {
pub internet_active: bool,
pub use_net_settings: bool,
}
#[derive(Debug, Clone)]
pub struct NetworkStage {
internet_active: bool,
state: State,
use_net_settings: bool,
reqire_network: bool,
spinner_frames: apng::Frames,
logo_handle: widget::image::Handle,
status_message: String,
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
@@ -82,7 +87,7 @@ fn check_connection_blocking() -> CheckResult {
}
}
Err(ex) => {
println!("Exception while trying to chen net connectivity: {}", ex);
log::error!("Exception while trying to chen net connectivity: {}", ex);
CheckResult::CheckError
}
}
@@ -95,50 +100,30 @@ async fn check_connected() -> CheckResult {
}
impl NetworkStage {
pub fn new(toml_config: &Table) -> Self {
let mut reqire_network = false;
if let Some(reqire_network_conf) = toml_config
.get("network")
.and_then(|v| v.as_table())
.and_then(|v| v.get("reqire_network"))
.and_then(|v| v.as_bool())
{
println!("Config value reqire_network read {}", reqire_network_conf);
reqire_network = reqire_network_conf;
} else {
println!("Error parsing network config. Using default values.");
}
pub fn new(k_config: KiraConfig) -> Self {
let spinner_frames =
apng::Frames::from_bytes(crate::theme::get_spiner_bytes()).unwrap();
apng::Frames::from_bytes(crate::kira_theming::get_spiner_bytes()).unwrap();
let logo_handle = widget::image::Handle::from_bytes(crate::kira_theming::get_logo_bytes());
Self {
logo_handle: logo_handle,
internet_active: false,
state: State::Cheking,
use_net_settings: false,
reqire_network: reqire_network,
use_net_settings: true,
spinner_frames: spinner_frames,
status_message: t!("network.checking").into(),
k_config: k_config,
}
}
fn gen_result(&self) -> StageResult {
StageResult {
name: "network".into(),
config: Some(HashMap::from([
(
"internet_active".to_string(),
stage::ConfigValue::Bool(self.internet_active),
),
(
"use_net_settings".to_string(),
stage::ConfigValue::Bool(self.use_net_settings),
),
])),
resuts: None,
error: None,
}
fn gen_result(&self) -> KiraConfig {
let mut k_config = self.k_config.clone();
k_config.res_network = Some(NetworkStageResult {
internet_active: self.internet_active,
use_net_settings: self.use_net_settings,
});
k_config
}
pub fn update(&mut self, message: Message) -> UpdateResult {
@@ -178,12 +163,12 @@ impl NetworkStage {
}
pub fn view(&self) -> iced::Element<'_, Message> {
let next_button = if (self.state == State::Cheking) || (!self.internet_active && self.reqire_network) {
widget::button(widget::text(t!("button.next")))
}
else {
widget::button(widget::text(t!("button.next"))).on_press(Message::Next)
};
let next_button =
if (self.state == State::Cheking) || (!self.internet_active && REQIRE_NETWORK) {
widget::button(widget::text(t!("button.next")))
} else {
widget::button(widget::text(t!("button.next"))).on_press(Message::Next)
};
let (back_button, check_button, reuse_checkbox) = match self.state {
State::Cheking => (
@@ -202,8 +187,16 @@ impl NetworkStage {
};
let spinner_container = match self.state {
State::Cheking => widget::container(apng(&self.spinner_frames)),
State::Normal => widget::container(widget::space().height(128).width(128)),
State::Cheking => widget::container(
apng(&self.spinner_frames)
.width(iced::Pixels(128.0).into())
.height(iced::Pixels(128.0).into()),
),
State::Normal => widget::container(
widget::image(&self.logo_handle)
.width(iced::Pixels(128.0))
.height(iced::Pixels(128.0)),
),
};
let info_column = widget::column![
+440
View File
@@ -0,0 +1,440 @@
// <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 partition stage!
*/
use std::char;
use crate::kira_color_bar;
use crate::kira_disk_layout::{BlkDev, FSType, PartInfo, PartLayout};
use crate::kira_size::KiraSize;
use crate::kira_sysinfo;
use crate::stage::{KiraConfig, StageAction};
use iced::{Alignment, Color, Element, Length, widget};
use log;
use rust_i18n::t;
const MIN_DISK_SIZE_MB: u64 = 8192;
const EFI_SIZE_MB: u64 = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwapMode {
NoSwap,
SwapNoHibernate,
SwapHibernate,
SwapCustom,
}
impl std::fmt::Display for SwapMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoSwap => f.write_str(t!("partition.no_swap").as_ref()),
Self::SwapNoHibernate => f.write_str(t!("partition.swap_no_hibernate").as_ref()),
Self::SwapHibernate => f.write_str(t!("partition.swap_hibernate").as_ref()),
Self::SwapCustom => f.write_str(t!("partition.swap_custom").as_ref()),
}
}
}
const SWAP_MODES: [SwapMode; 4] = [
SwapMode::NoSwap,
SwapMode::SwapNoHibernate,
SwapMode::SwapHibernate,
SwapMode::SwapCustom,
];
fn part_type_to_color(t: &FSType) -> Color {
match t {
FSType::VFAT => Color::from_rgb8(204, 121, 167),
FSType::XFS => Color::from_rgb8(0, 158, 115),
FSType::LUKS => Color::from_rgb8(240, 228, 66),
FSType::SWAP => Color::from_rgb8(213, 94, 0),
FSType::EXT4 => Color::from_rgb8(0, 114, 178),
FSType::BTRFS => Color::from_rgb8(86, 180, 233),
_ => Color::from_rgb8(230, 159, 0),
}
}
/// Converts PartInfo into data for ColorBar widget
///
fn part_to_ct_params(part: &PartInfo, dev_size: KiraSize) -> (String, u16, Color) {
let fill_ratio: u16 = ((part.size.b() as f32 / dev_size.b() as f32) * 11.0) as u16;
let fill_ratio = fill_ratio.max(1);
let part_color = part_type_to_color(&part.fs_type);
(part.to_string(), fill_ratio, part_color)
}
#[derive(Debug, Clone, PartialEq)]
pub struct PartitionStageResult {
pub swap_mode: SwapMode,
pub swap_size: KiraSize,
pub efi_size: KiraSize,
pub use_zram: bool,
pub secure_boot: bool,
pub encrypt_drive: bool,
pub password: Option<String>,
pub part_layout: PartLayout,
}
#[derive(Debug, Clone)]
pub struct PartitionStage {
device: Option<BlkDev>,
devices: Vec<BlkDev>,
swap_mode: Option<SwapMode>,
efi_size: KiraSize,
use_zram: bool,
secure_boot: bool,
encrypt_drive: bool,
secure_boot_in_setup_mode: bool,
custom_swap_size_mb: u32,
custom_swap_size_str: String,
selected_disk_parts: Option<Vec<(String, u16, Color)>>,
generated_disk_parts: Option<Vec<(String, u16, Color)>>,
generated_part_layout: Option<PartLayout>,
system_ram_size: KiraSize,
vram_of_all_gpus_size: KiraSize,
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
pub enum Message {
SelectDevice(BlkDev),
SelectSwapMode(SwapMode),
ToggleZram(bool),
ToggleSecureBoot(bool),
ToggleEncrypDrive(bool),
SwapSizeFieldChange(String),
SwapSizeIncrement,
SwapSizeDecrement,
Next,
Back,
}
impl PartitionStage {
pub fn new(k_config: KiraConfig) -> Result<Self, String> {
let maybe_dev_list = BlkDev::list_sys_blk_dev();
log::info!("hiii from partition");
let min_disk_size: KiraSize = KiraSize::new_mb(MIN_DISK_SIZE_MB);
let efi_size: KiraSize = KiraSize::new_mb(EFI_SIZE_MB);
let sec_boot_setup = match kira_sysinfo::get_secure_boot_status() {
Ok(sb_status) => sb_status.setup_mode,
Err(ex) => {
log::error!("Unable to get secure boot status! {}", ex);
false
}
};
match maybe_dev_list {
Ok(dev_list) => {
let acepted_dev: Vec<BlkDev> = dev_list
.iter()
.filter(|v| v.size.b() >= min_disk_size.b())
.cloned()
.collect();
// getting system memory info
let system_ram_size = KiraSize::new_kb(kira_sysinfo::get_meminfo()["MemTotal"]); // from kB to bytes
let vram_of_all_gpus_size = KiraSize::new_b(
kira_sysinfo::get_gpus_list()
.iter()
.fold(0u64, |acc, gpi| acc + gpi.vis_vram_total),
);
log::debug!(
"stage init ram {} vram {}",
system_ram_size,
vram_of_all_gpus_size
);
Ok(Self {
device: None,
devices: acepted_dev,
swap_mode: Some(SwapMode::SwapNoHibernate),
use_zram: true,
secure_boot: false,
encrypt_drive: false,
secure_boot_in_setup_mode: sec_boot_setup,
custom_swap_size_mb: 1024,
custom_swap_size_str: "1024MB".into(),
efi_size: efi_size,
selected_disk_parts: None,
generated_disk_parts: None,
generated_part_layout: None,
system_ram_size: system_ram_size,
vram_of_all_gpus_size: vram_of_all_gpus_size,
k_config: k_config,
})
}
Err(ex) => Err(ex),
}
}
fn get_swap_size(&self) -> KiraSize {
if let Some(s_mode) = self.swap_mode
&& s_mode == SwapMode::SwapCustom
{
return KiraSize::new_mb(self.custom_swap_size_mb as u64);
}
let total_ram = self.system_ram_size + self.vram_of_all_gpus_size;
let sys_ram_gb = self.system_ram_size.gb();
log::debug!("sys_ram_gb {}", sys_ram_gb);
let mut disk_swap_gb = if sys_ram_gb >= 32 {
1u64
} else if sys_ram_gb >= 24 {
2u64
} else if sys_ram_gb >= 16 {
sys_ram_gb / 2
} else if sys_ram_gb >= 8 {
sys_ram_gb
} else {
sys_ram_gb * 2
};
if self.use_zram {
disk_swap_gb = disk_swap_gb.saturating_sub(sys_ram_gb).max(2);
}
let disk_swap = KiraSize::new_gb(disk_swap_gb);
match self.swap_mode {
Some(SwapMode::NoSwap) => KiraSize::new_b(0),
Some(SwapMode::SwapHibernate) => disk_swap + total_ram,
Some(SwapMode::SwapNoHibernate) => disk_swap,
_ => KiraSize::new_gb(2),
}
}
fn gen_layout(&mut self) {
if let Some(dev) = self.device.clone() {
let swap_size: KiraSize = self.get_swap_size();
let patr_layout = PartLayout::gen_kira_layout(
dev.clone(),
self.efi_size,
swap_size,
None,
Some("lz4".into()),
);
self.generated_disk_parts = Some(
patr_layout
.part_list
.iter()
.map(|part_info| part_to_ct_params(part_info, dev.size))
.collect(),
);
self.generated_part_layout = Some(patr_layout);
}
}
fn gen_result(&mut self) -> KiraConfig {
let disk_layout = self
.generated_part_layout
.clone()
.expect("Layout must be generated!");
self.k_config.res_partition = Some(PartitionStageResult {
swap_mode: self.swap_mode.clone().unwrap(),
swap_size: self.get_swap_size(),
efi_size: self.efi_size,
use_zram: self.use_zram,
secure_boot: self.secure_boot,
encrypt_drive: true,
password: Some("123456".to_string()),
part_layout: disk_layout,
});
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> StageAction {
match message {
Message::SelectDevice(dev) => {
self.selected_disk_parts = match PartLayout::read_from_disk(&dev) {
Ok(dev_parts) => Some(
dev_parts
.part_list
.iter()
.map(|part_inf| part_to_ct_params(&part_inf, dev.size))
.collect(),
),
Err(ex) => {
log::error!("Error getting dev partitions info: {}", ex);
None
}
};
// let swap_size: KiraSize = self.get_swap_size();
// self.generated_disk_parts = Some(
// PartLayout::gen_kira_layout(dev.clone(), self.efi_size, swap_size)
// .part_list
// .iter()
// .map(|part_info| part_to_ct_params(part_info, dev.size))
// .collect(),
// );
self.device = Some(dev);
self.gen_layout();
StageAction::None
}
Message::SelectSwapMode(sm) => {
self.swap_mode = Some(sm);
self.gen_layout();
StageAction::None
}
Message::ToggleSecureBoot(t) => {
self.secure_boot = t;
StageAction::None
}
Message::ToggleEncrypDrive(t) => {
self.encrypt_drive = t;
StageAction::None
}
Message::ToggleZram(t) => {
self.use_zram = t;
self.gen_layout();
StageAction::None
}
Message::SwapSizeIncrement => {
self.custom_swap_size_mb = (self.custom_swap_size_mb / 1024u32)
.strict_mul(1024u32)
.saturating_add(1024u32);
self.custom_swap_size_str = format!("{}MB", self.custom_swap_size_mb);
self.gen_layout();
StageAction::None
}
Message::SwapSizeDecrement => {
self.custom_swap_size_mb = (self.custom_swap_size_mb / 1024u32)
.strict_mul(1024u32)
.saturating_sub(1024u32);
self.custom_swap_size_str = format!("{}MB", self.custom_swap_size_mb);
self.gen_layout();
StageAction::None
}
Message::SwapSizeFieldChange(f_v) => {
let mut only_num = f_v.trim().to_string();
only_num.retain(char::is_numeric);
self.custom_swap_size_mb = u32::from_str_radix(&only_num, 10).unwrap_or(1024);
self.custom_swap_size_str = format!("{}MB", self.custom_swap_size_mb);
self.gen_layout();
StageAction::None
}
Message::Back => StageAction::Back,
Message::Next => StageAction::Next(self.gen_result()),
}
}
pub fn view(&self) -> iced::Element<'_, Message> {
let swap_size_select: Element<'_, _> = match self.swap_mode {
Some(SwapMode::SwapCustom) => widget::row![
widget::text_input("", &self.custom_swap_size_str)
.width(100)
.on_input(Message::SwapSizeFieldChange),
widget::button("+").on_press(Message::SwapSizeIncrement),
widget::button("-").on_press(Message::SwapSizeDecrement),
]
.spacing(2)
.into(),
_ => widget::space().into(),
};
let dev_parts_text = match self.generated_disk_parts {
Some(_) => widget::text(t!("partition.current_after_install")),
None => widget::text(""),
};
let dev_parts_content = kira_color_bar::color_bar(self.generated_disk_parts.clone(), 64);
let dev_parts_content = widget::column![dev_parts_text, dev_parts_content];
let back_button = widget::button(widget::text(t!("button.back"))).on_press(Message::Back);
let next_button = if self.device.is_some() {
widget::button(widget::text(t!("button.next"))).on_press(Message::Next)
} else {
widget::button(widget::text(t!("button.next")))
};
let selected_dev_text = match self.selected_disk_parts {
Some(_) => widget::text(t!("partition.current_dev_layout")),
None => widget::text(""),
};
let selected_dev_content = kira_color_bar::color_bar(self.selected_disk_parts.clone(), 64);
let selected_dev_content = widget::column![selected_dev_text, selected_dev_content];
let use_sec_boot_checkbox = if self.secure_boot_in_setup_mode {
widget::checkbox(self.secure_boot)
.label(t!("partition.use_secure_boot"))
.on_toggle(Message::ToggleSecureBoot)
} else {
widget::checkbox(self.secure_boot).label(t!("partition.use_secure_boot"))
};
widget::column![
widget::container(
widget::column![
widget::rule::horizontal(2),
widget::text(t!("partition.select_device")),
widget::pick_list(
self.devices.clone(),
self.device.clone(),
Message::SelectDevice,
)
.placeholder(t!("partition.select_device_placeholder")),
widget::rule::horizontal(2),
widget::text(t!("partition.select_swapmode")),
widget::row![
widget::pick_list(SWAP_MODES, self.swap_mode, Message::SelectSwapMode,)
.placeholder(t!("partition.select_swapmode_placeholder")),
swap_size_select
]
.spacing(5)
.width(Length::Shrink)
.align_y(Alignment::Center),
widget::checkbox(self.use_zram)
.label(t!("partition.use_zram"))
.on_toggle(Message::ToggleZram),
widget::rule::horizontal(2),
use_sec_boot_checkbox,
widget::rule::horizontal(2),
widget::checkbox(self.encrypt_drive)
.label(t!("partition.encrypt_drive"))
.on_toggle(Message::ToggleEncrypDrive),
widget::rule::horizontal(2),
selected_dev_content,
dev_parts_content,
]
.padding(10)
.spacing(10)
.align_x(Alignment::Start)
)
.height(iced::Length::Fill)
.width(iced::Length::Fill)
.align_x(Alignment::Start)
.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()
}
}
+301
View File
@@ -0,0 +1,301 @@
// <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 Security stage, setup users logins and such
*/
use crate::stage::{KiraConfig, StageAction};
use iced::{Alignment, Color, widget};
use rust_i18n::t;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum PasswordStrenght {
Low,
Middle,
Hight,
}
impl PasswordStrenght {
pub fn from_password(password: &str) -> Self {
if password.len() < 8 {
Self::Low
} else if password.len() < 16 {
Self::Middle
} else {
Self::Hight
}
}
pub fn color(&self) -> Color {
match self {
Self::Low => Color::from_rgb8(212, 32, 32),
Self::Middle => Color::from_rgb8(212, 212, 32),
Self::Hight => Color::from_rgb8(32, 212, 32),
}
}
}
impl std::fmt::Display for PasswordStrenght {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Low => f.write_str(t!("security.pass_low").as_ref()),
Self::Middle => f.write_str(t!("security.pass_middle").as_ref()),
Self::Hight => f.write_str(t!("security.pass_hight").as_ref()),
}
}
}
#[derive(Debug, Clone)]
pub struct SecurityStage {
user_name: String,
user_full_name: String,
user_password: String,
root_password: String,
reuse_user_password_for_roor: bool,
user_pass_is_secure: bool,
root_pass_is_secure: bool,
user_password_strenght: PasswordStrenght,
root_password_strenght: PasswordStrenght,
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
pub struct SecurityStageResult {
pub user_name: String,
pub user_full_name: String,
pub user_password: String,
pub root_password: String,
}
#[derive(Debug, Clone)]
pub enum Message {
ToggleReusePass(bool),
UserNameChange(String),
UserFullNameChange(String),
UserPasswordChange(String),
RootPasswordChange(String),
ToggleUserPassSecure,
ToggleRootPassSecure,
Next,
Back,
}
/// Mothod safely retain first n chars in utf-8 string avoiding cutting utf-8 char in the middle
fn retain_first_n(s: &str, n: usize) -> &str {
s.char_indices()
.nth(n)
.and_then(|(idx, _)| Some(&s[..idx]))
.unwrap_or(s)
}
impl SecurityStage {
pub fn new(k_config: KiraConfig) -> Self {
Self {
user_name: "".into(),
user_full_name: "".into(),
user_password: "".into(),
root_password: "".into(),
reuse_user_password_for_roor: false,
user_pass_is_secure: true,
root_pass_is_secure: true,
user_password_strenght: PasswordStrenght::Low,
root_password_strenght: PasswordStrenght::Low,
k_config: k_config,
}
}
fn gen_result(&mut self) -> KiraConfig {
self.k_config.res_security = Some(SecurityStageResult {
user_name: self.user_name.clone(),
user_full_name: self.user_full_name.clone(),
user_password: self.user_password.clone(),
root_password: self.root_password.clone(),
});
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> StageAction {
match message {
Message::ToggleReusePass(reuse_pass) => {
self.reuse_user_password_for_roor = reuse_pass;
if reuse_pass {
self.root_password = self.user_password.clone();
}
StageAction::None
}
Message::UserNameChange(u_name) => {
let mut trimmed = u_name
.trim_start_matches(|c: char| !c.is_ascii_alphabetic())
.to_string();
trimmed.retain(|c| c.is_ascii_alphanumeric() || c == '-');
trimmed.truncate(30); // it is safe bacuse we retained only ascii characters
self.user_name = trimmed;
StageAction::None
}
Message::UserFullNameChange(f_name) => {
let mut res = f_name.clone();
res.retain(|c| c.is_alphanumeric() || ['_', '-', ' ', '.', ','].contains(&c));
self.user_full_name = retain_first_n(&res, 128).to_string();
StageAction::None
}
Message::UserPasswordChange(u_pass) => {
let mut res = u_pass;
res.retain(|c: char| !c.is_control());
self.user_password = res;
self.user_password_strenght = PasswordStrenght::from_password(&self.user_password);
if self.reuse_user_password_for_roor {
self.root_password_strenght = self.user_password_strenght;
self.root_password = self.user_password.clone();
}
StageAction::None
}
Message::RootPasswordChange(r_pass) => {
let mut res: String = r_pass;
res.retain(|c: char| !c.is_control());
self.root_password = res;
self.root_password_strenght = PasswordStrenght::from_password(&self.root_password);
StageAction::None
}
Message::ToggleUserPassSecure => {
self.user_pass_is_secure = !self.user_pass_is_secure;
StageAction::None
}
Message::ToggleRootPassSecure => {
self.root_pass_is_secure = !self.root_pass_is_secure;
StageAction::None
}
Message::Back => StageAction::Back,
Message::Next => StageAction::Next(self.gen_result()),
}
}
pub fn view(&self) -> iced::Element<'_, Message> {
let back_button = widget::button(widget::text(t!("button.back"))).on_press(Message::Back);
let next_button = if self.user_name.is_empty()
|| self.user_password.is_empty()
|| self.root_password.is_empty()
{
widget::button(widget::text(t!("button.next")))
} else {
widget::button(widget::text(t!("button.next"))).on_press(Message::Next)
};
let user_pass_sec_text = if self.user_pass_is_secure {
widget::text("👁")
} else {
widget::text("*")
};
let root_pass_sec_text = if self.root_pass_is_secure {
widget::text("👁")
} else {
widget::text("*")
};
let user_pass_sec_text = user_pass_sec_text
.width(12)
.align_x(Alignment::Center)
.align_y(Alignment::Center);
let root_pass_sec_text = root_pass_sec_text
.width(12)
.align_x(Alignment::Center)
.align_y(Alignment::Center);
let mut root_pass_input =
widget::text_input("", &self.root_password).secure(self.root_pass_is_secure);
if !self.reuse_user_password_for_roor {
root_pass_input = root_pass_input.on_input(Message::RootPasswordChange);
}
let user_p_strenght_text = widget::text(self.user_password_strenght.to_string())
.color(self.user_password_strenght.color());
let root_p_strenght_text = widget::text(self.root_password_strenght.to_string())
.color(self.root_password_strenght.color());
widget::column![
widget::container(
widget::column![
widget::column![
widget::text(t!("security.user_name")),
widget::text_input("", &self.user_name)
.width(256)
.on_input(Message::UserNameChange)
],
widget::column![
widget::text(t!("security.user_full_name")),
widget::text_input("", &self.user_full_name)
.width(256)
.on_input(Message::UserFullNameChange)
],
widget::column![
widget::text(t!("security.user_password")),
widget::row![
widget::text_input("", &self.user_password)
.width(512)
.on_input(Message::UserPasswordChange)
.secure(self.user_pass_is_secure),
widget::button(user_pass_sec_text)
.on_press(Message::ToggleUserPassSecure),
widget::container(user_p_strenght_text)
.align_x(Alignment::Center)
.style(widget::container::bordered_box)
.style(widget::container::rounded_box)
.padding(6)
.width(160),
]
.spacing(5)
.align_y(Alignment::Center)
],
widget::rule::horizontal(2),
widget::column![
widget::text(t!("security.root_password")),
widget::row![
root_pass_input.width(512),
widget::button(root_pass_sec_text)
.on_press(Message::ToggleRootPassSecure),
widget::container(root_p_strenght_text)
.align_x(Alignment::Center)
.style(widget::container::rounded_box)
.style(widget::container::bordered_box)
.padding(6)
.width(160),
]
.spacing(5)
.align_y(Alignment::Center),
widget::checkbox(self.reuse_user_password_for_roor)
.label(t!("security.reuse_user_password"))
.on_toggle(Message::ToggleReusePass),
],
]
.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()
}
}
+47 -50
View File
@@ -18,12 +18,13 @@
This is TimeZone stage, used to select timezone
*/
use crate::stage::{ConfigValue, StageAction, StageResult};
use crate::stage::{KiraConfig, StageAction};
use iced::{Alignment, Length, widget};
use log;
use rust_i18n::t;
use std::collections::HashMap;
mod scroll_list;
use crate::kira_scroll_list;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeZoneData {
@@ -57,12 +58,18 @@ pub struct TimeZoneStage {
zone_id: Option<usize>,
selected_zone: Option<TimeZoneData>,
selected_zone_text: String,
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
pub struct TimeZoneStageResult {
pub time_zone: TimeZoneData,
}
#[derive(Debug, Clone)]
pub enum Message {
SelectRegion(scroll_list::ListViewMessage),
SelectZone(scroll_list::ListViewMessage),
SelectRegion(kira_scroll_list::ListViewMessage),
SelectZone(kira_scroll_list::ListViewMessage),
Next,
Back,
}
@@ -99,7 +106,7 @@ fn get_timezones_blocking() -> Result<Vec<TimeZoneData>, String> {
}
impl TimeZoneStage {
pub fn new() -> Self {
pub fn new(k_config: KiraConfig) -> Self {
match get_timezones_blocking() {
Ok(time_zones_data_list) => {
let mut time_zones_map: HashMap<String, Vec<String>> = HashMap::new();
@@ -110,19 +117,25 @@ impl TimeZoneStage {
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 {
regions: time_zones_map.keys().cloned().collect(),
regions: regions,
zones: Vec::new(),
time_zones: Ok(time_zones_map),
region_id: None,
zone_id: None,
selected_zone: None,
selected_zone_text: String::new(),
k_config: k_config,
}
}
Err(ex) => {
println!(
log::error!(
"Exception while trying to get time zones list from system {}",
ex
);
@@ -134,45 +147,25 @@ impl TimeZoneStage {
zone_id: None,
selected_zone: None,
selected_zone_text: String::new(),
k_config: k_config,
}
}
}
}
fn gen_result(&self) -> StageResult {
if let Some(time_zone) = &self.selected_zone {
StageResult {
name: "time_zone".to_string(),
config: Some(HashMap::from([
(
"name".to_string(),
ConfigValue::String(time_zone.to_string()),
),
(
"region".to_string(),
ConfigValue::String(time_zone.region.clone()),
),
(
"zone".to_string(),
ConfigValue::String(time_zone.zone.clone()),
),
])),
resuts: None,
error: None,
}
} else {
StageResult {
name: "time_zone".to_string(),
config: None,
resuts: None,
error: None,
}
}
fn gen_result(&mut self) -> KiraConfig {
self.k_config.res_timezone = Some(TimeZoneStageResult {
time_zone: self
.selected_zone
.clone()
.expect("Timezone must be selected!"),
});
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> StageAction {
match message {
Message::SelectRegion(scroll_list::ListViewMessage::Select(region_id)) => {
Message::SelectRegion(kira_scroll_list::ListViewMessage::Select(region_id)) => {
let region = self.regions.get(region_id).unwrap();
self.zones = self
.time_zones
@@ -186,15 +179,15 @@ impl TimeZoneStage {
//self.selected_zone_text.clear();
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()
);
Message::SelectZone(kira_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());
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()),
@@ -213,12 +206,16 @@ impl TimeZoneStage {
widget::column![
widget::text(t!("timezone.select_timezone")),
widget::row![
scroll_list::list_view(&self.regions, self.region_id, Length::Shrink)
kira_scroll_list::list_view(&self.regions, self.region_id, Length::Shrink)
.map(Message::SelectRegion),
scroll_list::list_view(&self.zones, self.zone_id, Length::FillPortion(1))
.map(Message::SelectZone),
kira_scroll_list::list_view(
&self.zones,
self.zone_id,
Length::FillPortion(1)
)
.map(Message::SelectZone),
]
.padding(20)
.padding([0, 10])
.spacing(10)
.height(Length::Shrink),
widget::text(self.selected_zone_text.clone())
-110
View File
@@ -1,110 +0,0 @@
use std::fmt::Display;
use iced::widget::{Column, button, container, scrollable, text};
use iced::{Alignment, Color, Element, Length, Theme};
#[derive(Debug, Clone)]
pub enum ListViewMessage {
Select(usize),
}
fn unselected_button_style(theme: &Theme, status: button::Status) -> button::Style {
//let palette = theme.extended_palette();
use iced::{Border, border::Radius};
match status {
button::Status::Hovered => button::Style {
border: Border {
color: Color::from_rgb(0.8, 0.3, 0.3),
width: 2.0,
radius: Radius::new(5.0),
},
..button::primary(theme, status)
},
button::Status::Active => button::Style {
background: Some(iced::Background::Color(crate::theme::change_lightnes(
&theme.palette().primary,
-0.1,
))),
..button::primary(theme, status)
},
_ => button::primary(theme, status),
}
}
fn selected_button_style(theme: &Theme, status: button::Status) -> button::Style {
//let palette = theme.extended_palette();
match status {
button::Status::Active => button::Style {
background: Some(iced::Background::Color(Color::from_rgb(0.8, 0.3, 0.3))),
..button::primary(theme, status)
},
_ => button::Style {
background: Some(iced::Background::Color(Color::from_rgb(0.8, 0.3, 0.3))),
..button::primary(theme, status)
},
}
}
// fn view() -> container::Style {
// container::Style {
// text_color: None,
// background: None,
// border: Border {
// color: Color::BLACK,
// width: 2.0,
// radius: Radius::new(5.0),
// },
// shadow: None,
// snap: false,
// }
// }
// fn border_style(theme: &Theme) -> container::Style {
// use iced::{Border, border::Radius};
// container::Style {
// border: Border {
// color: Color::from_rgb(0.8, 0.3, 0.3),
// width: 2.0,
// radius: Radius::new(5.0),
// },
// ..container::bordered_box(theme)
// }
// }
// Assuming your Item struct or type implements Display or has a text representation
pub fn list_view<T: Display>(
items: &Vec<T>,
selected_id: Option<usize>,
container_height: Length,
) -> Element<'_, ListViewMessage> {
let mut column = Column::new()
.spacing(2)
.align_x(Alignment::Center)
.width(Length::Fill);
for (index, value) in items.iter().enumerate() {
// Create a row for each item, possibly with buttons or other controls
let list_item = if let Some(id) = selected_id
&& id == index
{
button(text(value.to_string())).style(selected_button_style)
} else {
button(text(value.to_string())).style(unselected_button_style)
};
let list_item = list_item
.width(Length::Fill)
.on_press(ListViewMessage::Select(index));
column = column.push(list_item);
}
// Wrap the column in a scrollable widget
container(scrollable(column).width(Length::Fill))
.style(container::bordered_box)
.height(container_height)
.padding(8)
.into()
}
+39 -59
View File
@@ -1,29 +1,29 @@
// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation>
// <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 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 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 Welcome stage, used to greet used and allow user to choose program language
*/
This is Welcome stage, used to greet used and allow user to choose program language
*/
use crate::{stage::{ConfigValue, StageAction, StageResult}, theme};
use crate::{
kira_theming,
stage::{KiraConfig, StageAction},
};
use iced::{Alignment, widget};
use rust_i18n::t;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocData {
@@ -37,10 +37,17 @@ impl std::fmt::Display for LocData {
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WelcomeStageResult {
pub loc_code: String,
pub loc_name: String,
}
#[derive(Debug, Clone)]
pub struct WelcomeStage {
locale: Option<LocData>,
locales: Vec<LocData>,
k_config: KiraConfig,
}
#[derive(Debug, Clone)]
@@ -51,8 +58,7 @@ pub enum Message {
}
impl WelcomeStage {
pub fn new() -> Self {
pub fn new(k_config: KiraConfig) -> Self {
Self {
locale: Some(LocData {
name: "English".into(),
@@ -65,43 +71,23 @@ impl WelcomeStage {
code: s.to_string(),
})
.collect(),
k_config: k_config,
}
}
fn gen_result(&self) -> StageResult {
if let Some(loc) = &self.locale {
StageResult {
name: "welcome".to_string(),
config: Some(HashMap::from([
(
"loc_code".to_string(),
ConfigValue::String(loc.code.clone()),
),
(
"loc_name".to_string(),
ConfigValue::String(loc.name.clone()),
),
])),
resuts: None,
error: None,
fn gen_result(&mut self) -> KiraConfig {
self.k_config.res_welcome = Some(if let Some(loc) = &self.locale {
WelcomeStageResult {
loc_code: loc.code.clone(),
loc_name: loc.name.clone(),
}
} else {
StageResult {
name: "welcome".to_string(),
config: Some(HashMap::from([
(
"loc_code".to_string(),
ConfigValue::String("en".to_string()),
),
(
"loc_name".to_string(),
ConfigValue::String("English".to_string()),
),
])),
resuts: None,
error: None,
WelcomeStageResult {
loc_code: "en".to_string(),
loc_name: "English".to_string(),
}
}
});
self.k_config.clone()
}
pub fn update(&mut self, message: Message) -> StageAction {
@@ -111,12 +97,7 @@ impl WelcomeStage {
self.locale = Some(loc);
StageAction::None
}
Message::Exit => StageAction::Abort(StageResult {
name: "welcome".to_string(),
config: None,
resuts: None,
error: None,
}),
Message::Exit => StageAction::Abort,
Message::Next => StageAction::Next(self.gen_result()),
}
}
@@ -126,8 +107,7 @@ impl WelcomeStage {
let exit_button = widget::button(widget::text(t!("button.exit"))).on_press(Message::Exit);
// Embed the image bytes into the executable
let welcom_logo_handle =
widget::image::Handle::from_bytes(theme::get_logo_bytes());
let welcom_logo_handle = widget::image::Handle::from_bytes(kira_theming::get_logo_bytes());
widget::column![
widget::container(