Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/app/install/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,36 @@ impl AppState {
self.open_info_popup("Installation already running. Please wait...".into());
return;
}

let original_bootloader_index = self.bootloader_index;
let mut bootloader_autocorrected = false;

// Auto-correct invalid bootloader selection on legacy (non-UEFI) systems.
// - systemd-boot requires UEFI
// - efistub (experimental) requires UEFI
if !self.is_uefi() && matches!(self.bootloader_index, 0 | 2) {
// GRUB is index 1 in `bootloader_index`.
self.debug_log(&format!(
"start_install: legacy detected (uefi=false), switching bootloader_index {} -> 1 (grub)",
self.bootloader_index
));
self.bootloader_index = 1;
bootloader_autocorrected = true;
}

// Guard: Desktop (KDE/GNOME) requires NetworkManager
let desktop_selected = self.experience_mode_index == 0;
let selects_kde = self.selected_desktop_envs.contains("KDE Plasma");
let selects_gnome = self.selected_desktop_envs.contains("GNOME");
let needs_nm = desktop_selected && (selects_kde || selects_gnome);
if needs_nm && self.network_mode_index != 2 {
if bootloader_autocorrected {
self.debug_log(&format!(
"start_install: reverting bootloader_index {} -> {} (install did not proceed)",
self.bootloader_index, original_bootloader_index
));
self.bootloader_index = original_bootloader_index;
}
self.popup_kind = Some(crate::core::types::PopupKind::NetworkManagerSwitchConfirm);
self.popup_open = true;
self.popup_items = vec![
Expand All @@ -39,6 +63,13 @@ impl AppState {
if let Some(msg) = self.validate_install_requirements() {
self.debug_log("start_install: rejected, requirements not met");
self.open_info_popup(msg);
if bootloader_autocorrected {
self.debug_log(&format!(
"start_install: reverting bootloader_index {} -> {} (install did not proceed)",
self.bootloader_index, original_bootloader_index
));
self.bootloader_index = original_bootloader_index;
}
return;
}
self.debug_log("start_install: starting install flow");
Expand Down
8 changes: 8 additions & 0 deletions src/app/swap_partition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ pub fn draw_swap_partition(frame: &mut ratatui::Frame, app: &mut AppState, area:

let mut lines: Vec<Line> = vec![Line::from(title), Line::from("")];

let ram_gib = app.detected_ram_mib as f64 / 1024.0;
let swap_gib = app.swap_size_mib as f64 / 1024.0;
lines.push(Line::from(Span::styled(
format!(" Detected RAM: {ram_gib:.1} GiB | Planned swap: {swap_gib:.2} GiB"),
Style::default().fg(Color::DarkGray),
)));
lines.push(Line::from(""));

let options = vec![(
"Swapon",
if app.swap_enabled {
Expand Down
32 changes: 32 additions & 0 deletions src/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,38 @@ pub fn strip_ansi_escape_codes(input: &str) -> String {
out
}

/// Detect total system RAM by reading `/proc/meminfo`.
/// Returns MiB. Falls back to 4096 MiB (4 GiB) if detection fails.
pub fn detect_ram_mib() -> u64 {
if let Ok(contents) = std::fs::read_to_string("/proc/meminfo") {
for line in contents.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let trimmed = rest.trim();
if let Some(kb_str) = trimmed.strip_suffix(" kB") {
if let Ok(kb) = kb_str.trim().parse::<u64>() {
return kb / 1024;
}
}
}
}
}
4096
}

/// Compute optimal swap partition size in MiB based on detected RAM.
/// - RAM <= 8 GiB: swap = RAM (at least 1 GiB)
/// - RAM > 8 GiB: swap = 8 GiB + half of the excess
/// - Maximum: 16 GiB
pub fn compute_swap_size_mib(ram_mib: u64) -> u64 {
let ram_mib = ram_mib.max(1024);
let swap_mib = if ram_mib <= 8192 {
ram_mib
} else {
8192 + (ram_mib - 8192) / 2
};
swap_mib.min(16384)
}

/// Sanitize a terminal output line for safe rendering inside the TUI.
/// Removes carriage returns, backspaces and ANSI escape sequences.
pub fn sanitize_terminal_output_line(input: &str) -> String {
Expand Down
32 changes: 21 additions & 11 deletions src/core/services/partitioning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ impl PartitioningService {
"parted -s {device} mkpart ESP fat32 {next_start} 1025MiB"
));
part_cmds.push(format!("parted -s {device} set 1 esp on"));
part_cmds.push(format!("mkfs.fat -F 32 {device}1"));
part_cmds.push(format!(
"mkfs.fat -F 32 {}",
Self::get_partition_path(device, 1)
));
next_start = "1025MiB".into();
} else {
part_cmds.push(format!(
Expand All @@ -102,17 +105,20 @@ impl PartitioningService {
next_start = "2MiB".into();
}

let swap_part_num: u32 = 2;
let root_part_num: u32 = if state.swap_enabled { 3 } else { 2 };
let swap_part_path = Self::get_partition_path(device, swap_part_num);
let root_part_path = Self::get_partition_path(device, root_part_num);

if state.swap_enabled {
let swap_end = if state.is_uefi() {
"5121MiB"
} else {
"4098MiB"
};
let swap_start_mib: u64 = if state.is_uefi() { 1025 } else { 2 };
let swap_end_mib = swap_start_mib + state.swap_size_mib;
let swap_end = format!("{swap_end_mib}MiB");
part_cmds.push(format!(
"parted -s {device} mkpart swap linux-swap {next_start} {swap_end}"
));
part_cmds.push(format!("mkswap {device}2"));
next_start = swap_end.into();
part_cmds.push(format!("mkswap {swap_part_path}"));
next_start = swap_end;
}

part_cmds.push(format!(
Expand All @@ -125,12 +131,16 @@ impl PartitioningService {
"modprobe -q dm_crypt 2>/dev/null || modprobe -q dm-crypt 2>/dev/null || true"
.into(),
);
part_cmds.push(format!("cryptsetup luksFormat --type luks2 -q {device}3"));
part_cmds.push(format!(
"cryptsetup luksFormat --type luks2 -q {root_part_path}"
));
part_cmds.push("udevadm settle".into());
part_cmds.push(format!("cryptsetup open --type luks {device}3 cryptroot"));
part_cmds.push(format!(
"cryptsetup open --type luks {root_part_path} cryptroot"
));
part_cmds.push("mkfs.btrfs -f /dev/mapper/cryptroot".into());
} else {
part_cmds.push(format!("mkfs.btrfs -f {device}3"));
part_cmds.push(format!("mkfs.btrfs -f {root_part_path}"));
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/core/services/sysconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ impl SysConfigService {
cmds.push("systemctl --root=/mnt enable systemd-timesyncd".into());
}

// Enable SSH daemon when the SSH server type is selected
// (openssh installs the `sshd.service` unit on Arch).
let wants_sshd = state.selected_server_types.contains("sshd");
let openssh_in_server_pkgs = state
.selected_server_packages
.get("sshd")
.map_or(false, |set| set.contains("openssh"));
let openssh_in_additional_pkgs = state
.additional_packages
.iter()
.any(|p| p.name == "openssh");
if wants_sshd && (openssh_in_server_pkgs || openssh_in_additional_pkgs) {
cmds.push("systemctl --root=/mnt enable sshd".into());
}

// Root password (set only if provided and confirmed)
if !state.root_password.is_empty() && state.root_password == state.root_password_confirm {
if state.dry_run {
Expand Down
39 changes: 23 additions & 16 deletions src/core/services/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ impl SystemService {
let mut package_set: BTreeSet<String> = BTreeSet::new();
let mut missing: Vec<String> = Vec::new();

let desktop_mode = state.experience_mode_index == 0; // 0: Desktop, 1: Minimal, 2: Server, 3: Xorg

// Essentials
for p in [
"base",
Expand Down Expand Up @@ -173,28 +175,33 @@ impl SystemService {
_ => {}
}

// Polkit for Hyprland or Sway
if state
.selected_desktop_envs
.iter()
.any(|e| e == "Hyprland" || e == "Sway")
// Polkit for Hyprland or Sway (desktop-only)
if desktop_mode
&& state
.selected_desktop_envs
.iter()
.any(|e| e == "Hyprland" || e == "Sway")
{
package_set.insert("polkit".into());
}

// Login manager (if set and not none)
if let Some(lm) = state.selected_login_manager.clone()
&& !lm.is_empty()
&& lm != "none"
{
package_set.insert(lm);
// Login manager (if set and not none) - desktop-only
if desktop_mode {
if let Some(lm) = state.selected_login_manager.clone()
&& !lm.is_empty()
&& lm != "none"
{
package_set.insert(lm);
}
}

// Desktop environment packages (only for selected environments)
for env in state.selected_desktop_envs.iter() {
if let Some(set) = state.selected_env_packages.get(env) {
for p in set.iter() {
package_set.insert(p.clone());
// Desktop environment packages (only for selected environments) - desktop-only
if desktop_mode {
for env in state.selected_desktop_envs.iter() {
if let Some(set) = state.selected_env_packages.get(env) {
for p in set.iter() {
package_set.insert(p.clone());
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/core/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ pub struct AppState {
// Swap Partition state
pub swap_focus_index: usize, // 0: toggle, 1: Continue
pub swap_enabled: bool,
pub detected_ram_mib: u64,
pub swap_size_mib: u64,

// Unified Kernel Images state
pub uki_focus_index: usize, // 0: toggle, 1: Continue
Expand Down Expand Up @@ -395,6 +397,9 @@ impl AppState {
let mut list_state = ListState::default();
list_state.select(Some(0));

let detected_ram = crate::common::utils::detect_ram_mib();
let swap_size = crate::common::utils::compute_swap_size_mib(detected_ram);

let mut s = Self {
install_completed: false,
reboot_prompt_open: false,
Expand Down Expand Up @@ -499,6 +504,8 @@ impl AppState {

swap_focus_index: 0,
swap_enabled: true,
detected_ram_mib: detected_ram,
swap_size_mib: swap_size,

uki_focus_index: 0,
uki_enabled: false,
Expand Down
12 changes: 6 additions & 6 deletions src/core/storage/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ impl StoragePlanner {
part_num += 1;
}

let swap_start = if is_uefi { "1025MiB" } else { "2MiB" };
let swap_start_mib: u64 = if is_uefi { 1025 } else { 2 };
let swap_end_mib = swap_start_mib + state.swap_size_mib;

if state.swap_enabled {
let swap_end = if is_uefi { "5121MiB" } else { "4098MiB" };
partitions.push(PlannedPartition {
number: part_num,
role: PartitionRole::Swap,
start: swap_start.into(),
end: swap_end.into(),
start: format!("{swap_start_mib}MiB"),
end: format!("{swap_end_mib}MiB"),
filesystem: FilesystemSpec {
fstype: "linux-swap".into(),
mkfs_options: vec![],
Expand All @@ -112,9 +112,9 @@ impl StoragePlanner {
}

let root_start = if state.swap_enabled {
if is_uefi { "5121MiB" } else { "4098MiB" }
format!("{swap_end_mib}MiB")
} else {
swap_start
format!("{swap_start_mib}MiB")
};

let encryption = if luks {
Expand Down
5 changes: 3 additions & 2 deletions src/render/sections/info/disks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,11 @@ pub(super) fn render(frame: &mut Frame, app: &mut AppState, area: Rect) {
info_lines.push(Line::from(format!("Selected drive: {dev}")));
}
info_lines.push(Line::from("Planned layout:"));
let swap_gib = app.swap_size_mib as f64 / 1024.0;
if app.is_uefi() {
info_lines.push(Line::from("- gpt: 1024MiB EFI (FAT, ESP) -> /boot"));
if app.swap_enabled {
info_lines.push(Line::from("- swap: 4GiB"));
info_lines.push(Line::from(format!("- swap: {swap_gib:.2}GiB")));
}
let enc = if app.disk_encryption_type_index == 1 {
" (LUKS)"
Expand All @@ -227,7 +228,7 @@ pub(super) fn render(frame: &mut Frame, app: &mut AppState, area: Rect) {
} else {
info_lines.push(Line::from("- gpt: 1MiB bios_boot [bios_grub]"));
if app.swap_enabled {
info_lines.push(Line::from("- swap: 4GiB"));
info_lines.push(Line::from(format!("- swap: {swap_gib:.2}GiB")));
}
let enc = if app.disk_encryption_type_index == 1 {
" (LUKS)"
Expand Down
5 changes: 5 additions & 0 deletions src/render/sections/info/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ pub(super) fn render(frame: &mut Frame, app: &mut AppState, area: Rect) {
"Disabled"
};
info_lines.push(Line::from(format!("Swapon: {swap}")));
let ram_gib = app.detected_ram_mib as f64 / 1024.0;
let swap_gib = app.swap_size_mib as f64 / 1024.0;
info_lines.push(Line::from(format!(
"Detected RAM: {ram_gib:.1} GiB | Planned swap size: {swap_gib:.2} GiB"
)));
info_lines.push(Line::from(
"Swapon can be used to activate the swap partition.",
));
Expand Down
Loading
Loading