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 @@
+
+
+
+
+ true
+ true
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ false
+ false
+ 10
+
+
+ network-wireless-no-route-symbolic
+
+
+
+
+ 2
+ 1
+ 10
+ 12
+ network-wireless-encrypted-symbolic
+ 10
+ false
+
+
+
+
+
+
+
+ Connected
+ false
+
+
+
+
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)