Compare commits

...

43 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
2c7b9c5b1f Automatic usernames 2026-02-02 15:20:23 +01:00
658040c138 Better - force selection, gray out button 2026-02-02 15:08:03 +01:00
7c899b8e86 Can't continue without selecting disk 2026-02-02 15:03:30 +01:00
a098f20a38 English as the only language option 2026-02-02 14:59:35 +01:00
3bff26928a Create additional_modules.py 2026-02-02 14:59:20 +01:00
001d07cc80 Update warning label to use WARNING instead of WARN 2026-02-02 12:22:13 +01:00
ae16c2ca23 Update README.md 2026-02-02 12:20:30 +01:00
0f448bfa6c Automatic partition sizes 2026-02-02 12:17:13 +01:00
f341fe0d60 Windows 8-aah welcome message 2026-02-02 12:03:06 +01:00
f642d7ef94 No toolbar, like a proper adwaita app should 2026-02-02 12:00:57 +01:00
f8b9cb62f9 Proper automatic/manual partitioning switch 2026-02-02 11:50:44 +01:00
62e1276881 Partition screen 2026-02-01 21:00:44 +01:00
17eac3de50 Actual disk names 2026-02-01 20:37:29 +01:00
17 changed files with 2010 additions and 108 deletions

View File

@@ -0,0 +1,17 @@
<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]
> 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") def main():
gi.require_version("Adw", "1") 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()
from gi.repository import Adw, Gio, Gtk 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
from .ui.window import InstallerWindow target = args.partition_disk or args.full_install
try:
print(f"Starting installation on {target}...")
print("Step 1: Partitioning...")
parts = auto_partition_disk(target)
class IridiumInstallerApp(Adw.Application): if args.full_install:
def __init__(self): 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__( super().__init__(
application_id="org.iridium.Installer", application_id="org.iridium.Installer",
flags=Gio.ApplicationFlags.FLAGS_NONE, flags=Gio.ApplicationFlags.FLAGS_NONE,
) )
self.mock_mode = mock_mode
def do_activate(self): def do_activate(self):
win = self.props.active_window win = self.props.active_window
if not win: if not win:
win = InstallerWindow(application=self) win = InstallerWindow(application=self, mock_mode=self.mock_mode)
win.present() win.present()
app = IridiumInstallerApp(mock_mode=args.mock)
def main():
app = IridiumInstallerApp()
return app.run(sys.argv) return app.run(sys.argv)

View File

@@ -0,0 +1,101 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class ModulesPage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.nvidia_drivers = False
self.chromebook_audio = 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
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24)
box.set_margin_bottom(24)
clamp.set_child(box)
# Title
title = Gtk.Label(label="Additional Modules")
title.add_css_class("title-1")
box.append(title)
descr = Gtk.Label(
label="Select additional components you would like to install. (Internet connection required)"
)
descr.set_wrap(True)
box.append(descr)
dont_worry = Gtk.Label(label="Don't worry, you can always install them later.")
dont_worry.set_wrap(True)
box.append(dont_worry)
# Preferences Group
self.modules_group = Adw.PreferencesGroup()
self.modules_group.set_title("Optional Features")
box.append(self.modules_group)
# NVIDIA Drivers
self.add_module_row(
self.module_titles["nvidia_drivers"],
"Install proprietary drivers for NVIDIA graphics cards for better performance.",
"nvidia_drivers",
)
# Chromebook Audio
self.add_module_row(
self.module_titles["chromebook_audio"],
"Install additional audio drivers for Chromebook devices.",
"chromebook_audio",
)
# Android Apps
self.add_module_row(
self.module_titles["android_apps"],
"Install Waydroid to run Android applications on Iridium OS.",
"android_apps",
)
def add_module_row(self, title, subtitle, attr_name):
row = Adw.ActionRow()
row.set_title(title)
row.set_subtitle(subtitle)
switch = Gtk.Switch()
switch.set_valign(Gtk.Align.CENTER)
# Set initial state
switch.set_active(getattr(self, attr_name))
# Connect signal
switch.connect("notify::active", self.on_switch_toggled, attr_name)
row.add_suffix(switch)
self.modules_group.add(row)
def on_switch_toggled(self, switch, gparam, attr_name):
setattr(self, attr_name, switch.get_active())
def get_modules(self):
enabled_modules = []
if self.nvidia_drivers:
enabled_modules.append(self.module_titles["nvidia_drivers"])
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

