Compare commits

...

8 Commits

Author SHA1 Message Date
aae235e81f Format icons SVG files for better readability 2026-02-03 12:03:25 +01:00
759b4c4007 Use the icon 2026-02-03 01:26:30 +01:00
9958b9a65b Create org.iridium.Installer-symbolic.svg 2026-02-03 01:26:25 +01:00
1afdf99a6f Temporary (or probably permanent, you know how this works) icon 2026-02-03 01:17:48 +01:00
26757288c4 Add mock mode support and input validation 2026-02-02 20:19:36 +01:00
7a69d62782 Module titles 2026-02-02 20:15:40 +01:00
f691be219d Summary page 2026-02-02 20:05:11 +01:00
281f7488af Update README.md 2026-02-02 20:03:40 +01:00
11 changed files with 364 additions and 30 deletions

View File

@@ -1,4 +1,17 @@
<div align="center">
<img src="data/icons/org.iridium.Installer.svg" alt="Iridium OS Installer Icon" width="128" height="128">
# Iridium OS Installer # Iridium OS Installer
</div>
> [!WARNING] > [!WARNING]
> For now this is only a mockup and not actually functional > For now this is only a mockup and not actually functional.
## What is Iridium Installer?
Iridium Installer is a modern, GTK4/Libadwaita based installer designed for Iridium OS. It aims to provide a simple and elegant installation experience.
## Features
- Modern UI using **GTK4** and **Libadwaita**
- Automatic and Manual partitioning modes
- User configuration
- Module selection

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg
height="16px"
viewBox="0 0 16 16"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 8 1 C 4.132812 1 1 4.132812 1 8 C 1 11.867188 4.132812 15 8 15 C 11.867188 15 15 11.867188 15 8 C 15 4.132812 11.867188 1 8 1 Z M 8 3 C 10.761719 3 13 5.238281 13 8 C 13 10.761719 10.761719 13 8 13 C 5.238281 13 3 10.761719 3 8 C 3 5.238281 5.238281 3 8 3 Z M 8 3"
fill="#2e3436"
/>
<path
d="M 8 5 C 6.34375 5 5 6.34375 5 8 C 5 9.65625 6.34375 11 8 11 C 9.65625 11 11 9.65625 11 8 C 11 6.34375 9.65625 5 8 5 Z M 8 6 C 9.105469 6 10 6.894531 10 8 C 10 9.105469 9.105469 10 8 10 C 6.894531 10 6 9.105469 6 8 C 6 6.894531 6.894531 6 8 6 Z M 8 6"
fill="#2e3436"
/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@@ -0,0 +1,39 @@
<svg
width="128"
height="128"
viewBox="0 0 128 128"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="circle-bg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#5E5C64" />
<stop offset="100%" stop-color="#3D3846" />
</linearGradient>
</defs>
<circle cx="64" cy="64" r="56" fill="url(#circle-bg)" />
<circle
cx="64"
cy="64"
r="40"
fill="none"
stroke="#F6D32D"
stroke-width="8"
stroke-dasharray="75 25"
stroke-linecap="round"
opacity="0.9"
/>
<circle
cx="64"
cy="64"
r="28"
fill="none"
stroke="#F5C211"
stroke-width="4"
stroke-dasharray="45 20"
stroke-linecap="round"
opacity="0.8"
/>
<circle cx="64" cy="64" r="10" fill="#F6D32D" />
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Iridium Installer
Comment=Install Iridium OS on your computer
Exec=iridium-installer
Icon=org.iridium.Installer
Type=Application
Categories=System;Settings;
StartupNotify=true
Terminal=false

View File

@@ -1,3 +1,4 @@
import argparse
import sys import sys
import gi import gi
@@ -11,21 +12,30 @@ from .ui.window import InstallerWindow
class IridiumInstallerApp(Adw.Application): class IridiumInstallerApp(Adw.Application):
def __init__(self): def __init__(self, mock_mode=False):
super().__init__( super().__init__(
application_id="org.iridium.Installer", application_id="org.iridium.Installer",
flags=Gio.ApplicationFlags.FLAGS_NONE, flags=Gio.ApplicationFlags.FLAGS_NONE,
) )
self.mock_mode = mock_mode
def do_activate(self): def do_activate(self):
win = self.props.active_window win = self.props.active_window
if not win: if not win:
win = InstallerWindow(application=self) win = InstallerWindow(application=self, mock_mode=self.mock_mode)
win.present() win.present()
def main(): def main():
app = IridiumInstallerApp() parser = argparse.ArgumentParser(description="Iridium OS Installer")
parser.add_argument(
"--mock",
action="store_true",
help="Run in mock mode (no actual changes to disk)",
)
args = parser.parse_args()
app = IridiumInstallerApp(mock_mode=args.mock)
return app.run(sys.argv) return app.run(sys.argv)

