Compare commits

...

36 Commits

Author SHA256 Message Date
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
26 changed files with 3376 additions and 1270 deletions
Generated
+176 -910
View File
File diff suppressed because it is too large Load Diff
+20 -3
View File
@@ -3,15 +3,32 @@ 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_moving_picture = "0"
log = "0.4"
rust-i18n = "3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
time-format = "1.2"
toml = "1"
[profile.release]
#lto = true
[dev-dependencies]
rand = "0.10"
[profile.dev]
opt-level = 2
codegen-units = 16
[profile.release]
lto = true
debug = false
incremental = false
codegen-units = 1
opt-level = 3
strip = true
+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()
}
}
+3
View File
@@ -1,3 +1,6 @@
[network]
reqire_network = true
[partition]
min_disk_size_mb = 8192
+680
View File
@@ -0,0 +1,680 @@
// <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, io::Read, process::ExitStatus};
///
/// 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()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FSType {
VFAT,
XFS,
LUKS,
SWAP,
EXT4,
BTRFS,
BCACHEFS,
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,
_ => 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 => "bcachefs",
// 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,
HOME,
SWAP,
LuksRoot,
LuksHome,
}
#[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 {
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,
})
}
}
/// 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
match Command::new("lsblk")
.args([
"-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;
log::debug!("part_end: {} end_mod: {}", part_end, end_mod);
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;
}
Some((align_size, align_start, align_end))
}
pub struct PartLayout {
pub dev: BlkDev,
pub empty_space: KiraSize,
pub part_list: Vec<PartInfo>,
}
impl PartLayout {
pub fn new(dev: BlkDev) -> Self {
Self {
empty_space: dev.size,
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() {
KiraSize::new_mb(2)
} else {
self.dev.size - self.empty_space
};
let (size, start, _) =
align_part(part_size.b(), part_start.b(), self.dev.sector_size.b()).unwrap();
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(size),
start: KiraSize::new_b(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(size);
self
}
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_classic_layout(dev: BlkDev, efi_size: KiraSize, swap_size: KiraSize) -> Self {
let mut res: PartLayout = PartLayout::new(dev);
// UEFI partition
res = res
.add_part(
efi_size,
FSType::VFAT,
Some("efi".into()),
Some("EFI".into()),
Some("/efi".into()),
Some(PartRole::EFI),
)
.add_part_reserve(
swap_size,
FSType::BTRFS,
Some("root".into()),
None,
Some("/".into()),
Some(PartRole::ROOT),
);
// swap
if swap_size.b() > 0 {
res = res.add_part(
swap_size,
FSType::SWAP,
Some("swap".into()),
None,
Some("SWAP".into()),
Some(PartRole::SWAP),
);
}
res
}
}
/// Executes parted command with given arguments
fn exec_parted(args: &Vec<&str>) -> Result<String, String> {
use std::process::Command;
match Command::new("parted").args(args).output() {
Err(ex) => Err(ex.to_string()),
Ok(out) => {
if out.status.success() {
String::from_utf8(out.stdout).map_err(|ex| ex.to_string())
} else {
Err(out.status.to_string())
}
}
}
}
fn parted_get_last_part_num(dev: &BlkDev) -> Result<u32, String> {
// parted -m /dev/loop0 print
match exec_parted(&vec!["-m", dev.full_name().as_str(), "print"]) {
Ok(res) => {
match res.lines().last().and_then(|last_line| {
last_line
.split_once(':')
.and_then(|(first_part, a)| u32::from_str_radix(first_part, 10).ok())
}) {
Some(n) => Ok(n),
None => Err("Error parsing parted output".to_string()),
}
}
Err(ex) => Err(ex),
}
}
/// init gpt partition table
/// parted --script $install_device mklabel gpt
/// Its blocking function
pub fn parted_init_gpt_part_table(dev: &BlkDev) -> Result<(), String> {
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"mklabel",
"gpt",
])
.map(|_| ())
}
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<(), String> {
let part_end = part.start + part.size;
// gpt label should be in double qutes
let gpt_label = if let Some(s) = &part.gpt_label {
format!("\"{}\"", s)
} else {
"\"\"".to_string()
};
// 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
pub fn bcachefs_format(
dev: &BlkDev,
compression: Option<String>,
password: Option<String>,
) -> Result<(), String> {
// 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
use std::fs;
use std::process::Command;
let mut arg = vec!["format", "--force", "--data_checksum=none"];
// if compression algorythm specified
if let Some(compresss_alg) = &compression {
arg.push(compresss_alg.as_str());
}
// if we want encrypted volume
if let Some(passw) = password {
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &passw).map_err(|ex| ex.to_string())?;
arg.push("--encrypted");
arg.push("--passphrase_file=/tmp/bzzpsspass.txt");
}
// add dev name at the end
let dev_name = dev.full_name();
arg.push(dev_name.as_str());
let out = Command::new("bcachefs")
.args(arg)
.output()
.map_err(|ex| ex.to_string())?;
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?)
}
}
/// unlok encrypted bcache volume
pub fn bcachefs_unlock(dev: &BlkDev, pass: String) -> Result<(), String> {
use std::fs;
use std::process::Command;
// bcachefs unlock --file=/tmp/bzzpsspass.txt /dev/loop0
let dev_name = dev.full_name();
let arg = vec!["unlock", "--file=/tmp/bzzpsspass.txt ", dev_name.as_str()];
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &pass).map_err(|ex| ex.to_string())?;
let out = Command::new("bcachefs")
.args(arg)
.output()
.map_err(|ex| ex.to_string())?;
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?)
}
}
/// create subvolume
pub fn bcachefs_create_subvolume(vol_path: &str) -> Result<(), String> {
use std::process::Command;
// sudo bcachefs subvolume create /mnt/test/nyan
let out = Command::new("bcachefs")
.args(["subvolume", "create", vol_path])
.output()
.map_err(|ex| ex.to_string())?;
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?)
}
}
#[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()),
}
}
+13 -13
View File
@@ -19,13 +19,13 @@
Modify it to your liking.
*/
use iced::widget::container;
use iced::{Border, Color, Theme, color, Background};
use iced::{Color, Theme, color};
// 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,18 +38,18 @@ pub fn change_lightnes(c: &Color, factor: f32) -> Color {
res
}
pub fn text_with_border(_theme: &Theme) -> container::Style {
// 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()
}
}
// 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!\nDo you want to proceed?"
}
+149 -51
View File
@@ -21,6 +21,7 @@
*/
use iced::widget;
use log;
use std::process::ExitCode;
use iced::{Element, Task};
@@ -32,9 +33,15 @@ 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_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 +49,10 @@ 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),
}
enum Message {
@@ -50,12 +61,17 @@ 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),
}
struct KiraState {
current_view: Views,
toml_config: toml::Table,
config: KiraConfig,
stages_save: Vec<Views>,
}
const CONFIG_PATH_STR: &str = "kira_config.toml";
@@ -69,7 +85,7 @@ fn load_kira_config(config_path: &str) -> Result<toml::Table, Box<dyn std::error
// 2. Parse the string into a Table
let table: Table = content.parse()?;
println!("{:?}", table);
log::info!("{:?}", table);
Ok(table)
}
@@ -82,24 +98,13 @@ impl KiraState {
config: KiraConfig {
config_trail: Vec::new(),
},
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,6 +112,10 @@ 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)
}
}
@@ -114,25 +123,31 @@ 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!");
log::info!("Config loaded!");
k_state.toml_config = conf;
k_state.current_view = Views::Welcome(WelcomeStage::new());
Task::none()
}
Err(ex) => {
println!("Error reading config {}: {}", CONFIG_PATH_STR, ex);
log::error!("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.config.config_trail.push(welcome_res);
let s_state = std::mem::replace(&mut k_state.current_view, Views::License(license::LicenseStage {}));
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 {
@@ -140,12 +155,16 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<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));
let s_state = std::mem::replace(&mut k_state.current_view, Views::Network(network::NetworkStage::new(&k_state.toml_config)));
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();
k_state.config.config_trail.pop();
Task::none()
}
StageAction::None => Task::none(),
}
} else {
@@ -160,12 +179,13 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
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());
let s_state = std::mem::replace(&mut k_state.current_view,Views::TimeZone(stages::timezone::TimeZoneStage::new()));
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();
k_state.config.config_trail.pop();
Task::none()
}
_ => Task::none(),
@@ -181,14 +201,15 @@ fn update(k_state: &mut KiraState, message: Message) -> Task<Message> {
match action {
StageAction::Next(tz_res) => {
k_state.config.config_trail.push(tz_res);
iced::exit()
let s_state = std::mem::replace(&mut k_state.current_view,Views::Locale(stages::locale::LocaleStage::new(&k_state.config)));
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 +217,99 @@ 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.config.config_trail.push(locale_res);
let s_state = std::mem::replace(&mut k_state.current_view,Views::Keyboard(stages::keyboard::KeyboardStage::new()));
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Back => {
k_state.config.config_trail.pop();
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) => {
match stages::partition::PartitionStage::new(&k_state.toml_config) {
Ok(part_stage) => {
k_state.config.config_trail.push(keyboard_res);
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.config.config_trail.pop();
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.config.config_trail.push(partition_res);
let s_state = std::mem::replace(&mut k_state.current_view,Views::Security(stages::security::SecurityStage::new()));
k_state.stages_save.push(s_state);
Task::none()
}
StageAction::Back => {
k_state.config.config_trail.pop();
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.config.config_trail.push(security_res);
//let s_state = std::mem::replace(&mut k_state.current_view,
Task::none()
}
StageAction::Back => {
k_state.config.config_trail.pop();
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 +320,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();
// }
+105 -26
View File
@@ -1,23 +1,22 @@
// <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 std::collections::HashMap;
@@ -29,15 +28,102 @@ pub enum ConfigValue {
F64(f64),
Bool(bool),
Vector(Vec<ConfigValue>),
Dictionary(HashMap<String, ConfigValue>)
Dictionary(HashMap<String, ConfigValue>),
}
#[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>
pub error: Option<String>,
}
impl StageResult {
pub fn new(name: &str) -> Self {
Self {
name: name.into(),
config: None,
error: None,
}
}
pub fn add_error(mut self, ex: String) -> Self {
self.error = Some(ex);
self
}
pub fn add_val(mut self, val_name: &str, conf_val: ConfigValue) -> Self {
match &mut self.config {
Some(c) => {
c.insert(val_name.into(), conf_val);
}
None => {
self.config = Some(HashMap::from([(val_name.into(), conf_val)]));
}
}
self
}
pub fn add_val_string(self, val_name: &str, v: String) -> Self {
self.add_val(val_name, ConfigValue::String(v))
}
pub fn add_val_bool(self, val_name: &str, v: bool) -> Self {
self.add_val(val_name, ConfigValue::Bool(v))
}
pub fn add_val_u64(self, val_name: &str, v: u64) -> Self {
self.add_val(val_name, ConfigValue::U64(v))
}
pub fn get_val(&self, val_name: &str) -> Option<ConfigValue> {
let v = self
.config
.as_ref()
.and_then(|hm| hm.get(val_name).cloned());
return v;
}
pub fn get_val_str(&self, val_name: &str) -> Option<String> {
match self.get_val(val_name) {
Some(ConfigValue::String(v)) => Some(v),
_ => None,
}
}
pub fn get_val_bool(&self, val_name: &str) -> Option<bool> {
match self.get_val(val_name) {
Some(ConfigValue::Bool(v)) => Some(v),
_ => None,
}
}
}
pub struct KiraConfig {
pub config_trail: Vec<StageResult>,
}
impl Default for KiraConfig {
fn default() -> Self {
Self {
config_trail: Vec::new(),
}
}
}
impl KiraConfig {
pub fn get_stage(&self, name: &str) -> Option<StageResult> {
self.config_trail
.iter()
.find(|v| v.name == name)
.and_then(|v| Some(v.clone()))
}
pub fn pop_last(&mut self) -> Option<StageResult> {
self.config_trail.pop()
}
pub fn push(&mut self, stage_result: StageResult) {
self.config_trail.push(stage_result);
}
}
#[derive(Debug, Clone)]
@@ -45,12 +131,5 @@ pub enum StageAction {
Back,
None,
Next(StageResult),
Abort(StageResult),
Abort,
}
pub struct KiraConfig {
pub config_trail: Vec<StageResult>,
}
+181
View File
@@ -0,0 +1,181 @@
// <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.
*/
///
/// So. I am goind to make dir structure lile this
/// /bzz/root
/// /bzz/home
/// Mount root from it and binmount home
///
use log;
use crate::{
kira_theming,
stage::{KiraConfig, StageAction, StageResult},
};
use iced::{Alignment, Task, futures, widget};
use rust_i18n::t;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallationState {
UserConfirmDialog,
Partitionning,
CopyFiles,
Configuring,
Finish,
Error,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InstallStage {
state: InstallationState,
progress: f32,
progress_message: String,
}
#[derive(Debug, Clone)]
pub enum Message {
UserConfirms,
UserDenies,
StartPartitioning,
UpdateProgress(Result<(f32, String), String>),
PartitioningFinish(Result<(), String>),
Back,
Cancel,
Finish,
}
#[derive(Debug)]
pub enum Update {
StgAct(StageAction),
Task(Task<Message>),
}
impl InstallStage {
pub fn new(config: &KiraConfig) -> Self {
Self {
state: InstallationState::UserConfirmDialog,
progress: 0.0,
progress_message: String::new(),
}
}
fn gen_result(&self) -> StageResult {
StageResult::new("install")
}
pub fn update(&mut self, message: Message) -> Update {
match message {
Message::UserDenies => Update::StgAct(StageAction::Back),
Message::UserConfirms => {
// starting installation process here
self.state = InstallationState::Partitionning;
Update::Task(Task::run(disk_part::check_connected(), Message::UpdateProgress))
//Update::Task(Task::perform(, Message::PartitioningFinish))
}
Message::StartPartitioning => Update::StgAct(StageAction::None),
Message::PartitioningFinish(res) => Update::StgAct(StageAction::None),
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::Cancel => Update::StgAct(StageAction::Abort),
Message::Back => Update::StgAct(StageAction::Back),
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 welcom_logo_handle = widget::image::Handle::from_bytes(kira_theming::get_logo_bytes());
let main_content = widget::column![
widget::container(
widget::column![
widget::image(welcom_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::column![
widget::text(t!("install.warning")),
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)
]
.width(iced::Length::Fill)
.align_y(Alignment::End)
.padding(10)
]
.align_x(Alignment::Center),
)
.into()
} else {
page_stack.into()
}
}
}
+398
View File
@@ -0,0 +1,398 @@
// <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 log;
use crate::kira_scroll_list::{self, ListViewMessage};
use crate::stage::{StageAction, StageResult};
use iced::{Alignment, Length, widget};
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>,
}
#[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() -> 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,
}
}
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,
}
}
}
}
fn gen_result(&self) -> StageResult {
if let Some(model) = &self.model
&& let Some(k_l) = &self.keyboard_layout
{
StageResult::new("keyboard")
.add_val_string("model", model.model.clone())
.add_val_string("layout", k_l.to_result())
} else {
StageResult::new("keyboard")
.add_error("Something go terrible wrong, there is no keyboard layouts.".into())
}
}
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()
}
}
+31 -39
View File
@@ -1,30 +1,26 @@
// <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;
@@ -47,27 +43,15 @@ pub enum Message {
impl LicenseStage {
fn accepted() -> StageResult {
StageResult {
name: "license".into(),
config: Some(HashMap::from([(
"accepted".to_string(),
stage::ConfigValue::Bool(true),
)])),
resuts: None,
error: None,
}
StageResult::new("license")
.add_val_bool("accepted", true)
}
pub fn update(&mut self, message: Message) -> stage::StageAction {
match message {
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 +63,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)
+303
View File
@@ -0,0 +1,303 @@
// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation>
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/*
This is Locales stage, used to choose global and numbers locale
*/
use crate::{
kira_theming,
stage::{ConfigValue, KiraConfig, StageAction, StageResult},
};
use iced::{Alignment, widget};
use rust_i18n::t;
use std::collections::HashMap;
const LOCALES_GEN_FILE_NAME: &str = "/etc/locale.gen";
const LOCALES_GEN_START_LINE: usize = 17;
// fn default_locale() -> (String, String) {
// ("C.UTF-8".to_string(), "UTF-8".to_string())
// }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocaleData {
code: String,
description: String,
}
impl std::fmt::Display for LocaleData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.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,
}
#[derive(Debug, Clone)]
pub enum Message {
SelectLangLocale(LocaleData),
SelectFormatsLocale(LocaleData),
Next,
Back,
}
impl LocaleStage {
pub fn new(kira_config: &KiraConfig) -> Self {
// get seelctet language code from welcome stage
let maybe_lang = kira_config
.get_stage("welcome")
.and_then(|v| v.config.and_then(|v| v.get("loc_code").cloned()));
let 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(ConfigValue::String(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,
}
}
fn gen_result(&self) -> StageResult {
if let Some(lang_locale) = &self.lang_locale
&& let Some(formats_locale) = &self.formats_locale
{
StageResult::new("locale")
.add_val_string("lang_locale", lang_locale.code.clone())
.add_val_string("formats_locale", formats_locale.code.clone())
} else {
let loc = LocaleData::default();
StageResult::new("locale")
.add_val_string("lang_locale", loc.code.clone())
.add_val_string("formats_locale", loc.code)
}
}
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;
+29 -30
View File
@@ -19,17 +19,16 @@
it needed for installation to process.
*/
use log;
use iced::{Alignment, Task, widget};
use iced_moving_picture::widget::apng;
use rust_i18n::t;
use std::collections::HashMap;
use toml::Table;
use crate::stage;
use crate::stage::StageResult;
#[derive(Debug, Clone)]
#[derive(PartialEq, Eq)]
pub enum State {
Cheking,
@@ -50,6 +49,7 @@ pub struct NetworkStage {
use_net_settings: bool,
reqire_network: bool,
spinner_frames: apng::Frames,
logo_handle: widget::image::Handle,
status_message: String,
}
@@ -82,7 +82,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
}
}
@@ -104,19 +104,22 @@ impl NetworkStage {
.and_then(|v| v.get("reqire_network"))
.and_then(|v| v.as_bool())
{
println!("Config value reqire_network read {}", reqire_network_conf);
log::debug!("Config value reqire_network read {}", reqire_network_conf);
reqire_network = reqire_network_conf;
} else {
println!("Error parsing network config. Using default values.");
log::error!("Error parsing network config. Using default values.");
}
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,
use_net_settings: true,
reqire_network: reqire_network,
spinner_frames: spinner_frames,
status_message: t!("network.checking").into(),
@@ -124,21 +127,9 @@ impl NetworkStage {
}
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,
}
StageResult::new("network")
.add_val_bool("internet_active", self.internet_active)
.add_val_bool("use_net_settings", self.use_net_settings)
}
pub fn update(&mut self, message: Message) -> UpdateResult {
@@ -178,12 +169,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 && self.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 +193,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![
+417
View File
@@ -0,0 +1,417 @@
// <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::{StageAction, StageResult};
use iced::{Alignment, Color, Element, Length, widget};
use log;
use rust_i18n::t;
use toml::Table;
#[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)]
pub struct PartitionStage {
device: Option<BlkDev>,
devices: Vec<BlkDev>,
swap_mode: Option<SwapMode>,
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)>>,
system_ram_size: KiraSize,
vram_of_all_gpus_size: KiraSize,
}
#[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(toml_config: &Table) -> Result<Self, String> {
let maybe_dev_list = BlkDev::list_sys_blk_dev();
log::info!("hiii from partition");
let min_disk_size_mb = toml_config
.get("partition")
.and_then(|v| v.as_table())
.and_then(|v| v.get("min_disk_size_mb"))
.and_then(|v| v.as_integer())
.unwrap_or(8192);
let min_disk_size: KiraSize = KiraSize::new_mb(min_disk_size_mb.try_into().unwrap_or(8192));
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(),
selected_disk_parts: None,
generated_disk_parts: None,
system_ram_size: system_ram_size,
vram_of_all_gpus_size: vram_of_all_gpus_size,
})
}
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();
self.generated_disk_parts = Some(
PartLayout::gen_classic_layout(dev.clone(), KiraSize::new_mb(512), swap_size)
.part_list
.iter()
.map(|part_info| part_to_ct_params(part_info, dev.size))
.collect(),
);
}
}
fn gen_result(&self) -> StageResult {
let swap_size: KiraSize = self.get_swap_size();
let dev_name = self
.device
.clone()
.expect("Device should be selected!")
.name;
StageResult::new("partition")
.add_val_string("device", dev_name)
.add_val_string(
"swap_mode",
self.swap_mode
.clone()
.unwrap_or(SwapMode::NoSwap)
.to_string(),
)
.add_val_u64("swap_size_mb", swap_size.mb())
.add_val_bool("use_zram", self.use_zram)
.add_val_bool("secure_boot", self.secure_boot)
.add_val_bool("encrypt_drive", self.encrypt_drive)
}
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_classic_layout(dev.clone(), KiraSize::new_mb(128), swap_size)
.part_list
.iter()
.map(|part_info| part_to_ct_params(part_info, dev.size))
.collect(),
);
self.device = Some(dev);
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()
}
}
+290
View File
@@ -0,0 +1,290 @@
// <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::{StageAction, StageResult};
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,
}
#[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() -> 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,
}
}
fn gen_result(&self) -> StageResult {
StageResult::new("security")
.add_val_string("user_name", self.user_name.clone())
.add_val_string("user_full_name", self.user_full_name.clone())
.add_val_string("user_full_name", self.user_full_name.clone())
.add_val_string("user_password", self.user_password.clone())
.add_val_string("root_password", self.root_password.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()
}
}
+30 -44
View File
@@ -18,12 +18,13 @@
This is TimeZone stage, used to select timezone
*/
use crate::stage::{ConfigValue, StageAction, StageResult};
use log;
use crate::stage::{StageAction, StageResult};
use iced::{Alignment, Length, widget};
use rust_i18n::t;
use std::collections::HashMap;
mod scroll_list;
use crate::kira_scroll_list;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeZoneData {
@@ -61,8 +62,8 @@ pub struct TimeZoneStage {
#[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,
}
@@ -110,9 +111,14 @@ 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,
@@ -122,7 +128,7 @@ impl TimeZoneStage {
}
}
Err(ex) => {
println!(
log::error!(
"Exception while trying to get time zones list from system {}",
ex
);
@@ -141,38 +147,18 @@ impl TimeZoneStage {
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,
}
StageResult::new("time_zone")
.add_val_string("name", time_zone.to_string())
.add_val_string("region", time_zone.region.clone())
.add_val_string("zone", time_zone.zone.clone())
} else {
StageResult {
name: "time_zone".to_string(),
config: None,
resuts: None,
error: None,
}
StageResult::new("time_zone")
}
}
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 +172,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 +199,12 @@ 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))
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()
}
+9 -39
View File
@@ -20,10 +20,9 @@
*/
use crate::{stage::{ConfigValue, StageAction, StageResult}, theme};
use crate::{stage::{StageAction, StageResult}, kira_theming};
use iced::{Alignment, widget};
use rust_i18n::t;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocData {
@@ -70,37 +69,13 @@ impl WelcomeStage {
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,
}
StageResult::new("welcome")
.add_val_string("loc_code", loc.code.clone())
.add_val_string("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,
}
StageResult::new("welcome")
.add_val_string("loc_code", "en".to_string())
.add_val_string("loc_name", "English".to_string())
}
}
@@ -111,12 +86,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()),
}
}
@@ -127,7 +97,7 @@ impl WelcomeStage {
// Embed the image bytes into the executable
let welcom_logo_handle =
widget::image::Handle::from_bytes(theme::get_logo_bytes());
widget::image::Handle::from_bytes(kira_theming::get_logo_bytes());
widget::column![
widget::container(