Partition screen
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user