diff --git a/iso/common.nix b/iso/common.nix
index fca5ed5..6da8025 100644
--- a/iso/common.nix
+++ b/iso/common.nix
@@ -2,7 +2,10 @@
let
sovranSource = builtins.path { path = ../.; name = "sovran-systemsos"; };
- installer = pkgs.writeShellScriptBin "sovran-install" (builtins.readFile ./installer.sh);
+
+ installerPy = pkgs.writeShellScriptBin "sovran-install" ''
+ exec ${pkgs.python3.withPackages (ps: [ ps.pygobject3 ])}/bin/python3 /etc/sovran/installer.py
+ '';
in
{
imports = [
@@ -13,11 +16,11 @@ in
image.baseName = lib.mkForce "Sovran_SystemsOS";
isoImage.splashImage = ./assets/splash-logo.png;
- # Disable GNOME first-run tour and initial setup so the installer autostarts instead
+ # 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 ];
- # Allow free user to run installer commands as root without a password
+ # Passwordless sudo for live ISO session
security.sudo.wheelNeedsPassword = false;
users.users.free = {
isNormalUser = true;
@@ -32,8 +35,10 @@ in
nix-bitcoin.generateSecrets = true;
environment.systemPackages = with pkgs; [
- installer
- zenity
+ installerPy
+ (python3.withPackages (ps: [ ps.pygobject3 ]))
+ gtk3
+ gobject-introspection
util-linux
disko
parted
@@ -46,14 +51,14 @@ in
];
environment.etc."sovran/logo.png".source = ./assets/splash-logo.png;
-
environment.etc."sovran/flake".source = sovranSource;
+ environment.etc."sovran/installer.py".source = ./installer.py;
environment.etc."xdg/autostart/sovran-installer.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Sovran Guided Installer
-Exec=bash -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus ${installer}/bin/sovran-install"
+Exec=bash -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus ${installerPy}/bin/sovran-install"
Terminal=false
X-GNOME-Autostart-enabled=true
'';
diff --git a/iso/installer.py b/iso/installer.py
new file mode 100644
index 0000000..b998b8e
--- /dev/null
+++ b/iso/installer.py
@@ -0,0 +1,600 @@
+#!/usr/bin/env python3
+import gi
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib, Pango, GdkPixbuf
+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):
+ log(f"$ {' '.join(cmd)}")
+ result = subprocess.run(cmd, capture_output=True, text=True, **kwargs)
+ log(result.stdout)
+ if result.returncode != 0:
+ log(result.stderr)
+ raise RuntimeError(result.stderr or f"Command failed: {cmd}")
+ return result.stdout.strip()
+
+def run_stream(cmd, text_buffer):
+ 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)
+ proc.wait()
+ if proc.returncode != 0:
+ raise RuntimeError(f"Command failed with code {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"]:
+ if nbytes < 1024:
+ return f"{nbytes:.1f} {unit}"
+ nbytes /= 1024
+ return f"{nbytes:.1f} PB"
+
+# ── Window base ────────────────────────────────────────────────────────────────
+
+class InstallerWindow(Gtk.Window):
+ 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)
+
+ self.stack = Gtk.Stack()
+ self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
+ self.stack.set_transition_duration(300)
+ self.add(self.stack)
+
+ self.role = None
+ self.boot_disk = None
+ self.boot_size = None
+ self.data_disk = None
+ self.data_size = None
+
+ self.show_welcome()
+ self.show_all()
+
+ # ── Helpers ────────────────────────────────────────────────────────────
+
+ def clear_stack(self):
+ for child in self.stack.get_children():
+ self.stack.remove(child)
+
+ 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 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)
+ 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)
+
+ bar.pack_start(Gtk.Label(), True, True, 0)
+
+ if next_cb:
+ btn = Gtk.Button(label=next_label)
+ btn.get_style_context().add_class("suggested-action")
+ btn.connect("clicked", next_cb)
+ bar.pack_end(btn, False, False, 0)
+
+ 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
+
+ # ── Step 1: Welcome & Role ─────────────────────────────────────────────
+
+ def show_welcome(self):
+ page = 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)
+
+ 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)
+ except Exception:
+ pass
+
+ title = Gtk.Label()
+ title.set_markup("Sovran Systems")
+ hero.pack_start(title, False, False, 0)
+
+ sub = Gtk.Label()
+ sub.set_markup("Be Digitally Sovereign")
+ hero.pack_start(sub, False, False, 0)
+
+ page.pack_start(hero, False, False, 0)
+
+ sep = Gtk.Separator()
+ sep.set_margin_start(40)
+ sep.set_margin_end(40)
+ page.pack_start(sep, False, False, 0)
+
+ # 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)
+
+ roles = [
+ ("Server + Desktop",
+ "The full Sovran 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.",
+ "Desktop Only"),
+ ("Node Only",
+ "Full Bitcoin node with Lightning and non-KYC buying and selling. No desktop.",
+ "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)
+
+ for label, desc, key in roles:
+ row = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
+ row.set_margin_bottom(8)
+
+ radio = Gtk.RadioButton(group=radio_group, label=label)
+ radio.set_name(key)
+ if radio_group is None:
+ radio_group = radio
+ radio.set_active(True)
+ radio.get_style_context().add_class("role-radio")
+ self._role_radios.append(radio)
+
+ 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")
+
+ row.pack_start(radio, False, False, 0)
+ row.pack_start(desc_lbl, False, False, 0)
+ role_box.pack_start(row, False, False, 0)
+
+ 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")
+
+ 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()
+
+ # ── Step 2: Disk Confirm ───────────────────────────────────────────────
+
+ def show_disk_confirm(self):
+ # Detect disks
+ try:
+ raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
+ except Exception as e:
+ self.error(str(e))
+ return
+
+ disks = []
+ for line in raw.splitlines():
+ parts = line.split()
+ if len(parts) >= 4 and parts[2] == "disk" and parts[3] == "0":
+ tran = parts[4] if len(parts) >= 5 else ""
+ if tran != "usb":
+ disks.append((parts[0], int(parts[1])))
+
+ if not disks:
+ self.error("No valid internal drives found. USB drives are excluded.")
+ return
+
+ disks.sort(key=lambda x: x[1])
+ self.boot_disk, self.boot_size = disks[0]
+ self.data_disk, self.data_size = None, None
+
+ BYTES_2TB = 2 * 1024 ** 4
+ if len(disks) >= 2:
+ d, s = disks[-1]
+ 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)
+
+ 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 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)
+
+ 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)
+ 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)
+
+ disk_frame.add(disk_inner)
+ body.pack_start(disk_frame, False, False, 0)
+
+ 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)
+
+ # 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)
+
+ self._confirm_entry = Gtk.Entry()
+ self._confirm_entry.set_placeholder_text("ERASE")
+ body.pack_start(self._confirm_entry, False, False, 0)
+
+ 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")
+
+ def on_confirm_next(self, btn):
+ if self._confirm_entry.get_text().strip() != "ERASE":
+ dlg = Gtk.MessageDialog(
+ transient_for=self,
+ modal=True,
+ message_type=Gtk.MessageType.WARNING,
+ buttons=Gtk.ButtonsType.OK,
+ text="You must type ERASE exactly to continue."
+ )
+ dlg.run()
+ dlg.destroy()
+ return
+ self.show_progress("Preparing Drives", "Partitioning and formatting your drives...", self.do_partition)
+
+ # ── Step 3 & 5: Progress with live log ────────────────────────────────
+
+ 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)
+
+ body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
+ body.set_margin_start(40)
+ body.set_margin_end(40)
+
+ spinner = Gtk.Spinner()
+ spinner.set_size_request(48, 48)
+ spinner.start()
+ body.pack_start(spinner, False, False, 0)
+
+ # Scrollable live log
+ sw = Gtk.ScrolledWindow()
+ sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ sw.set_size_request(-1, 260)
+
+ 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")
+
+ def thread_fn():
+ try:
+ worker_fn(buf)
+ except Exception as e:
+ self.error(str(e))
+
+ t = threading.Thread(target=thread_fn, daemon=True)
+ t.start()
+
+ # ── Worker: partition ─────────────────────────────────────────────────
+
+ def do_partition(self, buf):
+ GLib.idle_add(append_text, buf, "=== Partitioning drives ===\n")
+ boot_path = f"/dev/{self.boot_disk}"
+ cmd = [
+ "sudo", "disko", "--mode", "disko",
+ f"{FLAKE}/iso/disko.nix",
+ "--arg", "device", f'"{boot_path}"'
+ ]
+ if self.data_disk:
+ cmd += ["--arg", "dataDevice", f'"/dev/{self.data_disk}"']
+ run_stream(cmd, buf)
+
+ GLib.idle_add(append_text, buf, "\n=== Generating hardware config ===\n")
+ run_stream(["sudo", "nixos-generate-config", "--root", "/mnt"], buf)
+
+ GLib.idle_add(append_text, buf, "\n=== Copying flake to /mnt ===\n")
+ run(["sudo", "cp", "/mnt/etc/nixos/hardware-configuration.nix", "/tmp/hardware-configuration.nix"])
+ run(["sudo", "rm", "-rf", "/mnt/etc/nixos/"])
+ run(["sudo", "mkdir", "-p", "/mnt/etc/nixos"])
+ run(["sudo", "cp", "-a", f"{FLAKE}/.", "/mnt/etc/nixos/"])
+ run(["sudo", "cp", "/tmp/hardware-configuration.nix", "/mnt/etc/nixos/hardware-configuration.nix"])
+
+ GLib.idle_add(append_text, buf, "\n=== Writing role config ===\n")
+ self.write_role_state()
+
+ GLib.idle_add(self.show_install_step)
+
+ def write_role_state(self):
+ is_server = str(self.role == "Server+Desktop").lower()
+ is_desktop = str(self.role == "Desktop Only").lower()
+ is_node = str(self.role == "Node (Bitcoin-only)").lower()
+ content = f"""# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
+{{ config, lib, ... }}:
+{{
+ sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault {is_server};
+ sovran_systemsOS.roles.desktop = lib.mkDefault {is_desktop};
+ sovran_systemsOS.roles.node = lib.mkDefault {is_node};
+}}
+"""
+ proc = subprocess.run(
+ ["sudo", "tee", "/mnt/etc/nixos/role-state.nix"],
+ input=content, text=True, capture_output=True
+ )
+ 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 ───────────────────────────────
+
+ 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)
+
+ body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
+ body.set_margin_start(40)
+ body.set_margin_end(40)
+ body.set_margin_top(16)
+
+ icon = Gtk.Label()
+ icon.set_markup("✅")
+ body.pack_start(icon, False, False, 0)
+
+ 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)
+
+ note = Gtk.Label()
+ note.set_markup(
+ "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)
+
+ page.pack_start(body, True, True, 0)
+ page.pack_start(self.make_nav(
+ next_label="Install Now", next_cb=lambda b: self.show_progress(
+ "Installing Sovran SystemsOS",
+ "Building and installing your system. This will take a while...",
+ self.do_install
+ )
+ ), False, False, 0)
+ self.set_page(page, "ready")
+ return False
+
+ # ── Worker: install ───────────────────────────────────────────────────
+
+ def do_install(self, buf):
+ GLib.idle_add(append_text, buf, "=== Running nixos-install ===\n")
+ run_stream([
+ "sudo", "nixos-install",
+ "--root", "/mnt",
+ "--flake", "/mnt/etc/nixos#nixos"
+ ], buf)
+ GLib.idle_add(self.show_complete)
+
+ # ── Step 6: 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)
+
+ body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
+ body.set_margin_start(40)
+ body.set_margin_end(40)
+ body.set_margin_top(16)
+
+ icon = Gtk.Label()
+ icon.set_markup("🎉")
+ body.pack_start(icon, False, False, 0)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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.set_line_wrap(True)
+ note.set_max_width_chars(65)
+ note.set_justify(Gtk.Justification.CENTER)
+ body.pack_start(note, False, False, 0)
+
+ page.pack_start(body, True, True, 0)
+
+ reboot_btn = Gtk.Button(label="Reboot Now")
+ reboot_btn.get_style_context().add_class("suggested-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")
+ return False
+
+
+if __name__ == "__main__":
+ win = InstallerWindow()
+ Gtk.main()
\ No newline at end of file
diff --git a/iso/installer.sh b/iso/installer.sh
deleted file mode 100644
index 173c229..0000000
--- a/iso/installer.sh
+++ /dev/null
@@ -1,183 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-LOG=/tmp/sovran-install.log
-exec > >(tee -a "$LOG") 2>&1
-
-export PATH=/run/current-system/sw/bin:$PATH
-
-BYTES_2TB=$((2 * 1024 * 1024 * 1024 * 1024))
-LOGO="/etc/sovran/logo.png"
-
-human_size() {
- numfmt --to=iec --suffix=B "$1"
-}
-
-# ── 1. WELCOME & ROLE SELECTION ──────────────────────────────────────────
-
-ROLE=$(zenity --list --radiolist \
- --icon="$LOGO" \
- --width=700 --height=450 \
- --title="Welcome to Sovran_SystemsOS Installer" \
- --text="Sovran Systems\nBe Digitally Sovereign\n\nPlease select your installation type:" \
- --print-column=2 \
- --column="Select" --column="Role" \
- TRUE "Server + Desktop — Full sovereign experience: desktop, cloud, messaging, Bitcoin node." \
- FALSE "Desktop Only — Beautiful desktop without background server applications." \
- FALSE "Node Only — Full Bitcoin node with Lightning and non-KYC buying and selling." \
- || true)
-
-if [ -z "$ROLE" ]; then
- zenity --error --icon="$LOGO" --text="Installation cancelled."
- exit 1
-fi
-
-case "$ROLE" in
- Server*) ROLE="Server+Desktop" ;;
- Desktop*) ROLE="Desktop Only" ;;
- Node*) ROLE="Node (Bitcoin-only)" ;;
-esac
-
-# ── 2. FETCH DISKS ───────────────────────────────────────────────────────
-
-mapfile -t DISKS < <(lsblk -b -dno NAME,SIZE,TYPE,RO,TRAN -e 7,11 | awk '$3=="disk" && $4=="0" && $5!="usb" {print $1":"$2}')
-
-if [ "${#DISKS[@]}" -eq 0 ]; then
- zenity --error --icon="$LOGO" --text="No valid internal drives found. USB drives are ignored."
- exit 1
-fi
-
-IFS=$'\n' DISKS_SORTED=($(printf "%s\n" "${DISKS[@]}" | sort -t: -k2,2n))
-unset IFS
-
-BOOT_DISK="${DISKS_SORTED[0]%%:*}"
-BOOT_SIZE="${DISKS_SORTED[0]##*:}"
-
-DATA_DISK=""
-DATA_SIZE=""
-
-if [ "${#DISKS_SORTED[@]}" -ge 2 ]; then
- DATA_DISK="${DISKS_SORTED[-1]%%:*}"
- DATA_SIZE="${DISKS_SORTED[-1]##*:}"
-fi
-
-if [ -n "$DATA_DISK" ] && [ "$DATA_SIZE" -lt "$BYTES_2TB" ]; then
- zenity --warning --icon="$LOGO" --width=500 \
- --text="A second disk was detected (${DATA_DISK}), but it is smaller than 2TB and will not be used as a data disk."
- DATA_DISK=""
- DATA_SIZE=""
-fi
-
-SUMMARY="Boot disk: /dev/${BOOT_DISK} ($(human_size "$BOOT_SIZE"))"
-if [ -n "$DATA_DISK" ]; then
- SUMMARY="${SUMMARY}\nData disk: /dev/${DATA_DISK} ($(human_size "$DATA_SIZE"))"
-else
- SUMMARY="${SUMMARY}\nData disk: none detected"
-fi
-
-# ── 3. CONFIRM ERASE ─────────────────────────────────────────────────────
-
-CONFIRM=$(zenity --entry \
- --icon="$LOGO" \
- --width=560 \
- --title="Confirm Installation" \
- --text="WARNING: This will permanently erase all data on:\n\n${SUMMARY}\n\nType ERASE below to confirm and begin installation.")
-
-if [ "$CONFIRM" != "ERASE" ]; then
- zenity --error --icon="$LOGO" --text="Installation cancelled. Nothing was changed."
- exit 1
-fi
-
-BOOT_PATH="/dev/${BOOT_DISK}"
-DATA_PATH=""
-if [ -n "$DATA_DISK" ]; then
- DATA_PATH="/dev/${DATA_DISK}"
-fi
-
-# ── 4. PARTITION & FORMAT ─────────────────────────────────────────────────
-
-zenity --info \
- --icon="$LOGO" \
- --title="Preparing Drives" \
- --text="Please wait while your drives are being set up...\n\nThis may take a few minutes. Do not turn off your computer." \
- --width=520 &
-ZENITY_WAIT_PID=$!
-
-if [ -n "$DATA_PATH" ]; then
- sudo disko --mode disko /etc/sovran/flake/iso/disko.nix \
- --arg device '"'"$BOOT_PATH"'"' \
- --arg dataDevice '"'"$DATA_PATH"'"'
-else
- sudo disko --mode disko /etc/sovran/flake/iso/disko.nix \
- --arg device '"'"$BOOT_PATH"'"'
-fi
-
-kill $ZENITY_WAIT_PID 2>/dev/null || true
-
-# ── 5. COPY CONFIG ────────────────────────────────────────────────────────
-
-sudo nixos-generate-config --root /mnt
-
-cp /mnt/etc/nixos/hardware-configuration.nix /tmp/hardware-configuration.nix
-sudo rm -rf /mnt/etc/nixos/*
-sudo cp -a /etc/sovran/flake/* /mnt/etc/nixos/
-sudo cp /tmp/hardware-configuration.nix /mnt/etc/nixos/hardware-configuration.nix
-
-# ── 6. APPLY ROLE STATE & TEMPLATE ───────────────────────────────────────
-
-IS_SERVER="false"
-IS_DESKTOP="false"
-IS_NODE="false"
-
-case "$ROLE" in
- "Server+Desktop") IS_SERVER="true" ;;
- "Desktop Only") IS_DESKTOP="true" ;;
- "Node (Bitcoin-only)") IS_NODE="true" ;;
-esac
-
-sudo tee /mnt/etc/nixos/role-state.nix > /dev/null </dev/null || true
-
-# ── 9. COMPLETE ───────────────────────────────────────────────────────────
-
-zenity --info \
- --icon="$LOGO" \
- --width=600 \
- --title="Installation Complete!" \
- --text="Installation Successful!\n\nPlease write down your login details before rebooting:\n\nUsername: free\nPassword: free\n\nCRITICAL: Do not lose this password or you will be permanently locked out.\n\nAfter rebooting your system will finish setting up and save all app passwords (Nextcloud, Bitcoin, Matrix, etc.) to a secure PDF in your Documents folder.\n\nClick OK to reboot into your new system!"
-
-sudo reboot
diff --git a/result b/result
index d6c6901..f5a2b75 120000
--- a/result
+++ b/result
@@ -1 +1 @@
-/nix/store/0z2yggkzywnp2zgfl8khazs1vk92yw7q-Sovran_SystemsOS.iso
\ No newline at end of file
+/nix/store/yaw5krmqdcyw79z4d91hyg92fwdas91k-Sovran_SystemsOS.iso
\ No newline at end of file