@@ -0,0 +1,74 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class InstallModePage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mode = "automatic"
# Main Layout
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER) # Vertically Center
box.set_margin_top(24)
box.set_margin_bottom(24)
clamp.set_child(box)
# Title
title = Gtk.Label(label="Installation Type")
title.add_css_class("title-1")
box.append(title)
descr = Gtk.Label(label="How would you like to install Iridium OS?")
descr.set_wrap(True)
box.append(descr)
# Selection Group
group = Adw.PreferencesGroup()
box.append(group)
# Automatic
self.auto_row = Adw.ActionRow()
self.auto_row.set_title("Automatic")
self.auto_row.set_subtitle(
"Erase the selected disk and install Iridium. (Recommended)"
)
self.auto_row.set_icon_name("drive-harddisk-solidstate-symbolic")
group.add(self.auto_row)
self.auto_radio = Gtk.CheckButton()
self.auto_radio.set_active(True)
self.auto_radio.connect("toggled", self.on_mode_toggled, "automatic")
self.auto_row.add_suffix(self.auto_radio)
self.auto_row.set_activatable_widget(self.auto_radio)
# Manual
self.manual_row = Adw.ActionRow()
self.manual_row.set_title("Manual Partitioning")
self.manual_row.set_subtitle(
"Create or resize partitions yourself. For advanced users."
)
self.manual_row.set_icon_name("preferences-system-symbolic")
group.add(self.manual_row)
self.manual_radio = Gtk.CheckButton()
self.manual_radio.set_group(self.auto_radio)
self.manual_radio.connect("toggled", self.on_mode_toggled, "manual")
self.manual_row.add_suffix(self.manual_radio)
self.manual_row.set_activatable_widget(self.manual_radio)
def on_mode_toggled(self, button, mode_name):
if button.get_active():
self.mode = mode_name
def get_mode(self):
return self.mode

View File

