Compare commits

..

30 Commits

Author SHA1 Message Date
e611f174be Update os_install.py 2026-02-03 21:32:51 +01:00
b70ff549a7 Update partitioning.py 2026-02-03 21:31:17 +01:00
bb31814233 Update partitioning.py 2026-02-03 21:30:27 +01:00
00ba6e5c89 feat: verbose installation logs, improved auto-partitioning logic, and UI tweaks 2026-02-03 21:24:31 +01:00
7d43b82ce1 fix(ui): reorder partition dialog, fix mount point selection, add threaded install with logs 2026-02-03 21:06:16 +01:00
af86d357a4 fix(ui): correct mount point selection pre-fill and simplify partition creation dialog 2026-02-03 20:57:29 +01:00
848b2e7e74 fix: mount pseudo-fs during install, simplify partitioning, and add mount point config 2026-02-03 20:49:41 +01:00
dc417d15d3 fix(backend): use host config for dnf install to resolve repos 2026-02-03 20:29:03 +01:00
7a8e8ccbed fix(partitioning): avoid alignment errors by using fill-gap logic for full space 2026-02-03 20:19:25 +01:00
7371da2451 fix(backend): improve partition creation robustness and formatting 2026-02-03 20:03:24 +01:00
9f5bade34c feat(partitioning): support fat32 and add validation for system partition name 2026-02-03 19:53:51 +01:00
11fca148be fix(ui): handle NoneType fstype and remove duplicate code in partitioning page 2026-02-03 19:49:52 +01:00
b7a049d86f fix(ui): correct Gtk.Button instantiation in partitioning page 2026-02-03 18:13:04 +01:00
2676da751b Fix PartitioningPage being stuck on detecting due to duplicate function 2026-02-03 17:23:19 +01:00
22e1fa8f62 Implement manual partition management (create/delete/mountpoints) 2026-02-03 17:07:45 +01:00
efe25f3e11 Ensure disk device path has /dev/ prefix 2026-02-03 17:01:36 +01:00
2f9338af0a Fix logic error in page transitions 2026-02-03 16:43:19 +01:00
33d989cfad Hook up GUI Install button to backend logic 2026-02-03 16:34:36 +01:00
2377b0269a Implement minimal OS and Bootloader installation logic 2026-02-03 16:28:55 +01:00
3c4870b104 Implement basic partitioning backend and CLI interface 2026-02-03 16:20:59 +01:00
aae235e81f Format icons SVG files for better readability 2026-02-03 12:03:25 +01:00
759b4c4007 Use the icon 2026-02-03 01:26:30 +01:00
9958b9a65b Create org.iridium.Installer-symbolic.svg 2026-02-03 01:26:25 +01:00
1afdf99a6f Temporary (or probably permanent, you know how this works) icon 2026-02-03 01:17:48 +01:00
26757288c4 Add mock mode support and input validation 2026-02-02 20:19:36 +01:00
7a69d62782 Module titles 2026-02-02 20:15:40 +01:00
f691be219d Summary page 2026-02-02 20:05:11 +01:00
281f7488af Update README.md 2026-02-02 20:03:40 +01:00
931f701591 Center content vertically in user and welcome pages 2026-02-02 15:26:03 +01:00
52897a1dd0 Why the fuck was it in that order 2026-02-02 15:24:40 +01:00
16 changed files with 1180 additions and 96 deletions

View File

@@ -1,4 +1,17 @@
# Iridium OS Installer <div align="center">
<img src="data/icons/org.iridium.Installer.svg" alt="Iridium OS Installer Icon" width="128" height="128">
# Iridium OS Installer
</div>
> [!WARNING] > [!WARNING]
> For now this is only a mockup and not actually functional > For now this is only a mockup and not actually functional.
## What is Iridium Installer?
Iridium Installer is a modern, GTK4/Libadwaita based installer designed for Iridium OS. It aims to provide a simple and elegant installation experience.
## Features
- Modern UI using **GTK4** and **Libadwaita**
- Automatic and Manual partitioning modes
- User configuration
- Module selection

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg
height="16px"
viewBox="0 0 16 16"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 8 1 C 4.132812 1 1 4.132812 1 8 C 1 11.867188 4.132812 15 8 15 C 11.867188 15 15 11.867188 15 8 C 15 4.132812 11.867188 1 8 1 Z M 8 3 C 10.761719 3 13 5.238281 13 8 C 13 10.761719 10.761719 13 8 13 C 5.238281 13 3 10.761719 3 8 C 3 5.238281 5.238281 3 8 3 Z M 8 3"
fill="#2e3436"
/>
<path
d="M 8 5 C 6.34375 5 5 6.34375 5 8 C 5 9.65625 6.34375 11 8 11 C 9.65625 11 11 9.65625 11 8 C 11 6.34375 9.65625 5 8 5 Z M 8 6 C 9.105469 6 10 6.894531 10 8 C 10 9.105469 9.105469 10 8 10 C 6.894531 10 6 9.105469 6 8 C 6 6.894531 6.894531 6 8 6 Z M 8 6"
fill="#2e3436"
/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@@ -0,0 +1,39 @@
<svg
width="128"
height="128"
viewBox="0 0 128 128"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="circle-bg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#5E5C64" />
<stop offset="100%" stop-color="#3D3846" />
</linearGradient>
</defs>
<circle cx="64" cy="64" r="56" fill="url(#circle-bg)" />
<circle
cx="64"
cy="64"
r="40"
fill="none"
stroke="#F6D32D"
stroke-width="8"
stroke-dasharray="75 25"
stroke-linecap="round"
opacity="0.9"
/>
<circle
cx="64"
cy="64"
r="28"
fill="none"
stroke="#F5C211"
stroke-width="4"
stroke-dasharray="45 20"
stroke-linecap="round"
opacity="0.8"
/>
<circle cx="64" cy="64" r="10" fill="#F6D32D" />
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Iridium Installer
Comment=Install Iridium OS on your computer
Exec=iridium-installer
Icon=org.iridium.Installer
Type=Application
Categories=System;Settings;
StartupNotify=true
Terminal=false

View File

View File

