From 6035e6355fef528e3a22a53dc9fb93156d8f6383 Mon Sep 17 00:00:00 2001 From: ericbsd Date: Sun, 21 Dec 2025 20:40:28 -0400 Subject: [PATCH 1/3] Add install_station service and related configuration updates --- install_station/create_cfg.py | 6 ++++++ setup.py | 3 ++- src/install_station | 36 +++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100755 src/install_station diff --git a/install_station/create_cfg.py b/install_station/create_cfg.py index 71391ac..6220be9 100644 --- a/install_station/create_cfg.py +++ b/install_station/create_cfg.py @@ -156,5 +156,11 @@ class Configuration: f.write("runCommand=pw userdel -n ghostbsd -r\n") f.write("runCommand=sed -i '' 's/ghostbsd/root/g' /etc/gettytab\n") f.write("runCommand=sed -i '' 's/ghostbsd/root/g' /etc/ttys\n") + f.write("runCommand=echo '# WARNING: Do NOT set initial_setup_enable=YES manually!' >> /etc/rc.conf\n") + f.write("runCommand=echo '# This service is ONLY for first boot after installation.' >> /etc/rc.conf\n") + f.write("runCommand=echo '# It will automatically disable itself after running.' >> /etc/rc.conf\n") + f.write("runCommand=sysrc initial_setup_enable=YES\n") + f.write("runCommand=sed -i '' '/^autologin-user=/d' /usr/local/etc/lightdm/lightdm.conf\n") + f.write("runCommand=sed -i '' '/^autologin-session=/d' /usr/local/etc/lightdm/lightdm.conf\n") except IOError as e: raise IOError(f"Failed to write configuration file: {e}") from e diff --git a/setup.py b/setup.py index d5340c2..3c6c45e 100755 --- a/setup.py +++ b/setup.py @@ -157,7 +157,8 @@ lib_install_station_backend_query = [ data_files = [ (f'{prefix}/lib/install-station', ['src/ghostbsd-style.css']), (f'{prefix}/lib/install-station/backend-query', lib_install_station_backend_query), - (f'{prefix}/lib/install-station/image', lib_install_station_image) + (f'{prefix}/lib/install-station/image', lib_install_station_image), + (f'{prefix}/etc/rc.d', ['src/install_station']) ] data_files.extend(data_file_list(f'{prefix}/share/locale', 'build/mo')) diff --git a/src/install_station b/src/install_station new file mode 100755 index 0000000..c92e336 --- /dev/null +++ b/src/install_station @@ -0,0 +1,36 @@ +#!/bin/sh +# +# PROVIDE: install_station +# REQUIRE: LOGIN FILESYSTEMS +# BEFORE: lightdm +# KEYWORD: shutdown + +. /etc/rc.subr + +name="install_station" +rcvar="${name}_enable" +start_cmd="${name}_start" +stop_cmd=":" + +install_station_start() +{ + # Create .xinitrc for root to run installer + cat > /root/.xinitrc <<'EOF' +exec marco & +exec feh --bg-fill /usr/local/share/backgrounds/ghostbsd/blue-layered-stripes.jpg & +exec install-station +EOF + chmod 755 /root/.xinitrc + + # Start X as root - this will block until install-station exits + su -l root -c "startx -- :0 vt09" + + # Cleanup + rm -f /root/.xinitrc + + # Disable this service for future boots + sysrc install_station_enable=NO +} + +load_rc_config $name +run_rc_command "$1" \ No newline at end of file From 4d4c54671eecc4bf419545f02d18cea1d7c97c57 Mon Sep 17 00:00:00 2001 From: ericbsd Date: Wed, 13 May 2026 08:11:34 -0300 Subject: [PATCH 2/3] Rework ZFS page UI layout and swap sizing Redesign the ZFS configuration page with a two-column layout (settings on the left, disk list on the right), add a user-editable swap size field that defaults to actual RAM size, make the pool name always editable, and simplify pool type values to plain identifiers (stripe, mirror, raidz1/2/3). Consolidate duplicated next-button sensitivity logic into _update_next_button(), replace deprecated Gtk.STOCK icons with icon names, encrypt swap when GELI is enabled, and bump version to 0.4. --- install_station/partition.py | 5 +- install_station/system_calls.py | 15 ++ install_station/use_zfs.py | 439 +++++++++++++++----------------- setup.py | 2 +- 4 files changed, 220 insertions(+), 241 deletions(-) diff --git a/install_station/partition.py b/install_station/partition.py index 63a578d..7e9f1bb 100644 --- a/install_station/partition.py +++ b/install_station/partition.py @@ -7,6 +7,7 @@ import re from time import sleep from subprocess import Popen, PIPE, STDOUT, call from install_station.data import query, zfs_datasets, InstallationData +from install_station.system_calls import get_ram_size_mb # Define required file paths @@ -779,7 +780,7 @@ class AutoFreeSpace: InstallationData.slice = main_slice.replace(drive, "") root_size = int(main_size) - swap_size = 2048 + swap_size = get_ram_size_mb() root_size -= swap_size part_list = disk_db[drive]['partitions'][main_slice]['partition-list'] @@ -861,7 +862,7 @@ class AutoFreeSpace: InstallationData.disk = drive InstallationData.scheme = 'partscheme=GPT' root_size = int(main_size) - swap_size = 2048 + swap_size = get_ram_size_mb() root_size -= int(swap_size) if self.bios_type == "UEFI" and efi_exist is False: boot_size = 256 diff --git a/install_station/system_calls.py b/install_station/system_calls.py index 93343a6..fac2b0d 100644 --- a/install_station/system_calls.py +++ b/install_station/system_calls.py @@ -258,6 +258,21 @@ def timezone_dictionary() -> dict[str, list[str]]: return dictionary +def get_ram_size_mb() -> int: + """Query the system RAM size in megabytes using sysctl. + + Returns: + System RAM size in MB + """ + output = Popen( + 'sysctl -n hw.realmem', + shell=True, + stdout=PIPE, + universal_newlines=True + ).stdout.read().strip() + return int(output) // 1048576 + + def zfs_disk_query() -> list[str]: """Query available disks for ZFS installation. diff --git a/install_station/use_zfs.py b/install_station/use_zfs.py index 1e0441b..6c59024 100644 --- a/install_station/use_zfs.py +++ b/install_station/use_zfs.py @@ -3,6 +3,7 @@ from install_station.common import password_strength from install_station.data import InstallationData, zfs_datasets, be_name, logo, get_text from install_station.partition import bios_or_uefi from install_station.system_calls import ( + get_ram_size_mb, zfs_disk_query, zfs_disk_size_query, ) @@ -38,13 +39,13 @@ class ZFS: zfs_disk_list = [] pool_type = 'stripe' scheme = 'GPT' - zpool = False disk_encrypt = False - mirror = 'single disk' + mirror = 'none' vbox1 = None - + # UI elements as class variables pool = None + swap_entry = None password = None repassword = None mirrorTips = None @@ -69,28 +70,19 @@ class ZFS: # Validate required fields are populated if not cls.zfs_disk_list: raise ValueError("No disks selected for ZFS configuration") - - if cls.zpool and not cls.pool.get_text().strip(): - raise ValueError("Pool name cannot be empty when zpool is enabled") - + if cls.disk_encrypt and not cls.password.get_text().strip(): raise ValueError("Password cannot be empty when disk encryption is enabled") - + size = int(cls.zfs_disk_list[0].partition('-')[2].rstrip()) - 512 - swap = 0 - zfs_num = size - swap - if cls.disk_encrypt is True: - dgeli = '.eli' - else: - dgeli = '' + swap_size = int(cls.swap_entry.get_text() or '0') + zfs_num = size - swap_size + dgeli = '.eli' if cls.disk_encrypt else '' # Store configuration data in InstallationData instead of writing to file InstallationData.zfs_config_data = [] - - if cls.zpool is True: - InstallationData.zfs_config_data.append(f"zpoolName={cls.pool.get_text()}\n") - else: - InstallationData.zfs_config_data.append("#zpoolName=None\n") + + InstallationData.zfs_config_data.append(f"zpoolName={cls.pool.get_text()}\n") InstallationData.zfs_config_data.append(f"beName={be_name}\n") InstallationData.zfs_config_data.append('ashift=12\n\n') disk = cls.zfs_disk_list[0].partition('-')[0].rstrip() @@ -98,18 +90,13 @@ class ZFS: InstallationData.zfs_config_data.append('partition=ALL\n') InstallationData.zfs_config_data.append(f'partscheme={cls.scheme}\n') InstallationData.zfs_config_data.append('commitDiskPart\n\n') - if cls.pool_type == 'none': + if len(cls.zfs_disk_list) <= 1: pool_disk = '\n' else: zfs_disk = cls.zfs_disk_list - disk_len = len(zfs_disk) - 1 - num = 1 mirror_dsk = '' - while disk_len != 0: - mirror_dsk += ' ' + zfs_disk[num].partition('-')[0].rstrip() - print(mirror_dsk) - num += 1 - disk_len -= 1 + for i in range(1, len(zfs_disk)): + mirror_dsk += ' ' + zfs_disk[i].partition('-')[0].rstrip() pool_disk = f' ({cls.pool_type}:{mirror_dsk})\n' if bios_or_uefi() == "UEFI": zfs_num = zfs_num - 100 @@ -118,19 +105,58 @@ class ZFS: # adding zero to use remaining space zfs_part = f'disk0-part=ZFS{dgeli} {zfs_num} {zfs_datasets}{pool_disk}' InstallationData.zfs_config_data.append(zfs_part) - if swap != 0: - InstallationData.zfs_config_data.append('disk0-part=swap 0 none\n') - if cls.disk_encrypt is True: + # encpass must be on the line immediately after the .eli partition + if cls.disk_encrypt: InstallationData.zfs_config_data.append(f'encpass={cls.password.get_text()}\n') else: InstallationData.zfs_config_data.append('#encpass=None\n') + if swap_size != 0: + if cls.disk_encrypt: + InstallationData.zfs_config_data.append('disk0-part=SWAP.eli 0 none\n') + else: + InstallationData.zfs_config_data.append('disk0-part=SWAP 0 none\n') InstallationData.zfs_config_data.append('commitDiskLabel\n') + @classmethod + def _disk_count_valid(cls): + """ + Check if the number of selected disks meets the pool type requirement. + + Returns: + bool: True if enough disks are selected for the current pool type + """ + count = len(cls.zfs_disk_list) + if cls.mirror == "stripe": + return count >= 1 + elif cls.mirror == "mirror": + return count >= 2 + elif cls.mirror == "raidz1": + return count == 3 + elif cls.mirror == "raidz2": + return count == 4 + elif cls.mirror == "raidz3": + return count == 5 + return False + + @classmethod + def _update_next_button(cls): + """ + Update next button sensitivity based on disk count and encryption state. + + When encryption is enabled, passwords must also match. + """ + if cls.disk_encrypt: + passwd_match = (cls.password.get_text() == cls.repassword.get_text() + and len(cls.password.get_text()) > 0) + Button.next_button.set_sensitive(cls._disk_count_valid() and passwd_match) + else: + Button.next_button.set_sensitive(cls._disk_count_valid()) + @classmethod def scheme_selection(cls, combobox): """ Handle partition scheme selection from combo box. - + Args: combobox: ComboBox widget containing scheme options (GPT/MBR) """ @@ -143,80 +169,48 @@ class ZFS: def mirror_selection(cls, combobox): """ Handle pool type selection and update UI accordingly. - + Sets the pool type (stripe, mirror, RAIDZ1/2/3) and updates the tip text and next button sensitivity based on the number of selected disks. - + Args: combobox: ComboBox widget containing pool type options """ model = combobox.get_model() index = combobox.get_active() - data = model[index][0] # Get the internal value (English) + data = model[index][0] cls.mirror = data - if cls.mirror == "1+ disks Stripe": + smallest_msg = get_text("(select the smallest disk first)") + if cls.mirror == "stripe": cls.pool_type = 'stripe' cls.mirrorTips.set_text( - get_text("Please select 1 or more drive for stripe (select the smallest disk first)")) - if len(cls.zfs_disk_list) >= 1: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "2+ disks Mirror": + get_text("Select 1 or more drives, no redundancy") + " " + smallest_msg) + elif cls.mirror == "mirror": cls.pool_type = 'mirror' - mir_msg1 = get_text("Please select 2 drive for mirroring (select the smallest disk first)") - cls.mirrorTips.set_text(mir_msg1) - if len(cls.zfs_disk_list) >= 2: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "3 disks RAIDZ1": + cls.mirrorTips.set_text( + get_text("Select 2 or more drives for mirroring") + " " + smallest_msg) + elif cls.mirror == "raidz1": cls.pool_type = 'raidz1' - cls.mirrorTips.set_text(get_text("Please select 3 drive for RAIDZ1 (select the smallest disk first)")) - if len(cls.zfs_disk_list) == 3: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "4 disks RAIDZ2": + cls.mirrorTips.set_text( + get_text("Select 3 drives for RAIDZ1") + " " + smallest_msg) + elif cls.mirror == "raidz2": cls.pool_type = 'raidz2' - cls.mirrorTips.set_text(get_text("Please select 4 drive for RAIDZ2 (select the smallest disk first)")) - if len(cls.zfs_disk_list) == 4: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "5 disks RAIDZ3": + cls.mirrorTips.set_text( + get_text("Select 4 drives for RAIDZ2") + " " + smallest_msg) + elif cls.mirror == "raidz3": cls.pool_type = 'raidz3' - cls.mirrorTips.set_text(get_text("Please select 5 drive for RAIDZ3 (select the smallest disk first)")) - if len(cls.zfs_disk_list) == 5: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - - @classmethod - def on_check_poll(cls, widget): - """ - Handle custom pool name checkbox toggle. - - Enables or disables the pool name entry field based on checkbox state. - - Args: - widget: CheckButton widget for pool name enable/disable - """ - if widget.get_active(): - cls.pool.set_sensitive(True) - cls.zpool = True - else: - cls.pool.set_sensitive(False) - cls.zpool = False + cls.mirrorTips.set_text( + get_text("Select 5 drives for RAIDZ3") + " " + smallest_msg) + cls._update_next_button() @classmethod def on_check_encrypt(cls, widget): """ Handle disk encryption checkbox toggle. - + Enables or disables password fields and updates next button sensitivity based on encryption state and current disk selection. - + Args: widget: CheckButton widget for disk encryption enable/disable """ @@ -229,31 +223,37 @@ class ZFS: cls.password.set_sensitive(False) cls.repassword.set_sensitive(False) cls.disk_encrypt = False - if cls.mirror == "1+ disks Stripe": - if len(cls.zfs_disk_list) >= 1: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "2+ disks Mirror": - if len(cls.zfs_disk_list) >= 2: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "3 disks RAIDZ1": - if len(cls.zfs_disk_list) == 3: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "4 disks RAIDZ2": - if len(cls.zfs_disk_list) == 4: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "5 disks RAIDZ3": - if len(cls.zfs_disk_list) == 5: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) + cls._update_next_button() + + @classmethod + def on_password_changed(cls, _widget): + """ + Handle password entry changes and update strength display. + + Wraps the common password_strength function to extract the text + from the Entry widget and pass it with the strength label. + + Args: + _widget: Entry widget that triggered the change (unused) + """ + password_strength(cls.password.get_text(), cls.strenght_label) + + @classmethod + def digit_only(cls, widget, text, length, position): + """ + Block non-digit characters from being inserted into the swap entry. + + Connected to the swap entry 'insert-text' signal to prevent + non-numeric input before it enters the field. + + Args: + widget: Entry widget receiving the input + text: Text being inserted + length: Length of the text being inserted + position: Cursor position at time of insertion + """ + if not text.isdigit(): + widget.stop_emission_by_name('insert-text') @classmethod def initialize(cls): @@ -271,7 +271,8 @@ class ZFS: """ cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0) cls.vbox1.show() - # Chose disk + + # Disk list in a scrolled window sw = Gtk.ScrolledWindow(hexpand=True, vexpand=True) sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) @@ -319,37 +320,38 @@ class ZFS: tree_selection.set_mode(Gtk.SelectionMode.SINGLE) sw.add(treeview) sw.show() + cls.mirrorTips = Gtk.Label(label=get_text('Please select one drive')) cls.mirrorTips.set_justify(Gtk.Justification.LEFT) cls.mirrorTips.set_alignment(0.01, 0.5) - # Mirror, raidz and stripe + + # Pool Layout cls.mirror = 'none' - mirror_label = Gtk.Label(label=get_text('Pool Type')) + mirror_label = Gtk.Label(label=get_text('Pool Layout')) mirror_label.set_use_markup(True) - mirror_box = Gtk.ComboBox() - mirror_store = Gtk.ListStore(str, str) # value, display_text - mirror_store.append(["1+ disks Stripe", get_text("1+ disks Stripe")]) - mirror_store.append(["2+ disks Mirror", get_text("2+ disks Mirror")]) - mirror_store.append(["3 disks RAIDZ1", get_text("3 disks RAIDZ1")]) - mirror_store.append(["4 disks RAIDZ2", get_text("4 disks RAIDZ2")]) - mirror_store.append(["5 disks RAIDZ3", get_text("5 disks RAIDZ3")]) - mirror_box.set_model(mirror_store) - renderer = Gtk.CellRendererText() - mirror_box.pack_start(renderer, True) - mirror_box.add_attribute(renderer, "text", 1) # Display column 1 (translated text) + mirror_box = Gtk.ComboBoxText() + mirror_box.append_text("stripe") + mirror_box.append_text("mirror") + mirror_box.append_text("raidz1") + mirror_box.append_text("raidz2") + mirror_box.append_text("raidz3") mirror_box.connect('changed', cls.mirror_selection) mirror_box.set_active(0) - # Pool Name - cls.zpool = False - pool_name_label = Gtk.Label(label=get_text('Pool Name')) - pool_name_label.set_use_markup(True) + # Pool Name (always editable) + pool_label = Gtk.Label(label=get_text('Pool Name')) + pool_label.set_use_markup(True) cls.pool = Gtk.Entry() cls.pool.set_text('zroot') + + # Swap Size + swap_label = Gtk.Label(label=get_text('Swap Size(MB)')) + swap_label.set_use_markup(True) + cls.swap_entry = Gtk.Entry() + cls.swap_entry.set_text(str(get_ram_size_mb())) + cls.swap_entry.connect('insert-text', cls.digit_only) + # Creating MBR or GPT drive - scheme_label = Gtk.Label(label='Partition Scheme') - scheme_label.set_use_markup(True) - # Adding a combo box to selecting MBR or GPT sheme. cls.scheme = 'GPT' shemebox = Gtk.ComboBoxText() shemebox.append_text("GPT") @@ -360,52 +362,85 @@ class ZFS: shemebox.set_sensitive(False) else: shemebox.set_sensitive(True) + # GELI Disk encryption cls.disk_encrypt = False - encrypt_check = Gtk.CheckButton(label=get_text("Encrypt Disk")) + encrypt_check = Gtk.CheckButton(label=get_text("Encrypt Disk (GELI)")) encrypt_check.connect("toggled", cls.on_check_encrypt) encrypt_check.set_sensitive(True) - # password + + # Password cls.passwd_label = Gtk.Label(label=get_text("Password")) cls.password = Gtk.Entry() cls.password.set_sensitive(False) cls.password.set_visibility(False) - cls.password.connect("changed", password_strength) + cls.password.connect("changed", cls.on_password_changed) cls.strenght_label = Gtk.Label() cls.strenght_label.set_alignment(0.1, 0.5) - cls.vpasswd_label = Gtk.Label(label=get_text("Verify it")) + cls.strenght_label.set_size_request(-1, 20) + + # Verify password + cls.vpasswd_label = Gtk.Label(label=get_text("Confirm")) cls.repassword = Gtk.Entry() cls.repassword.set_sensitive(False) cls.repassword.set_visibility(False) cls.repassword.connect("changed", cls.password_verification) - # set image for password matching + + # Password match image cls.img = Gtk.Image() - cls.img.set_alignment(0.2, 0.5) - # table = Gtk.Table(12, 12, True) - grid = Gtk.Grid() - grid.set_row_spacing(10) - # grid.set_column_homogeneous(True) - # grid.set_row_homogeneous(True) - # grid.attach(Title, 1, 1, 10, 1) - grid.attach(mirror_label, 1, 2, 1, 1) - grid.attach(mirror_box, 2, 2, 1, 1) - grid.attach(pool_name_label, 7, 2, 2, 1) - grid.attach(cls.pool, 9, 2, 2, 1) - grid.attach(cls.mirrorTips, 1, 3, 8, 1) - # grid.attach(zfs4kcheck, 9, 3, 2, 1) - grid.attach(sw, 1, 4, 10, 3) - # grid.attach(scheme_label, 1, 9, 1, 1) - # grid.attach(shemebox, 2, 9, 1, 1) - # grid.attach(cls.swap_encrypt_check, 9, 15, 11, 12) - # grid.attach(swap_mirror_check, 9, 15, 11, 12) - # grid.attach(encrypt_check, 2, 8, 2, 1) - # grid.attach(cls.passwd_label, 1, 9, 1, 1) - # grid.attach(cls.password, 2, 9, 2, 1) - # grid.attach(cls.strenght_label, 4, 9, 2, 1) - # grid.attach(cls.vpasswd_label, 1, 10, 1, 1) - # grid.attach(cls.repassword, 2, 10, 2, 1) - # grid.attach(cls.img, 4, 10, 2, 1) - cls.vbox1.pack_start(grid, True, True, 10) + cls.img.set_size_request(20, 20) + + # Two-column layout: left settings, right disk list + hbox_main = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10) + + # Left panel: settings grid + left_grid = Gtk.Grid() + left_grid.set_row_spacing(4) + left_grid.set_column_spacing(6) + left_grid.set_size_request(200, -1) + + mirror_label.set_alignment(0, 0.5) + pool_label.set_alignment(0, 0.5) + swap_label.set_alignment(0, 0.5) + cls.passwd_label.set_alignment(0, 0.5) + cls.vpasswd_label.set_alignment(0, 0.5) + + # Row 0-1: Pool Layout + left_grid.attach(mirror_label, 0, 0, 2, 1) + left_grid.attach(mirror_box, 0, 1, 2, 1) + # Row 2-3: Pool Name + left_grid.attach(pool_label, 0, 2, 2, 1) + left_grid.attach(cls.pool, 0, 3, 2, 1) + # Row 4-5: Swap Size + left_grid.attach(swap_label, 0, 4, 2, 1) + left_grid.attach(cls.swap_entry, 0, 5, 2, 1) + # Row 6: Separator + sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + left_grid.attach(sep, 0, 6, 2, 1) + # Row 7: Encrypt Disk checkbox + left_grid.attach(encrypt_check, 0, 7, 2, 1) + # Row 8: Password label + strength indicator + left_grid.attach(cls.passwd_label, 0, 8, 1, 1) + left_grid.attach(cls.strenght_label, 1, 8, 1, 1) + # Row 9: Password input + left_grid.attach(cls.password, 0, 9, 2, 1) + # Row 10: Confirm label + match icon + left_grid.attach(cls.vpasswd_label, 0, 10, 1, 1) + left_grid.attach(cls.img, 1, 10, 1, 1) + # Row 11: Confirm input + left_grid.attach(cls.repassword, 0, 11, 2, 1) + + hbox_main.pack_start(left_grid, False, False, 10) + + # Right panel: tips + disk list + right_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0) + cls.mirrorTips.set_alignment(0, 0.5) + right_panel.pack_start(cls.mirrorTips, False, False, 0) + right_panel.pack_start(sw, True, True, 0) + + hbox_main.pack_start(right_panel, True, True, 10) + + cls.vbox1.pack_start(hbox_main, True, True, 10) return @classmethod @@ -465,59 +500,11 @@ class ZFS: model[path][3] = not model[path][3] if model[path][3] is False: cls.zfs_disk_list.remove(model[path][0] + "-" + model[path][1]) - if cls.mirror == "1+ disks Stripe": - if len(cls.zfs_disk_list) >= 1: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "2+ disks Mirror": - if len(cls.zfs_disk_list) >= 2: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "3 disks RAIDZ1": - if len(cls.zfs_disk_list) == 3: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "4 disks RAIDZ2": - if len(cls.zfs_disk_list) == 4: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "5 disks RAIDZ3": - if len(cls.zfs_disk_list) == 5: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) + cls._update_next_button() else: if cls.check_if_small_disk(model[path][1]) is False: cls.zfs_disk_list.extend([model[path][0] + "-" + model[path][1]]) - if cls.mirror == "1+ disks Stripe": - if len(cls.zfs_disk_list) >= 1: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "2+ disks Mirror": - if len(cls.zfs_disk_list) >= 2: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "3 disks RAIDZ1": - if len(cls.zfs_disk_list) == 3: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "4 disks RAIDZ2": - if len(cls.zfs_disk_list) == 4: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "5 disks RAIDZ3": - if len(cls.zfs_disk_list) == 5: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) + cls._update_next_button() else: cls.check_cell.set_sensitive(False) cls.small_disk_warning() @@ -589,32 +576,8 @@ class ZFS: _widget: Entry widget that triggered the verification (unused) """ if cls.password.get_text() == cls.repassword.get_text(): - cls.img.set_from_stock(Gtk.STOCK_YES, 5) - if cls.mirror == "1+ disks Stripe": - if len(cls.zfs_disk_list) >= 1: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "2+ disks Mirror": - if len(cls.zfs_disk_list) >= 2: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "3 disks RAIDZ1": - if len(cls.zfs_disk_list) == 3: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "4 disks RAIDZ2": - if len(cls.zfs_disk_list) == 4: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) - elif cls.mirror == "5 disks RAIDZ3": - if len(cls.zfs_disk_list) == 5: - Button.next_button.set_sensitive(True) - else: - Button.next_button.set_sensitive(False) + cls.img.set_from_icon_name("gtk-yes", Gtk.IconSize.MENU) + cls._update_next_button() else: - cls.img.set_from_stock(Gtk.STOCK_NO, 5) + cls.img.set_from_icon_name("gtk-no", Gtk.IconSize.MENU) Button.next_button.set_sensitive(False) diff --git a/setup.py b/setup.py index 3c6c45e..2c66d60 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ from DistUtilsExtra.command.build_i18n import build_i18n from DistUtilsExtra.command.clean_i18n import clean_i18n prefix = sys.prefix -__VERSION__ = '0.1' +__VERSION__ = '0.4' PROGRAM_VERSION = __VERSION__ From 708aed30d63e4fe28bb04c70967b9ce64f0371b1 Mon Sep 17 00:00:00 2001 From: ericbsd Date: Wed, 13 May 2026 19:23:54 -0300 Subject: [PATCH 3/3] Fix ZFS page validation and Next button reliability Replace the swap size text entry with a SpinButton capped to the selected disk size, consolidate _disk_count_valid and _update_next_button into a single _is_ready method that checks all preconditions, update the Next button from on_password_changed so it disables when passwords stop matching, re-enable the Next button in back_page so navigating back no longer leaves it stuck disabled, reduce the left panel gap, and fix a startup crash from mirror_selection firing before swap_entry was created. --- install_station/interface_controller.py | 2 +- install_station/use_zfs.py | 89 ++++++++++--------------- 2 files changed, 38 insertions(+), 53 deletions(-) diff --git a/install_station/interface_controller.py b/install_station/interface_controller.py index 9e3e788..e4004ad 100644 --- a/install_station/interface_controller.py +++ b/install_station/interface_controller.py @@ -298,4 +298,4 @@ class Interface: current_page_widget = cls.page.get_nth_page(cls.page.get_current_page()) title_text = cls.page.get_tab_label_text(current_page_widget) Window.set_title(title_text) - # Button.next_button.set_sensitive(True) + Button.next_button.set_sensitive(True) diff --git a/install_station/use_zfs.py b/install_station/use_zfs.py index 6c59024..ccdcd90 100644 --- a/install_station/use_zfs.py +++ b/install_station/use_zfs.py @@ -40,7 +40,7 @@ class ZFS: pool_type = 'stripe' scheme = 'GPT' disk_encrypt = False - mirror = 'none' + mirror = 'stripe' vbox1 = None # UI elements as class variables @@ -75,7 +75,7 @@ class ZFS: raise ValueError("Password cannot be empty when disk encryption is enabled") size = int(cls.zfs_disk_list[0].partition('-')[2].rstrip()) - 512 - swap_size = int(cls.swap_entry.get_text() or '0') + swap_size = cls.swap_entry.get_value_as_int() zfs_num = size - swap_size dgeli = '.eli' if cls.disk_encrypt else '' @@ -118,39 +118,32 @@ class ZFS: InstallationData.zfs_config_data.append('commitDiskLabel\n') @classmethod - def _disk_count_valid(cls): + def _is_ready(cls): """ - Check if the number of selected disks meets the pool type requirement. + Check if all conditions are met to proceed to the next page. Returns: - bool: True if enough disks are selected for the current pool type + bool: True if disk count, swap entry, and encryption requirements are all satisfied. """ count = len(cls.zfs_disk_list) if cls.mirror == "stripe": - return count >= 1 + disks_ok = count >= 1 elif cls.mirror == "mirror": - return count >= 2 + disks_ok = count >= 2 elif cls.mirror == "raidz1": - return count == 3 + disks_ok = count == 3 elif cls.mirror == "raidz2": - return count == 4 + disks_ok = count == 4 elif cls.mirror == "raidz3": - return count == 5 - return False - - @classmethod - def _update_next_button(cls): - """ - Update next button sensitivity based on disk count and encryption state. - - When encryption is enabled, passwords must also match. - """ - if cls.disk_encrypt: - passwd_match = (cls.password.get_text() == cls.repassword.get_text() - and len(cls.password.get_text()) > 0) - Button.next_button.set_sensitive(cls._disk_count_valid() and passwd_match) + disks_ok = count == 5 else: - Button.next_button.set_sensitive(cls._disk_count_valid()) + disks_ok = False + if cls.disk_encrypt: + encrypt_ok = (cls.password.get_text() == cls.repassword.get_text() + and len(cls.password.get_text()) > 0) + else: + encrypt_ok = True + return disks_ok and encrypt_ok @classmethod def scheme_selection(cls, combobox): @@ -201,7 +194,7 @@ class ZFS: cls.pool_type = 'raidz3' cls.mirrorTips.set_text( get_text("Select 5 drives for RAIDZ3") + " " + smallest_msg) - cls._update_next_button() + Button.next_button.set_sensitive(cls._is_ready()) @classmethod def on_check_encrypt(cls, widget): @@ -223,7 +216,7 @@ class ZFS: cls.password.set_sensitive(False) cls.repassword.set_sensitive(False) cls.disk_encrypt = False - cls._update_next_button() + Button.next_button.set_sensitive(cls._is_ready()) @classmethod def on_password_changed(cls, _widget): @@ -237,23 +230,7 @@ class ZFS: _widget: Entry widget that triggered the change (unused) """ password_strength(cls.password.get_text(), cls.strenght_label) - - @classmethod - def digit_only(cls, widget, text, length, position): - """ - Block non-digit characters from being inserted into the swap entry. - - Connected to the swap entry 'insert-text' signal to prevent - non-numeric input before it enters the field. - - Args: - widget: Entry widget receiving the input - text: Text being inserted - length: Length of the text being inserted - position: Cursor position at time of insertion - """ - if not text.isdigit(): - widget.stop_emission_by_name('insert-text') + Button.next_button.set_sensitive(cls._is_ready()) @classmethod def initialize(cls): @@ -326,7 +303,7 @@ class ZFS: cls.mirrorTips.set_alignment(0.01, 0.5) # Pool Layout - cls.mirror = 'none' + cls.mirror = 'stripe' mirror_label = Gtk.Label(label=get_text('Pool Layout')) mirror_label.set_use_markup(True) mirror_box = Gtk.ComboBoxText() @@ -335,8 +312,8 @@ class ZFS: mirror_box.append_text("raidz1") mirror_box.append_text("raidz2") mirror_box.append_text("raidz3") - mirror_box.connect('changed', cls.mirror_selection) mirror_box.set_active(0) + mirror_box.connect('changed', cls.mirror_selection) # Pool Name (always editable) pool_label = Gtk.Label(label=get_text('Pool Name')) @@ -347,9 +324,10 @@ class ZFS: # Swap Size swap_label = Gtk.Label(label=get_text('Swap Size(MB)')) swap_label.set_use_markup(True) - cls.swap_entry = Gtk.Entry() - cls.swap_entry.set_text(str(get_ram_size_mb())) - cls.swap_entry.connect('insert-text', cls.digit_only) + ram_mb = get_ram_size_mb() + adj = Gtk.Adjustment(ram_mb, 0, ram_mb, 1, 100, 0) + cls.swap_entry = Gtk.SpinButton(adjustment=adj, numeric=True) + cls.swap_entry.set_editable(True) # Creating MBR or GPT drive cls.scheme = 'GPT' @@ -397,7 +375,7 @@ class ZFS: left_grid = Gtk.Grid() left_grid.set_row_spacing(4) left_grid.set_column_spacing(6) - left_grid.set_size_request(200, -1) + left_grid.set_hexpand(False) mirror_label.set_alignment(0, 0.5) pool_label.set_alignment(0, 0.5) @@ -500,14 +478,21 @@ class ZFS: model[path][3] = not model[path][3] if model[path][3] is False: cls.zfs_disk_list.remove(model[path][0] + "-" + model[path][1]) - cls._update_next_button() else: if cls.check_if_small_disk(model[path][1]) is False: cls.zfs_disk_list.extend([model[path][0] + "-" + model[path][1]]) - cls._update_next_button() else: cls.check_cell.set_sensitive(False) cls.small_disk_warning() + return True + + # Update swap SpinButton upper limit based on first selected disk + if cls.zfs_disk_list: + disk_size = int(cls.zfs_disk_list[0].partition('-')[2].rstrip()) - 512 + cls.swap_entry.get_adjustment().set_upper(disk_size) + else: + cls.swap_entry.get_adjustment().set_upper(get_ram_size_mb()) + Button.next_button.set_sensitive(cls._is_ready()) print(cls.zfs_disk_list) return True @@ -577,7 +562,7 @@ class ZFS: """ if cls.password.get_text() == cls.repassword.get_text(): cls.img.set_from_icon_name("gtk-yes", Gtk.IconSize.MENU) - cls._update_next_button() + Button.next_button.set_sensitive(cls._is_ready()) else: cls.img.set_from_icon_name("gtk-no", Gtk.IconSize.MENU) Button.next_button.set_sensitive(False)