@@ -0,0 +1,617 @@
import json
import os
import subprocess
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gdk, Gio, GObject, Gtk
CSS = """
.part-segment {
border-radius: 0;
box-shadow: none;
border: none;
}
.part-segment:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.part-segment:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.part-efi {
background-color: #3584e4; /* Blue */
color: white;
}
.part-root {
background-color: #2ec27e; /* Green */
color: white;
}
.part-swap {
background-color: #e66100; /* Orange */
color: white;
}
.part-empty {
background-color: #deddda; /* Gray */
color: black;
}
"""
def get_total_memory() -> int:
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 calculate_auto_partitions(disk_device):
disk_size = 0
try:
# Get disk size in bytes
result = subprocess.run(
["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE"],
capture_output=True,
text=True,
)
if result.returncode == 0:
data = json.loads(result.stdout)
devices = data.get("blockdevices", [])
dev_name = disk_device.replace("/dev/", "")
for dev in devices:
if dev.get("name") == dev_name:
disk_size = int(dev.get("size", 0))
break
except Exception as e:
print(f"Error getting disk size for auto partitioning: {e}")
return []
if disk_size == 0:
return []
ram_size = get_total_memory()
disk_mb = disk_size / (1024 * 1024)
# Defaults
efi_size = 2 * 1024 * 1024 * 1024
swap_size = ram_size + (2 * 1024 * 1024 * 1024)
min_root_size = 10 * 1024 * 1024 * 1024 # 10GB
total_required = efi_size + swap_size + min_root_size
use_swap = True
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
partitions = [
{
"type": "partition",
"name": "EFI System",
"filesystem": "vfat",
"mount_point": "/boot/efi",
"size": f"{efi_size / (1024**3):.1f} GB",
"bytes": efi_size,
"style_class": "part-efi",
},
{
"type": "partition",
"name": "Root",
"filesystem": "ext4",
"mount_point": "/",
"size": f"{root_size / (1024**3):.1f} GB",
"bytes": root_size,
"style_class": "part-root",
},
]
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
class PartitionSegment(Gtk.Button):
def __init__(self, part_data, page, *args, **kwargs):
super().__init__(*args, **kwargs)
self.part_data = part_data
self.page = page
self.add_css_class("part-segment")
self.add_css_class(part_data["style_class"])
self.set_hexpand(part_data.get("expand", False))
if "width_request" in part_data:
self.set_size_request(part_data["width_request"], -1)
if part_data.get("label"):
lbl = Gtk.Label(label=part_data["label"])
lbl.set_ellipsize(3)
self.set_child(lbl)
self.connect("clicked", self.on_clicked)
right_click = Gtk.GestureClick()
right_click.set_button(3)
right_click.connect("pressed", self.on_right_click)
self.add_controller(right_click)
def on_clicked(self, button):
self.page.show_details_window(self.part_data)
def on_right_click(self, gesture, n_press, x, y):
self.page.show_context_menu(self, x, y)
class PartitioningPage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
provider = Gtk.CssProvider()
provider.load_from_data(CSS.encode())
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
clamp = Adw.Clamp()
clamp.set_maximum_size(800)
self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24)
box.set_margin_bottom(24)
clamp.set_child(box)
title = Gtk.Label(label="Manual Partitioning")
title.add_css_class("title-1")
box.append(title)
descr = Gtk.Label(
label="Modify the partition layout below. Right-click segments for options."
)
descr.set_wrap(True)
box.append(descr)
disk_info_group = Adw.PreferencesGroup()
disk_info_group.set_title("Selected Drive")
box.append(disk_info_group)
row = Adw.ActionRow()
row.set_title("Current Disk")
row.set_subtitle("Detecting...")
row.set_icon_name("drive-harddisk-symbolic")
disk_info_group.add(row)
self.disk_row = row
bar_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
bar_box.set_spacing(12)
box.append(bar_box)
bar_label = Gtk.Label(label="Partition Layout", xalign=0)
bar_label.add_css_class("heading")
bar_box.append(bar_label)
self.visual_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.visual_bar.add_css_class("card")
self.visual_bar.set_overflow(Gtk.Overflow.HIDDEN)
self.visual_bar.set_size_request(-1, 80)
bar_box.append(self.visual_bar)
legend_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
legend_box.set_spacing(12)
legend_box.set_halign(Gtk.Align.CENTER)
bar_box.append(legend_box)
self.add_legend_item(legend_box, "part-efi", "EFI System")
self.add_legend_item(legend_box, "part-root", "Root Filesystem")
self.add_legend_item(legend_box, "part-empty", "Free Space")
self.partitions = []
# Initially empty until loaded with a specific disk
def load_partitions(self, disk_device=None):
self.current_disk_path = disk_device
target_disk = None
try:
result = subprocess.run(
["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,PKNAME"],
capture_output=True,
text=True,
)
if result.returncode == 0:
data = json.loads(result.stdout)
devices = data.get("blockdevices", [])
if disk_device:
# Look for the specific device (match name, e.g. "nvme0n1")
# disk_device from storage page is usually just the name like "nvme0n1" or "sda"
# But if it's full path /dev/sda, we need to strip /dev/
dev_name = disk_device.replace("/dev/", "")
for dev in devices:
if dev.get("name") == dev_name:
target_disk = dev
break
# If no specific disk requested or not found, maybe fallback or do nothing
# The previous logic found the first one. Let's keep strict if device is provided.
if not target_disk and not disk_device:
for dev in devices:
if (
dev.get("type") == "disk"
and not dev.get("name").startswith("zram")
and not dev.get("name").startswith("loop")
):
target_disk = dev
break
except Exception as e:
print(f"Error loading disks: {e}")
if target_disk:
self.disk_row.set_title(target_disk.get("name", "Unknown Disk"))
self.disk_row.set_subtitle(
f"Size: {self.format_size(target_disk.get('size', 0))}"
)
raw_parts = target_disk.get("children", [])
self.partitions = []
total_disk_size = int(target_disk.get("size", 0))
covered_size = 0
for p in raw_parts:
p_size = int(p.get("size", 0))
covered_size += p_size
fstype = (p.get("fstype") or "").lower()
style = "part-root"
if "fat" in fstype or "efi" in fstype:
style = "part-efi"
elif "swap" in fstype:
style = "part-swap"
self.partitions.append(
{
"type": "partition",
"name": p.get("name"),
"filesystem": p.get("fstype") or "Unknown",
"label": p.get("name"),
"mount_point": p.get("mountpoint", ""),
"size": self.format_size(p_size),
"bytes": p_size,
"style_class": style,
"expand": False,
}
)
remaining = total_disk_size - covered_size
if remaining > 100 * 1024 * 1024:
self.partitions.append(
{
"type": "empty",
"name": "Unallocated",
"size": self.format_size(remaining),
"bytes": remaining,
"style_class": "part-empty",
"expand": False,
}
)
total_display_units = 1000
for p in self.partitions:
ratio = p["bytes"] / total_disk_size if total_disk_size > 0 else 0
width = int(ratio * 800)
if width < 50:
width = 50
p["width_request"] = width
else:
self.partitions = [
{
"type": "empty",
"name": "No Disk Selected",
"size": "-",
"style_class": "part-empty",
"width_request": 300,
}
]
self.refresh_bar()
def format_size(self, size_bytes):
size = float(size_bytes)
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} PB"
def add_legend_item(self, box, style_class, label_text):
item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
item.set_spacing(6)
swatch = Gtk.Box()
swatch.set_size_request(16, 16)
swatch.add_css_class(style_class)
swatch.add_css_class("circular")
label = Gtk.Label(label=label_text)
item.append(swatch)
item.append(label)
box.append(item)
def refresh_bar(self):
child = self.visual_bar.get_first_child()
while child:
next_child = child.get_next_sibling()
self.visual_bar.remove(child)
child = next_child
for part in self.partitions:
segment = PartitionSegment(part, self)
self.visual_bar.append(segment)
def show_details_window(self, data):
win = Adw.Window(title="Partition Details")
win.set_transient_for(self.get_root())
win.set_modal(True)
win.set_default_size(300, 200)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(12)
box.set_margin_top(24)
box.set_margin_bottom(24)
box.set_margin_start(24)
box.set_margin_end(24)
win.set_content(box)
title = Gtk.Label(label=data.get("name", "Unknown"))
title.add_css_class("title-2")
box.append(title)
grid = Gtk.Grid()
grid.set_column_spacing(12)
grid.set_row_spacing(6)
grid.set_halign(Gtk.Align.CENTER)
box.append(grid)
rows = [
("Type", data.get("type", "-")),
("Size", data.get("size", "-")),
]
if data.get("type") == "partition":
rows.append(("Filesystem", data.get("filesystem", "-")))
rows.append(("Mount Point", data.get("mount_point") or "None"))
for i, (key, val) in enumerate(rows):
k_lbl = Gtk.Label(label=key, xalign=1)
k_lbl.add_css_class("dim-label")
v_lbl = Gtk.Label(label=val, xalign=0)
grid.attach(k_lbl, 0, i, 1, 1)
grid.attach(v_lbl, 1, i, 1, 1)
btn = Gtk.Button(label="Close")
btn.connect("clicked", lambda b: win.close())
btn.set_halign(Gtk.Align.CENTER)
box.append(btn)
win.present()
def show_context_menu(self, widget, x, y):
data = widget.part_data
self.selected_disk_path = self.current_disk_path # Assumes we store this
popover = Gtk.Popover()
popover.set_parent(widget)
menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
popover.set_child(menu_box)
def add_menu_item(label, icon_name, callback, destructive=False):
btn = Gtk.Button(label=label)
btn.add_css_class("flat")
if destructive:
btn.add_css_class("destructive-action")
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
icon = Gtk.Image.new_from_icon_name(icon_name)
lbl = Gtk.Label(label=label)
content.append(icon)
content.append(lbl)
btn.set_child(content)
btn.connect("clicked", lambda b: [popover.popdown(), callback()])
menu_box.append(btn)
if data.get("type") == "partition":
add_menu_item(
"Select Mount Point",
"folder-open-symbolic",
lambda: self.select_mount_point(data),
)
separator = Gtk.Separator()
menu_box.append(separator)
add_menu_item(
"Delete",
"user-trash-symbolic",
lambda: self.delete_part(data),
destructive=True,
)
elif data.get("type") == "empty":
add_menu_item(
"Create Partition",
"list-add-symbolic",
lambda: self.create_part_dialog(data),
)
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):
return {"partitions": self.partitions}