@@ -0,0 +1,260 @@
import subprocess
import logging
import os
logger = logging.getLogger(__name__)
class CommandResult:
def __init__(self, stdout, stderr, returncode):
self.stdout = stdout
self.stderr = stderr
self.returncode = returncode
def run_command(cmd, check=True):
logger.info(f"Running command: {' '.join(cmd)}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
stdout_lines = []
stderr_lines = []
def read_stream(stream, line_list, log_level):
for line in stream:
line_clean = line.strip()
if line_clean:
log_level(line_clean)
line_list.append(line)
import threading
t1 = threading.Thread(target=read_stream, args=(process.stdout, stdout_lines, logger.info))
t2 = threading.Thread(target=read_stream, args=(process.stderr, stderr_lines, logger.error))
t1.start()
t2.start()
t1.join()
t2.join()
returncode = process.wait()
stdout_str = "".join(stdout_lines)
stderr_str = "".join(stderr_lines)
if check and returncode != 0:
raise subprocess.CalledProcessError(returncode, cmd, output=stdout_str, stderr=stderr_str)
return CommandResult(stdout_str, stderr_str, returncode)
def get_partition_device(disk_device, partition_number):
"""
Returns the partition device path.
Handles NVMe style (p1) vs sd style (1).
"""
if disk_device[-1].isdigit():
return f"{disk_device}p{partition_number}"
return f"{disk_device}{partition_number}"
def get_total_memory():
"""Returns total system memory in bytes."""
try:
return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
except (ValueError, OSError):
with open("/proc/meminfo", "r") as f:
for line in f:
if line.startswith("MemTotal:"):
parts = line.split()
return int(parts[1]) * 1024
return 0
def get_disk_size(disk_device):
"""Returns disk size in bytes using blockdev."""
try:
res = run_command(["blockdev", "--getsize64", disk_device])
return int(res.stdout.strip())
except Exception as e:
logger.error(f"Failed to get disk size: {e}")
return 0
def auto_partition_disk(disk_device):
"""
Automatically partitions the disk:
1. EFI System Partition (2GB standard, 1GB small)
2. Root (Remaining)
3. Swap (RAM + 2GB, or 0 if small)
Layout: P1=EFI, P3=Swap (End), P2=Root (Middle)
"""
logger.info(f"Starting auto-partitioning on {disk_device}")
# Calculate sizes
disk_size = get_disk_size(disk_device)
ram_size = get_total_memory()
# Defaults
efi_mb = 2048
swap_mb = int((ram_size / (1024*1024)) + 2048)
min_root_mb = 10240 # 10GB
total_required_mb = efi_mb + swap_mb + min_root_mb
disk_mb = disk_size / (1024*1024)
use_swap = True
if disk_mb < total_required_mb:
logger.warning("Disk too small for standard layout. Adjusting...")
efi_mb = 1024
use_swap = False
# Check minimal viability
if disk_mb < (efi_mb + min_root_mb):
raise Exception("Disk too small for installation (Need ~11GB)")
# 1. Zap the disk (destroy all data)
run_command(["sgdisk", "-Z", disk_device])
# 2. Create new GPT table
run_command(["sgdisk", "-o", disk_device])
# 3. Create EFI Partition (Part 1, Start)
run_command(["sgdisk", "-n", f"1:0:+{efi_mb}M", "-t", "1:ef00", "-c", "1:EFI System", disk_device])
# 4. Create Swap Partition (Part 3, End) - If enabled
if use_swap:
# sgdisk negative start is from end of disk
# We use partition 3 for Swap
run_command(["sgdisk", "-n", f"3:-{swap_mb}M:0", "-t", "3:8200", "-c", "3:Swap", disk_device])
# 5. Create Root Partition (Part 2, Fill Gap)
# This fills the space between P1 and P3 (or end if no swap)
run_command(["sgdisk", "-n", "2:0:0", "-t", "2:8300", "-c", "2:Root", disk_device])
# Inform kernel of changes
run_command(["partprobe", disk_device])
import time
time.sleep(1)
# 6. Format Partitions
efi_part = get_partition_device(disk_device, 1)
root_part = get_partition_device(disk_device, 2)
swap_part = get_partition_device(disk_device, 3) if use_swap else None
logger.info("Formatting EFI partition...")
run_command(["mkfs.vfat", "-F32", efi_part])
if use_swap:
logger.info("Formatting Swap partition...")
run_command(["mkswap", swap_part])
logger.info("Formatting Root partition...")
run_command(["mkfs.ext4", "-F", root_part])
logger.info("Partitioning and formatting complete.")
result = {
"efi": efi_part,
"root": root_part
}
if use_swap:
result["swap"] = swap_part
else:
# If no swap, we should probably return None or handle it in fstab generation
# backend/os_install.py expects "swap" key for UUID.
# We should probably pass a dummy or None, and update os_install to handle it.
result["swap"] = None
return result
def mount_partitions(partition_info, mount_root="/mnt"):
"""
Mounts the partitions into mount_root.
"""
import os
# 1. Mount Root
if not os.path.exists(mount_root):
os.makedirs(mount_root)
run_command(["mount", partition_info["root"], mount_root])
# 2. Mount EFI
efi_mount = os.path.join(mount_root, "boot/efi")
if not os.path.exists(efi_mount):
os.makedirs(efi_mount, exist_ok=True)
run_command(["mount", partition_info["efi"], efi_mount])
# 3. Enable Swap
if partition_info.get("swap"):
run_command(["swapon", partition_info["swap"]])
logger.info(f"Partitions mounted at {mount_root}")
def create_partition(disk_device, size_mb, type_code="8300", name="Linux filesystem", fstype=None):
"""
Creates a new partition on the disk and formats it.
type_code: ef00 (EFI), 8200 (Swap), 8300 (Linux)
fstype: ext4, fat32, swap
"""
# Find next available partition number
res = run_command(["sgdisk", "-p", disk_device])
# Very basic parsing to find the next number
existing_nums = []
for line in res.stdout.splitlines():
parts = line.split()
if parts and parts[0].isdigit():
existing_nums.append(int(parts[0]))
next_num = 1
while next_num in existing_nums:
next_num += 1
# Use -g (mbrtogpt) to ensure we can work on the disk if it was MBR
# size_mb=0 means use all available space in the gap
size_spec = f"+{size_mb}M" if size_mb > 0 else "0"
run_command([
"sgdisk",
"-g",
"-n", f"{next_num}:0:{size_spec}",
"-t", f"{next_num}:{type_code}",
"-c", f"{next_num}:{name}",
disk_device
])
run_command(["partprobe", disk_device])
# Wait for partition node to appear
try:
run_command(["udevadm", "settle", "--timeout=5"])
except Exception:
import time
time.sleep(2)
part_dev = get_partition_device(disk_device, next_num)
if fstype == "fat32":
run_command(["mkfs.vfat", "-F32", part_dev])
elif fstype == "ext4":
run_command(["mkfs.ext4", "-F", part_dev])
elif fstype == "swap":
run_command(["mkswap", part_dev])
return next_num
def delete_partition(disk_device, part_num):
"""Deletes a partition by number."""
run_command(["sgdisk", "-d", str(part_num), disk_device])
run_command(["partprobe", disk_device])
def wipe_disk(disk_device):
"""Zaps the disk and creates a new GPT table."""
run_command(["sgdisk", "-Z", disk_device])
run_command(["sgdisk", "-o", disk_device])
run_command(["partprobe", disk_device])

View File

@@ -0,0 +1,170 @@
import logging
import os
import subprocess
import sys
from contextlib import contextmanager
logger = logging.getLogger(__name__)
class CommandResult:
def __init__(self, stdout, stderr, returncode):
self.stdout = stdout
self.stderr = stderr
self.returncode = returncode
def run_command(cmd, check=True):
logger.info(f"Running command: {' '.join(cmd)}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1, # Line buffered
)
stdout_lines = []
stderr_lines = []
# Helper to read stream
def read_stream(stream, line_list, log_level):
for line in stream:
line_clean = line.strip()
if line_clean:
log_level(line_clean)
line_list.append(line)
import threading
t1 = threading.Thread(
target=read_stream, args=(process.stdout, stdout_lines, logger.info)
)
t2 = threading.Thread(
target=read_stream, args=(process.stderr, stderr_lines, logger.error)
)
t1.start()
t2.start()
t1.join()
t2.join()
returncode = process.wait()
stdout_str = "".join(stdout_lines)
stderr_str = "".join(stderr_lines)
if check and returncode != 0:
raise subprocess.CalledProcessError(
returncode, cmd, output=stdout_str, stderr=stderr_str
)
return CommandResult(stdout_str, stderr_str, returncode)
@contextmanager
def mount_pseudo_fs(mount_root):
"""
Context manager to bind mount /dev, /proc, and /sys into mount_root.
"""
logger.info(f"Mounting pseudo-filesystems to {mount_root}...")
mounts = ["dev", "proc", "sys"]
mounted_paths = []
try:
for fs in mounts:
target = os.path.join(mount_root, fs)
os.makedirs(target, exist_ok=True)
run_command(["mount", "--bind", f"/{fs}", target])
mounted_paths.append(target)
yield
finally:
logger.info(f"Unmounting pseudo-filesystems from {mount_root}...")
for path in reversed(mounted_paths):
try:
run_command(["umount", "-l", path])
except Exception as e:
logger.warning(f"Failed to unmount {path}: {e}")
def install_minimal_os(mount_root, releasever="43"):
"""
Installs minimal Fedora packages to mount_root.
"""
logger.info(f"Installing minimal Fedora {releasever} to {mount_root}...")
packages = [
"basesystem",
"bash",
"coreutils",
"kernel",
"systemd",
"dnf",
"grub2-efi-x64",
"shim-x64",
"efibootmgr",
"passwd",
"rootfiles",
"vim-minimal",
]
cmd = [
"dnf",
"install",
"-y",
f"--installroot={mount_root}",
f"--releasever={releasever}",
"--use-host-config",
"--setopt=install_weak_deps=False",
"--nodocs",
] + packages
with mount_pseudo_fs(mount_root):
run_command(cmd)
logger.info("Base system installation complete.")
def configure_system(mount_root, partition_info):
"""
Basic configuration: fstab and grub.
"""
logger.info("Configuring system...")
# 1. Generate fstab
def get_uuid(dev):
res = run_command(["blkid", "-s", "UUID", "-o", "value", dev])
return res.stdout.strip()
root_uuid = get_uuid(partition_info["root"])
efi_uuid = get_uuid(partition_info["efi"])
swap_entry = ""
if partition_info.get("swap"):
swap_uuid = get_uuid(partition_info["swap"])
swap_entry = f"UUID={swap_uuid} none swap defaults 0 0\n"
fstab_content = f"""
UUID={root_uuid} / ext4 defaults 1 1
UUID={efi_uuid} /boot/efi vfat defaults 0 2
{swap_entry}
"""
os.makedirs(os.path.join(mount_root, "etc"), exist_ok=True)
with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
f.write(fstab_content)
# 2. Configure GRUB
with mount_pseudo_fs(mount_root):
# grub2-mkconfig -o /boot/grub2/grub.cfg
chroot_cmd = [
"chroot",
mount_root,
"grub2-mkconfig",
"-o",
"/boot/grub2/grub.cfg",
]
run_command(chroot_cmd)
logger.info("System configuration complete.")

View File

@@ -1,31 +1,85 @@
import argparse
import sys import sys
import logging
import gi # Configure logging
logging.basicConfig(level=logging.INFO)
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, Gtk
from .ui.window import InstallerWindow
class IridiumInstallerApp(Adw.Application):
def __init__(self):
super().__init__(
application_id="org.iridium.Installer",
flags=Gio.ApplicationFlags.FLAGS_NONE,
)
def do_activate(self):
win = self.props.active_window
if not win:
win = InstallerWindow(application=self)
win.present()
def main(): def main():
app = IridiumInstallerApp() parser = argparse.ArgumentParser(description="Iridium OS Installer")
parser.add_argument(
"--mock",
action="store_true",
help="Run in mock mode (no actual changes to disk)",
)
parser.add_argument(
"--partition-disk",
help="Automatically partition the specified disk (WARNING: DESTROYS DATA)",
)
parser.add_argument(
"--full-install",
help="Run a full minimal installation on the specified disk (WARNING: DESTROYS DATA)",
)
args = parser.parse_args()
if args.partition_disk or args.full_install:
from .backend.disk import auto_partition_disk, mount_partitions
from .backend.os_install import install_minimal_os, configure_system
target = args.partition_disk or args.full_install
try:
print(f"Starting installation on {target}...")
print("Step 1: Partitioning...")
parts = auto_partition_disk(target)
if args.full_install:
print("Step 2: Mounting...")
mount_root = "/mnt"
mount_partitions(parts, mount_root)
print("Step 3: Installing OS (this may take a while)...")
install_minimal_os(mount_root)
print("Step 4: Configuring Bootloader...")
configure_system(mount_root, parts)
print("Installation complete! You can now reboot.")
else:
print("Partitioning successful!")
print(f"EFI: {parts['efi']}")
print(f"Swap: {parts['swap']}")
print(f"Root: {parts['root']}")
return 0
except Exception as e:
print(f"Installation failed: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
return 1
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gio
from .ui.window import InstallerWindow
class IridiumInstallerApp(Adw.Application):
def __init__(self, mock_mode=False):
super().__init__(
application_id="org.iridium.Installer",
flags=Gio.ApplicationFlags.FLAGS_NONE,
)
self.mock_mode = mock_mode
def do_activate(self):
win = self.props.active_window
if not win:
win = InstallerWindow(application=self, mock_mode=self.mock_mode)
win.present()
app = IridiumInstallerApp(mock_mode=args.mock)
return app.run(sys.argv) return app.run(sys.argv)

View File

@@ -13,6 +13,12 @@ class ModulesPage(Adw.Bin):
self.chromebook_audio = False self.chromebook_audio = False
self.android_apps = False self.android_apps = False
self.module_titles = {
"nvidia_drivers": "Proprietary NVIDIA Drivers",
"chromebook_audio": "Chromebook Audio Fixes",
"android_apps": "Android Apps Support",
}
# Main Layout # Main Layout
clamp = Adw.Clamp() clamp = Adw.Clamp()
clamp.set_maximum_size(600) clamp.set_maximum_size(600)
@@ -46,21 +52,21 @@ class ModulesPage(Adw.Bin):
# NVIDIA Drivers # NVIDIA Drivers
self.add_module_row( self.add_module_row(
"NVIDIA Proprietary Drivers", self.module_titles["nvidia_drivers"],
"Install proprietary drivers for NVIDIA graphics cards for better performance.", "Install proprietary drivers for NVIDIA graphics cards for better performance.",
"nvidia_drivers", "nvidia_drivers",
) )
# Chromebook Audio # Chromebook Audio
self.add_module_row( self.add_module_row(
"Chromebook Audio Fixes", self.module_titles["chromebook_audio"],
"Install additional audio drivers for Chromebook devices.", "Install additional audio drivers for Chromebook devices.",
"chromebook_audio", "chromebook_audio",
) )
# Android Apps # Android Apps
self.add_module_row( self.add_module_row(
"Android Apps Support", self.module_titles["android_apps"],
"Install Waydroid to run Android applications on Iridium OS.", "Install Waydroid to run Android applications on Iridium OS.",
"android_apps", "android_apps",
) )
@@ -85,8 +91,11 @@ class ModulesPage(Adw.Bin):
setattr(self, attr_name, switch.get_active()) setattr(self, attr_name, switch.get_active())
def get_modules(self): def get_modules(self):
return { enabled_modules = []
"nvidia_drivers": self.nvidia_drivers, if self.nvidia_drivers:
"chromebook_audio": self.chromebook_audio, enabled_modules.append(self.module_titles["nvidia_drivers"])
"android_apps": self.android_apps, if self.chromebook_audio:
} enabled_modules.append(self.module_titles["chromebook_audio"])
if self.android_apps:
enabled_modules.append(self.module_titles["android_apps"])
return enabled_modules

View File

@@ -43,7 +43,6 @@ CSS = """
def get_total_memory() -> int: def get_total_memory() -> int:
"""Returns total system memory in bytes."""
try: try:
return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
except (ValueError, OSError): except (ValueError, OSError):
@@ -56,12 +55,6 @@ def get_total_memory() -> int:
def calculate_auto_partitions(disk_device): def calculate_auto_partitions(disk_device):
"""
Generates an automatic partition layout.
- 2GB EFI
- RAM + 2GB Swap
- Rest Root (ext4)
"""
disk_size = 0 disk_size = 0
try: try:
# Get disk size in bytes # Get disk size in bytes
@@ -87,16 +80,24 @@ def calculate_auto_partitions(disk_device):
return [] return []
ram_size = get_total_memory() ram_size = get_total_memory()
disk_mb = disk_size / (1024 * 1024)
# Sizes in bytes # Defaults
efi_size = 2 * 1024 * 1024 * 1024 efi_size = 2 * 1024 * 1024 * 1024
swap_size = ram_size + (2 * 1024 * 1024 * 1024) swap_size = ram_size + (2 * 1024 * 1024 * 1024)
min_root_size = 10 * 1024 * 1024 * 1024 # 10GB
# Check if disk is large enough total_required = efi_size + swap_size + min_root_size
min_root = 10 * 1024 * 1024 * 1024 # minimum 10GB for root
if disk_size < (efi_size + swap_size + min_root): use_swap = True
print("Disk too small for automatic partitioning scheme.")
return [] if disk_size < total_required:
efi_size = 1 * 1024 * 1024 * 1024
use_swap = False
swap_size = 0
if disk_size < (efi_size + min_root_size):
print("Disk too small for automatic partitioning scheme.")
return []
root_size = disk_size - efi_size - swap_size root_size = disk_size - efi_size - swap_size
@@ -110,15 +111,6 @@ def calculate_auto_partitions(disk_device):
"bytes": efi_size, "bytes": efi_size,
"style_class": "part-efi", "style_class": "part-efi",
}, },
{
"type": "partition",
"name": "Swap",
"filesystem": "swap",
"mount_point": "[SWAP]",
"size": f"{swap_size / (1024**3):.1f} GB",
"bytes": swap_size,
"style_class": "part-swap",
},
{ {
"type": "partition", "type": "partition",
"name": "Root", "name": "Root",
@@ -130,6 +122,19 @@ def calculate_auto_partitions(disk_device):
}, },
] ]
if use_swap:
partitions.append(
{
"type": "partition",
"name": "Swap",
"filesystem": "swap",
"mount_point": "[SWAP]",
"size": f"{swap_size / (1024**3):.1f} GB",
"bytes": swap_size,
"style_class": "part-swap",
}
)
return partitions return partitions
@@ -236,6 +241,7 @@ class PartitioningPage(Adw.Bin):
# Initially empty until loaded with a specific disk # Initially empty until loaded with a specific disk
def load_partitions(self, disk_device=None): def load_partitions(self, disk_device=None):
self.current_disk_path = disk_device
target_disk = None target_disk = None
try: try:
@@ -290,7 +296,7 @@ class PartitioningPage(Adw.Bin):
p_size = int(p.get("size", 0)) p_size = int(p.get("size", 0))
covered_size += p_size covered_size += p_size
fstype = p.get("fstype", "").lower() fstype = (p.get("fstype") or "").lower()
style = "part-root" style = "part-root"
if "fat" in fstype or "efi" in fstype: if "fat" in fstype or "efi" in fstype:
style = "part-efi" style = "part-efi"
@@ -301,7 +307,7 @@ class PartitioningPage(Adw.Bin):
{ {
"type": "partition", "type": "partition",
"name": p.get("name"), "name": p.get("name"),
"filesystem": p.get("fstype", "Unknown"), "filesystem": p.get("fstype") or "Unknown",
"label": p.get("name"), "label": p.get("name"),
"mount_point": p.get("mountpoint", ""), "mount_point": p.get("mountpoint", ""),
"size": self.format_size(p_size), "size": self.format_size(p_size),
@@ -428,46 +434,184 @@ class PartitioningPage(Adw.Bin):
def show_context_menu(self, widget, x, y): def show_context_menu(self, widget, x, y):
data = widget.part_data data = widget.part_data
self.selected_disk_path = self.current_disk_path # Assumes we store this
popover = Gtk.Popover() popover = Gtk.Popover()
popover.set_parent(widget) popover.set_parent(widget)
menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
menu_box.set_spacing(0)
popover.set_child(menu_box) popover.set_child(menu_box)
def add_menu_item(label, icon_name=None, destructive=False): def add_menu_item(label, icon_name, callback, destructive=False):
btn = Gtk.Button(label=label) btn = Gtk.Button(label=label)
btn.add_css_class("flat") btn.add_css_class("flat")
btn.set_halign(Gtk.Align.FILL)
if destructive: if destructive:
btn.add_css_class("destructive-action") btn.add_css_class("destructive-action")
if icon_name: content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) icon = Gtk.Image.new_from_icon_name(icon_name)
content.set_spacing(12) lbl = Gtk.Label(label=label)
icon = Gtk.Image.new_from_icon_name(icon_name) content.append(icon)
lbl = Gtk.Label(label=label) content.append(lbl)
content.append(icon) btn.set_child(content)
content.append(lbl)
btn.set_child(content)
btn.connect("clicked", lambda b: [popover.popdown(), callback()])
menu_box.append(btn) menu_box.append(btn)
return btn
if data.get("type") == "partition": if data.get("type") == "partition":
add_menu_item("Select Mount Point", "folder-open-symbolic") add_menu_item(
add_menu_item("Format", "drive-harddisk-symbolic") "Select Mount Point",
add_menu_item("Resize", "object-resize-symbolic") "folder-open-symbolic",
lambda: self.select_mount_point(data),
)
separator = Gtk.Separator() separator = Gtk.Separator()
menu_box.append(separator) menu_box.append(separator)
btn_del = add_menu_item("Delete", "user-trash-symbolic", destructive=True) add_menu_item(
btn_del.add_css_class("error") "Delete",
"user-trash-symbolic",
lambda: self.delete_part(data),
destructive=True,
)
elif data.get("type") == "empty": elif data.get("type") == "empty":
add_menu_item("Create Partition", "list-add-symbolic") add_menu_item(
"Create Partition",
"list-add-symbolic",
lambda: self.create_part_dialog(data),
)
popover.popup() popover.popup()
def select_mount_point(self, data):
win = Adw.Window(
title="Select Mount Point", modal=True, transient_for=self.get_root()
)
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=12,
margin_top=24,
margin_bottom=24,
margin_start=24,
margin_end=24,
)
win.set_content(box)
options = ["/", "/boot/efi", "[SWAP]", "None"]
dropdown = Gtk.DropDown.new_from_strings(options)
current_mp = data.get("mount_point")
# Ensure we have a valid string comparison
if not current_mp:
current_mp = "None"
if current_mp in options:
dropdown.set_selected(options.index(current_mp))
else:
dropdown.set_selected(options.index("None"))
box.append(Gtk.Label(label=f"Mount point for {data['name']}:"))
box.append(dropdown)
def on_apply(b):
selected = options[dropdown.get_selected()]
data["mount_point"] = selected if selected != "None" else ""
win.close()
self.refresh_bar()
btn = Gtk.Button(label="Apply")
btn.add_css_class("suggested-action")
btn.connect("clicked", on_apply)
box.append(btn)
win.present()
def delete_part(self, data):
# Extract partition number from name (e.g., nvme0n1p3 -> 3)
import re
from ...backend.disk import delete_partition
match = re.search(r"(\d+)$", data["name"])
if match:
part_num = int(match.group(1))
delete_partition(self.current_disk_path, part_num)
self.load_partitions(self.current_disk_path)
def create_part_dialog(self, data):
win = Adw.Window(
title="Create Partition", modal=True, transient_for=self.get_root()
)
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=12,
margin_top=24,
margin_bottom=24,
margin_start=24,
margin_end=24,
)
win.set_content(box)
# Type dropdown
types = {"Root (Linux)": "8300", "EFI System": "ef00", "Swap": "8200"}
type_names = list(types.keys())
type_dropdown = Gtk.DropDown.new_from_strings(type_names)
box.append(Gtk.Label(label="Partition Type:"))
box.append(type_dropdown)
# Size entry
size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
size_entry = Gtk.Entry(text=str(int(data["bytes"] / (1024 * 1024))))
size_entry.set_width_chars(15)
size_box.append(size_entry)
size_box.append(Gtk.Label(label="MB"))
box.append(Gtk.Label(label="Size:"))
box.append(size_box)
error_label = Gtk.Label(label="")
error_label.add_css_class("error")
box.append(error_label)
def on_create(b):
from ...backend.disk import create_partition
selected_type = type_names[type_dropdown.get_selected()]
# Default name and fstype based on type
if selected_type == "EFI System":
name = "EFI System"
fstype = "fat32"
elif selected_type == "Swap":
name = "Swap"
fstype = "swap"
else:
name = "Linux filesystem"
fstype = "ext4"
size_text = size_entry.get_text()
try:
size_mb = int(size_text)
max_mb = int(data["bytes"] / (1024 * 1024))
if size_mb >= max_mb:
size_mb = 0
except ValueError:
error_label.set_text("Invalid size value")
return
type_code = types[selected_type]
try:
create_partition(
self.current_disk_path, size_mb, type_code, name, fstype
)
win.close()
self.load_partitions(self.current_disk_path)
except Exception as e:
error_label.set_text(f"Error: {str(e)}")
btn = Gtk.Button(label="Create")
btn.add_css_class("suggested-action")
btn.connect("clicked", on_create)
box.append(btn)
win.present()
def get_config(self): def get_config(self):
return {"partitions": self.partitions} return {"partitions": self.partitions}

View File

@@ -89,8 +89,13 @@ class StoragePage(Adw.Bin):
def on_disk_toggled(self, button, device_name): def on_disk_toggled(self, button, device_name):
if button.get_active(): if button.get_active():
self.selected_disk = device_name # Prepend /dev/ if it's just the name
self.emit("disk-selected", device_name) full_path = device_name
if not device_name.startswith("/dev/"):
full_path = f"/dev/{device_name}"
self.selected_disk = full_path
self.emit("disk-selected", full_path)
def get_selected_disk(self): def get_selected_disk(self):
return self.selected_disk return self.selected_disk

View File

@@ -0,0 +1,85 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class SummaryPage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.status_page = Adw.StatusPage()
self.status_page.set_title("Summary")
self.status_page.set_description("Review your choices before installing")
self.status_page.set_icon_name("document-save-symbolic")
self.set_child(self.status_page)
# Main content box
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.status_page.set_child(clamp)
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
content_box.set_spacing(24)
clamp.set_child(content_box)
# Storage and partitioning
self.storage_group = Adw.PreferencesGroup()
self.storage_group.set_title("Storage & Partitioning")
content_box.append(self.storage_group)
self.disk_row = Adw.ActionRow()
self.disk_row.set_title("Target Disk")
self.storage_group.add(self.disk_row)
self.mode_row = Adw.ActionRow()
self.mode_row.set_title("Partitioning Mode")
self.storage_group.add(self.mode_row)
# User configuration
self.user_group = Adw.PreferencesGroup()
self.user_group.set_title("User Configuration")
content_box.append(self.user_group)
self.fullname_row = Adw.ActionRow()
self.fullname_row.set_title("Full Name")
self.user_group.add(self.fullname_row)
self.username_row = Adw.ActionRow()
self.username_row.set_title("Username")
self.user_group.add(self.username_row)
self.hostname_row = Adw.ActionRow()
self.hostname_row.set_title("Hostname")
self.user_group.add(self.hostname_row)
# Software
self.software_group = Adw.PreferencesGroup()
self.software_group.set_title("Software")
content_box.append(self.software_group)
self.modules_row = Adw.ActionRow()
self.modules_row.set_title("Additional Modules")
self.software_group.add(self.modules_row)
def update_summary(self, disk_info, mode, partitions, user_info, modules):
# Update Disk
self.disk_row.set_subtitle(str(disk_info))
# Update Mode
self.mode_row.set_subtitle(mode.capitalize())
# Update User Info
self.fullname_row.set_subtitle(user_info.get("fullname", ""))
self.username_row.set_subtitle(user_info.get("username", ""))
self.hostname_row.set_subtitle(user_info.get("hostname", ""))
# Update Modules
if modules:
self.modules_row.set_subtitle(", ".join(modules))
else:
self.modules_row.set_subtitle("None")

View File

@@ -1,20 +1,29 @@
import re
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, GObject, Gtk
class UserPage(Adw.Bin): class UserPage(Adw.Bin):
__gsignals__ = {
"validity-changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_valid = False
clamp = Adw.Clamp() clamp = Adw.Clamp()
clamp.set_maximum_size(500) clamp.set_maximum_size(500)
self.set_child(clamp) self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24) box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24) box.set_margin_top(24)
box.set_margin_bottom(24) box.set_margin_bottom(24)
clamp.set_child(box) clamp.set_child(box)
@@ -31,6 +40,7 @@ class UserPage(Adw.Bin):
self.fullname_row.set_title("Full Name") self.fullname_row.set_title("Full Name")
self.fullname_row.set_text("Administrator") self.fullname_row.set_text("Administrator")
self.fullname_row.connect("notify::text", self.on_fullname_changed) self.fullname_row.connect("notify::text", self.on_fullname_changed)
self.fullname_row.connect("notify::text", self.on_input_changed)
group.add(self.fullname_row) group.add(self.fullname_row)
self.username_row = Adw.EntryRow() self.username_row = Adw.EntryRow()
@@ -39,14 +49,17 @@ class UserPage(Adw.Bin):
self.username_handler_id = self.username_row.connect( self.username_handler_id = self.username_row.connect(
"notify::text", self.on_username_changed "notify::text", self.on_username_changed
) )
self.username_row.connect("notify::text", self.on_input_changed)
group.add(self.username_row) group.add(self.username_row)
self.password_row = Adw.PasswordEntryRow() self.password_row = Adw.PasswordEntryRow()
self.password_row.set_title("Password") self.password_row.set_title("Password")
self.password_row.connect("notify::text", self.on_input_changed)
group.add(self.password_row) group.add(self.password_row)
self.confirm_row = Adw.PasswordEntryRow() self.confirm_row = Adw.PasswordEntryRow()
self.confirm_row.set_title("Confirm Password") self.confirm_row.set_title("Confirm Password")
self.confirm_row.connect("notify::text", self.on_input_changed)
group.add(self.confirm_row) group.add(self.confirm_row)
# Hostname # Hostname
@@ -57,10 +70,14 @@ class UserPage(Adw.Bin):
self.hostname_row = Adw.EntryRow() self.hostname_row = Adw.EntryRow()
self.hostname_row.set_title("Hostname") self.hostname_row.set_title("Hostname")
self.hostname_row.set_text("iridium") self.hostname_row.set_text("iridium")
self.hostname_row.connect("notify::text", self.on_input_changed)
host_group.add(self.hostname_row) host_group.add(self.hostname_row)
self.username_manually_set = False self.username_manually_set = False
# Initial validation
self.validate()
def on_fullname_changed(self, entry, _pspec): def on_fullname_changed(self, entry, _pspec):
if self.username_manually_set: if self.username_manually_set:
return return
@@ -112,6 +129,53 @@ class UserPage(Adw.Bin):
def on_username_changed(self, entry, _pspec): def on_username_changed(self, entry, _pspec):
self.username_manually_set = True self.username_manually_set = True
def on_input_changed(self, *args):
self.validate()
def validate(self):
valid = True
# Username validation
username = self.username_row.get_text()
if not username:
valid = False
self.username_row.add_css_class("error")
elif not re.match(r"^[a-z_][a-z0-9_-]*$", username):
valid = False
self.username_row.add_css_class("error")
else:
self.username_row.remove_css_class("error")
# Password validation
pw = self.password_row.get_text()
confirm = self.confirm_row.get_text()
if not pw:
valid = False
# Don't show error for empty password initially, just invalidate
# It looks a lot nicer like that
if pw != confirm:
valid = False
self.confirm_row.add_css_class("error")
else:
self.confirm_row.remove_css_class("error")
# Hostname validation
hostname = self.hostname_row.get_text()
if not hostname:
valid = False
self.hostname_row.add_css_class("error")
else:
self.hostname_row.remove_css_class("error")
if self._is_valid != valid:
self._is_valid = valid
self.emit("validity-changed", valid)
def is_valid(self):
return self._is_valid
def get_user_info(self): def get_user_info(self):
return { return {
"fullname": self.fullname_row.get_text(), "fullname": self.fullname_row.get_text(),

View File

@@ -1,8 +1,10 @@
import os
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, Gdk, Gtk
class WelcomePage(Adw.Bin): class WelcomePage(Adw.Bin):
@@ -12,7 +14,25 @@ class WelcomePage(Adw.Bin):
page = Adw.StatusPage() page = Adw.StatusPage()
page.set_title("Welcome to Iridium OS") page.set_title("Welcome to Iridium OS")
page.set_description("The installation will begin shortly") page.set_description("The installation will begin shortly")
page.set_icon_name("system-software-install-symbolic")
# Load custom icon
icon_path = os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"data",
"icons",
"org.iridium.Installer.svg",
)
if os.path.exists(icon_path):
try:
paintable = Gdk.Texture.new_from_filename(icon_path)
page.set_paintable(paintable)
except Exception:
page.set_icon_name("system-software-install-symbolic")
else:
page.set_icon_name("system-software-install-symbolic")
# Content Box # Content Box
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
@@ -28,7 +48,6 @@ class WelcomePage(Adw.Bin):
"English", "English",
] ]
dropdown = Gtk.DropDown.new_from_strings(languages) dropdown = Gtk.DropDown.new_from_strings(languages)
dropdown.set_margin_bottom(20)
box.append(dropdown) box.append(dropdown)
page.set_child(box) page.set_child(box)

View File

@@ -1,23 +1,39 @@
import gi import gi
import threading
import logging
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, Gtk, GLib
from .pages.additional_modules import ModulesPage from .pages.additional_modules import ModulesPage
from .pages.install_mode import InstallModePage from .pages.install_mode import InstallModePage
from .pages.partitioning import PartitioningPage, calculate_auto_partitions from .pages.partitioning import PartitioningPage, calculate_auto_partitions
from .pages.storage import StoragePage from .pages.storage import StoragePage
from .pages.summary import SummaryPage
from .pages.user import UserPage from .pages.user import UserPage
from .pages.welcome import WelcomePage from .pages.welcome import WelcomePage
class LogHandler(logging.Handler):
def __init__(self, callback):
super().__init__()
self.callback = callback
def emit(self, record):
msg = self.format(record)
GLib.idle_add(self.callback, msg)
class InstallerWindow(Adw.ApplicationWindow): class InstallerWindow(Adw.ApplicationWindow):
def __init__(self, *args, **kwargs): def __init__(self, mock_mode=False, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.mock_mode = mock_mode
self.set_default_size(900, 650) self.set_default_size(900, 650)
self.set_title("Iridium Installer") self.set_title("Iridium Installer" + (" (MOCK MODE)" if mock_mode else ""))
self.set_icon_name("org.iridium.Installer")
self.toolbar_view = Adw.ToolbarView() self.toolbar_view = Adw.ToolbarView()
self.set_content(self.toolbar_view) self.set_content(self.toolbar_view)
@@ -70,6 +86,8 @@ class InstallerWindow(Adw.ApplicationWindow):
self.partitioning_page = PartitioningPage() self.partitioning_page = PartitioningPage()
self.modules_page = ModulesPage() self.modules_page = ModulesPage()
self.user_page = UserPage() self.user_page = UserPage()
self.user_page.connect("validity-changed", self.on_user_validity_changed)
self.summary_page = SummaryPage()
# Add Pages # Add Pages
self.add_page(self.welcome_page, "welcome") self.add_page(self.welcome_page, "welcome")
@@ -78,11 +96,14 @@ class InstallerWindow(Adw.ApplicationWindow):
self.add_page(self.partitioning_page, "partitioning") self.add_page(self.partitioning_page, "partitioning")
self.add_page(self.modules_page, "modules") self.add_page(self.modules_page, "modules")
self.add_page(self.user_page, "user") self.add_page(self.user_page, "user")
self.add_page(self.summary_page, "summary")
# Initialize view # Initialize view
if self.page_ids: if self.page_ids:
self.stack.set_visible_child_name(self.page_ids[0]) self.stack.set_visible_child_name(self.page_ids[0])
self.update_buttons() self.update_buttons()
self.log_handler = None
def add_page(self, widget, name): def add_page(self, widget, name):
self.stack.add_named(widget, name) self.stack.add_named(widget, name)
@@ -91,6 +112,9 @@ class InstallerWindow(Adw.ApplicationWindow):
def on_disk_selected(self, page, device_name): def on_disk_selected(self, page, device_name):
self.update_buttons() self.update_buttons()
def on_user_validity_changed(self, page, valid):
self.update_buttons()
def update_buttons(self): def update_buttons(self):
# Back button state # Back button state
self.back_button.set_sensitive(self.current_page_index > 0) self.back_button.set_sensitive(self.current_page_index > 0)
@@ -102,11 +126,15 @@ class InstallerWindow(Adw.ApplicationWindow):
forced_selection = True forced_selection = True
if current_page_name == "storage": if current_page_name == "storage":
forced_selection = self.storage_page.get_selected_disk() is not None forced_selection = self.storage_page.get_selected_disk() is not None
elif current_page_name == "user":
forced_selection = self.user_page.is_valid()
self.next_button.set_sensitive(forced_selection) self.next_button.set_sensitive(forced_selection)
if self.current_page_index == len(self.page_ids) - 1: if current_page_name == "summary":
self.next_button.set_label("Install") self.next_button.set_label(
"Install" + (" (MOCK)" if self.mock_mode else "")
)
self.next_button.add_css_class("destructive-action") self.next_button.add_css_class("destructive-action")
self.next_button.remove_css_class("suggested-action") self.next_button.remove_css_class("suggested-action")
else: else:
@@ -131,6 +159,117 @@ class InstallerWindow(Adw.ApplicationWindow):
self.stack.set_visible_child_name(self.page_ids[self.current_page_index]) self.stack.set_visible_child_name(self.page_ids[self.current_page_index])
self.update_buttons() self.update_buttons()
def show_progress_page(self, message):
prog_page = Adw.StatusPage()
prog_page.set_title(message)
prog_page.set_description("Please wait while we set up your system.")
spinner = Gtk.Spinner()
spinner.set_size_request(32, 32)
spinner.set_halign(Gtk.Align.CENTER)
spinner.start()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
box.append(spinner)
# Log view
self.log_buffer = Gtk.TextBuffer()
self.log_view = Gtk.TextView(buffer=self.log_buffer)
self.log_view.set_editable(False)
self.log_view.set_monospace(True)
self.log_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
scrolled = Gtk.ScrolledWindow()
scrolled.set_child(self.log_view)
scrolled.set_vexpand(True)
scrolled.set_size_request(-1, 200)
# Style the log view background
# css_provider = Gtk.CssProvider()
# css_provider.load_from_data(b"textview { background-color: #1e1e1e; color: #ffffff; }")
# self.log_view.get_style_context().add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
expander = Gtk.Expander(label="Detailed Logs")
expander.set_child(scrolled)
box.append(expander)
prog_page.set_child(box)
name = "progress_install"
self.stack.add_named(prog_page, name)
self.stack.set_visible_child_name(name)
# Hide navigation buttons during install
self.bottom_bar.set_visible(False)
# Attach log handler
self.log_handler = LogHandler(self.append_log)
logging.getLogger().addHandler(self.log_handler)
logging.getLogger().setLevel(logging.INFO)
def append_log(self, msg):
end_iter = self.log_buffer.get_end_iter()
self.log_buffer.insert(end_iter, msg + "\n")
# Auto-scroll
mark = self.log_buffer.create_mark("end", end_iter, False)
self.log_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
def show_finish_page(self, message, success=True):
if self.log_handler:
logging.getLogger().removeHandler(self.log_handler)
self.log_handler = None
finish_page = Adw.StatusPage()
finish_page.set_title(message)
if success:
finish_page.set_icon_name("emblem-ok-symbolic")
finish_page.set_description("You can now restart your computer.")
btn_label = "Restart"
else:
finish_page.set_icon_name("dialog-error-symbolic")
finish_page.set_description("An error occurred during installation.")
btn_label = "Close"
btn = Gtk.Button(label=btn_label)
btn.set_halign(Gtk.Align.CENTER)
btn.add_css_class("suggested-action")
btn.connect("clicked", lambda b: self.close())
finish_page.set_child(btn)
name = "finish_install"
self.stack.add_named(finish_page, name)
self.stack.set_visible_child_name(name)
def run_installation(self, disk, mode, modules, user_info):
try:
from ..backend.disk import auto_partition_disk, mount_partitions
from ..backend.os_install import install_minimal_os, configure_system
# Step 1: Partitioning
logging.info("Step 1: Partitioning...")
parts = auto_partition_disk(disk)
# Step 2: Mounting
logging.info("Step 2: Mounting...")
mount_root = "/mnt"
mount_partitions(parts, mount_root)
# Step 3: OS Installation
logging.info("Step 3: OS Installation...")
install_minimal_os(mount_root)
# Step 4: Configure
logging.info("Step 4: Configuration...")
configure_system(mount_root, parts)
GLib.idle_add(self.show_finish_page, "Installation Successful!", True)
except Exception as e:
logging.error(f"Installation failed: {e}")
import traceback
traceback.print_exc()
GLib.idle_add(self.show_finish_page, f"Installation Failed: {e}", False)
def on_next_clicked(self, button): def on_next_clicked(self, button):
# Logic before transition # Logic before transition
current_page_name = self.page_ids[self.current_page_index] current_page_name = self.page_ids[self.current_page_index]
@@ -151,15 +290,11 @@ class InstallerWindow(Adw.ApplicationWindow):
next_index = self.page_ids.index("partitioning") next_index = self.page_ids.index("partitioning")
if current_page_name == "user": if current_page_name == "user":
print("Install process triggered!") # Prepare summary instead of installing immediately
disk = self.storage_page.get_selected_disk() disk = self.storage_page.get_selected_disk()
print(f"Disk: {disk}")
mode = self.install_mode_page.get_mode() mode = self.install_mode_page.get_mode()
print(f"Mode: {mode}")
modules = self.modules_page.get_modules() modules = self.modules_page.get_modules()
print(f"Modules: {modules}")
user_info = self.user_page.get_user_info() user_info = self.user_page.get_user_info()
print(f"User: {user_info}")
partitions_config = {} partitions_config = {}
if mode == "manual": if mode == "manual":
@@ -168,10 +303,45 @@ class InstallerWindow(Adw.ApplicationWindow):
partitions = calculate_auto_partitions(disk) partitions = calculate_auto_partitions(disk)
partitions_config = {"partitions": partitions} partitions_config = {"partitions": partitions}
print(f"Partitioning: {partitions_config}") # Update summary page
self.summary_page.update_summary(
disk_info=disk,
mode=mode,
partitions=partitions_config,
user_info=user_info,
modules=modules,
)
# Proceed to summary page (which is next_index after user)
if current_page_name == "summary":
# THIS IS THE REAL INSTALL TRIGGER
print("Install process triggered!")
disk = self.storage_page.get_selected_disk()
mode = self.install_mode_page.get_mode()
modules = self.modules_page.get_modules()
user_info = self.user_page.get_user_info()
if self.mock_mode:
print("!!! MOCK MODE ENABLED - NO CHANGES WILL BE MADE !!!")
print(f"Target Disk: {disk}")
print(f"Mode: {mode}")
print(f"Modules: {modules}")
print(f"User: {user_info}")
print("Simulation complete.")
# Show success in UI even in mock
self.show_finish_page("Mock Installation Complete!")
else:
self.show_progress_page("Installing Iridium OS...")
thread = threading.Thread(
target=self.run_installation,
args=(disk, mode, modules, user_info),
daemon=True
)
thread.start()
return return
if next_index < len(self.page_ids): if next_index < len(self.page_ids):
self.current_page_index = next_index self.current_page_index = next_index
self.stack.set_visible_child_name(self.page_ids[self.current_page_index]) self.stack.set_visible_child_name(self.page_ids[self.current_page_index])
self.update_buttons() self.update_buttons()

31
run.sh
View File

@@ -1,5 +1,32 @@
#!/bin/bash #!/bin/bash
# Install icons to ~/.local/share/icons if not already installed
ICON_DIR="$HOME/.local/share/icons/hicolor/128x128/apps"
SYMBOLIC_DIR="$HOME/.local/share/icons/hicolor/symbolic/apps"
mkdir -p "$ICON_DIR"
mkdir -p "$SYMBOLIC_DIR"
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Copy main icon if not exists or if source is newer
if [ ! -f "$ICON_DIR/org.iridium.Installer.svg" ] || [ "${SCRIPT_DIR}/data/icons/org.iridium.Installer.svg" -nt "$ICON_DIR/org.iridium.Installer.svg" ]; then
cp "${SCRIPT_DIR}/data/icons/org.iridium.Installer.svg" "$ICON_DIR/"
echo "Installed 128x128 icon"
fi
# Copy symbolic icon if not exists or if source is newer
if [ ! -f "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg" ] || [ "${SCRIPT_DIR}/data/icons/org.iridium.Installer-symbolic.svg" -nt "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg" ]; then
cp "${SCRIPT_DIR}/data/icons/org.iridium.Installer-symbolic.svg" "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg"
echo "Installed symbolic icon"
fi
# Update icon cache if gtk-update-icon-cache exists
if command -v gtk-update-icon-cache &> /dev/null; then
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
fi
export GSETTINGS_SCHEMA_DIR=. export GSETTINGS_SCHEMA_DIR=.
echo "Starting Iridium Installer..." python3 -m iridium_installer.main "$@"
python3 -m iridium_installer.main