View File

@@ -13,6 +13,12 @@ class ModulesPage(Adw.Bin):
self.chromebook_audio = False self.chromebook_audio = False
self.android_apps = False self.android_apps = False
self.module_titles = {
"nvidia_drivers": "Proprietary NVIDIA Drivers",
"chromebook_audio": "Chromebook Audio Fixes",
"android_apps": "Android Apps Support",
}
# Main Layout # Main Layout
clamp = Adw.Clamp() clamp = Adw.Clamp()
clamp.set_maximum_size(600) clamp.set_maximum_size(600)
@@ -46,21 +52,21 @@ class ModulesPage(Adw.Bin):
# NVIDIA Drivers # NVIDIA Drivers
self.add_module_row( self.add_module_row(
"Proprietary NVIDIA Drivers", self.module_titles["nvidia_drivers"],
"Install proprietary drivers for NVIDIA graphics cards for better performance.", "Install proprietary drivers for NVIDIA graphics cards for better performance.",
"nvidia_drivers", "nvidia_drivers",
) )
# Chromebook Audio # Chromebook Audio
self.add_module_row( self.add_module_row(
"Chromebook Audio Fixes", self.module_titles["chromebook_audio"],
"Install additional audio drivers for Chromebook devices.", "Install additional audio drivers for Chromebook devices.",
"chromebook_audio", "chromebook_audio",
) )
# Android Apps # Android Apps
self.add_module_row( self.add_module_row(
"Android Apps Support", self.module_titles["android_apps"],
"Install Waydroid to run Android applications on Iridium OS.", "Install Waydroid to run Android applications on Iridium OS.",
"android_apps", "android_apps",
) )
@@ -85,8 +91,11 @@ class ModulesPage(Adw.Bin):
setattr(self, attr_name, switch.get_active()) setattr(self, attr_name, switch.get_active())
def get_modules(self): def get_modules(self):
return { enabled_modules = []
"nvidia_drivers": self.nvidia_drivers, if self.nvidia_drivers:
"chromebook_audio": self.chromebook_audio, enabled_modules.append(self.module_titles["nvidia_drivers"])
"android_apps": self.android_apps, if self.chromebook_audio:
} enabled_modules.append(self.module_titles["chromebook_audio"])
if self.android_apps:
enabled_modules.append(self.module_titles["android_apps"])
return enabled_modules

View File

@@ -0,0 +1,85 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class SummaryPage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.status_page = Adw.StatusPage()
self.status_page.set_title("Summary")
self.status_page.set_description("Review your choices before installing")
self.status_page.set_icon_name("document-save-symbolic")
self.set_child(self.status_page)
# Main content box
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.status_page.set_child(clamp)
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
content_box.set_spacing(24)
clamp.set_child(content_box)
# Storage and partitioning
self.storage_group = Adw.PreferencesGroup()
self.storage_group.set_title("Storage & Partitioning")
content_box.append(self.storage_group)
self.disk_row = Adw.ActionRow()
self.disk_row.set_title("Target Disk")
self.storage_group.add(self.disk_row)
self.mode_row = Adw.ActionRow()
self.mode_row.set_title("Partitioning Mode")
self.storage_group.add(self.mode_row)
# User configuration
self.user_group = Adw.PreferencesGroup()
self.user_group.set_title("User Configuration")
content_box.append(self.user_group)
self.fullname_row = Adw.ActionRow()
self.fullname_row.set_title("Full Name")
self.user_group.add(self.fullname_row)
self.username_row = Adw.ActionRow()
self.username_row.set_title("Username")
self.user_group.add(self.username_row)
self.hostname_row = Adw.ActionRow()
self.hostname_row.set_title("Hostname")
self.user_group.add(self.hostname_row)
# Software
self.software_group = Adw.PreferencesGroup()
self.software_group.set_title("Software")
content_box.append(self.software_group)
self.modules_row = Adw.ActionRow()
self.modules_row.set_title("Additional Modules")
self.software_group.add(self.modules_row)
def update_summary(self, disk_info, mode, partitions, user_info, modules):
# Update Disk
self.disk_row.set_subtitle(str(disk_info))
# Update Mode
self.mode_row.set_subtitle(mode.capitalize())
# Update User Info
self.fullname_row.set_subtitle(user_info.get("fullname", ""))
self.username_row.set_subtitle(user_info.get("username", ""))
self.hostname_row.set_subtitle(user_info.get("hostname", ""))
# Update Modules
if modules:
self.modules_row.set_subtitle(", ".join(modules))
else:
self.modules_row.set_subtitle("None")