View File

@@ -1,14 +1,24 @@
import json
import subprocess
from functools import partial
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 StoragePage(Adw.Bin): class StoragePage(Adw.Bin):
__gsignals__ = {
"disk-selected": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.selected_disk = None
# Main Layout # Main Layout
clamp = Adw.Clamp() clamp = Adw.Clamp()
clamp.set_maximum_size(600) clamp.set_maximum_size(600)
@@ -16,6 +26,7 @@ class StoragePage(Adw.Bin):
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)
@@ -32,11 +43,13 @@ class StoragePage(Adw.Bin):
box.append(descr) box.append(descr)
# Disk List # Disk List
group = Adw.PreferencesGroup() self.disk_group = Adw.PreferencesGroup()
group.set_title("Available Disks") self.disk_group.set_title("Available Disks")
box.append(group) box.append(self.disk_group)
# Mock Disks # Fetch real disks or fallback to mock
disks = self.get_disks()
if not disks:
disks = [ disks = [
( (
"NVMe Samsung 970 EVO (500GB)", "NVMe Samsung 970 EVO (500GB)",
@@ -47,37 +60,78 @@ class StoragePage(Adw.Bin):
] ]
self.disk_rows = [] self.disk_rows = []
for name, dev, icon in disks: self.first_radio = None
for i, (name, dev, icon) in enumerate(disks):
row = Adw.ActionRow() row = Adw.ActionRow()
row.set_title(name) row.set_title(name)
row.set_subtitle(f"/dev/{dev}") row.set_subtitle(f"/dev/{dev}")
row.set_icon_name(icon) row.set_icon_name(icon)
# Radio button for selection # Radio button for selection
if not self.disk_rows: if not self.first_radio:
radio = Gtk.CheckButton() radio = Gtk.CheckButton()
self.first_radio = radio self.first_radio = radio
# No default selection to force user interaction, for safety reasons
else: else:
radio = Gtk.CheckButton() radio = Gtk.CheckButton()
radio.set_group(self.first_radio) radio.set_group(self.first_radio)
radio.set_valign(Gtk.Align.CENTER) radio.set_valign(Gtk.Align.CENTER)
# Connect signal to update selection
radio.connect("toggled", self.on_disk_toggled, dev)
row.add_suffix(radio) row.add_suffix(radio)
row.set_activatable_widget(radio) row.set_activatable_widget(radio)
group.add(row) self.disk_group.add(row)
self.disk_rows.append(row) self.disk_rows.append(row)
# Partitioning Options def on_disk_toggled(self, button, device_name):
part_group = Adw.PreferencesGroup() if button.get_active():
part_group.set_title("Configuration") # Prepend /dev/ if it's just the name
box.append(part_group) full_path = device_name
if not device_name.startswith("/dev/"):
full_path = f"/dev/{device_name}"
auto_row = Adw.ActionRow() self.selected_disk = full_path
auto_row.set_title("Automatic Partitioning") self.emit("disk-selected", full_path)
auto_row.set_subtitle("Erase disk and install Iridium")
part_switch = Gtk.Switch() def get_selected_disk(self):
part_switch.set_active(True) return self.selected_disk
part_switch.set_valign(Gtk.Align.CENTER)
auto_row.add_suffix(part_switch) def get_disks(self):
part_group.add(auto_row) try:
# lsblk -J -o NAME,SIZE,MODEL,TYPE,TRAN
result = subprocess.run(
["lsblk", "-J", "-o", "NAME,SIZE,MODEL,TYPE,TRAN"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"lsblk failed with code {result.returncode}: {result.stderr}")
return []
data = json.loads(result.stdout)
disks = []
for device in data.get("blockdevices", []):
# Filter for physical disks usually
if device.get("type") == "disk":
model = device.get("model")
size = device.get("size")
name = f"{model} ({size})" if model else f"Unknown Drive ({size})"
dev = device.get("name")
tran = device.get("tran", "").lower() if device.get("tran") else ""
if "usb" in tran:
icon = "drive-removable-media-symbolic"
elif "nvme" in dev:
icon = "drive-harddisk-solidstate-symbolic"
else:
icon = "drive-harddisk-symbolic"
disks.append((name, dev, icon))
return disks
except Exception as e:
print(f"Error getting disks: {e}")
return []

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)
@@ -29,18 +38,28 @@ class UserPage(Adw.Bin):
self.fullname_row = Adw.EntryRow() self.fullname_row = Adw.EntryRow()
self.fullname_row.set_title("Full Name") self.fullname_row.set_title("Full Name")
self.fullname_row.set_text("Administrator")
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()
self.username_row.set_title("Username") self.username_row.set_title("Username")
self.username_row.set_text("admin")
self.username_handler_id = self.username_row.connect(
"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
@@ -50,16 +69,117 @@ 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-pc") 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)
# Administrator self.username_manually_set = False
admin_group = Adw.PreferencesGroup()
admin_group.set_margin_top(12)
box.append(admin_group)
admin_row = Adw.SwitchRow() # Initial validation
admin_row.set_title("Make this user administrator") self.validate()
admin_row.set_subtitle("Add to sudoers group")
admin_row.set_active(True) def on_fullname_changed(self, entry, _pspec):
admin_group.add(admin_row) if self.username_manually_set:
return
username = entry.get_text().lower()
if username == "administrator":
username = "admin"
for sign in (
" ",
"-",
"_",
".",
",",
":",
";",
"!",
"?",
"/",
"\\",
"|",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"[",
"]",
"{",
"}",
"'",
'"',
"`",
"~",
"<",
">",
"=",
"+",
):
username = username.replace(sign, "_")
self.username_row.handler_block(self.username_handler_id)
self.username_row.set_text(username)
self.username_row.handler_unblock(self.username_handler_id)
def on_username_changed(self, entry, _pspec):
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):
return {
"fullname": self.fullname_row.get_text(),
"username": self.username_row.get_text(),
"password": self.password_row.get_text(),
"hostname": self.hostname_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):
@@ -11,7 +13,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 future of computing is here. Let's get you set up.") page.set_description("The installation will begin shortly")
# 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") page.set_icon_name("system-software-install-symbolic")
# Content Box # Content Box
@@ -25,15 +45,9 @@ class WelcomePage(Adw.Bin):
# Language Dropdown # Language Dropdown
languages = [ languages = [
"English (US)", "English",
"Spanish",
"French",
"German",
"Japanese",
"Chinese (Simplified)",
] ]
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,85 +1,140 @@
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.install_mode import InstallModePage
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")
# Main Layout self.toolbar_view = Adw.ToolbarView()
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.set_content(self.toolbar_view)
self.set_content(main_box)
# Header Bar # Header Bar (Top)
header = Adw.HeaderBar() self.header_bar = Adw.HeaderBar()
main_box.append(header) self.toolbar_view.add_top_bar(self.header_bar)
# Content Stack # Content Stack
self.stack = Adw.ViewStack() self.stack = Adw.ViewStack()
self.stack.set_vexpand(True) self.stack.set_vexpand(True)
main_box.append(self.stack) self.toolbar_view.set_content(self.stack)
# Navigation Bar (Bottom) # Navigation Bar (Bottom)
nav_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.bottom_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
nav_box.add_css_class("toolbar") self.bottom_bar.add_css_class("toolbar")
nav_box.set_margin_top(12) self.bottom_bar.set_margin_top(12)
nav_box.set_margin_bottom(12) self.bottom_bar.set_margin_bottom(12)
nav_box.set_margin_start(12) self.bottom_bar.set_margin_start(12)
nav_box.set_margin_end(12) self.bottom_bar.set_margin_end(12)
nav_box.set_spacing(12) self.bottom_bar.set_spacing(12)
main_box.append(nav_box) self.toolbar_view.add_bottom_bar(self.bottom_bar)
# Back Button # Back Button
self.back_button = Gtk.Button(label="Back") self.back_button = Gtk.Button(label="Back")
self.back_button.connect("clicked", self.on_back_clicked) self.back_button.connect("clicked", self.on_back_clicked)
self.back_button.set_sensitive(False) self.back_button.set_sensitive(False)
nav_box.append(self.back_button) self.bottom_bar.append(self.back_button)
# Spacer to push Next button to the right # Spacer to push Next button to the right
spacer = Gtk.Label() spacer = Gtk.Label()
spacer.set_hexpand(True) spacer.set_hexpand(True)
nav_box.append(spacer) self.bottom_bar.append(spacer)
# Next Button # Next Button
self.next_button = Gtk.Button(label="Next") self.next_button = Gtk.Button(label="Next")
self.next_button.add_css_class("suggested-action") self.next_button.add_css_class("suggested-action")
self.next_button.connect("clicked", self.on_next_clicked) self.next_button.connect("clicked", self.on_next_clicked)
nav_box.append(self.next_button) self.bottom_bar.append(self.next_button)
# Page Management # Page Management
self.page_ids = [] self.page_ids = []
self.current_page_index = 0 self.current_page_index = 0
# Initialize Pages
self.welcome_page = WelcomePage()
self.storage_page = StoragePage()
self.storage_page.connect("disk-selected", self.on_disk_selected)
self.install_mode_page = InstallModePage()
self.partitioning_page = PartitioningPage()
self.modules_page = ModulesPage()
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(WelcomePage(), "welcome") self.add_page(self.welcome_page, "welcome")
self.add_page(StoragePage(), "storage") self.add_page(self.storage_page, "storage")
self.add_page(UserPage(), "user") self.add_page(self.install_mode_page, "install_mode")
self.add_page(self.partitioning_page, "partitioning")
self.add_page(self.modules_page, "modules")
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.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)
self.page_ids.append(name) self.page_ids.append(name)
def on_disk_selected(self, page, device_name):
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)
# Next button label/state # Next button label/state
if self.current_page_index == len(self.page_ids) - 1: current_page_name = self.page_ids[self.current_page_index]
self.next_button.set_label("Install")
# Forced selection logic
forced_selection = True
if current_page_name == "storage":
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)
if current_page_name == "summary":
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:
@@ -88,15 +143,205 @@ class InstallerWindow(Adw.ApplicationWindow):
self.next_button.add_css_class("suggested-action") self.next_button.add_css_class("suggested-action")
def on_back_clicked(self, button): def on_back_clicked(self, button):
if self.current_page_index > 0: current_page = self.page_ids[self.current_page_index]
self.current_page_index -= 1
# Default: go back one page
next_prev_index = self.current_page_index - 1
if current_page == "modules":
mode = self.install_mode_page.get_mode()
if mode == "automatic":
# Skip partitioning (which is immediately before modules)
next_prev_index = self.page_ids.index("install_mode")
if next_prev_index >= 0:
self.current_page_index = next_prev_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()
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):
if self.current_page_index < len(self.page_ids) - 1: # Logic before transition
self.current_page_index += 1 current_page_name = self.page_ids[self.current_page_index]
if current_page_name == "storage":
selected_disk = self.storage_page.get_selected_disk()
self.partitioning_page.load_partitions(selected_disk)
next_index = self.current_page_index + 1
if current_page_name == "install_mode":
mode = self.install_mode_page.get_mode()
if mode == "automatic":
# Skip partitioning page
next_index = self.page_ids.index("modules")
else:
# Go to partitioning page
next_index = self.page_ids.index("partitioning")
if current_page_name == "user":
# Prepare summary instead of installing immediately
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()
partitions_config = {}
if mode == "manual":
partitions_config = self.partitioning_page.get_config()
elif mode == "automatic":
partitions = calculate_auto_partitions(disk)
partitions_config = {"partitions": partitions}
# 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
if next_index < len(self.page_ids):
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()
else:
print("Install process triggered!")

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