added python installer and common
This commit is contained in:
@@ -16,11 +16,9 @@ in
|
|||||||
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
||||||
isoImage.splashImage = ./assets/splash-logo.png;
|
isoImage.splashImage = ./assets/splash-logo.png;
|
||||||
|
|
||||||
# Disable GNOME first-run tour and initial setup
|
|
||||||
services.gnome.gnome-initial-setup.enable = false;
|
services.gnome.gnome-initial-setup.enable = false;
|
||||||
environment.gnome.excludePackages = with pkgs; [ gnome-tour gnome-user-docs ];
|
environment.gnome.excludePackages = with pkgs; [ gnome-tour gnome-user-docs ];
|
||||||
|
|
||||||
# Passwordless sudo for live ISO session
|
|
||||||
security.sudo.wheelNeedsPassword = false;
|
security.sudo.wheelNeedsPassword = false;
|
||||||
users.users.free = {
|
users.users.free = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
@@ -37,7 +35,8 @@ in
|
|||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
installerPy
|
installerPy
|
||||||
(python3.withPackages (ps: [ ps.pygobject3 ]))
|
(python3.withPackages (ps: [ ps.pygobject3 ]))
|
||||||
gtk3
|
gtk4
|
||||||
|
libadwaita
|
||||||
gobject-introspection
|
gobject-introspection
|
||||||
util-linux
|
util-linux
|
||||||
disko
|
disko
|
||||||
|
|||||||
674
iso/installer.py
674
iso/installer.py
@@ -1,39 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
from gi.repository import Gtk, GLib, Pango, GdkPixbuf
|
gi.require_version("Adw", "1")
|
||||||
|
from gi.repository import Gtk, Adw, GLib, Pango, GdkPixbuf, Gio
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
LOGO = "/etc/sovran/logo.png"
|
LOGO = "/etc/sovran/logo.png"
|
||||||
LOG = "/tmp/sovran-install.log"
|
LOG = "/tmp/sovran-install.log"
|
||||||
FLAKE = "/etc/sovran/flake"
|
FLAKE = "/etc/sovran/flake"
|
||||||
|
|
||||||
logfile = open(LOG, "a")
|
logfile = open(LOG, "a")
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
logfile.write(msg + "\n")
|
logfile.write(msg + "\n")
|
||||||
logfile.flush()
|
logfile.flush()
|
||||||
|
|
||||||
def run(cmd, **kwargs):
|
def run(cmd):
|
||||||
log(f"$ {' '.join(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)
|
log(result.stdout)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
log(result.stderr)
|
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()
|
return result.stdout.strip()
|
||||||
|
|
||||||
def run_stream(cmd, text_buffer):
|
def run_stream(cmd, buf):
|
||||||
log(f"$ {' '.join(cmd)}")
|
log(f"$ {' '.join(cmd)}")
|
||||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
log(line.rstrip())
|
log(line.rstrip())
|
||||||
GLib.idle_add(append_text, text_buffer, line)
|
GLib.idle_add(append_text, buf, line)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
if proc.returncode != 0:
|
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):
|
def append_text(buf, text):
|
||||||
buf.insert(buf.get_end_iter(), text)
|
buf.insert(buf.get_end_iter(), text)
|
||||||
@@ -46,22 +47,27 @@ def human_size(nbytes):
|
|||||||
nbytes /= 1024
|
nbytes /= 1024
|
||||||
return f"{nbytes:.1f} PB"
|
return f"{nbytes:.1f} PB"
|
||||||
|
|
||||||
# ── Window base ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class InstallerWindow(Gtk.Window):
|
# ── Application ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class InstallerApp(Adw.Application):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(title="Sovran_SystemsOS Installer")
|
super().__init__(application_id="com.sovransystems.installer")
|
||||||
self.set_default_size(800, 560)
|
self.connect("activate", self.on_activate)
|
||||||
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()
|
def on_activate(self, app):
|
||||||
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
|
self.win = InstallerWindow(application=app)
|
||||||
self.stack.set_transition_duration(300)
|
self.win.present()
|
||||||
self.add(self.stack)
|
|
||||||
|
|
||||||
|
# ── 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.role = None
|
||||||
self.boot_disk = None
|
self.boot_disk = None
|
||||||
@@ -69,148 +75,133 @@ class InstallerWindow(Gtk.Window):
|
|||||||
self.data_disk = None
|
self.data_disk = None
|
||||||
self.data_size = None
|
self.data_size = None
|
||||||
|
|
||||||
self.show_welcome()
|
# Root navigation view
|
||||||
self.show_all()
|
self.nav = Adw.NavigationView()
|
||||||
|
self.set_content(self.nav)
|
||||||
|
|
||||||
# ── Helpers ────────────────────────────────────────────────────────────
|
self.push_welcome()
|
||||||
|
|
||||||
def clear_stack(self):
|
# ── Navigation helpers ─────────────────────────────────────────────────
|
||||||
for child in self.stack.get_children():
|
|
||||||
self.stack.remove(child)
|
|
||||||
|
|
||||||
def set_page(self, widget, name="page"):
|
def push_page(self, title, child, show_back=False):
|
||||||
self.clear_stack()
|
page = Adw.NavigationPage(title=title, tag=title)
|
||||||
self.stack.add_named(widget, name)
|
toolbar = Adw.ToolbarView()
|
||||||
self.stack.set_visible_child_name(name)
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def make_header(self, title, subtitle=None):
|
header = Adw.HeaderBar()
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
header.set_show_end_title_buttons(False)
|
||||||
box.set_margin_top(32)
|
if not show_back:
|
||||||
box.set_margin_bottom(16)
|
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_start(40)
|
||||||
box.set_margin_end(40)
|
box.set_margin_end(40)
|
||||||
|
|
||||||
lbl = Gtk.Label()
|
|
||||||
lbl.set_markup(f"<span font='24' weight='heavy'>{title}</span>")
|
|
||||||
lbl.set_halign(Gtk.Align.START)
|
|
||||||
box.pack_start(lbl, False, False, 0)
|
|
||||||
|
|
||||||
if subtitle:
|
|
||||||
sub = Gtk.Label()
|
|
||||||
sub.set_markup(f"<span font='13' foreground='#888888'>{subtitle}</span>")
|
|
||||||
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:
|
if back_label and back_cb:
|
||||||
btn = Gtk.Button(label=back_label)
|
btn = Gtk.Button(label=back_label)
|
||||||
btn.connect("clicked", back_cb)
|
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:
|
if next_cb:
|
||||||
btn = Gtk.Button(label=next_label)
|
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)
|
btn.connect("clicked", next_cb)
|
||||||
bar.pack_end(btn, False, False, 0)
|
box.append(btn)
|
||||||
|
|
||||||
return bar
|
return box
|
||||||
|
|
||||||
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("<span font='48'>❌</span>")
|
|
||||||
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"<span font='11' foreground='#888'>Full log: {LOG}</span>")
|
|
||||||
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 ─────────────────────────────────────────────
|
# ── Step 1: Welcome & Role ─────────────────────────────────────────────
|
||||||
|
|
||||||
def show_welcome(self):
|
def push_welcome(self):
|
||||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
|
||||||
# Hero banner
|
# Hero
|
||||||
hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
hero.set_margin_top(36)
|
hero.set_margin_top(32)
|
||||||
hero.set_margin_bottom(20)
|
hero.set_margin_bottom(24)
|
||||||
|
hero.set_halign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
if os.path.exists(LOGO):
|
if os.path.exists(LOGO):
|
||||||
try:
|
try:
|
||||||
pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(LOGO, 80, 80, True)
|
img = Gtk.Image.new_from_file(LOGO)
|
||||||
img = Gtk.Image.new_from_pixbuf(pb)
|
img.set_pixel_size(96)
|
||||||
hero.pack_start(img, False, False, 0)
|
hero.append(img)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
title = Gtk.Label()
|
title = Gtk.Label()
|
||||||
title.set_markup("<span font='28' weight='heavy'>Sovran Systems</span>")
|
title.set_markup("<span size='xx-large' weight='heavy'>Sovran Systems</span>")
|
||||||
hero.pack_start(title, False, False, 0)
|
hero.append(title)
|
||||||
|
|
||||||
sub = Gtk.Label()
|
sub = Gtk.Label()
|
||||||
sub.set_markup("<span font='14' style='italic' foreground='#888888'>Be Digitally Sovereign</span>")
|
sub.set_markup("<span size='large' style='italic' foreground='#888888'>Be Digitally Sovereign</span>")
|
||||||
hero.pack_start(sub, False, False, 0)
|
hero.append(sub)
|
||||||
|
|
||||||
page.pack_start(hero, False, False, 0)
|
outer.append(hero)
|
||||||
|
|
||||||
sep = Gtk.Separator()
|
sep = Gtk.Separator()
|
||||||
sep.set_margin_start(40)
|
sep.set_margin_start(40)
|
||||||
sep.set_margin_end(40)
|
sep.set_margin_end(40)
|
||||||
page.pack_start(sep, False, False, 0)
|
outer.append(sep)
|
||||||
|
|
||||||
# Role selection
|
# Role label
|
||||||
lbl = Gtk.Label()
|
role_lbl = Gtk.Label()
|
||||||
lbl.set_markup("<span font='13'>Select your installation type:</span>")
|
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
||||||
lbl.set_margin_top(20)
|
role_lbl.set_halign(Gtk.Align.START)
|
||||||
lbl.set_margin_start(40)
|
role_lbl.set_margin_start(40)
|
||||||
lbl.set_halign(Gtk.Align.START)
|
role_lbl.set_margin_top(20)
|
||||||
page.pack_start(lbl, False, False, 0)
|
role_lbl.set_margin_bottom(8)
|
||||||
|
outer.append(role_lbl)
|
||||||
|
|
||||||
|
# Role cards
|
||||||
roles = [
|
roles = [
|
||||||
("Server + Desktop",
|
("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"),
|
"Server+Desktop"),
|
||||||
("Desktop Only",
|
("Desktop Only",
|
||||||
"A beautiful, easy-to-use desktop without the background server applications.",
|
"A beautiful, easy-to-use desktop without the background server applications.",
|
||||||
@@ -220,56 +211,54 @@ class InstallerWindow(Gtk.Window):
|
|||||||
"Node (Bitcoin-only)"),
|
"Node (Bitcoin-only)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
radio_group = None
|
|
||||||
self._role_radios = []
|
self._role_radios = []
|
||||||
role_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
radio_group = None
|
||||||
role_box.set_margin_start(40)
|
cards_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
role_box.set_margin_end(40)
|
cards_box.set_margin_start(40)
|
||||||
role_box.set_margin_top(8)
|
cards_box.set_margin_end(40)
|
||||||
|
|
||||||
for label, desc, key in roles:
|
for label, desc, key in roles:
|
||||||
row = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
card = Adw.ActionRow()
|
||||||
row.set_margin_bottom(8)
|
card.set_title(label)
|
||||||
|
card.set_subtitle(desc)
|
||||||
|
|
||||||
radio = Gtk.RadioButton(group=radio_group, label=label)
|
radio = Gtk.CheckButton()
|
||||||
radio.set_name(key)
|
radio.set_name(key)
|
||||||
if radio_group is None:
|
if radio_group is None:
|
||||||
radio_group = radio
|
radio_group = radio
|
||||||
radio.set_active(True)
|
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)
|
self._role_radios.append(radio)
|
||||||
|
cards_box.append(card)
|
||||||
|
|
||||||
desc_lbl = Gtk.Label(label=desc)
|
outer.append(cards_box)
|
||||||
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)
|
outer.append(Gtk.Label(label="", vexpand=True))
|
||||||
row.pack_start(desc_lbl, False, False, 0)
|
outer.append(self.nav_row(
|
||||||
role_box.pack_start(row, False, False, 0)
|
next_label="Next →",
|
||||||
|
next_cb=self.on_role_next
|
||||||
|
))
|
||||||
|
|
||||||
page.pack_start(role_box, False, False, 0)
|
self.push_page("Welcome to Sovran_SystemsOS Installer", outer)
|
||||||
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):
|
def on_role_next(self, btn):
|
||||||
for radio in self._role_radios:
|
for radio in self._role_radios:
|
||||||
if radio.get_active():
|
if radio.get_active():
|
||||||
self.role = radio.get_name()
|
self.role = radio.get_name()
|
||||||
break
|
break
|
||||||
self.show_disk_confirm()
|
self.push_disk_confirm()
|
||||||
|
|
||||||
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
|
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
|
||||||
|
|
||||||
def show_disk_confirm(self):
|
def push_disk_confirm(self):
|
||||||
# Detect disks
|
|
||||||
try:
|
try:
|
||||||
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
|
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error(str(e))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
disks = []
|
disks = []
|
||||||
@@ -281,7 +270,7 @@ class InstallerWindow(Gtk.Window):
|
|||||||
disks.append((parts[0], int(parts[1])))
|
disks.append((parts[0], int(parts[1])))
|
||||||
|
|
||||||
if not disks:
|
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
|
return
|
||||||
|
|
||||||
disks.sort(key=lambda x: x[1])
|
disks.sort(key=lambda x: x[1])
|
||||||
@@ -294,133 +283,119 @@ class InstallerWindow(Gtk.Window):
|
|||||||
if s >= BYTES_2TB:
|
if s >= BYTES_2TB:
|
||||||
self.data_disk, self.data_size = d, s
|
self.data_disk, self.data_size = d, s
|
||||||
|
|
||||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
outer = 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)
|
# Disk info group
|
||||||
body.set_margin_start(40)
|
disk_group = Adw.PreferencesGroup()
|
||||||
body.set_margin_end(40)
|
disk_group.set_title("Drives to be erased")
|
||||||
body.set_margin_top(8)
|
disk_group.set_margin_top(24)
|
||||||
|
disk_group.set_margin_start(40)
|
||||||
|
disk_group.set_margin_end(40)
|
||||||
|
|
||||||
# Disk info box
|
boot_row = Adw.ActionRow()
|
||||||
disk_frame = Gtk.Frame()
|
boot_row.set_title("Boot Disk")
|
||||||
disk_frame.set_shadow_type(Gtk.ShadowType.IN)
|
boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}")
|
||||||
disk_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
boot_row.add_prefix(Gtk.Image.new_from_icon_name("drive-harddisk-symbolic"))
|
||||||
disk_inner.set_margin_top(12)
|
disk_group.add(boot_row)
|
||||||
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"<b>Boot disk:</b> /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:
|
if self.data_disk:
|
||||||
data_lbl = Gtk.Label()
|
data_row = Adw.ActionRow()
|
||||||
data_lbl.set_markup(f"<b>Data disk:</b> /dev/{self.data_disk} ({human_size(self.data_size)})")
|
data_row.set_title("Data Disk")
|
||||||
data_lbl.set_halign(Gtk.Align.START)
|
data_row.set_subtitle(f"/dev/{self.data_disk} — {human_size(self.data_size)}")
|
||||||
disk_inner.pack_start(data_lbl, False, False, 0)
|
data_row.add_prefix(Gtk.Image.new_from_icon_name("drive-harddisk-symbolic"))
|
||||||
|
disk_group.add(data_row)
|
||||||
else:
|
else:
|
||||||
no_data = Gtk.Label()
|
no_row = Adw.ActionRow()
|
||||||
no_data.set_markup("<b>Data disk:</b> none detected (requires 2TB+)")
|
no_row.set_title("Data Disk")
|
||||||
no_data.set_halign(Gtk.Align.START)
|
no_row.set_subtitle("None detected (requires 2 TB or larger)")
|
||||||
disk_inner.pack_start(no_data, False, False, 0)
|
no_row.add_prefix(Gtk.Image.new_from_icon_name("drive-harddisk-symbolic"))
|
||||||
|
disk_group.add(no_row)
|
||||||
|
|
||||||
disk_frame.add(disk_inner)
|
outer.append(disk_group)
|
||||||
body.pack_start(disk_frame, False, False, 0)
|
|
||||||
|
|
||||||
warn = Gtk.Label()
|
# Warning banner
|
||||||
warn.set_markup(
|
banner = Adw.Banner()
|
||||||
"<span foreground='#cc0000' weight='bold'>⚠ This action cannot be undone. "
|
banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.")
|
||||||
"All existing data on the above disk(s) will be permanently destroyed.</span>"
|
banner.set_revealed(True)
|
||||||
)
|
banner.set_margin_top(16)
|
||||||
warn.set_line_wrap(True)
|
banner.set_margin_start(40)
|
||||||
warn.set_max_width_chars(65)
|
banner.set_margin_end(40)
|
||||||
warn.set_halign(Gtk.Align.START)
|
outer.append(banner)
|
||||||
body.pack_start(warn, False, False, 0)
|
|
||||||
|
|
||||||
# Confirm entry
|
# Confirm entry group
|
||||||
confirm_lbl = Gtk.Label(label='Type ERASE to confirm:')
|
entry_group = Adw.PreferencesGroup()
|
||||||
confirm_lbl.set_halign(Gtk.Align.START)
|
entry_group.set_title("Type ERASE to confirm")
|
||||||
body.pack_start(confirm_lbl, False, False, 0)
|
entry_group.set_margin_top(16)
|
||||||
|
entry_group.set_margin_start(40)
|
||||||
|
entry_group.set_margin_end(40)
|
||||||
|
|
||||||
self._confirm_entry = Gtk.Entry()
|
entry_row = Adw.EntryRow()
|
||||||
self._confirm_entry.set_placeholder_text("ERASE")
|
entry_row.set_title("Confirmation")
|
||||||
body.pack_start(self._confirm_entry, False, False, 0)
|
self._confirm_entry = entry_row
|
||||||
|
entry_group.add(entry_row)
|
||||||
|
|
||||||
page.pack_start(body, False, False, 0)
|
outer.append(entry_group)
|
||||||
page.pack_start(Gtk.Label(), True, True, 0)
|
outer.append(Gtk.Label(label="", vexpand=True))
|
||||||
page.pack_start(self.make_nav(
|
outer.append(self.nav_row(
|
||||||
back_label="← Back", back_cb=lambda b: self.show_welcome(),
|
back_label="← Back",
|
||||||
next_label="Begin Installation", next_cb=self.on_confirm_next
|
back_cb=lambda b: self.nav.pop(),
|
||||||
), False, False, 0)
|
next_label="Begin Installation",
|
||||||
self.set_page(page, "disks")
|
next_cb=self.on_confirm_next
|
||||||
|
))
|
||||||
|
|
||||||
|
self.push_page("Confirm Installation", outer, show_back=True)
|
||||||
|
|
||||||
def on_confirm_next(self, btn):
|
def on_confirm_next(self, btn):
|
||||||
if self._confirm_entry.get_text().strip() != "ERASE":
|
if self._confirm_entry.get_text().strip() != "ERASE":
|
||||||
dlg = Gtk.MessageDialog(
|
dlg = Adw.MessageDialog(
|
||||||
transient_for=self,
|
transient_for=self,
|
||||||
modal=True,
|
heading="Confirmation Required",
|
||||||
message_type=Gtk.MessageType.WARNING,
|
body="You must type ERASE exactly to proceed."
|
||||||
buttons=Gtk.ButtonsType.OK,
|
|
||||||
text="You must type ERASE exactly to continue."
|
|
||||||
)
|
)
|
||||||
dlg.run()
|
dlg.add_response("ok", "OK")
|
||||||
dlg.destroy()
|
dlg.present()
|
||||||
return
|
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):
|
def push_progress(self, title, subtitle, worker_fn):
|
||||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
outer = 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)
|
status = Adw.StatusPage()
|
||||||
body.set_margin_start(40)
|
status.set_title(title)
|
||||||
body.set_margin_end(40)
|
status.set_description(subtitle)
|
||||||
|
status.set_icon_name("emblem-synchronizing-symbolic")
|
||||||
|
status.set_vexpand(False)
|
||||||
|
outer.append(status)
|
||||||
|
|
||||||
spinner = Gtk.Spinner()
|
spinner = Gtk.Spinner()
|
||||||
spinner.set_size_request(48, 48)
|
spinner.set_size_request(32, 32)
|
||||||
|
spinner.set_halign(Gtk.Align.CENTER)
|
||||||
spinner.start()
|
spinner.start()
|
||||||
body.pack_start(spinner, False, False, 0)
|
outer.append(spinner)
|
||||||
|
|
||||||
# Scrollable live log
|
sw, buf = self.make_scrolled_log()
|
||||||
sw = Gtk.ScrolledWindow()
|
sw.set_margin_start(40)
|
||||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
sw.set_margin_end(40)
|
||||||
sw.set_size_request(-1, 260)
|
sw.set_margin_top(12)
|
||||||
|
sw.set_margin_bottom(12)
|
||||||
|
sw.set_vexpand(True)
|
||||||
|
outer.append(sw)
|
||||||
|
|
||||||
tv = Gtk.TextView()
|
self.push_page(title, outer)
|
||||||
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():
|
def thread_fn():
|
||||||
try:
|
try:
|
||||||
worker_fn(buf)
|
worker_fn(buf)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error(str(e))
|
GLib.idle_add(self.show_error, str(e))
|
||||||
|
|
||||||
t = threading.Thread(target=thread_fn, daemon=True)
|
threading.Thread(target=thread_fn, daemon=True).start()
|
||||||
t.start()
|
|
||||||
|
|
||||||
# ── Worker: partition ─────────────────────────────────────────────────
|
# ── Worker: partition ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -448,8 +423,9 @@ class InstallerWindow(Gtk.Window):
|
|||||||
|
|
||||||
GLib.idle_add(append_text, buf, "\n=== Writing role config ===\n")
|
GLib.idle_add(append_text, buf, "\n=== Writing role config ===\n")
|
||||||
self.write_role_state()
|
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):
|
def write_role_state(self):
|
||||||
is_server = str(self.role == "Server+Desktop").lower()
|
is_server = str(self.role == "Server+Desktop").lower()
|
||||||
@@ -470,131 +446,163 @@ class InstallerWindow(Gtk.Window):
|
|||||||
log(proc.stdout)
|
log(proc.stdout)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
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"])
|
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):
|
def push_ready(self):
|
||||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
outer = 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)
|
status = Adw.StatusPage()
|
||||||
body.set_margin_start(40)
|
status.set_title("Drives Ready")
|
||||||
body.set_margin_end(40)
|
status.set_description("Your drives have been partitioned successfully.")
|
||||||
body.set_margin_top(16)
|
status.set_icon_name("emblem-ok-symbolic")
|
||||||
|
status.set_vexpand(True)
|
||||||
|
|
||||||
icon = Gtk.Label()
|
details = Adw.PreferencesGroup()
|
||||||
icon.set_markup("<span font='48'>✅</span>")
|
details.set_margin_start(40)
|
||||||
body.pack_start(icon, False, False, 0)
|
details.set_margin_end(40)
|
||||||
|
|
||||||
info = Gtk.Label()
|
role_row = Adw.ActionRow()
|
||||||
info.set_markup(
|
role_row.set_title("Installation Type")
|
||||||
f"<b>Role:</b> {self.role}\n"
|
role_row.set_subtitle(self.role)
|
||||||
f"<b>Boot disk:</b> /dev/{self.boot_disk} ({human_size(self.boot_size)})\n" +
|
details.add(role_row)
|
||||||
(f"<b>Data disk:</b> /dev/{self.data_disk} ({human_size(self.data_size)})"
|
|
||||||
if self.data_disk else "<b>Data disk:</b> none")
|
boot_row = Adw.ActionRow()
|
||||||
)
|
boot_row.set_title("Boot Disk")
|
||||||
info.set_halign(Gtk.Align.CENTER)
|
boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}")
|
||||||
info.set_justify(Gtk.Justification.CENTER)
|
details.add(boot_row)
|
||||||
body.pack_start(info, False, False, 0)
|
|
||||||
|
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 = Gtk.Label()
|
||||||
note.set_markup(
|
note.set_markup(
|
||||||
"<span foreground='#888'>The next step will install the full system.\n"
|
"<span foreground='#888888'>The next step will install the full system.\n"
|
||||||
"This may take <b>20–40 minutes</b> depending on your internet speed.\n"
|
"This may take <b>20–40 minutes</b> depending on your internet speed.\n"
|
||||||
"Do not turn off your computer.</span>"
|
"Do not turn off your computer.</span>"
|
||||||
)
|
)
|
||||||
note.set_justify(Gtk.Justification.CENTER)
|
note.set_justify(Gtk.Justification.CENTER)
|
||||||
note.set_line_wrap(True)
|
note.set_wrap(True)
|
||||||
body.pack_start(note, False, False, 0)
|
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)
|
outer.append(status_box)
|
||||||
page.pack_start(self.make_nav(
|
outer.append(self.nav_row(
|
||||||
next_label="Install Now", next_cb=lambda b: self.show_progress(
|
next_label="Install Now",
|
||||||
|
next_cb=lambda b: self.push_progress(
|
||||||
"Installing Sovran SystemsOS",
|
"Installing Sovran SystemsOS",
|
||||||
"Building and installing your system. This will take a while...",
|
"Building and installing your system. Please wait...",
|
||||||
self.do_install
|
self.do_install
|
||||||
)
|
)
|
||||||
), False, False, 0)
|
))
|
||||||
self.set_page(page, "ready")
|
|
||||||
|
self.push_page("Ready to Install", outer)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ── Worker: install ───────────────────────────────────────────────────
|
# ── Worker: install ───────────────────────────────────────────────────
|
||||||
|
|
||||||
def do_install(self, buf):
|
def do_install(self, buf):
|
||||||
GLib.idle_add(append_text, buf, "=== Running nixos-install ===\n")
|
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([
|
run_stream([
|
||||||
"sudo", "nixos-install",
|
"sudo", "nixos-install",
|
||||||
"--root", "/mnt",
|
"--root", "/mnt",
|
||||||
"--flake", "/mnt/etc/nixos#nixos"
|
"--flake", "/mnt/etc/nixos#nixos"
|
||||||
], buf)
|
], buf)
|
||||||
GLib.idle_add(self.show_complete)
|
|
||||||
|
|
||||||
# ── Step 6: Complete ──────────────────────────────────────────────────
|
GLib.idle_add(self.push_complete)
|
||||||
|
|
||||||
def show_complete(self):
|
# ── Step 6: Complete ───────────────────────────────────────────────────
|
||||||
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)
|
def push_complete(self):
|
||||||
body.set_margin_start(40)
|
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
body.set_margin_end(40)
|
|
||||||
body.set_margin_top(16)
|
|
||||||
|
|
||||||
icon = Gtk.Label()
|
status = Adw.StatusPage()
|
||||||
icon.set_markup("<span font='48'>🎉</span>")
|
status.set_title("Installation Complete!")
|
||||||
body.pack_start(icon, False, False, 0)
|
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_group = Adw.PreferencesGroup()
|
||||||
creds_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
creds_group.set_title("⚠ Write down your login details before rebooting")
|
||||||
creds_inner.set_margin_top(10)
|
creds_group.set_margin_start(40)
|
||||||
creds_inner.set_margin_bottom(10)
|
creds_group.set_margin_end(40)
|
||||||
creds_inner.set_margin_start(16)
|
|
||||||
creds_inner.set_margin_end(16)
|
|
||||||
|
|
||||||
user_lbl = Gtk.Label()
|
user_row = Adw.ActionRow()
|
||||||
user_lbl.set_markup("<b>Username:</b> free")
|
user_row.set_title("Username")
|
||||||
user_lbl.set_halign(Gtk.Align.START)
|
user_row.set_subtitle("free")
|
||||||
creds_inner.pack_start(user_lbl, False, False, 0)
|
creds_group.add(user_row)
|
||||||
|
|
||||||
pass_lbl = Gtk.Label()
|
pass_row = Adw.ActionRow()
|
||||||
pass_lbl.set_markup("<b>Password:</b> free")
|
pass_row.set_title("Password")
|
||||||
pass_lbl.set_halign(Gtk.Align.START)
|
pass_row.set_subtitle("free")
|
||||||
creds_inner.pack_start(pass_lbl, False, False, 0)
|
creds_group.add(pass_row)
|
||||||
|
|
||||||
creds_frame.add(creds_inner)
|
note_row = Adw.ActionRow()
|
||||||
body.pack_start(creds_frame, False, False, 0)
|
note_row.set_title("App Passwords")
|
||||||
|
note_row.set_subtitle(
|
||||||
note = Gtk.Label()
|
"After rebooting, all app passwords (Nextcloud, Bitcoin, Matrix, etc.) "
|
||||||
note.set_markup(
|
"will be saved to a secure PDF in your Documents folder."
|
||||||
"🚨 <b>Do not lose this password</b> — 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 <b>Documents</b> folder."
|
|
||||||
)
|
)
|
||||||
note.set_line_wrap(True)
|
creds_group.add(note_row)
|
||||||
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)
|
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 = 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"]))
|
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"<span foreground='#888888' size='small'>Full log: {LOG}</span>")
|
||||||
|
log_lbl.set_margin_bottom(24)
|
||||||
|
|
||||||
|
outer.append(status)
|
||||||
|
outer.append(log_lbl)
|
||||||
|
|
||||||
|
self.push_page("Error", outer)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
win = InstallerWindow()
|
app = InstallerApp()
|
||||||
Gtk.main()
|
app.run(None)
|
||||||
|
|||||||
Reference in New Issue
Block a user