Files
installer/iridium_installer/ui/pages/partitioning.py

591 lines
20 KiB
Python

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:
"""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 calculate_auto_partitions(disk_device):
"""
Generates an automatic partition layout.
- 2GB EFI
- RAM + 2GB Swap
- Rest Root (ext4)
"""
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()
# Sizes in bytes
efi_size = 2 * 1024 * 1024 * 1024
swap_size = ram_size + (2 * 1024 * 1024 * 1024)
# Check if disk is large enough
min_root = 10 * 1024 * 1024 * 1024 # minimum 10GB for root
if disk_size < (efi_size + swap_size + min_root):
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": "Swap",
"filesystem": "swap",
"mount_point": "[SWAP]",
"size": f"{swap_size / (1024**3):.1f} GB",
"bytes": swap_size,
"style_class": "part-swap",
},
{
"type": "partition",
"name": "Root",
"filesystem": "ext4",
"mount_point": "/",
"size": f"{root_size / (1024**3):.1f} GB",
"bytes": root_size,
"style_class": "part-root",
},
]
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)
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):
from ...backend.disk import delete_partition
# Extract partition number from name (e.g., nvme0n1p3 -> 3)
import re
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)
# Name entry
name_entry = Gtk.Entry(text="Linux filesystem")
box.append(Gtk.Label(label="Partition Name:"))
box.append(name_entry)
# Size entry
size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
size_entry = Gtk.Entry(text=str(int(data["bytes"] / (1024*1024))))
size_box.append(size_entry)
size_box.append(Gtk.Label(label="MB"))
box.append(Gtk.Label(label="Size:"))
box.append(size_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)
# Filesystem dropdown
fs_options = ["ext4", "fat32", "swap"]
fs_dropdown = Gtk.DropDown.new_from_strings(fs_options)
box.append(Gtk.Label(label="Filesystem:"))
box.append(fs_dropdown)
# Auto-update FS and Name based on Type
def on_type_changed(dropdown, pspec):
selected_type = type_names[dropdown.get_selected()]
if selected_type == "EFI System":
fs_dropdown.set_selected(1) # fat32
name_entry.set_text("EFI System")
elif selected_type == "Swap":
fs_dropdown.set_selected(2) # swap
name_entry.set_text("Swap")
else:
fs_dropdown.set_selected(0) # ext4
name_entry.set_text("Linux filesystem")
type_dropdown.connect("notify::selected", on_type_changed)
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
name = name_entry.get_text()
selected_type = type_names[type_dropdown.get_selected()]
if selected_type == "EFI System" and name == "Linux filesystem":
error_label.set_text("Can't set partition name to Linux filesystem")
return
size_text = size_entry.get_text()
try:
size_mb = int(size_text)
# If they are using the max proposed size, use 0 to avoid alignment issues
# with sgdisk +sizeM logic exceeding disk boundaries.
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]
fstype = fs_options[fs_dropdown.get_selected()]
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}