diff --git a/README.md b/README.md index 6ef4bcb1..8130d0a3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ - libadwaita-1-dev - gettext - desktop-file-utils +- libnm-dev +- libnma-dev +- libnma-gtk4-dev ### Build ```bash diff --git a/VERSION b/VERSION index 38f77a65..7ec1d6db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 +2.1.0 diff --git a/data/icons/hicolor/symbolic/actions/background-app-ghost-symbolic.svg b/data/icons/hicolor/symbolic/actions/background-app-ghost-symbolic.svg new file mode 100644 index 00000000..9f0b98b9 --- /dev/null +++ b/data/icons/hicolor/symbolic/actions/background-app-ghost-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/meson.build b/data/icons/meson.build index 18eaf4e4..0c6245d0 100644 --- a/data/icons/meson.build +++ b/data/icons/meson.build @@ -22,6 +22,10 @@ install_data( install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) ) +install_data( + join_paths(actions_dir, 'background-app-ghost-symbolic.svg'), + install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'actions') +) install_data( join_paths(actions_dir, 'vanilla-container-terminal-symbolic.svg'), install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'actions') diff --git a/debian/changelog b/debian/changelog index b3fdb76f..0ec5da3a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,27 @@ +vanilla-first-setup (2.1.0) unstable; urgency=critical + + * Add network step + + -- Mateus Melchiades Mon, 26 Jul 2023 10:52:00 -0300 + +vanilla-first-setup (2.0.7) unstable; urgency=critical + + * Correctly use log_file variable from recipe + + -- Mateus Melchiades Mon, 30 Jul 2023 19:18:00 -0300 + +vanilla-first-setup (2.0.6) unstable; urgency=critical + + * Post script fixes + + -- Mateus Melchiades Mon, 27 Jul 2023 10:35:00 -0300 + +vanilla-first-setup (2.0.5) unstable; urgency=critical + + * Default user cleanup fixes + + -- Mateus Melchiades Mon, 27 Jul 2023 09:04:00 -0300 + vanilla-first-setup (2.0.3) unstable; urgency=critical * Change log font diff --git a/debian/control b/debian/control index 1ef36796..b958e80d 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: vanilla-first-setup Section: utils Priority: optional Maintainer: Mirko Brombin -Build-Depends: +Build-Depends: build-essential, debhelper, python3, @@ -11,7 +11,10 @@ Build-Depends: gettext, desktop-file-utils, make, - libjpeg-dev + libjpeg-dev, + libnm-dev, + libnma-dev, + libnma-gtk4-dev Homepage: https://github.com/mirkobrombin/vanilla-first-setup/ Vcs-Browser: hhttps://github.com/mirkobrombin/vanilla-first-setup Vcs-Git: https://github.com/mirkobrombin/vanilla-first-setup.git @@ -24,5 +27,8 @@ Depends: python3, libadwaita-1-0, gir1.2-gtk-4.0, gir1.2-adw-1, - gir1.2-vte-3.91 -Description: This utility is meant to be used in Ubuntu Vanilla GNOME as a first-setup wizard. \ No newline at end of file + gir1.2-vte-3.91, + libnm0, + libnma0, + libnma-gtk4-0 +Description: This utility is meant to be used in Vanilla GNOME as a first-setup wizard. diff --git a/recipe.json b/recipe.json index 612b217b..5537724a 100644 --- a/recipe.json +++ b/recipe.json @@ -35,6 +35,10 @@ } }, "steps": { + "network": { + "template": "network", + "protected": true + }, "conn-check": { "template": "conn-check", "protected": true diff --git a/vanilla_first_setup/defaults/applications.py b/vanilla_first_setup/defaults/applications.py index 0fb8bc2e..38f659b7 100644 --- a/vanilla_first_setup/defaults/applications.py +++ b/vanilla_first_setup/defaults/applications.py @@ -19,9 +19,9 @@ from vanilla_first_setup.dialog import VanillaDialog -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/layout-applications.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/layout-applications.ui") class VanillaLayoutApplications(Adw.Bin): - __gtype_name__ = 'VanillaLayoutApplications' + __gtype_name__ = "VanillaLayoutApplications" status_page = Gtk.Template.Child() bundles_list = Gtk.Template.Child() @@ -57,9 +57,9 @@ def present_customize(widget, dialog, apps_list, item): apps_list.remove(app["apps_action_row"]) except KeyError: pass - if self.__window.builder.get_temp_finals("packages")["vars"]["flatpak"] == True: + if self.__window.builder.get_temp_finals("packages")["vars"]["flatpak"]: package_manager = "flatpak" - elif self.__window.builder.get_temp_finals("packages")["vars"]["snap"] == True: + elif self.__window.builder.get_temp_finals("packages")["vars"]["snap"]: try: package_manager = "snap" except KeyError: @@ -71,7 +71,11 @@ def present_customize(widget, dialog, apps_list, item): _apps_action_row = Adw.ActionRow( title=app["name"], ) - _app_icon = Gtk.Image.new_from_resource("/org/vanillaos/FirstSetup/assets/bundle-app-icons/" + app["icon"] + ".png") + _app_icon = Gtk.Image.new_from_resource( + "/org/vanillaos/FirstSetup/assets/bundle-app-icons/" + + app["icon"] + + ".png" + ) _app_icon.set_icon_size(Gtk.IconSize.LARGE) _app_icon.add_css_class("lowres-icon") _apps_action_row.add_prefix(_app_icon) @@ -100,10 +104,10 @@ def apply_preferences(widget, dialog, apps_list, item): for item in self.__step["bundles"]: _selection_dialog = VanillaDialog( - self.__window, - "Select Applications", - "Description", - ) + self.__window, + "Select Applications", + "Description", + ) _cancel_button = Gtk.Button() _apply_button = Gtk.Button() @@ -118,7 +122,9 @@ def apply_preferences(widget, dialog, apps_list, item): _header_bar.set_show_start_title_buttons(False) _apps_list = Adw.PreferencesGroup() - _apps_list.set_description("The following list includes only applications available in your preferred package manager.") + _apps_list.set_description( + "The following list includes only applications available in your preferred package manager." + ) _apps_page = Adw.PreferencesPage() _apps_page.add(_apps_list) @@ -131,8 +137,7 @@ def apply_preferences(widget, dialog, apps_list, item): selection_dialogs.append(_selection_dialog) _action_row = Adw.ActionRow( - title=item["title"], - subtitle=item.get("subtitle", "") + title=item["title"], subtitle=item.get("subtitle", "") ) _switcher = Gtk.Switch() _switcher.set_active(item.get("default", False)) @@ -145,9 +150,13 @@ def apply_preferences(widget, dialog, apps_list, item): _customize.add_css_class("flat") _action_row.add_suffix(_customize) - _customize.connect("clicked", present_customize, selection_dialogs[-1], _apps_list, item) + _customize.connect( + "clicked", present_customize, selection_dialogs[-1], _apps_list, item + ) _cancel_button.connect("clicked", close_customize, selection_dialogs[-1]) - _apply_button.connect("clicked", apply_preferences, selection_dialogs[-1], _apps_list, item) + _apply_button.connect( + "clicked", apply_preferences, selection_dialogs[-1], _apps_list, item + ) self.bundles_list.add(_action_row) diff --git a/vanilla_first_setup/defaults/conn_check.py b/vanilla_first_setup/defaults/conn_check.py index 4d38231d..f924b970 100644 --- a/vanilla_first_setup/defaults/conn_check.py +++ b/vanilla_first_setup/defaults/conn_check.py @@ -16,17 +16,23 @@ from requests import Session from collections import OrderedDict -import requests + +import logging import os -from gi.repository import Gtk, GLib, Adw +from gi.repository import Gtk, Adw from vanilla_first_setup.utils.run_async import RunAsync +from gettext import gettext as _ + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("FirstSetup::Conn_Check") -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/default-conn-check.ui') + +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/default-conn-check.ui") class VanillaDefaultConnCheck(Adw.Bin): - __gtype_name__ = 'VanillaDefaultConnCheck' + __gtype_name__ = "VanillaDefaultConnCheck" btn_recheck = Gtk.Template.Child() status_page = Gtk.Template.Child() @@ -37,12 +43,16 @@ def __init__(self, window, distro_info, key, step, **kwargs): self.__distro_info = distro_info self.__key = key self.__step = step + self.__step_num = step["num"] - # connection check start - self.__start_conn_check() + self.__ignore_callback = False # signals self.btn_recheck.connect("clicked", self.__on_btn_recheck_clicked) + self.__window.carousel.connect("page-changed", self.__conn_check) + self.__window.btn_back.connect( + "clicked", self.__on_btn_back_clicked, self.__window.carousel.get_position() + ) @property def step_id(self): @@ -51,32 +61,49 @@ def step_id(self): def get_finals(self): return {} - def __start_conn_check(self): + def __on_btn_back_clicked(self, data, idx): + if idx + 1 != self.__step_num: + return + self.__ignore_callback = True + + def __conn_check(self, carousel=None, idx=None): + if idx is not None and idx != self.__step_num: + return + def async_fn(): if "VANILLA_SKIP_CONN_CHECK" in os.environ: return True try: s = Session() - headers = OrderedDict({ - 'Accept-Encoding': 'gzip, deflate, br', - 'Host': "vanillaos.org", - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0' - }) + headers = OrderedDict( + { + "Accept-Encoding": "gzip, deflate, br", + "Host": "vanillaos.org", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0", + } + ) s.headers = headers - s.get(f"https://vanillaos.org/", headers=headers, verify=True) + s.get("https://vanillaos.org/", headers=headers, verify=True) return True - except: + except Exception as e: + logger.error(f"Connection check failed: {str(e)}") return False def callback(res, *args): + if self.__ignore_callback: + self.__ignore_callback = False + return + if res: self.__window.next() return self.status_page.set_icon_name("network-wired-disconnected-symbolic") self.status_page.set_title(_("No Internet Connection!")) - self.status_page.set_description(_("First Setup requires an active internet connection")) + self.status_page.set_description( + _("First Setup requires an active internet connection") + ) self.btn_recheck.set_visible(True) RunAsync(async_fn, callback) @@ -85,5 +112,7 @@ def __on_btn_recheck_clicked(self, widget, *args): widget.set_visible(False) self.status_page.set_icon_name("content-loading-symbolic") self.status_page.set_title(_("Checking Connection…")) - self.status_page.set_description(_("Please wait until the connection check is done.")) - self.__start_conn_check() + self.status_page.set_description( + _("Please wait until the connection check is done.") + ) + self.__conn_check() diff --git a/vanilla_first_setup/defaults/hostname.py b/vanilla_first_setup/defaults/hostname.py index 58c0c870..247023af 100644 --- a/vanilla_first_setup/defaults/hostname.py +++ b/vanilla_first_setup/defaults/hostname.py @@ -15,15 +15,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys -import time -import re, subprocess, shutil -from gi.repository import Gtk, Gio, GLib, Adw +import re +from gi.repository import Gtk, Adw -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/default-hostname.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/default-hostname.ui") class VanillaDefaultHostname(Adw.Bin): - __gtype_name__ = 'VanillaDefaultHostname' + __gtype_name__ = "VanillaDefaultHostname" btn_next = Gtk.Template.Child() hostname_entry = Gtk.Template.Child() @@ -40,7 +38,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): # signals self.btn_next.connect("clicked", self.__on_btn_next_clicked) - self.hostname_entry.connect('changed', self.__on_hostname_entry_changed) + self.hostname_entry.connect("changed", self.__on_hostname_entry_changed) @property def step_id(self): @@ -51,18 +49,14 @@ def __on_btn_next_clicked(self, widget): def get_finals(self): return { - "vars": { - "setHostname": True - }, + "vars": {"setHostname": True}, "funcs": [ { "if": "setHostname", "type": "command", - "commands": [ - "hostnamectl set-hostname " + self.hostname - ] + "commands": ["hostnamectl set-hostname " + self.hostname], } - ] + ], } def __on_hostname_entry_changed(self, *args): @@ -70,12 +64,14 @@ def __on_hostname_entry_changed(self, *args): if self.__validate_hostname(_hostname): self.hostname = _hostname - self.hostname_entry.remove_css_class('error') + self.hostname_entry.remove_css_class("error") self.__verify_continue() return - self.__window.toast("Hostname cannot contain special characters. Please choose another hostname.") - self.hostname_entry.add_css_class('error') + self.__window.toast( + "Hostname cannot contain special characters. Please choose another hostname." + ) + self.hostname_entry.add_css_class("error") self.hostname = "" self.__verify_continue() @@ -83,7 +79,7 @@ def __validate_hostname(self, hostname): if len(hostname) > 50: return False - allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?. + +import logging +import time +from gettext import gettext as _ +from operator import attrgetter +from threading import Lock, Timer + +from gi.repository import NM, NMA4, Adw, Gtk + +from vanilla_first_setup.utils.run_async import RunAsync + + +logger = logging.getLogger("FirstSetup::Network") + +# Dictionary mapping security types to a tuple containing +# their pretty name and whether it is a secure protocol. +# If security is None, it means that no padlock icon is shown. +# If security is False, a warning symbol appears instead of a padlock. +AP_SECURITY_TYPES = { + "none": (None, None), + "wep": (False, _("Insecure network (WEP)")), + "wpa": (True, _("Secure network (WPA)")), + "wpa2": (True, _("Secure network (WPA2)")), + "sae": (True, _("Secure network (WPA3)")), + "owe": (None, None), + "owe_tm": (None, None), +} + +# PyGObject-libnm doesn't seem to expose these values, so we have redefine them +NM_802_11_AP_FLAGS_PRIVACY = 0x00000001 +NM_802_11_AP_SEC_NONE = 0x00000000 + +NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200 +NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192 = 0x00002000 +NM_802_11_AP_SEC_KEY_MGMT_OWE = 0x00000800 +NM_802_11_AP_SEC_KEY_MGMT_OWE_TM = 0x00001000 +NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100 +NM_802_11_AP_SEC_KEY_MGMT_SAE = 0x00000400 + + +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/wireless-row.ui") +class WirelessRow(Adw.ActionRow): + __gtype_name__ = "WirelessRow" + + signal_icon = Gtk.Template.Child() + secure_icon = Gtk.Template.Child() + connected_label = Gtk.Template.Child() + + def __init__(self, window, client, device: NM.DeviceWifi, ap, **kwargs): + super().__init__(**kwargs) + self.__window = window + self.client = client + self.ap = ap + self.device = device + self.refresh_ui() + + self.set_activatable(True) + self.connect("activated", self.__show_connect_dialog) + + @property + def ssid(self): + ssid = self.ap.get_ssid() + if ssid is not None: + ssid = ssid.get_data().decode("utf-8") + else: + ssid = "" + return ssid + + @property + def signal_strength(self): + return self.ap.get_strength() + + @property + def connected(self): + active_connection = self.device.get_active_connection() + if active_connection is not None: + if active_connection.get_id() == self.ssid: + return True + return False + + def refresh_ui(self): + # We use the same strength logic as gnome-control-center + strength = self.signal_strength + if strength < 20: + icon_name = "network-wireless-signal-none-symbolic" + elif strength < 40: + icon_name = "network-wireless-signal-weak-symbolic" + elif strength < 50: + icon_name = "network-wireless-signal-ok-symbolic" + elif strength < 80: + icon_name = "network-wireless-signal-good-symbolic" + else: + icon_name = "network-wireless-signal-excellent-symbolic" + + self.set_title(self.ssid) + self.signal_icon.set_from_icon_name(icon_name) + secure, tooltip = self.__get_security() + if secure is not None: + if not secure: + self.secure_icon.set_from_icon_name("warning-small-symbolic") + else: + self.secure_icon.set_from_icon_name( + "network-wireless-encrypted-symbolic" + ) + + self.secure_icon.set_visible(secure is not None) + if tooltip is not None: + self.secure_icon.set_tooltip_text(tooltip) + + self.connected_label.set_visible(self.connected) + + def __get_security(self) -> tuple[bool | None, str | None]: + flags = self.ap.get_flags() + rsn_flags = self.ap.get_rsn_flags() + wpa_flags = self.ap.get_wpa_flags() + + # Copying logic used in gnome-control-center because this is a mess + if ( + not (flags & NM_802_11_AP_FLAGS_PRIVACY) + and wpa_flags == NM_802_11_AP_SEC_NONE + and rsn_flags == NM_802_11_AP_SEC_NONE + ): + return AP_SECURITY_TYPES["none"] + elif ( + (flags & NM_802_11_AP_FLAGS_PRIVACY) + and wpa_flags == NM_802_11_AP_SEC_NONE + and rsn_flags == NM_802_11_AP_SEC_NONE + ): + return AP_SECURITY_TYPES["wep"] + elif ( + (flags & NM_802_11_AP_FLAGS_PRIVACY) + and wpa_flags != NM_802_11_AP_SEC_NONE + and rsn_flags != NM_802_11_AP_SEC_NONE + ): + return AP_SECURITY_TYPES["wpa"] + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE: + return AP_SECURITY_TYPES["sae"] + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_OWE: + return AP_SECURITY_TYPES["owe"] + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_OWE_TM: + return AP_SECURITY_TYPES["owe_tm"] + else: + return AP_SECURITY_TYPES["wpa2"] + + @property + def __key_mgmt(self): + # Key management used for the connection. One of "none" (WEP or no + # password protection), "ieee8021x" (Dynamic WEP), "owe" (Opportunistic + # Wireless Encryption), "wpa-psk" (WPA2 + WPA3 personal), "sae" (WPA3 + # personal only), "wpa-eap" (WPA2 + WPA3 enterprise) or + # "wpa-eap-suite-b-192" (WPA3 enterprise only). + rsn_flags = self.ap.get_rsn_flags() + wpa_flags = self.ap.get_wpa_flags() + + if wpa_flags == NM_802_11_AP_SEC_NONE and rsn_flags == NM_802_11_AP_SEC_NONE: + return "none" + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X: + return "ieee8021x" + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192: + return "wpa-eap-suite-b-192" + elif ( + rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_OWE + or rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_OWE_TM + ): + return "owe" + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK: + return "wpa-psk" + elif rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE: + return "sae" + + def __show_connect_dialog(self, data): + dialog = NMA4.WifiDialog.new( + self.client, self.__construct_connection(), self.device, self.ap, False + ) + dialog.set_modal(True) + dialog.set_transient_for(self.__window) + + dialog.connect("response", self.__on_dialog_response) + + dialog.show() + + def __on_dialog_response(self, dialog, response_id): + def connect_cb(client, result, data): + try: + ac = client.add_and_activate_connection_finish(result) + logger.debug("ActiveConnection {}".format(ac.get_path())) + except Exception as e: + logger.error("Error:", e) + + if response_id == -6: + dialog.close() + elif response_id == -5: + conn, _, _ = dialog.get_connection() + self.client.add_and_activate_connection_async( + conn, self.device, self.ap.get_path(), None, connect_cb, None + ) + dialog.close() + + def __construct_connection(self): + connection = NM.SimpleConnection.new() + s_con = NM.SettingConnection.new() + s_con.set_property(NM.SETTING_CONNECTION_ID, self.ssid) + s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") + s_wifi = NM.SettingWireless.new() + s_wifi.set_property(NM.SETTING_WIRELESS_SSID, self.ap.get_ssid()) + s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure") + s_wsec = NM.SettingWirelessSecurity.new() + s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, self.__key_mgmt) + s_ip4 = NM.SettingIP4Config.new() + s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") + s_ip6 = NM.SettingIP6Config.new() + s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") + + connection.add_setting(s_con) + connection.add_setting(s_wifi) + connection.add_setting(s_wsec) + connection.add_setting(s_ip4) + connection.add_setting(s_ip6) + + return connection + + +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/default-network.ui") +class VanillaDefaultNetwork(Adw.Bin): + __gtype_name__ = "VanillaDefaultNetwork" + + wired_group = Gtk.Template.Child() + wireless_group = Gtk.Template.Child() + hidden_network_row = Gtk.Template.Child() + proxy_settings_row = Gtk.Template.Child() + advanced_group = Gtk.Template.Child() + btn_next = Gtk.Template.Child() + + def __init__(self, window, distro_info, key, step, **kwargs): + super().__init__(**kwargs) + self.__window = window + self.__distro_info = distro_info + self.__key = key + self.__step = step + self.__nm_client = NM.Client.new() + + self.__devices = [] + self.__wired_children = [] + self.__wireless_children = {} + + self.__last_wifi_scan = 0 + + # Prevent concurrency issues when re-scanning Wi-Fi devices. + # Since we reload the list every time there's a state change, + # there's a high change that it coincides with a periodic + # refresh operation. + self.__wifi_lock = Lock() + + # Since we have a dedicated page for checking connectivity, + # we only need to make sure the user has some type of + # connection set up, be it wired or wireless. + self.has_eth_connection = False + self.has_wifi_connection = False + + self.__get_network_devices() + self.__start_auto_refresh() + + # TODO: Remove once implemented + self.advanced_group.set_visible(False) + + self.__nm_client.connect("device-added", self.__add_new_device) + self.__nm_client.connect("device-added", self.__remove_device) + self.btn_next.connect("clicked", self.__window.next) + self.connect("realize", self.__try_skip_page) + + def __try_skip_page(self, data): + # Skip page if already connected to the internet + if self.has_eth_connection or self.has_wifi_connection: + self.__window.next() + + @property + def step_id(self): + return self.__key + + def get_finals(self): + return {} + + def set_btn_next(self, state: bool): + if state: + if not self.btn_next.has_css_class("suggested-action"): + self.btn_next.add_css_class("suggested-action") + self.btn_next.set_sensitive(True) + else: + if self.btn_next.has_css_class("suggested-action"): + self.btn_next.remove_css_class("suggested-action") + self.btn_next.set_sensitive(False) + + def __get_network_devices(self): + devices = self.__nm_client.get_devices() + eth_devices = 0 + wifi_devices = 0 + for device in devices: + if device.is_real(): + device_type = device.get_device_type() + if device_type == NM.DeviceType.ETHERNET: + self.__add_ethernet_connection(device) + eth_devices += 1 + elif device_type == NM.DeviceType.WIFI: + device.connect("state-changed", self.__on_state_changed) + self.has_wifi_connection = ( + device.get_active_connection() is not None + ) + self.__refresh_wifi_list(device) + wifi_devices += 1 + else: + continue + + self.__devices.append(device) + + self.wired_group.set_visible(eth_devices > 0) + self.wireless_group.set_visible(wifi_devices > 0) + + def __add_new_device(self, client, device): + self.__devices.append(device) + + def __remove_device(self, client, device): + self.__devices.remove(device) + + def __on_state_changed(self, device, new_state, old_state, reason): + self.has_wifi_connection = device.get_active_connection() is not None + self.__refresh() + + def __refresh(self): + for child in self.__wired_children: + self.wired_group.remove(child) + + self.__wired_children = [] + + for device in self.__devices: + device_type = device.get_device_type() + if device_type == NM.DeviceType.WIFI: + self.__scan_wifi(device) + + self.set_btn_next(self.has_eth_connection or self.has_wifi_connection) + + def __start_auto_refresh(self): + def run_async(): + while True: + self.__refresh() + time.sleep(10) + + RunAsync(run_async, None) + + def __device_status(self, conn: NM.Device): + connected = False + match conn.get_state(): + case NM.DeviceState.ACTIVATED: + status = _("Connected") + connected = True + case NM.DeviceState.NEED_AUTH: + status = _("Authentication required") + case [ + NM.DeviceState.PREPARE, + NM.DeviceState.CONFIG, + NM.DeviceState.IP_CONFIG, + NM.DeviceState.IP_CHECK, + NM.DeviceState.SECONDARIES, + ]: + status = _("Connecting") + case NM.DeviceState.DISCONNECTED: + status = _("Disconnected") + case NM.DeviceState.DEACTIVATING: + status = _("Disconnecting") + case NM.DeviceState.FAILED: + status = _("Connection Failed") + case NM.DeviceState.UNKNOWN: + status = _("Status Unknown") + case NM.DeviceState.UNMANAGED: + status = _("Unmanaged") + case NM.DeviceState.UNAVAILABLE: + status = _("Unavailable") + + return status, connected + + def __add_ethernet_connection(self, conn: NM.DeviceEthernet): + status, connected = self.__device_status(conn) + if connected: + status += f" - {conn.get_speed()} Mbps" + self.has_eth_connection = True + else: + self.has_eth_connection = False + + # Wired devices with no cable plugged in are shown as unavailable + if conn.get_state() == NM.DeviceState.UNAVAILABLE: + status = _("Cable Unplugged") + + eth_conn = Adw.ActionRow(title=status) + self.wired_group.add(eth_conn) + self.__wired_children.append(eth_conn) + + def __refresh_wifi_list(self, conn: NM.DeviceWifi): + while conn.get_last_scan() == self.__last_wifi_scan: + time.sleep(0.25) + + networks = {} + for ap in conn.get_access_points(): + ssid = ap.get_ssid() + if ssid is None: + continue + + ssid = ssid.get_data().decode("utf-8") + if ssid in networks.keys(): + networks[ssid].append(ap) + else: + networks[ssid] = [ap] + + self.__wifi_lock.acquire() + + # Invalidate current list + for ssid, (child, clean) in self.__wireless_children.items(): + self.__wireless_children[ssid] = (child, True) + + for ssid, aps in networks.items(): + max_strength = -1 + best_ap = None + for ap in aps: + ap_strength = ap.get_strength() + if ap_strength > max_strength: + max_strength = ap_strength + best_ap = ap + + # Try to re-use entries with the same SSID + if ssid in self.__wireless_children.keys(): + child = self.__wireless_children[ssid][0] + child.ap = best_ap + child.refresh_ui() + self.__wireless_children[ssid] = (child, False) + continue + + # Create new row if SSID is new + wifi_network = WirelessRow(self.__window, self.__nm_client, conn, best_ap) + self.wireless_group.add(wifi_network) + self.__wireless_children[ssid] = (wifi_network, False) + + # Remove invalid rows + invalid_ssids = [] + for ssid, (child, clean) in self.__wireless_children.items(): + self.wireless_group.remove(child) + if clean: + invalid_ssids.append(ssid) + + for ssid in invalid_ssids: + del self.__wireless_children[ssid] + + for row in self.__sorted_wireless_children: + self.wireless_group.add(row) + + self.__wifi_lock.release() + + def __scan_wifi(self, conn: NM.DeviceWifi): + self.__last_wifi_scan = conn.get_last_scan() + conn.request_scan_async() + + t = Timer(1.5, self.__refresh_wifi_list, [conn]) + t.start() + + @property + def __sorted_wireless_children(self): + def multisort(xs, specs): + for key, reverse in reversed(specs): + xs.sort(key=attrgetter(key), reverse=reverse) + return xs + + # 1 - Is connected + # 2 - Signal strength + # 3 - Alphabetically + return multisort( + [it[0] for it in list(self.__wireless_children.values())], + (("connected", True), ("signal_strength", True), ("ssid", True)), + ) diff --git a/vanilla_first_setup/defaults/user.py b/vanilla_first_setup/defaults/user.py index a7981c15..f02379a6 100644 --- a/vanilla_first_setup/defaults/user.py +++ b/vanilla_first_setup/defaults/user.py @@ -16,15 +16,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys -import time -import re, subprocess, shutil -from gi.repository import Gtk, Gio, GLib, Adw +import re +import subprocess +import shutil +from gi.repository import Gtk, Adw -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/default-user.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/default-user.ui") class VanillaDefaultUser(Adw.Bin): - __gtype_name__ = 'VanillaDefaultUser' + __gtype_name__ = "VanillaDefaultUser" btn_next = Gtk.Template.Child() fullname_entry = Gtk.Template.Child() @@ -48,10 +48,10 @@ def __init__(self, window, distro_info, key, step, **kwargs): # signals self.btn_next.connect("clicked", self.__on_btn_next_clicked) - self.fullname_entry.connect('changed', self.__on_fullname_entry_changed) - self.username_entry.connect('changed', self.__on_username_entry_changed) - self.password_entry.connect('changed', self.__on_password_changed) - self.password_confirmation.connect('changed', self.__on_password_changed) + self.fullname_entry.connect("changed", self.__on_fullname_entry_changed) + self.username_entry.connect("changed", self.__on_username_entry_changed) + self.password_entry.connect("changed", self.__on_password_changed) + self.password_confirmation.connect("changed", self.__on_password_changed) @property def step_id(self): @@ -63,20 +63,18 @@ def __on_btn_next_clicked(self, widget): def get_finals(self): return { - "vars": { - "createUser": True - }, + "vars": {"createUser": True}, "funcs": [ { "if": "createUser", "type": "command", "commands": [ - f"adduser --quiet --disabled-password --shell /bin/bash --gecos \"{self.fullname}\" {self.username}", - f"echo \"{self.username}:{self.password_entry.get_text()}\" | chpasswd", - f"usermod -a -G sudo,adm,lpadmin {self.username}" - ] + f'adduser --quiet --disabled-password --shell /bin/bash --gecos "{self.fullname}" {self.username}', + f'echo "{self.username}:{self.password_entry.get_text()}" | chpasswd', + f"usermod -a -G sudo,adm,lpadmin {self.username}", + ], } - ] + ], } def __on_fullname_entry_changed(self, *args): @@ -102,9 +100,11 @@ def __on_username_entry_changed(self, *args): _input = self.username_entry.get_text() # cannot contain special characters - if re.search(r'[^a-z0-9_-]', _input): + if re.search(r"[^a-z0-9_-]", _input): _status = False - self.__window.toast("Username cannot contain special characters or uppercase letters. Please choose another username.") + self.__window.toast( + "Username cannot contain special characters or uppercase letters. Please choose another username." + ) # cannot be empty elif not _input: @@ -114,39 +114,40 @@ def __on_username_entry_changed(self, *args): # cannot be root elif _input == "root": _status = False - self.__window.toast("root user is reserved. Please choose another username.") + self.__window.toast( + "root user is reserved. Please choose another username." + ) if not _status: - self.username_entry.add_css_class('error') + self.username_entry.add_css_class("error") self.username_filled = False self.__verify_continue() else: - self.username_entry.remove_css_class('error') + self.username_entry.remove_css_class("error") self.username_filled = True self.__verify_continue() self.username = _input - def __on_password_changed(self, *args): password = self.password_entry.get_text() - if password == self.password_confirmation.get_text() \ - and password.strip(): - self.password_filled = True; - self.password_confirmation.remove_css_class('error') + if password == self.password_confirmation.get_text() and password.strip(): + self.password_filled = True + self.password_confirmation.remove_css_class("error") self.password = self.__encrypt_password(password) else: - self.password_filled = False; - self.password_confirmation.add_css_class('error') + self.password_filled = False + self.password_confirmation.add_css_class("error") - self.__verify_continue(); + self.__verify_continue() def __verify_continue(self): - self.btn_next.set_sensitive(self.fullname_filled and self.password_filled and self.username_filled) + self.btn_next.set_sensitive( + self.fullname_filled and self.password_filled and self.username_filled + ) def __encrypt_password(self, password): command = subprocess.run( - [shutil.which("openssl"), "passwd", "-crypt", password], - capture_output=True + [shutil.which("openssl"), "passwd", "-crypt", password], capture_output=True ) - password_encrypted = command.stdout.decode('utf-8').strip('\n') + password_encrypted = command.stdout.decode("utf-8").strip("\n") return password_encrypted diff --git a/vanilla_first_setup/defaults/welcome.py b/vanilla_first_setup/defaults/welcome.py index 6b69b2db..fcc86b67 100644 --- a/vanilla_first_setup/defaults/welcome.py +++ b/vanilla_first_setup/defaults/welcome.py @@ -20,9 +20,9 @@ from vanilla_first_setup.utils.run_async import RunAsync -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/default-welcome.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/default-welcome.ui") class VanillaDefaultWelcome(Adw.Bin): - __gtype_name__ = 'VanillaDefaultWelcome' + __gtype_name__ = "VanillaDefaultWelcome" btn_advanced = Gtk.Template.Child() btn_next = Gtk.Template.Child() @@ -30,40 +30,40 @@ class VanillaDefaultWelcome(Adw.Bin): title_label = Gtk.Template.Child() welcome = [ - 'Welcome', - 'Benvenuto', - 'Bienvenido', - 'Bienvenue', - 'Willkommen', - 'Bem-vindo', - 'Добро пожаловать', - '欢迎', - 'ようこそ', - '환영합니다', - 'أهلا بك', - 'ברוך הבא', - 'Καλώς ήρθατε', - 'Hoşgeldiniz', - 'Welkom', - 'Witamy', - 'Välkommen', - 'Tervetuloa', - 'Vítejte', - 'Üdvözöljük', - 'Bun venit', - 'Vitajte', - 'Tere tulemast', - 'Sveiki atvykę', - 'Dobrodošli', - 'خوش آمدید', - 'आपका स्वागत है', - 'স্বাগতম', - 'வரவேற்கிறோம்', - 'స్వాగతం', - 'मुबारक हो', - 'સુસ્વાગત છે', - 'ಸುಸ್ವಾಗತ', - 'സ്വാഗതം' + "Welcome", + "Benvenuto", + "Bienvenido", + "Bienvenue", + "Willkommen", + "Bem-vindo", + "Добро пожаловать", + "欢迎", + "ようこそ", + "환영합니다", + "أهلا بك", + "ברוך הבא", + "Καλώς ήρθατε", + "Hoşgeldiniz", + "Welkom", + "Witamy", + "Välkommen", + "Tervetuloa", + "Vítejte", + "Üdvözöljük", + "Bun venit", + "Vitajte", + "Tere tulemast", + "Sveiki atvykę", + "Dobrodošli", + "خوش آمدید", + "आपका स्वागत है", + "স্বাগতম", + "வரவேற்கிறோம்", + "స్వాగతం", + "मुबारक हो", + "સુસ્વાગત છે", + "ಸುಸ್ವಾಗತ", + "സ്വാഗതം", ] def __init__(self, window, distro_info, key, step, **kwargs): @@ -82,7 +82,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): # set distro logo self.status_page.set_icon_name(self.__distro_info["logo"]) - + @property def step_id(self): return self.__key diff --git a/vanilla_first_setup/dialog.py b/vanilla_first_setup/dialog.py index 46e89549..bc578389 100644 --- a/vanilla_first_setup/dialog.py +++ b/vanilla_first_setup/dialog.py @@ -17,9 +17,9 @@ from gi.repository import Gtk, Adw -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/dialog.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/dialog.ui") class VanillaDialog(Adw.Window): - __gtype_name__ = 'VanillaDialog' + __gtype_name__ = "VanillaDialog" label_text = Gtk.Template.Child() @@ -33,5 +33,9 @@ def hide(action, callback=None): self.hide() shortcut_controller = Gtk.ShortcutController.new() - shortcut_controller.add_shortcut(Gtk.Shortcut.new(Gtk.ShortcutTrigger.parse_string('Escape'), Gtk.CallbackAction.new(hide))) - self.add_controller(shortcut_controller) \ No newline at end of file + shortcut_controller.add_shortcut( + Gtk.Shortcut.new( + Gtk.ShortcutTrigger.parse_string("Escape"), Gtk.CallbackAction.new(hide) + ) + ) + self.add_controller(shortcut_controller) diff --git a/vanilla_first_setup/gtk/default-network.ui b/vanilla_first_setup/gtk/default-network.ui new file mode 100644 index 00000000..15815133 --- /dev/null +++ b/vanilla_first_setup/gtk/default-network.ui @@ -0,0 +1,102 @@ + + + + + diff --git a/vanilla_first_setup/gtk/default-welcome.ui b/vanilla_first_setup/gtk/default-welcome.ui index f48a2a70..d91e63a2 100644 --- a/vanilla_first_setup/gtk/default-welcome.ui +++ b/vanilla_first_setup/gtk/default-welcome.ui @@ -13,7 +13,6 @@ 8 1 - @@ -35,7 +34,6 @@ - Make your choices, this wizard will take care of everything. @@ -74,4 +72,4 @@ - \ No newline at end of file + diff --git a/vanilla_first_setup/gtk/window.ui b/vanilla_first_setup/gtk/window.ui index 18124b31..c13e6c82 100644 --- a/vanilla_first_setup/gtk/window.ui +++ b/vanilla_first_setup/gtk/window.ui @@ -38,6 +38,7 @@ False False False + False diff --git a/vanilla_first_setup/gtk/wireless-row.ui b/vanilla_first_setup/gtk/wireless-row.ui new file mode 100644 index 00000000..7f1e7e09 --- /dev/null +++ b/vanilla_first_setup/gtk/wireless-row.ui @@ -0,0 +1,38 @@ + + + + + diff --git a/vanilla_first_setup/layouts/preferences.py b/vanilla_first_setup/layouts/preferences.py index 9bed9f0c..3cc8c9f0 100644 --- a/vanilla_first_setup/layouts/preferences.py +++ b/vanilla_first_setup/layouts/preferences.py @@ -14,14 +14,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from gettext import gettext as _ from gi.repository import Gtk, Adw from vanilla_first_setup.dialog import VanillaDialog -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/layout-preferences.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/layout-preferences.ui") class VanillaLayoutPreferences(Adw.Bin): - __gtype_name__ = 'VanillaLayoutPreferences' + __gtype_name__ = "VanillaLayoutPreferences" status_page = Gtk.Template.Child() prefs_list = Gtk.Template.Child() @@ -38,7 +39,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): # signals self.btn_next.connect("clicked", self.__next_step) - + @property def step_id(self): return self.__key @@ -50,8 +51,7 @@ def __build_ui(self): for item in self.__step["preferences"]: _action_row = Adw.ActionRow( - title=item["title"], - subtitle=item.get("subtitle", "") + title=item["title"], subtitle=item.get("subtitle", "") ) _switcher = Gtk.Switch() _switcher.set_active(item.get("default", False)) @@ -66,7 +66,7 @@ def __build_ui(self): self.prefs_list.add(_action_row) self.__register_widgets.append((item["id"], _switcher)) - + def __next_step(self, widget): ws = self.__step.get("without_selection", {}) @@ -91,9 +91,11 @@ def get_finals(self): for _id, switcher in self.__register_widgets: finals["vars"][_id] = switcher.get_active() - if not any([x[1].get_active() for x in self.__register_widgets]) \ - and ws.get("allowed", True) \ - and ws.get("final", None): + if ( + not any([x[1].get_active() for x in self.__register_widgets]) + and ws.get("allowed", True) + and ws.get("final", None) + ): finals["vars"]["_managed"] = True finals["funcs"].extend(ws["final"]) diff --git a/vanilla_first_setup/layouts/yes_no.py b/vanilla_first_setup/layouts/yes_no.py index de747f86..5ecfcdef 100644 --- a/vanilla_first_setup/layouts/yes_no.py +++ b/vanilla_first_setup/layouts/yes_no.py @@ -14,16 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import time -from gi.repository import Gtk, Gio, GLib, Adw +from gi.repository import Gtk, Adw -from vanilla_first_setup.utils.run_async import RunAsync from vanilla_first_setup.dialog import VanillaDialog -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/layout-yes-no.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/layout-yes-no.ui") class VanillaLayoutYesNo(Adw.Bin): - __gtype_name__ = 'VanillaLayoutYesNo' + __gtype_name__ = "VanillaLayoutYesNo" status_page = Gtk.Template.Child() btn_no = Gtk.Template.Child() @@ -43,7 +41,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): self.btn_yes.connect("clicked", self.__on_response, True) self.btn_no.connect("clicked", self.__on_response, False) self.btn_info.connect("clicked", self.__on_info) - + @property def step_id(self): return self.__key @@ -70,16 +68,14 @@ def __on_info(self, _): dialog = VanillaDialog( self.__window, self.__step["buttons"]["info"]["title"], - self.__step["buttons"]["info"]["text"] + self.__step["buttons"]["info"]["text"], ) dialog.show() def get_finals(self): return { - "vars": { - self.__key: self.__response - }, - "funcs": [x for x in self.__step["final"]] + "vars": {self.__key: self.__response}, + "funcs": [x for x in self.__step["final"]], } def __get_default(self): diff --git a/vanilla_first_setup/main.py b/vanilla_first_setup/main.py index a6c05da5..4c8e41b4 100644 --- a/vanilla_first_setup/main.py +++ b/vanilla_first_setup/main.py @@ -14,18 +14,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from vanilla_first_setup.window import VanillaWindow +from gi.repository import Gtk, Gdk, Gio, GLib, Adw import os import gi import sys import logging from gettext import gettext as _ -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') -gi.require_version('Vte', '3.91') - -from gi.repository import Gtk, Gdk, Gio, GLib, Adw, Vte, Pango -from vanilla_first_setup.window import VanillaWindow +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +gi.require_version("Vte", "3.91") logging.basicConfig(level=logging.INFO) @@ -36,13 +35,15 @@ class FirstSetupApplication(Adw.Application): """The main application singleton class.""" def __init__(self): - super().__init__(application_id='org.vanillaos.FirstSetup', - flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) + super().__init__( + application_id="org.vanillaos.FirstSetup", + flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, + ) self.post_script = None self.user = os.environ.get("USER") self.new_user = False - self.create_action('quit', self.close, ['q']) + self.create_action("quit", self.close, ["q"]) self.__register_arguments() def __register_arguments(self): @@ -53,7 +54,7 @@ def __register_arguments(self): GLib.OptionFlags.NONE, GLib.OptionArg.STRING, _("Run a post script"), - None + None, ) self.add_main_option( "new-user", @@ -61,7 +62,7 @@ def __register_arguments(self): GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Run as a new user"), - None + None, ) def do_command_line(self, command_line): @@ -90,11 +91,16 @@ def do_activate(self): Gtk.StyleContext.add_provider_for_display( display=Gdk.Display.get_default(), provider=provider, - priority=Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + priority=Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) win = self.props.active_window if not win: - win = VanillaWindow(application=self, post_script=self.post_script, user=self.user, new_user=self.new_user) + win = VanillaWindow( + application=self, + post_script=self.post_script, + user=self.user, + new_user=self.new_user, + ) win.present() def create_action(self, name, callback, shortcuts=None): diff --git a/vanilla_first_setup/utils/builder.py b/vanilla_first_setup/utils/builder.py index 8b21b7f5..4c6aa05f 100644 --- a/vanilla_first_setup/utils/builder.py +++ b/vanilla_first_setup/utils/builder.py @@ -26,6 +26,7 @@ from vanilla_first_setup.defaults.theme import VanillaDefaultTheme from vanilla_first_setup.defaults.user import VanillaDefaultUser from vanilla_first_setup.defaults.hostname import VanillaDefaultHostname +from vanilla_first_setup.defaults.network import VanillaDefaultNetwork from vanilla_first_setup.layouts.preferences import VanillaLayoutPreferences from vanilla_first_setup.layouts.yes_no import VanillaLayoutYesNo @@ -36,6 +37,7 @@ templates = { + "network": VanillaDefaultNetwork, "conn-check": VanillaDefaultConnCheck, "welcome": VanillaDefaultWelcome, "theme": VanillaDefaultTheme, @@ -43,12 +45,11 @@ "hostname": VanillaDefaultHostname, "preferences": VanillaLayoutPreferences, "yes-no": VanillaLayoutYesNo, - "applications": VanillaLayoutApplications + "applications": VanillaLayoutApplications, } class Builder: - def __init__(self, window, new_user: bool = False): self.__window = window self.__new_user = new_user @@ -68,12 +69,12 @@ def __load(self): if not os.path.exists(log_path): try: - open(log_path, 'a').close() - except OSError: - logger.warning("failed to create log file: %s" % log_path) + open(log_path, "a").close() + except OSError as e: + logger.warning(f"failed to create log file: {log_path}: {e}") logging.warning("No log will be stored.") - for key, step in self.__recipe.raw["steps"].items(): + for i, (key, step) in enumerate(self.__recipe.raw["steps"].items()): _status = True _protected = False @@ -81,13 +82,20 @@ def __load(self): _condition_met = False for command in step["display-conditions"]: try: - logger.info("Performing display-condition: %s" % command) - output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT) - if output.decode("utf-8") == "" or output.decode("utf-8") == "1": - logger.info("Step %s skipped due to display-conditions" % key) + logger.info(f"Performing display-condition: {command}") + output = subprocess.check_output( + command, shell=True, stderr=subprocess.STDOUT + ) + if ( + output.decode("utf-8") == "" + or output.decode("utf-8") == "1" + ): + logger.info( + "Step {key} skipped due to display-conditions" + ) break except subprocess.CalledProcessError: - logger.info("Step %s skipped due to display-conditions" % key) + logger.info(f"Step {key} skipped due to display-conditions") break else: _condition_met = True @@ -102,7 +110,10 @@ def __load(self): _protected = step.get("protected", False) if step["template"] in templates: - _widget = templates[step["template"]](self.__window, self.distro_info, key, step) + step["num"] = i + _widget = templates[step["template"]]( + self.__window, self.distro_info, key, step + ) self.__register_widgets.append((_widget, _status, _protected)) def get_temp_finals(self, step_id: str): @@ -132,5 +143,5 @@ def recipe(self): def distro_info(self): return { "name": self.__recipe.raw["distro_name"], - "logo": self.__recipe.raw["distro_logo"] + "logo": self.__recipe.raw["distro_logo"], } diff --git a/vanilla_first_setup/utils/processor.py b/vanilla_first_setup/utils/processor.py index 9bc29703..0c9a2f3f 100644 --- a/vanilla_first_setup/utils/processor.py +++ b/vanilla_first_setup/utils/processor.py @@ -24,19 +24,21 @@ class Processor: - @staticmethod def get_setup_commands(log_path, pre_run, post_run, commands): commands = pre_run + commands + post_run out_run = "" next_boot = [] - next_boot_script_path = os.path.expanduser("/etc/org.vanillaos.FirstSetup.nextBoot") - next_boot_autostart_path = os.path.expanduser("/etc/xdg/autostart/org.vanillaos.FirstSetup.nextBoot.desktop") + next_boot_script_path = os.path.expanduser( + "/etc/org.vanillaos.FirstSetup.nextBoot" + ) + next_boot_autostart_path = os.path.expanduser( + "/etc/xdg/autostart/org.vanillaos.FirstSetup.nextBoot.desktop" + ) done_file = "/etc/vanilla-first-setup-done" abroot_bin = shutil.which("abroot") - logger.info("processing the following commands: \n%s" % - '\n'.join(commands)) + logger.info("processing the following commands: \n%s" % "\n".join(commands)) # Collect all the commands that should be run at the next boot for command in commands: @@ -46,7 +48,7 @@ def get_setup_commands(log_path, pre_run, post_run, commands): # generating a temporary file to store all the commands so we can # run them all at once - with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: f.write("#!/bin/sh\n") f.write("# This file was created by FirstSetup\n") f.write("# Do not edit this file manually\n\n") @@ -66,13 +68,15 @@ def get_setup_commands(log_path, pre_run, post_run, commands): # nextBoot commands are collected in /etc/org.vanillaos.FirstSetup.nextBoot # and executed at the next boot by a desktop entry if len(next_boot) > 0: - f.write("cat < "+next_boot_script_path+"\n") + f.write("cat < " + next_boot_script_path + "\n") f.write("#!/bin/sh\n") f.write("# This file was created by FirstSetup\n") f.write("# Do not edit this file manually\n\n") for command in next_boot: f.write(f"{command}\n") - f.write("cat <<2EOF > ~/.local/share/applications/org.vanillaos.FirstSetup.desktop\n") + f.write( + "cat <<2EOF > ~/.local/share/applications/org.vanillaos.FirstSetup.desktop\n" + ) f.write("[Desktop Entry]\n") f.write("Name=FirstSetup\n") f.write("Comment=FirstSetup\n") @@ -83,12 +87,15 @@ def get_setup_commands(log_path, pre_run, post_run, commands): f.write("2EOF\n") f.write("EOF\n") - f.write("chmod +x "+next_boot_script_path+"\n") - f.write("cat < "+next_boot_autostart_path+"\n") + f.write("chmod +x " + next_boot_script_path + "\n") + f.write("cat < " + next_boot_autostart_path + "\n") f.write("[Desktop Entry]\n") f.write("Name=FirstSetup Next Boot\n") f.write("Comment=Run FirstSetup commands at the next boot\n") - f.write("Exec=vanilla-first-setup --run-post-script 'sh %s'\n" % next_boot_script_path) + f.write( + "Exec=vanilla-first-setup --run-post-script 'sh %s'\n" + % next_boot_script_path + ) f.write("Terminal=false\n") f.write("Type=Application\n") f.write("X-GNOME-Autostart-enabled=true\n") @@ -136,7 +143,9 @@ def hide_first_setup(user: str = None): if user is None: user = os.environ.get("USER") - autostart_file = "/home/%s/.config/autostart/org.vanillaos.FirstSetup.desktop" % user + autostart_file = ( + "/home/%s/.config/autostart/org.vanillaos.FirstSetup.desktop" % user + ) if os.path.exists(autostart_file): os.remove(autostart_file) diff --git a/vanilla_first_setup/utils/recipe.py b/vanilla_first_setup/utils/recipe.py index 3bfa1080..873b2f87 100644 --- a/vanilla_first_setup/utils/recipe.py +++ b/vanilla_first_setup/utils/recipe.py @@ -24,7 +24,6 @@ class RecipeLoader: - recipe_path = "/usr/share/org.vanillaos.FirstSetup/recipe.json" def __init__(self): @@ -36,7 +35,7 @@ def __load(self): self.recipe_path = os.environ["VANILLA_CUSTOM_RECIPE"] logger.info(f"Loading recipe from {self.recipe_path}") - + if os.path.exists(self.recipe_path): with open(self.recipe_path, "r") as f: self.__recipe = json.load(f) diff --git a/vanilla_first_setup/utils/run_async.py b/vanilla_first_setup/utils/run_async.py index 18a174b9..6e3cf6c4 100644 --- a/vanilla_first_setup/utils/run_async.py +++ b/vanilla_first_setup/utils/run_async.py @@ -35,13 +35,13 @@ class RunAsync(threading.Thread): def __init__(self, task_func, callback=None, *args, **kwargs): if "DEBUG_MODE" in os.environ: import faulthandler + faulthandler.enable() self.source_id = None assert threading.current_thread() is threading.main_thread() - super(RunAsync, self).__init__( - target=self.__target, args=args, kwargs=kwargs) + super(RunAsync, self).__init__(target=self.__target, args=args, kwargs=kwargs) self.task_func = task_func @@ -59,13 +59,14 @@ def __target(self, *args, **kwargs): try: result = self.task_func(*args, **kwargs) except Exception as exception: - logger.error("Error while running async job: " - f"{self.task_func}\nException: {exception}") + logger.error( + "Error while running async job: " + f"{self.task_func}\nException: {exception}" + ) error = exception _ex_type, _ex_value, trace = sys.exc_info() traceback.print_tb(trace) - traceback_info = '\n'.join(traceback.format_tb(trace)) self.source_id = GLib.idle_add(self.callback, result, error) return self.source_id diff --git a/vanilla_first_setup/vanilla-first-setup.gresource.xml b/vanilla_first_setup/vanilla-first-setup.gresource.xml index 88e4b46c..1ae951e4 100644 --- a/vanilla_first_setup/vanilla-first-setup.gresource.xml +++ b/vanilla_first_setup/vanilla-first-setup.gresource.xml @@ -14,12 +14,15 @@ gtk/default-welcome.ui gtk/default-user.ui gtk/default-hostname.ui + gtk/default-network.ui + gtk/wireless-row.ui gtk/layout-preferences.ui gtk/layout-yes-no.ui gtk/layout-applications.ui + ../data/icons/hicolor/symbolic/actions/background-app-ghost-symbolic.svg ../data/icons/hicolor/symbolic/actions/vanilla-package-symbolic.svg ../data/icons/hicolor/symbolic/actions/vanilla-container-terminal-symbolic.svg ../data/icons/hicolor/symbolic/actions/vanilla-puzzle-piece-symbolic.svg diff --git a/vanilla_first_setup/views/done.py b/vanilla_first_setup/views/done.py index 8d8d329c..8141b7a7 100644 --- a/vanilla_first_setup/views/done.py +++ b/vanilla_first_setup/views/done.py @@ -14,14 +14,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import subprocess -from sys import intern +from gettext import gettext as _ from gi.repository import Gtk, Adw +import subprocess + -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/done.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/done.ui") class VanillaDone(Adw.Bin): - __gtype_name__ = 'VanillaDone' + __gtype_name__ = "VanillaDone" status_page = Gtk.Template.Child() btn_reboot = Gtk.Template.Child() @@ -70,7 +71,9 @@ def set_result(self, result, terminal=None): if not result: self.status_page.set_icon_name("dialog-error-symbolic") self.status_page.set_title(_("Something went wrong")) - self.status_page.set_description(_("Please contact the distribution developers.")) + self.status_page.set_description( + _("Please contact the distribution developers.") + ) if len(out) > 0: self.log_output.set_text(out) self.log_box.set_visible(True) @@ -78,7 +81,7 @@ def set_result(self, result, terminal=None): self.btn_close.set_visible(True) def __on_reboot_clicked(self, button): - subprocess.run(['gnome-session-quit', '--reboot']) + subprocess.run(["gnome-session-quit", "--reboot"]) def __on_close_clicked(self, button): self.__window.close() diff --git a/vanilla_first_setup/views/post_script.py b/vanilla_first_setup/views/post_script.py index f13e5832..8756fee5 100644 --- a/vanilla_first_setup/views/post_script.py +++ b/vanilla_first_setup/views/post_script.py @@ -14,15 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import time from gi.repository import Gtk, Gio, Gdk, GLib, Adw, Vte, Pango -from vanilla_first_setup.utils.run_async import RunAsync - -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/post-script.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/post-script.ui") class VanillaPostScript(Adw.Bin): - __gtype_name__ = 'VanillaPostScript' + __gtype_name__ = "VanillaPostScript" console_output = Gtk.Template.Child() @@ -47,9 +44,24 @@ def __build_ui(self): self.console_output.append(self.__terminal) self.__terminal.connect("child-exited", self.on_vte_child_exited) - palette = ["#353535", "#c01c28", "#26a269", "#a2734c", "#12488b", "#a347ba", "#2aa1b3", - "#cfcfcf", "#5d5d5d", "#f66151", "#33d17a", "#e9ad0c", "#2a7bde", "#c061cb", - "#33c7de", "#ffffff"] + palette = [ + "#353535", + "#c01c28", + "#26a269", + "#a2734c", + "#12488b", + "#a347ba", + "#2aa1b3", + "#cfcfcf", + "#5d5d5d", + "#f66151", + "#33d17a", + "#e9ad0c", + "#2a7bde", + "#c061cb", + "#33c7de", + "#ffffff", + ] FOREGROUND = palette[0] BACKGROUND = palette[15] @@ -61,11 +73,11 @@ def __build_ui(self): self.colors = [Gdk.RGBA() for c in palette] [color.parse(s) for (color, s) in zip(self.colors, palette)] - desktop_schema = Gio.Settings.new('org.gnome.desktop.interface') - if desktop_schema.get_enum('color-scheme') == 0: + desktop_schema = Gio.Settings.new("org.gnome.desktop.interface") + if desktop_schema.get_enum("color-scheme") == 0: self.fg.parse(FOREGROUND) self.bg.parse(BACKGROUND) - elif desktop_schema.get_enum('color-scheme') == 1: + elif desktop_schema.get_enum("color-scheme") == 1: self.fg.parse(FOREGROUND_DARK) self.bg.parse(BACKGROUND_DARK) self.__terminal.set_colors(self.fg, self.bg, self.colors) diff --git a/vanilla_first_setup/views/progress.py b/vanilla_first_setup/views/progress.py index db36f690..feecdf02 100644 --- a/vanilla_first_setup/views/progress.py +++ b/vanilla_first_setup/views/progress.py @@ -15,16 +15,16 @@ # along with this program. If not, see . import time -from gi.repository import Gtk, Gdk, Gio, GLib, Adw, Vte, Pango +from gi.repository import Gtk, Gdk, Gio, GLib, Vte, Pango from vanilla_first_setup.utils.run_async import RunAsync from vanilla_first_setup.views.tour import VanillaTour -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/progress.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/progress.ui") class VanillaProgress(Gtk.Box): - __gtype_name__ = 'VanillaProgress' + __gtype_name__ = "VanillaProgress" carousel_tour = Gtk.Template.Child() tour_button = Gtk.Template.Child() @@ -37,7 +37,7 @@ class VanillaProgress(Gtk.Box): def __init__(self, window, tour: dict, **kwargs): super().__init__(**kwargs) self.__window = window - self.__tour = tour + self.__tour = tour self.__success_fn = None self.__terminal = Vte.Terminal() self.__font = Pango.FontDescription() @@ -70,8 +70,24 @@ def __build_ui(self): self.console_output.append(self.__terminal) self.__terminal.connect("child-exited", self.on_vte_child_exited) - palette = ["#353535", "#c01c28", "#26a269", "#a2734c", "#12488b", "#a347ba", "#2aa1b3", "#cfcfcf", "#5d5d5d", "#f66151", "#33d17a", "#e9ad0c", "#2a7bde", "#c061cb", "#33c7de", "#ffffff"] - + palette = [ + "#353535", + "#c01c28", + "#26a269", + "#a2734c", + "#12488b", + "#a347ba", + "#2aa1b3", + "#cfcfcf", + "#5d5d5d", + "#f66151", + "#33d17a", + "#e9ad0c", + "#2a7bde", + "#c061cb", + "#33c7de", + "#ffffff", + ] FOREGROUND = palette[0] BACKGROUND = palette[15] FOREGROUND_DARK = palette[15] @@ -82,11 +98,11 @@ def __build_ui(self): self.colors = [Gdk.RGBA() for c in palette] [color.parse(s) for (color, s) in zip(self.colors, palette)] - desktop_schema = Gio.Settings.new('org.gnome.desktop.interface') - if desktop_schema.get_enum('color-scheme') == 0: + desktop_schema = Gio.Settings.new("org.gnome.desktop.interface") + if desktop_schema.get_enum("color-scheme") == 0: self.fg.parse(FOREGROUND) self.bg.parse(BACKGROUND) - elif desktop_schema.get_enum('color-scheme') == 1: + elif desktop_schema.get_enum("color-scheme") == 1: self.fg.parse(FOREGROUND_DARK) self.bg.parse(BACKGROUND_DARK) self.__terminal.set_colors(self.fg, self.bg, self.colors) @@ -97,11 +113,11 @@ def __build_ui(self): self.__start_tour() def __switch_tour(self, *args): - cur_index = self.carousel_tour.get_position() - page = self.carousel_tour.get_nth_page(cur_index + 1) + cur_index = self.carousel_tour.get_position() + 1 + if cur_index == self.carousel_tour.get_n_pages(): + cur_index = 0 - if page is None: - page = self.carousel_tour.get_nth_page(0) + page = self.carousel_tour.get_nth_page(cur_index) self.carousel_tour.scroll_to(page, True) @@ -137,5 +153,5 @@ def start(self, setup_commands, success_fn, *fn_args): None, -1, None, - None + None, ) diff --git a/vanilla_first_setup/views/tour.py b/vanilla_first_setup/views/tour.py index c085a62b..8aad4471 100644 --- a/vanilla_first_setup/views/tour.py +++ b/vanilla_first_setup/views/tour.py @@ -18,9 +18,9 @@ from gi.repository import Gtk, Adw -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/tour.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/tour.ui") class VanillaTour(Adw.Bin): - __gtype_name__ = 'VanillaTour' + __gtype_name__ = "VanillaTour" status_page = Gtk.Template.Child() btn_read_more = Gtk.Template.Child() @@ -38,8 +38,8 @@ def __build_ui(self): self.status_page.set_icon_name(self.__tour["icon"]) self.status_page.set_title(self.__tour["title"]) self.status_page.set_description(self.__tour["description"]) - + self.btn_read_more.set_visible(bool("read_more_link" in self.__tour)) - + def __on_read_more(self, e): webbrowser.open_new_tab(self.__tour["read_more_link"]) diff --git a/vanilla_first_setup/window.py b/vanilla_first_setup/window.py index 9ac58b08..2b5af354 100644 --- a/vanilla_first_setup/window.py +++ b/vanilla_first_setup/window.py @@ -14,23 +14,23 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import time -import contextlib +from gettext import gettext as _ from gi.repository import Gtk, GObject, Adw +import contextlib + from vanilla_first_setup.utils.builder import Builder from vanilla_first_setup.utils.parser import Parser from vanilla_first_setup.utils.processor import Processor -from vanilla_first_setup.utils.run_async import RunAsync from vanilla_first_setup.views.progress import VanillaProgress from vanilla_first_setup.views.done import VanillaDone from vanilla_first_setup.views.post_script import VanillaPostScript -@Gtk.Template(resource_path='/org/vanillaos/FirstSetup/gtk/window.ui') +@Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/window.ui") class VanillaWindow(Adw.ApplicationWindow): - __gtype_name__ = 'VanillaWindow' + __gtype_name__ = "VanillaWindow" __gsignals__ = { "page-changed": (GObject.SignalFlags.RUN_FIRST, None, (str,)), } @@ -64,10 +64,12 @@ def __init__(self, post_script: str, user: str, new_user: bool = False, **kwargs # system views self.__view_done = VanillaDone( - self, reboot=False, title=_("Done!"), + self, + reboot=False, + title=_("Done!"), description=_("Your device is ready to use."), fail_title=_("Error!"), - fail_description=_("Something went wrong.") + fail_description=_("Something went wrong."), ) # this builds the UI for the post script only @@ -140,7 +142,7 @@ def __on_page_changed(self, *args): self.emit("page-changed", page.step_id) if page not in pages_check: - self.btn_back.set_visible(cur_index > 1.0) + self.btn_back.set_visible(cur_index not in [0.0, 2.0]) self.carousel_indicator_dots.set_visible(cur_index != 0.0) self.headerbar.set_show_end_title_buttons(cur_index != 0.0) return @@ -172,7 +174,7 @@ def __on_page_changed(self, *args): self.recipe.get("log_file", "/tmp/vanilla_first_setup.log"), self.recipe.get("pre_run", []), self.recipe.get("post_run"), - commands + commands, ) self.__view_progress.start(res, Processor.hide_first_setup, self.__user) @@ -181,7 +183,14 @@ def set_installation_result(self, result, terminal): self.__view_done.set_result(result, terminal) self.next() - def next(self, widget: Gtk.Widget = None, result: bool = None, rebuild: bool = False, mode: int = 0, *args): + def next( + self, + widget: Gtk.Widget = None, + result: bool = None, + rebuild: bool = False, + mode: int = 0, + *args + ): if rebuild: self.rebuild_ui(mode)