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