View File

@@ -1,14 +1,22 @@
import re
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, GObject, Gtk
class UserPage(Adw.Bin): class UserPage(Adw.Bin):
__gsignals__ = {
"validity-changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_valid = False
clamp = Adw.Clamp() clamp = Adw.Clamp()
clamp.set_maximum_size(500) clamp.set_maximum_size(500)
self.set_child(clamp) self.set_child(clamp)
@@ -32,6 +40,7 @@ class UserPage(Adw.Bin):
self.fullname_row.set_title("Full Name") self.fullname_row.set_title("Full Name")
self.fullname_row.set_text("Administrator") self.fullname_row.set_text("Administrator")
self.fullname_row.connect("notify::text", self.on_fullname_changed) self.fullname_row.connect("notify::text", self.on_fullname_changed)
self.fullname_row.connect("notify::text", self.on_input_changed)
group.add(self.fullname_row) group.add(self.fullname_row)
self.username_row = Adw.EntryRow() self.username_row = Adw.EntryRow()
@@ -40,14 +49,17 @@ class UserPage(Adw.Bin):
self.username_handler_id = self.username_row.connect( self.username_handler_id = self.username_row.connect(
"notify::text", self.on_username_changed "notify::text", self.on_username_changed
) )
self.username_row.connect("notify::text", self.on_input_changed)
group.add(self.username_row) group.add(self.username_row)
self.password_row = Adw.PasswordEntryRow() self.password_row = Adw.PasswordEntryRow()
self.password_row.set_title("Password") self.password_row.set_title("Password")
self.password_row.connect("notify::text", self.on_input_changed)
group.add(self.password_row) group.add(self.password_row)
self.confirm_row = Adw.PasswordEntryRow() self.confirm_row = Adw.PasswordEntryRow()
self.confirm_row.set_title("Confirm Password") self.confirm_row.set_title("Confirm Password")
self.confirm_row.connect("notify::text", self.on_input_changed)
group.add(self.confirm_row) group.add(self.confirm_row)
# Hostname # Hostname
@@ -58,10 +70,14 @@ class UserPage(Adw.Bin):
self.hostname_row = Adw.EntryRow() self.hostname_row = Adw.EntryRow()
self.hostname_row.set_title("Hostname") self.hostname_row.set_title("Hostname")
self.hostname_row.set_text("iridium") self.hostname_row.set_text("iridium")
self.hostname_row.connect("notify::text", self.on_input_changed)
host_group.add(self.hostname_row) host_group.add(self.hostname_row)
self.username_manually_set = False self.username_manually_set = False
# Initial validation
self.validate()
def on_fullname_changed(self, entry, _pspec): def on_fullname_changed(self, entry, _pspec):
if self.username_manually_set: if self.username_manually_set:
return return
@@ -113,6 +129,53 @@ class UserPage(Adw.Bin):
def on_username_changed(self, entry, _pspec): def on_username_changed(self, entry, _pspec):
self.username_manually_set = True self.username_manually_set = True
def on_input_changed(self, *args):
self.validate()
def validate(self):
valid = True
# Username validation
username = self.username_row.get_text()
if not username:
valid = False
self.username_row.add_css_class("error")
elif not re.match(r"^[a-z_][a-z0-9_-]*$", username):
valid = False
self.username_row.add_css_class("error")
else:
self.username_row.remove_css_class("error")
# Password validation
pw = self.password_row.get_text()
confirm = self.confirm_row.get_text()
if not pw:
valid = False
# Don't show error for empty password initially, just invalidate
# It looks a lot nicer like that
if pw != confirm:
valid = False
self.confirm_row.add_css_class("error")
else:
self.confirm_row.remove_css_class("error")
# Hostname validation
hostname = self.hostname_row.get_text()
if not hostname:
valid = False
self.hostname_row.add_css_class("error")
else:
self.hostname_row.remove_css_class("error")
if self._is_valid != valid:
self._is_valid = valid
self.emit("validity-changed", valid)
def is_valid(self):
return self._is_valid
def get_user_info(self): def get_user_info(self):
return { return {
"fullname": self.fullname_row.get_text(), "fullname": self.fullname_row.get_text(),

View File

@@ -1,8 +1,10 @@
import os
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, Gdk, Gtk
class WelcomePage(Adw.Bin): class WelcomePage(Adw.Bin):
@@ -12,13 +14,30 @@ class WelcomePage(Adw.Bin):
page = Adw.StatusPage() page = Adw.StatusPage()
page.set_title("Welcome to Iridium OS") page.set_title("Welcome to Iridium OS")
page.set_description("The installation will begin shortly") page.set_description("The installation will begin shortly")
# Load custom icon
icon_path = os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"data",
"icons",
"org.iridium.Installer.svg",
)
if os.path.exists(icon_path):
try:
paintable = Gdk.Texture.new_from_filename(icon_path)
page.set_paintable(paintable)
except Exception:
page.set_icon_name("system-software-install-symbolic")
else:
page.set_icon_name("system-software-install-symbolic") page.set_icon_name("system-software-install-symbolic")
# Content Box # Content Box
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(12) box.set_spacing(12)
box.set_halign(Gtk.Align.CENTER) box.set_halign(Gtk.Align.CENTER)
box.set_valign(Gtk.Align.CENTER)
lbl = Gtk.Label(label="Select your language:") lbl = Gtk.Label(label="Select your language:")
lbl.add_css_class("heading") lbl.add_css_class("heading")
@@ -29,7 +48,6 @@ class WelcomePage(Adw.Bin):
"English", "English",
] ]
dropdown = Gtk.DropDown.new_from_strings(languages) dropdown = Gtk.DropDown.new_from_strings(languages)
dropdown.set_margin_bottom(20)
box.append(dropdown) box.append(dropdown)
page.set_child(box) page.set_child(box)

