Compare commits

..

2 Commits

Author SHA1 Message Date
62e1276881 Partition screen 2026-02-01 21:00:44 +01:00
17eac3de50 Actual disk names 2026-02-01 20:37:29 +01:00
3 changed files with 471 additions and 30 deletions

View File

@@ -0,0 +1,377 @@
import json
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;
}
"""
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_margin_top(24)
box.set_margin_bottom(24)
clamp.set_child(box)
title = Gtk.Label(label="Disk Configuration")
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):
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", "").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", "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
popover = Gtk.Popover()
popover.set_parent(widget)
menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
menu_box.set_spacing(0)
popover.set_child(menu_box)
def add_menu_item(label, icon_name=None, destructive=False):
btn = Gtk.Button(label=label)
btn.add_css_class("flat")
btn.set_halign(Gtk.Align.FILL)
if destructive:
btn.add_css_class("destructive-action")
if icon_name:
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
content.set_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)
menu_box.append(btn)
return btn
if data.get("type") == "partition":
add_menu_item("Select Mount Point", "folder-open-symbolic")
add_menu_item("Format", "drive-harddisk-symbolic")
add_menu_item("Resize", "object-resize-symbolic")
separator = Gtk.Separator()
menu_box.append(separator)
btn_del = add_menu_item("Delete", "user-trash-symbolic", destructive=True)
btn_del.add_css_class("error")
elif data.get("type") == "empty":
add_menu_item("Create Partition", "list-add-symbolic")
popover.popup()

View File

@@ -1,3 +1,7 @@
import json
import subprocess
from functools import partial
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
@@ -9,6 +13,8 @@ class StoragePage(Adw.Bin):
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)
@@ -32,11 +38,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 +55,73 @@ 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
# Default selection
self.selected_disk = dev
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") self.selected_disk = device_name
box.append(part_group)
auto_row = Adw.ActionRow() def get_selected_disk(self):
auto_row.set_title("Automatic Partitioning") return self.selected_disk
auto_row.set_subtitle("Erase disk and install Iridium")
part_switch = Gtk.Switch() def get_disks(self):
part_switch.set_active(True) try:
part_switch.set_valign(Gtk.Align.CENTER) # lsblk -J -o NAME,SIZE,MODEL,TYPE,TRAN
auto_row.add_suffix(part_switch) result = subprocess.run(
part_group.add(auto_row) ["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

@@ -4,6 +4,7 @@ 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
from .pages.partitioning import PartitioningPage
from .pages.storage import StoragePage from .pages.storage import StoragePage
from .pages.user import UserPage from .pages.user import UserPage
from .pages.welcome import WelcomePage from .pages.welcome import WelcomePage
@@ -60,10 +61,17 @@ class InstallerWindow(Adw.ApplicationWindow):
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.partitioning_page = PartitioningPage()
self.user_page = UserPage()
# 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.partitioning_page, "partitioning")
self.add_page(self.user_page, "user")
# Initialize view # Initialize view
if self.page_ids: if self.page_ids:
@@ -94,6 +102,18 @@ class InstallerWindow(Adw.ApplicationWindow):
self.update_buttons() self.update_buttons()
def on_next_clicked(self, button): def on_next_clicked(self, button):
# Logic before transition
current_page_name = self.page_ids[self.current_page_index]
if current_page_name == "storage":
# Pass selected disk to partitioning page
selected_disk = self.storage_page.get_selected_disk()
if selected_disk:
self.partitioning_page.load_partitions(selected_disk)
else:
# Optionally handle no disk selected (alert user)
pass
if self.current_page_index < len(self.page_ids) - 1: if self.current_page_index < len(self.page_ids) - 1:
self.current_page_index += 1 self.current_page_index += 1
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])