From dea2ab6784414c9d5bee49282d4a1d256cbf3903 Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Sun, 29 Mar 2026 12:02:25 -0500 Subject: [PATCH] added python installer and common --- iso/common.nix | 5 +- iso/installer.py | 676 ++++++++++++++++++++++++----------------------- result | 2 +- 3 files changed, 345 insertions(+), 338 deletions(-) diff --git a/iso/common.nix b/iso/common.nix index 6da8025..2dc74d0 100644 --- a/iso/common.nix +++ b/iso/common.nix @@ -16,11 +16,9 @@ in image.baseName = lib.mkForce "Sovran_SystemsOS"; isoImage.splashImage = ./assets/splash-logo.png; - # Disable GNOME first-run tour and initial setup services.gnome.gnome-initial-setup.enable = false; environment.gnome.excludePackages = with pkgs; [ gnome-tour gnome-user-docs ]; - # Passwordless sudo for live ISO session security.sudo.wheelNeedsPassword = false; users.users.free = { isNormalUser = true; @@ -37,7 +35,8 @@ in environment.systemPackages = with pkgs; [ installerPy (python3.withPackages (ps: [ ps.pygobject3 ])) - gtk3 + gtk4 + libadwaita gobject-introspection util-linux disko diff --git a/iso/installer.py b/iso/installer.py index b998b8e..625da05 100644 --- a/iso/installer.py +++ b/iso/installer.py @@ -1,67 +1,73 @@ #!/usr/bin/env python3 import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Pango, GdkPixbuf +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Gtk, Adw, GLib, Pango, GdkPixbuf, Gio import subprocess import threading import os -import sys LOGO = "/etc/sovran/logo.png" LOG = "/tmp/sovran-install.log" FLAKE = "/etc/sovran/flake" + logfile = open(LOG, "a") def log(msg): logfile.write(msg + "\n") logfile.flush() -def run(cmd, **kwargs): +def run(cmd): log(f"$ {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True, **kwargs) + result = subprocess.run(cmd, capture_output=True, text=True) log(result.stdout) if result.returncode != 0: log(result.stderr) - raise RuntimeError(result.stderr or f"Command failed: {cmd}") + raise RuntimeError(result.stderr.strip() or f"Command failed: {' '.join(cmd)}") return result.stdout.strip() -def run_stream(cmd, text_buffer): +def run_stream(cmd, buf): log(f"$ {' '.join(cmd)}") proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) for line in proc.stdout: log(line.rstrip()) - GLib.idle_add(append_text, text_buffer, line) + GLib.idle_add(append_text, buf, line) proc.wait() if proc.returncode != 0: - raise RuntimeError(f"Command failed with code {proc.returncode}. See {LOG}") + raise RuntimeError(f"Command failed (exit {proc.returncode}). See {LOG}") def append_text(buf, text): buf.insert(buf.get_end_iter(), text) return False def human_size(nbytes): - for unit in ["B","KB","MB","GB","TB"]: + for unit in ["B", "KB", "MB", "GB", "TB"]: if nbytes < 1024: return f"{nbytes:.1f} {unit}" nbytes /= 1024 return f"{nbytes:.1f} PB" -# ── Window base ──────────────────────────────────────────────────────────────── -class InstallerWindow(Gtk.Window): +# ── Application ──────────────────────────────────────────────────────────────── + +class InstallerApp(Adw.Application): def __init__(self): - super().__init__(title="Sovran_SystemsOS Installer") - self.set_default_size(800, 560) - self.set_position(Gtk.WindowPosition.CENTER) - self.set_resizable(False) - if os.path.exists(LOGO): - self.set_icon_from_file(LOGO) - self.connect("delete-event", Gtk.main_quit) + super().__init__(application_id="com.sovransystems.installer") + self.connect("activate", self.on_activate) - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT) - self.stack.set_transition_duration(300) - self.add(self.stack) + def on_activate(self, app): + self.win = InstallerWindow(application=app) + self.win.present() + + +# ── Main Window ──────────────────────────────────────────────────────────────── + +class InstallerWindow(Adw.ApplicationWindow): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.set_title("Sovran_SystemsOS Installer") + self.set_default_size(820, 600) + self.set_resizable(False) self.role = None self.boot_disk = None @@ -69,148 +75,133 @@ class InstallerWindow(Gtk.Window): self.data_disk = None self.data_size = None - self.show_welcome() - self.show_all() + # Root navigation view + self.nav = Adw.NavigationView() + self.set_content(self.nav) - # ── Helpers ──────────────────────────────────────────────────────────── + self.push_welcome() - def clear_stack(self): - for child in self.stack.get_children(): - self.stack.remove(child) + # ── Navigation helpers ───────────────────────────────────────────────── - def set_page(self, widget, name="page"): - self.clear_stack() - self.stack.add_named(widget, name) - self.stack.set_visible_child_name(name) - self.show_all() + def push_page(self, title, child, show_back=False): + page = Adw.NavigationPage(title=title, tag=title) + toolbar = Adw.ToolbarView() - def make_header(self, title, subtitle=None): - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - box.set_margin_top(32) - box.set_margin_bottom(16) + header = Adw.HeaderBar() + header.set_show_end_title_buttons(False) + if not show_back: + header.set_show_start_title_buttons(False) + toolbar.add_top_bar(header) + toolbar.set_content(child) + + page.set_child(toolbar) + self.nav.push(page) + + def replace_page(self, title, child): + # Pop all and push fresh + while self.nav.get_visible_page() is not None: + try: + self.nav.pop() + except Exception: + break + self.push_page(title, child) + + # ── Shared widgets ───────────────────────────────────────────────────── + + def make_scrolled_log(self): + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + sw.set_vexpand(True) + + tv = Gtk.TextView() + tv.set_editable(False) + tv.set_cursor_visible(False) + tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + tv.set_monospace(True) + tv.add_css_class("log-view") + buf = tv.get_buffer() + + def on_changed(b): + GLib.idle_add(lambda: sw.get_vadjustment().set_value( + sw.get_vadjustment().get_upper() + )) + buf.connect("changed", on_changed) + + sw.set_child(tv) + return sw, buf + + def nav_row(self, back_label=None, back_cb=None, next_label="Continue", next_cb=None): + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + box.set_margin_top(12) + box.set_margin_bottom(24) box.set_margin_start(40) box.set_margin_end(40) - lbl = Gtk.Label() - lbl.set_markup(f"{title}") - lbl.set_halign(Gtk.Align.START) - box.pack_start(lbl, False, False, 0) - - if subtitle: - sub = Gtk.Label() - sub.set_markup(f"{subtitle}") - sub.set_halign(Gtk.Align.START) - box.pack_start(sub, False, False, 0) - - sep = Gtk.Separator() - sep.set_margin_top(12) - box.pack_start(sep, False, False, 0) - return box - - def make_nav(self, back_label=None, back_cb=None, next_label="Continue", next_cb=None): - bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) - bar.set_margin_top(8) - bar.set_margin_bottom(20) - bar.set_margin_start(40) - bar.set_margin_end(40) - if back_label and back_cb: btn = Gtk.Button(label=back_label) btn.connect("clicked", back_cb) - bar.pack_start(btn, False, False, 0) + box.append(btn) - bar.pack_start(Gtk.Label(), True, True, 0) + spacer = Gtk.Label() + spacer.set_hexpand(True) + box.append(spacer) if next_cb: btn = Gtk.Button(label=next_label) - btn.get_style_context().add_class("suggested-action") + btn.add_css_class("suggested-action") + btn.add_css_class("pill") btn.connect("clicked", next_cb) - bar.pack_end(btn, False, False, 0) + box.append(btn) - return bar - - def error(self, msg): - GLib.idle_add(self._show_error, msg) - - def _show_error(self, msg): - page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - page.pack_start(self.make_header("Installation Error", "Something went wrong."), False, False, 0) - - body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) - body.set_margin_start(40) - body.set_margin_end(40) - - icon = Gtk.Label() - icon.set_markup("") - body.pack_start(icon, False, False, 0) - - lbl = Gtk.Label(label=msg) - lbl.set_line_wrap(True) - lbl.set_max_width_chars(60) - lbl.set_justify(Gtk.Justification.CENTER) - body.pack_start(lbl, False, False, 0) - - log_lbl = Gtk.Label() - log_lbl.set_markup(f"Full log: {LOG}") - body.pack_start(log_lbl, False, False, 0) - - page.pack_start(body, True, True, 0) - - quit_btn = Gtk.Button(label="Close Installer") - quit_btn.connect("clicked", Gtk.main_quit) - nav = Gtk.Box() - nav.set_margin_bottom(20) - nav.set_margin_end(40) - nav.pack_end(quit_btn, False, False, 0) - page.pack_start(nav, False, False, 0) - - self.set_page(page) - return False + return box # ── Step 1: Welcome & Role ───────────────────────────────────────────── - def show_welcome(self): - page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + def push_welcome(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - # Hero banner - hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - hero.set_margin_top(36) - hero.set_margin_bottom(20) + # Hero + hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + hero.set_margin_top(32) + hero.set_margin_bottom(24) + hero.set_halign(Gtk.Align.CENTER) if os.path.exists(LOGO): try: - pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(LOGO, 80, 80, True) - img = Gtk.Image.new_from_pixbuf(pb) - hero.pack_start(img, False, False, 0) + img = Gtk.Image.new_from_file(LOGO) + img.set_pixel_size(96) + hero.append(img) except Exception: pass title = Gtk.Label() - title.set_markup("Sovran Systems") - hero.pack_start(title, False, False, 0) + title.set_markup("Sovran Systems") + hero.append(title) sub = Gtk.Label() - sub.set_markup("Be Digitally Sovereign") - hero.pack_start(sub, False, False, 0) + sub.set_markup("Be Digitally Sovereign") + hero.append(sub) - page.pack_start(hero, False, False, 0) + outer.append(hero) sep = Gtk.Separator() sep.set_margin_start(40) sep.set_margin_end(40) - page.pack_start(sep, False, False, 0) + outer.append(sep) - # Role selection - lbl = Gtk.Label() - lbl.set_markup("Select your installation type:") - lbl.set_margin_top(20) - lbl.set_margin_start(40) - lbl.set_halign(Gtk.Align.START) - page.pack_start(lbl, False, False, 0) + # Role label + role_lbl = Gtk.Label() + role_lbl.set_markup("Choose your installation type:") + role_lbl.set_halign(Gtk.Align.START) + role_lbl.set_margin_start(40) + role_lbl.set_margin_top(20) + role_lbl.set_margin_bottom(8) + outer.append(role_lbl) + # Role cards roles = [ ("Server + Desktop", - "The full Sovran experience: beautiful desktop + your own cloud, secure messaging, Bitcoin node, and more.", + "Full sovereign experience: beautiful desktop, your own cloud, secure messaging, Bitcoin node, and more.", "Server+Desktop"), ("Desktop Only", "A beautiful, easy-to-use desktop without the background server applications.", @@ -220,56 +211,54 @@ class InstallerWindow(Gtk.Window): "Node (Bitcoin-only)"), ] - radio_group = None self._role_radios = [] - role_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - role_box.set_margin_start(40) - role_box.set_margin_end(40) - role_box.set_margin_top(8) + radio_group = None + cards_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + cards_box.set_margin_start(40) + cards_box.set_margin_end(40) for label, desc, key in roles: - row = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) - row.set_margin_bottom(8) + card = Adw.ActionRow() + card.set_title(label) + card.set_subtitle(desc) - radio = Gtk.RadioButton(group=radio_group, label=label) + radio = Gtk.CheckButton() radio.set_name(key) if radio_group is None: radio_group = radio radio.set_active(True) - radio.get_style_context().add_class("role-radio") + else: + radio.set_group(radio_group) + + card.add_prefix(radio) + card.set_activatable_widget(radio) self._role_radios.append(radio) + cards_box.append(card) - desc_lbl = Gtk.Label(label=desc) - desc_lbl.set_line_wrap(True) - desc_lbl.set_max_width_chars(70) - desc_lbl.set_halign(Gtk.Align.START) - desc_lbl.set_margin_start(28) - desc_lbl.get_style_context().add_class("dim-label") + outer.append(cards_box) - row.pack_start(radio, False, False, 0) - row.pack_start(desc_lbl, False, False, 0) - role_box.pack_start(row, False, False, 0) + outer.append(Gtk.Label(label="", vexpand=True)) + outer.append(self.nav_row( + next_label="Next →", + next_cb=self.on_role_next + )) - page.pack_start(role_box, False, False, 0) - page.pack_start(Gtk.Label(), True, True, 0) - page.pack_start(self.make_nav(next_label="Next →", next_cb=self.on_role_next), False, False, 0) - self.set_page(page, "welcome") + self.push_page("Welcome to Sovran_SystemsOS Installer", outer) def on_role_next(self, btn): for radio in self._role_radios: if radio.get_active(): self.role = radio.get_name() break - self.show_disk_confirm() + self.push_disk_confirm() # ── Step 2: Disk Confirm ─────────────────────────────────────────────── - def show_disk_confirm(self): - # Detect disks + def push_disk_confirm(self): try: raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"]) except Exception as e: - self.error(str(e)) + self.show_error(str(e)) return disks = [] @@ -281,7 +270,7 @@ class InstallerWindow(Gtk.Window): disks.append((parts[0], int(parts[1]))) if not disks: - self.error("No valid internal drives found. USB drives are excluded.") + self.show_error("No valid internal drives found. USB drives are excluded.") return disks.sort(key=lambda x: x[1]) @@ -294,133 +283,119 @@ class InstallerWindow(Gtk.Window): if s >= BYTES_2TB: self.data_disk, self.data_size = d, s - page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - page.pack_start(self.make_header( - "Confirm Installation", - "Review the disks below. ALL DATA will be permanently erased." - ), False, False, 0) + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - body.set_margin_start(40) - body.set_margin_end(40) - body.set_margin_top(8) + # Disk info group + disk_group = Adw.PreferencesGroup() + disk_group.set_title("Drives to be erased") + disk_group.set_margin_top(24) + disk_group.set_margin_start(40) + disk_group.set_margin_end(40) - # Disk info box - disk_frame = Gtk.Frame() - disk_frame.set_shadow_type(Gtk.ShadowType.IN) - disk_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) - disk_inner.set_margin_top(12) - disk_inner.set_margin_bottom(12) - disk_inner.set_margin_start(16) - disk_inner.set_margin_end(16) - - boot_lbl = Gtk.Label() - boot_lbl.set_markup(f"Boot disk: /dev/{self.boot_disk} ({human_size(self.boot_size)})") - boot_lbl.set_halign(Gtk.Align.START) - disk_inner.pack_start(boot_lbl, False, False, 0) + boot_row = Adw.ActionRow() + boot_row.set_title("Boot Disk") + boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}") + boot_row.add_prefix(Gtk.Image.new_from_icon_name("drive-harddisk-symbolic")) + disk_group.add(boot_row) if self.data_disk: - data_lbl = Gtk.Label() - data_lbl.set_markup(f"Data disk: /dev/{self.data_disk} ({human_size(self.data_size)})") - data_lbl.set_halign(Gtk.Align.START) - disk_inner.pack_start(data_lbl, False, False, 0) + data_row = Adw.ActionRow() + data_row.set_title("Data Disk") + data_row.set_subtitle(f"/dev/{self.data_disk} — {human_size(self.data_size)}") + data_row.add_prefix(Gtk.Image.new_from_icon_name("drive-harddisk-symbolic")) + disk_group.add(data_row) else: - no_data = Gtk.Label() - no_data.set_markup("Data disk: none detected (requires 2TB+)") - no_data.set_halign(Gtk.Align.START) - disk_inner.pack_start(no_data, False, False, 0) + no_row = Adw.ActionRow() + no_row.set_title("Data Disk") + no_row.set_subtitle("None detected (requires 2 TB or larger)") + no_row.add_prefix(Gtk.Image.new_from_icon_name("drive-harddisk-symbolic")) + disk_group.add(no_row) - disk_frame.add(disk_inner) - body.pack_start(disk_frame, False, False, 0) + outer.append(disk_group) - warn = Gtk.Label() - warn.set_markup( - "⚠ This action cannot be undone. " - "All existing data on the above disk(s) will be permanently destroyed." - ) - warn.set_line_wrap(True) - warn.set_max_width_chars(65) - warn.set_halign(Gtk.Align.START) - body.pack_start(warn, False, False, 0) + # Warning banner + banner = Adw.Banner() + banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.") + banner.set_revealed(True) + banner.set_margin_top(16) + banner.set_margin_start(40) + banner.set_margin_end(40) + outer.append(banner) - # Confirm entry - confirm_lbl = Gtk.Label(label='Type ERASE to confirm:') - confirm_lbl.set_halign(Gtk.Align.START) - body.pack_start(confirm_lbl, False, False, 0) + # Confirm entry group + entry_group = Adw.PreferencesGroup() + entry_group.set_title("Type ERASE to confirm") + entry_group.set_margin_top(16) + entry_group.set_margin_start(40) + entry_group.set_margin_end(40) - self._confirm_entry = Gtk.Entry() - self._confirm_entry.set_placeholder_text("ERASE") - body.pack_start(self._confirm_entry, False, False, 0) + entry_row = Adw.EntryRow() + entry_row.set_title("Confirmation") + self._confirm_entry = entry_row + entry_group.add(entry_row) - page.pack_start(body, False, False, 0) - page.pack_start(Gtk.Label(), True, True, 0) - page.pack_start(self.make_nav( - back_label="← Back", back_cb=lambda b: self.show_welcome(), - next_label="Begin Installation", next_cb=self.on_confirm_next - ), False, False, 0) - self.set_page(page, "disks") + outer.append(entry_group) + outer.append(Gtk.Label(label="", vexpand=True)) + outer.append(self.nav_row( + back_label="← Back", + back_cb=lambda b: self.nav.pop(), + next_label="Begin Installation", + next_cb=self.on_confirm_next + )) + + self.push_page("Confirm Installation", outer, show_back=True) def on_confirm_next(self, btn): if self._confirm_entry.get_text().strip() != "ERASE": - dlg = Gtk.MessageDialog( + dlg = Adw.MessageDialog( transient_for=self, - modal=True, - message_type=Gtk.MessageType.WARNING, - buttons=Gtk.ButtonsType.OK, - text="You must type ERASE exactly to continue." + heading="Confirmation Required", + body="You must type ERASE exactly to proceed." ) - dlg.run() - dlg.destroy() + dlg.add_response("ok", "OK") + dlg.present() return - self.show_progress("Preparing Drives", "Partitioning and formatting your drives...", self.do_partition) + self.push_progress( + "Preparing Drives", + "Partitioning and formatting your drives...", + self.do_partition + ) - # ── Step 3 & 5: Progress with live log ──────────────────────────────── + # ── Step 3 & 5: Progress ────────────────────────────────────────────── - def show_progress(self, title, subtitle, worker_fn): - page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - page.pack_start(self.make_header(title, subtitle), False, False, 0) + def push_progress(self, title, subtitle, worker_fn): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - body.set_margin_start(40) - body.set_margin_end(40) + status = Adw.StatusPage() + status.set_title(title) + status.set_description(subtitle) + status.set_icon_name("emblem-synchronizing-symbolic") + status.set_vexpand(False) + outer.append(status) spinner = Gtk.Spinner() - spinner.set_size_request(48, 48) + spinner.set_size_request(32, 32) + spinner.set_halign(Gtk.Align.CENTER) spinner.start() - body.pack_start(spinner, False, False, 0) + outer.append(spinner) - # Scrollable live log - sw = Gtk.ScrolledWindow() - sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - sw.set_size_request(-1, 260) + sw, buf = self.make_scrolled_log() + sw.set_margin_start(40) + sw.set_margin_end(40) + sw.set_margin_top(12) + sw.set_margin_bottom(12) + sw.set_vexpand(True) + outer.append(sw) - tv = Gtk.TextView() - tv.set_editable(False) - tv.set_cursor_visible(False) - tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - tv.modify_font(Pango.FontDescription("Monospace 9")) - tv.get_style_context().add_class("log-view") - buf = tv.get_buffer() - sw.add(tv) - body.pack_start(sw, True, True, 0) - - # Auto-scroll to bottom - def on_changed(b): - adj = sw.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) - buf.connect("changed", on_changed) - - page.pack_start(body, True, True, 0) - self.set_page(page, "progress") + self.push_page(title, outer) def thread_fn(): try: worker_fn(buf) except Exception as e: - self.error(str(e)) + GLib.idle_add(self.show_error, str(e)) - t = threading.Thread(target=thread_fn, daemon=True) - t.start() + threading.Thread(target=thread_fn, daemon=True).start() # ── Worker: partition ───────────────────────────────────────────────── @@ -448,8 +423,9 @@ class InstallerWindow(Gtk.Window): GLib.idle_add(append_text, buf, "\n=== Writing role config ===\n") self.write_role_state() + GLib.idle_add(append_text, buf, "Done.\n") - GLib.idle_add(self.show_install_step) + GLib.idle_add(self.push_ready) def write_role_state(self): is_server = str(self.role == "Server+Desktop").lower() @@ -470,131 +446,163 @@ class InstallerWindow(Gtk.Window): log(proc.stdout) if proc.returncode != 0: raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}") - run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"]) - # ── Step 4: Confirm before full install ─────────────────────────────── + # ── Step 4: Ready to install ─────────────────────────────────────────── - def show_install_step(self): - page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - page.pack_start(self.make_header( - "Drives Ready", - "Your drives have been partitioned. Ready to install Sovran SystemsOS." - ), False, False, 0) + def push_ready(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - body.set_margin_start(40) - body.set_margin_end(40) - body.set_margin_top(16) + status = Adw.StatusPage() + status.set_title("Drives Ready") + status.set_description("Your drives have been partitioned successfully.") + status.set_icon_name("emblem-ok-symbolic") + status.set_vexpand(True) - icon = Gtk.Label() - icon.set_markup("") - body.pack_start(icon, False, False, 0) + details = Adw.PreferencesGroup() + details.set_margin_start(40) + details.set_margin_end(40) - info = Gtk.Label() - info.set_markup( - f"Role: {self.role}\n" - f"Boot disk: /dev/{self.boot_disk} ({human_size(self.boot_size)})\n" + - (f"Data disk: /dev/{self.data_disk} ({human_size(self.data_size)})" - if self.data_disk else "Data disk: none") - ) - info.set_halign(Gtk.Align.CENTER) - info.set_justify(Gtk.Justification.CENTER) - body.pack_start(info, False, False, 0) + role_row = Adw.ActionRow() + role_row.set_title("Installation Type") + role_row.set_subtitle(self.role) + details.add(role_row) + + boot_row = Adw.ActionRow() + boot_row.set_title("Boot Disk") + boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}") + details.add(boot_row) + + if self.data_disk: + data_row = Adw.ActionRow() + data_row.set_title("Data Disk") + data_row.set_subtitle(f"/dev/{self.data_disk} — {human_size(self.data_size)}") + details.add(data_row) + + status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) + status_box.append(status) + status_box.append(details) note = Gtk.Label() note.set_markup( - "The next step will install the full system.\n" + "The next step will install the full system.\n" "This may take 20–40 minutes depending on your internet speed.\n" "Do not turn off your computer." ) note.set_justify(Gtk.Justification.CENTER) - note.set_line_wrap(True) - body.pack_start(note, False, False, 0) + note.set_wrap(True) + note.set_margin_top(16) + note.set_margin_start(40) + note.set_margin_end(40) + status_box.append(note) - page.pack_start(body, True, True, 0) - page.pack_start(self.make_nav( - next_label="Install Now", next_cb=lambda b: self.show_progress( + outer.append(status_box) + outer.append(self.nav_row( + next_label="Install Now", + next_cb=lambda b: self.push_progress( "Installing Sovran SystemsOS", - "Building and installing your system. This will take a while...", + "Building and installing your system. Please wait...", self.do_install ) - ), False, False, 0) - self.set_page(page, "ready") + )) + + self.push_page("Ready to Install", outer) return False # ── Worker: install ─────────────────────────────────────────────────── def do_install(self, buf): GLib.idle_add(append_text, buf, "=== Running nixos-install ===\n") + + for f in ["/mnt/etc/nixos/role-state.nix", "/mnt/etc/nixos/custom.nix"]: + if not os.path.exists(f): + raise RuntimeError(f"Required file missing: {f}") + run_stream([ "sudo", "nixos-install", "--root", "/mnt", "--flake", "/mnt/etc/nixos#nixos" ], buf) - GLib.idle_add(self.show_complete) - # ── Step 6: Complete ────────────────────────────────────────────────── + GLib.idle_add(self.push_complete) - def show_complete(self): - page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - page.pack_start(self.make_header("Installation Complete! 🎉", "Welcome to Sovran SystemsOS."), False, False, 0) + # ── Step 6: Complete ─────────────────────────────────────────────────── - body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) - body.set_margin_start(40) - body.set_margin_end(40) - body.set_margin_top(16) + def push_complete(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - icon = Gtk.Label() - icon.set_markup("🎉") - body.pack_start(icon, False, False, 0) + status = Adw.StatusPage() + status.set_title("Installation Complete!") + status.set_description("Welcome to Sovran SystemsOS.") + status.set_icon_name("emblem-ok-symbolic") + status.set_vexpand(True) - creds_frame = Gtk.Frame(label=" Write down your login details ") - creds_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - creds_inner.set_margin_top(10) - creds_inner.set_margin_bottom(10) - creds_inner.set_margin_start(16) - creds_inner.set_margin_end(16) + creds_group = Adw.PreferencesGroup() + creds_group.set_title("⚠ Write down your login details before rebooting") + creds_group.set_margin_start(40) + creds_group.set_margin_end(40) - user_lbl = Gtk.Label() - user_lbl.set_markup("Username: free") - user_lbl.set_halign(Gtk.Align.START) - creds_inner.pack_start(user_lbl, False, False, 0) + user_row = Adw.ActionRow() + user_row.set_title("Username") + user_row.set_subtitle("free") + creds_group.add(user_row) - pass_lbl = Gtk.Label() - pass_lbl.set_markup("Password: free") - pass_lbl.set_halign(Gtk.Align.START) - creds_inner.pack_start(pass_lbl, False, False, 0) + pass_row = Adw.ActionRow() + pass_row.set_title("Password") + pass_row.set_subtitle("free") + creds_group.add(pass_row) - creds_frame.add(creds_inner) - body.pack_start(creds_frame, False, False, 0) - - note = Gtk.Label() - note.set_markup( - "🚨 Do not lose this password — you will be permanently locked out if you forget it.\n\n" - "📁 After rebooting, your system will finish setting up and save all app passwords\n" - "(Nextcloud, Bitcoin, Matrix, etc.) to a secure PDF in your Documents folder." + note_row = Adw.ActionRow() + note_row.set_title("App Passwords") + note_row.set_subtitle( + "After rebooting, all app passwords (Nextcloud, Bitcoin, Matrix, etc.) " + "will be saved to a secure PDF in your Documents folder." ) - note.set_line_wrap(True) - note.set_max_width_chars(65) - note.set_justify(Gtk.Justification.CENTER) - body.pack_start(note, False, False, 0) + creds_group.add(note_row) - page.pack_start(body, True, True, 0) + content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) + content_box.append(status) + content_box.append(creds_group) + outer.append(content_box) reboot_btn = Gtk.Button(label="Reboot Now") - reboot_btn.get_style_context().add_class("suggested-action") + reboot_btn.add_css_class("suggested-action") + reboot_btn.add_css_class("pill") + reboot_btn.add_css_class("destructive-action") reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"])) - nav = Gtk.Box() - nav.set_margin_bottom(20) - nav.set_margin_end(40) - nav.pack_end(reboot_btn, False, False, 0) - page.pack_start(nav, False, False, 0) - self.set_page(page, "complete") + nav = Gtk.Box() + nav.set_margin_bottom(24) + nav.set_margin_end(40) + nav.set_halign(Gtk.Align.END) + nav.append(reboot_btn) + outer.append(nav) + + self.push_page("Complete", outer) + return False + + # ── Error screen ─────────────────────────────────────────────────────── + + def show_error(self, msg): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + status = Adw.StatusPage() + status.set_title("Installation Error") + status.set_description(msg) + status.set_icon_name("dialog-error-symbolic") + status.set_vexpand(True) + + log_lbl = Gtk.Label() + log_lbl.set_markup(f"Full log: {LOG}") + log_lbl.set_margin_bottom(24) + + outer.append(status) + outer.append(log_lbl) + + self.push_page("Error", outer) return False if __name__ == "__main__": - win = InstallerWindow() - Gtk.main() \ No newline at end of file + app = InstallerApp() + app.run(None) diff --git a/result b/result index f5a2b75..eb37b95 120000 --- a/result +++ b/result @@ -1 +1 @@ -/nix/store/yaw5krmqdcyw79z4d91hyg92fwdas91k-Sovran_SystemsOS.iso \ No newline at end of file +/nix/store/mjb94jp3bsb532zc7hsp8wf8v8s450k5-Sovran_SystemsOS.iso \ No newline at end of file