View File

@@ -8,16 +8,20 @@ from .pages.additional_modules import ModulesPage
from .pages.install_mode import InstallModePage from .pages.install_mode import InstallModePage
from .pages.partitioning import PartitioningPage, calculate_auto_partitions from .pages.partitioning import PartitioningPage, calculate_auto_partitions
from .pages.storage import StoragePage from .pages.storage import StoragePage
from .pages.summary import SummaryPage
from .pages.user import UserPage from .pages.user import UserPage
from .pages.welcome import WelcomePage from .pages.welcome import WelcomePage
class InstallerWindow(Adw.ApplicationWindow): class InstallerWindow(Adw.ApplicationWindow):
def __init__(self, *args, **kwargs): def __init__(self, mock_mode=False, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.mock_mode = mock_mode
self.set_default_size(900, 650) self.set_default_size(900, 650)
self.set_title("Iridium Installer") self.set_title("Iridium Installer" + (" (MOCK MODE)" if mock_mode else ""))
self.set_icon_name("org.iridium.Installer")
self.toolbar_view = Adw.ToolbarView() self.toolbar_view = Adw.ToolbarView()
self.set_content(self.toolbar_view) self.set_content(self.toolbar_view)
@@ -70,6 +74,8 @@ class InstallerWindow(Adw.ApplicationWindow):
self.partitioning_page = PartitioningPage() self.partitioning_page = PartitioningPage()
self.modules_page = ModulesPage() self.modules_page = ModulesPage()
self.user_page = UserPage() self.user_page = UserPage()
self.user_page.connect("validity-changed", self.on_user_validity_changed)
self.summary_page = SummaryPage()
# Add Pages # Add Pages
self.add_page(self.welcome_page, "welcome") self.add_page(self.welcome_page, "welcome")
@@ -78,6 +84,7 @@ class InstallerWindow(Adw.ApplicationWindow):
self.add_page(self.partitioning_page, "partitioning") self.add_page(self.partitioning_page, "partitioning")
self.add_page(self.modules_page, "modules") self.add_page(self.modules_page, "modules")
self.add_page(self.user_page, "user") self.add_page(self.user_page, "user")
self.add_page(self.summary_page, "summary")
# Initialize view # Initialize view
if self.page_ids: if self.page_ids:
@@ -91,6 +98,9 @@ class InstallerWindow(Adw.ApplicationWindow):
def on_disk_selected(self, page, device_name): def on_disk_selected(self, page, device_name):
self.update_buttons() self.update_buttons()
def on_user_validity_changed(self, page, valid):
self.update_buttons()
def update_buttons(self): def update_buttons(self):
# Back button state # Back button state
self.back_button.set_sensitive(self.current_page_index > 0) self.back_button.set_sensitive(self.current_page_index > 0)
@@ -102,11 +112,15 @@ class InstallerWindow(Adw.ApplicationWindow):
forced_selection = True forced_selection = True
if current_page_name == "storage": if current_page_name == "storage":
forced_selection = self.storage_page.get_selected_disk() is not None forced_selection = self.storage_page.get_selected_disk() is not None
elif current_page_name == "user":
forced_selection = self.user_page.is_valid()
self.next_button.set_sensitive(forced_selection) self.next_button.set_sensitive(forced_selection)
if self.current_page_index == len(self.page_ids) - 1: if current_page_name == "summary":
self.next_button.set_label("Install") self.next_button.set_label(
"Install" + (" (MOCK)" if self.mock_mode else "")
)
self.next_button.add_css_class("destructive-action") self.next_button.add_css_class("destructive-action")
self.next_button.remove_css_class("suggested-action") self.next_button.remove_css_class("suggested-action")
else: else:
@@ -151,15 +165,11 @@ class InstallerWindow(Adw.ApplicationWindow):
next_index = self.page_ids.index("partitioning") next_index = self.page_ids.index("partitioning")
if current_page_name == "user": if current_page_name == "user":
print("Install process triggered!") # Prepare summary instead of installing immediately
disk = self.storage_page.get_selected_disk() disk = self.storage_page.get_selected_disk()
print(f"Disk: {disk}")
mode = self.install_mode_page.get_mode() mode = self.install_mode_page.get_mode()
print(f"Mode: {mode}")
modules = self.modules_page.get_modules() modules = self.modules_page.get_modules()
print(f"Modules: {modules}")
user_info = self.user_page.get_user_info() user_info = self.user_page.get_user_info()
print(f"User: {user_info}")
partitions_config = {} partitions_config = {}
if mode == "manual": if mode == "manual":
@@ -168,7 +178,42 @@ class InstallerWindow(Adw.ApplicationWindow):
partitions = calculate_auto_partitions(disk) partitions = calculate_auto_partitions(disk)
partitions_config = {"partitions": partitions} partitions_config = {"partitions": partitions}
print(f"Partitioning: {partitions_config}") # Update summary page
self.summary_page.update_summary(
disk_info=disk,
mode=mode,
partitions=partitions_config,
user_info=user_info,
modules=modules,
)
# Proceed to summary page (which is next_index after user)
if current_page_name == "summary":
# THIS IS THE REAL INSTALL TRIGGER
print("Install process triggered!")
disk = self.storage_page.get_selected_disk()
mode = self.install_mode_page.get_mode()
modules = self.modules_page.get_modules()
user_info = self.user_page.get_user_info()
partitions_config = {}
if mode == "manual":
partitions_config = self.partitioning_page.get_config()
elif mode == "automatic":
partitions = calculate_auto_partitions(disk)
partitions_config = {"partitions": partitions}
if self.mock_mode:
print("!!! MOCK MODE ENABLED - NO CHANGES WILL BE MADE !!!")
print(f"Target Disk: {disk}")
print(f"Mode: {mode}")
print(f"Modules: {modules}")
print(f"User: {user_info}")
print(f"Partition Config: {partitions_config}")
print("Simulation complete.")
else:
print("NOT IMPLEMENTED")
return return
if next_index < len(self.page_ids): if next_index < len(self.page_ids):

31
run.sh
View File

@@ -1,5 +1,32 @@
#!/bin/bash #!/bin/bash
# Install icons to ~/.local/share/icons if not already installed
ICON_DIR="$HOME/.local/share/icons/hicolor/128x128/apps"
SYMBOLIC_DIR="$HOME/.local/share/icons/hicolor/symbolic/apps"
mkdir -p "$ICON_DIR"
mkdir -p "$SYMBOLIC_DIR"
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Copy main icon if not exists or if source is newer
if [ ! -f "$ICON_DIR/org.iridium.Installer.svg" ] || [ "${SCRIPT_DIR}/data/icons/org.iridium.Installer.svg" -nt "$ICON_DIR/org.iridium.Installer.svg" ]; then
cp "${SCRIPT_DIR}/data/icons/org.iridium.Installer.svg" "$ICON_DIR/"
echo "Installed 128x128 icon"
fi
# Copy symbolic icon if not exists or if source is newer
if [ ! -f "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg" ] || [ "${SCRIPT_DIR}/data/icons/org.iridium.Installer-symbolic.svg" -nt "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg" ]; then
cp "${SCRIPT_DIR}/data/icons/org.iridium.Installer-symbolic.svg" "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg"
echo "Installed symbolic icon"
fi
# Update icon cache if gtk-update-icon-cache exists
if command -v gtk-update-icon-cache &> /dev/null; then
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
fi
export GSETTINGS_SCHEMA_DIR=. export GSETTINGS_SCHEMA_DIR=.
echo "Starting Iridium Installer..." python3 -m iridium_installer.main "$@"
python3 -m iridium_installer.main