From 62e12768810f86dcc820557783edd57c5004e52b Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Sun, 1 Feb 2026 21:00:44 +0100 Subject: [PATCH] Partition screen --- iridium_installer/ui/pages/partitioning.py | 367 +++++++++++++++++---- iridium_installer/ui/pages/storage.py | 17 +- iridium_installer/ui/window.py | 26 +- 3 files changed, 344 insertions(+), 66 deletions(-) diff --git a/iridium_installer/ui/pages/partitioning.py b/iridium_installer/ui/pages/partitioning.py index 009a968..962f422 100644 --- a/iridium_installer/ui/pages/partitioning.py +++ b/iridium_installer/ui/pages/partitioning.py @@ -1,15 +1,89 @@ +import json +import subprocess + import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import Adw, Gdk, Gtk +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) - # Main Layout + 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) @@ -20,29 +94,27 @@ class PartitioningPage(Adw.Bin): box.set_margin_bottom(24) clamp.set_child(box) - # Title title = Gtk.Label(label="Disk Configuration") title.add_css_class("title-1") box.append(title) descr = Gtk.Label( - label="Review and modify the partition layout for your installation." + label="Modify the partition layout below. Right-click segments for options." ) descr.set_wrap(True) box.append(descr) - # Selected Disk Info (Mock) disk_info_group = Adw.PreferencesGroup() disk_info_group.set_title("Selected Drive") box.append(disk_info_group) row = Adw.ActionRow() - row.set_title("NVMe Samsung 970 EVO") - row.set_subtitle("500 GB - /dev/nvme0n1") - row.set_icon_name("drive-harddisk-solidstate-symbolic") + 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 - # Partition Bar (The "Horizontal Bar") bar_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) bar_box.set_spacing(12) box.append(bar_box) @@ -51,70 +123,142 @@ class PartitioningPage(Adw.Bin): bar_label.add_css_class("heading") bar_box.append(bar_label) - # The Visual Bar - # We use a horizontal box with homogeneous=False - visual_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - visual_bar.add_css_class("card") # Gives it a border/background - visual_bar.set_overflow(Gtk.Overflow.HIDDEN) - visual_bar.set_size_request(-1, 60) # Height of the bar - bar_box.append(visual_bar) + 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) - # Segment 1: EFI - seg1 = Gtk.Box() - seg1.set_hexpand(False) - seg1.set_size_request(50, -1) # Mock width for small partition - seg1.add_css_class("accent") # Blue-ish usually - # Add a tooltip or label inside? - seg1.set_tooltip_text("/dev/nvme0n1p1 (EFI) - 512 MB") - visual_bar.append(seg1) + bar_box.append(self.visual_bar) - # Separator (optional, or just rely on boxes) - - # Segment 2: Root - seg2 = Gtk.Box() - seg2.set_hexpand(True) # Takes remaining space - seg2.add_css_class("success") # Green-ish usually - seg2.set_tooltip_text("/dev/nvme0n1p2 (Root) - 499.5 GB") - visual_bar.append(seg2) - - # Legend 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, "accent", "EFI System") - self.add_legend_item(legend_box, "success", "Root Filesystem") + 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") - # Partition Table (Detailed List) - part_list_group = Adw.PreferencesGroup() - part_list_group.set_title("Partitions") - box.append(part_list_group) + self.partitions = [] + # Initially empty until loaded with a specific disk - p1 = Adw.ActionRow() - p1.set_title("/dev/nvme0n1p1") - p1.set_subtitle("EFI System Partition - FAT32") - p1.add_suffix(Gtk.Label(label="512 MB")) - part_list_group.add(p1) + def load_partitions(self, disk_device=None): + target_disk = None - p2 = Adw.ActionRow() - p2.set_title("/dev/nvme0n1p2") - p2.set_subtitle("Root - Ext4") - p2.add_suffix(Gtk.Label(label="499.5 GB")) - part_list_group.add(p2) + 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", []) - # Controls - actions_group = Adw.PreferencesGroup() - box.append(actions_group) + 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/", "") - btn_row = Adw.ActionRow() - btn_row.set_title("Manual Partitioning") - btn_row.set_subtitle("Open external partition editor (GParted)") + for dev in devices: + if dev.get("name") == dev_name: + target_disk = dev + break - edit_btn = Gtk.Button(label="Open Editor") - edit_btn.set_valign(Gtk.Align.CENTER) - btn_row.add_suffix(edit_btn) - actions_group.add(btn_row) + # 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) @@ -123,10 +267,111 @@ class PartitioningPage(Adw.Bin): swatch = Gtk.Box() swatch.set_size_request(16, 16) swatch.add_css_class(style_class) - swatch.add_css_class("circular") # Make it round if supported, or just a box + 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() diff --git a/iridium_installer/ui/pages/storage.py b/iridium_installer/ui/pages/storage.py index 18d2372..a6543d0 100644 --- a/iridium_installer/ui/pages/storage.py +++ b/iridium_installer/ui/pages/storage.py @@ -1,5 +1,6 @@ import json import subprocess +from functools import partial import gi @@ -12,6 +13,8 @@ class StoragePage(Adw.Bin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.selected_disk = None + # Main Layout clamp = Adw.Clamp() clamp.set_maximum_size(600) @@ -54,7 +57,7 @@ class StoragePage(Adw.Bin): self.disk_rows = [] self.first_radio = None - for name, dev, icon in disks: + for i, (name, dev, icon) in enumerate(disks): row = Adw.ActionRow() row.set_title(name) row.set_subtitle(f"/dev/{dev}") @@ -64,17 +67,29 @@ class StoragePage(Adw.Bin): if not self.first_radio: radio = Gtk.CheckButton() self.first_radio = radio + # Default selection + self.selected_disk = dev else: radio = Gtk.CheckButton() radio.set_group(self.first_radio) radio.set_valign(Gtk.Align.CENTER) + # Connect signal to update selection + radio.connect("toggled", self.on_disk_toggled, dev) + row.add_suffix(radio) row.set_activatable_widget(radio) self.disk_group.add(row) self.disk_rows.append(row) + def on_disk_toggled(self, button, device_name): + if button.get_active(): + self.selected_disk = device_name + + def get_selected_disk(self): + return self.selected_disk + def get_disks(self): try: # lsblk -J -o NAME,SIZE,MODEL,TYPE,TRAN diff --git a/iridium_installer/ui/window.py b/iridium_installer/ui/window.py index 7582c1d..72a5202 100644 --- a/iridium_installer/ui/window.py +++ b/iridium_installer/ui/window.py @@ -61,11 +61,17 @@ class InstallerWindow(Adw.ApplicationWindow): self.page_ids = [] 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 - self.add_page(WelcomePage(), "welcome") - self.add_page(StoragePage(), "storage") - self.add_page(PartitioningPage(), "partitioning") - self.add_page(UserPage(), "user") + self.add_page(self.welcome_page, "welcome") + self.add_page(self.storage_page, "storage") + self.add_page(self.partitioning_page, "partitioning") + self.add_page(self.user_page, "user") # Initialize view if self.page_ids: @@ -96,6 +102,18 @@ class InstallerWindow(Adw.ApplicationWindow): self.update_buttons() 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: self.current_page_index += 1 self.stack.set_visible_child_name(self.page_ids[self.current_page_index])