diff --git a/.gitignore b/.gitignore index 053b721..492b1ef 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ debian/vanilla-first-setup.debhelper.log debian/vanilla-first-setup debian/.debhelper */__pycache__ -*.pyc \ No newline at end of file +*.pyc +/localbuild \ No newline at end of file diff --git a/recipe.json b/recipe.json index 70d7c29..b3961cb 100644 --- a/recipe.json +++ b/recipe.json @@ -38,11 +38,10 @@ "description": "Choose one or more package managers to install", "without_selection": { "allowed": true, - "message": "You have chosen not to install any package manager, you will only be able to install packages using the package manager (apx).\n\nGNOME Software will be disabled since it is not compatible with the On-Demand Immutability.", + "message": "You have chosen not to install any package manager, you will only be able to install packages using the package manager (apx).\n\nGNOME Software will be disabled.", "title": "No package manager selected", "final": [ { - "if": "warp::immutability", "type": "command", "commands": [ "apt remove -y gnome-software" @@ -75,7 +74,7 @@ "type": "command", "commands": [ "apt install -y flatpak gnome-software-plugin-flatpak", - "!outRun echo '[Desktop Entry]\nType=Application\nExec=kgx -e bash -c \"echo 'Configuring Flathub..' && flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo && rm -f ~/.config/autostart/flathub.desktop && echo 'Flathub installed successfully' || echo 'Failed installing Flathub. Are you connected to the internet?'\"\nName=Flatpak Flathub\nComment=Add Flathub repository to Flatpak\n' > ~/.config/autostart/flathub.desktop" + "!nextBoot flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo" ] }, { @@ -90,6 +89,231 @@ } ] }, + "apps": { + "template": "applications", + "icon": "org.gnome.Software-symbolic", + "title": "Applications", + "description": "Choose which applications to install.", + "bundles": [ + { + "id": "essential-apps", + "title": "Essential Applications", + "subtitle": "Core GNOME apps like Calendar or Calculator.", + "default": true, + "applications" : [ + { + "name" : "Calculator", + "icon" : "org.gnome.Calculator", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Calendar", + "icon" : "org.gnome.Calendar", + "flatpak" : true + }, + { + "name" : "Characters", + "icon" : "org.gnome.Characters", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Cheese", + "icon" : "org.gnome.Cheese", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Clocks", + "icon" : "org.gnome.clocks", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Contacts", + "icon" : "org.gnome.Contacts", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Disk Usage Analyzer", + "icon" : "org.gnome.baobab", + "flatpak" : true + }, + { + "name" : "Document Viewer", + "icon" : "org.gnome.Evince", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Extensions", + "icon" : "org.gnome.Extensions", + "flatpak" : true + }, + { + "name" : "Fonts", + "icon" : "org.gnome.font-viewer", + "flatpak" : true + }, + { + "name" : "Image Viewer", + "icon" : "org.gnome.eog", + "flatpak" : true, + "snap" : true + }, + { + "name" : "Logs", + "icon" : "org.gnome.Logs", + "flatpak" : "org.gnome.Logs", + "snap" : true + }, + { + "name" : "Maps", + "icon" : "org.gnome.Maps", + "flatpak" : true + }, + { + "name" : "Music", + "icon" : "org.gnome.Music", + "flatpak" : true + }, + { + "name" : "Photos", + "icon" : "org.gnome.Photos", + "flatpak" : true + }, + { + "name" : "Text Editor", + "icon" : "org.gnome.TextEditor", + "flatpak" : true + }, + { + "name" : "Videos", + "icon" : "org.gnome.Totem", + "flatpak" : true + }, + { + "name" : "Weather", + "icon" : "org.gnome.Weather", + "flatpak" : true, + "snap" : true + } + ] + }, + { + "id": "utilities", + "title": "Common Utilities", + "subtitle": "Useful utilities like Bottles.", + "default": true, + "applications" : [ + { + "name" : "Bottles", + "icon" : "com.usebottles.bottles", + "flatpak" : "com.usebottles.bottles" + } + ] + } + ], + "final": [ + { + "if" : "Calculator", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Calculator || snap install gnome-calculator"] + }, + { + "if" : "Calendar", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Calendar"] + }, + { + "if" : "Characters", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Characters || snap install gnome-characters"] + }, + { + "if" : "Cheese", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Cheese || snap install cheese"] + }, + { + "if" : "Clocks", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.clocks || snap install gnome-clocks"] + }, + { + "if" : "Contacts", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Contacts || snap install gnome-contacts"] + }, + { + "if" : "Disk Usage Analyzer", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.baobab"] + }, + { + "if" : "Document Viewer", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Evince || snap install evince"] + }, + { + "if" : "Extensions", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Extensions"] + }, + { + "if" : "Fonts", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Fonts"] + }, + { + "if" : "Image Viewer", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.eog || snap install eog"] + }, + { + "if" : "Logs", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Logs || snap install gnome-logs"] + }, + { + "if" : "Maps", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Maps"] + }, + { + "if" : "Music", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Music"] + }, + { + "if" : "Photos", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Photos"] + }, + { + "if" : "Text Editor", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.TextEditor"] + }, + { + "if" : "Videos", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Totem"] + }, + { + "if" : "Weather", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y org.gnome.Weather || snap install gnome-weather"] + }, + { + "if" : "Bottles", + "type" : "command", + "commands" : ["!nextBoot flatpak install -y com.usebottles.bottles"] + } + ] + }, "nvidia": { "template": "yes-no", "display-conditions": [ diff --git a/vanilla_first_setup/defaults/applications.py b/vanilla_first_setup/defaults/applications.py new file mode 100644 index 0000000..1b2a73e --- /dev/null +++ b/vanilla_first_setup/defaults/applications.py @@ -0,0 +1,181 @@ +# applications.py +# +# Copyright 2022 mirkobrombin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundationat version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gi.repository import Gtk, Adw + +from vanilla_first_setup.dialog import VanillaDialog + + +@Gtk.Template(resource_path='/io/github/vanilla-os/FirstSetup/gtk/layout-applications.ui') +class VanillaLayoutApplications(Adw.Bin): + __gtype_name__ = 'VanillaLayoutApplications' + + status_page = Gtk.Template.Child() + bundles_list = 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.__register_widgets = [] + self.__build_ui() + + # signals + self.btn_next.connect("clicked", self.__next_step) + + @property + def step_id(self): + return self.__key + + def __build_ui(self): + self.status_page.set_icon_name(self.__step["icon"]) + self.status_page.set_title(self.__step["title"]) + self.status_page.set_description(self.__step["description"]) + selection_dialogs = [] + _index = 0 + + def present_customize(widget, dialog, apps_list, item): + for app in item["applications"]: + try: + apps_list.remove(app["apps_action_row"]) + except KeyError: + pass + if self.__window.builder.get_temp_finals("packages")["vars"]["flatpak"] == True: + package_manager = "flatpak" + elif self.__window.builder.get_temp_finals("packages")["vars"]["snap"] == True: + try: + package_manager = "snap" + except KeyError: + continue + else: + continue + try: + if app[package_manager]: + _apps_action_row = Adw.ActionRow( + title=app["name"], + icon_name=app["icon"] + ) + _app_switcher = Gtk.Switch() + _app_switcher.set_active(True) + _app_switcher.set_valign(Gtk.Align.CENTER) + _apps_action_row.add_suffix(_app_switcher) + apps_list.add(_apps_action_row) + app["apps_action_row"] = _apps_action_row + app["switch"] = _app_switcher + try: + app["switch"].set_active(app["active"]) + except KeyError: + pass + except KeyError: + continue + dialog.show() + + def close_customize(widget, dialog): + dialog.hide() + + def apply_preferences(widget, dialog, apps_list, item): + for app in item["applications"]: + app["active"] = app["switch"].get_active() + dialog.hide() + + for item in self.__step["bundles"]: + _selection_dialog = VanillaDialog( + self.__window, + "Select Applications", + "Description", + ) + + _cancel_button = Gtk.Button() + _apply_button = Gtk.Button() + _cancel_button.set_label("Cancel") + _apply_button.set_label("Apply") + _apply_button.add_css_class("suggested-action") + + _header_bar = Adw.HeaderBar() + _header_bar.pack_start(_cancel_button) + _header_bar.pack_end(_apply_button) + _header_bar.set_show_end_title_buttons(False) + _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_page = Adw.PreferencesPage() + _apps_page.add(_apps_list) + + _box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + _box.append(_header_bar) + _box.append(_apps_page) + + _selection_dialog.set_content(_box) + _selection_dialog.set_default_size(500, 600) + selection_dialogs.append(_selection_dialog) + + _action_row = Adw.ActionRow( + title=item["title"], + subtitle=item.get("subtitle", "") + ) + _switcher = Gtk.Switch() + _switcher.set_active(item.get("default", False)) + _switcher.set_valign(Gtk.Align.CENTER) + _action_row.add_suffix(_switcher) + + _customize = Gtk.Button() + _customize.set_icon_name("go-next-symbolic") + _customize.set_valign(Gtk.Align.CENTER) + _customize.add_css_class("flat") + _action_row.add_suffix(_customize) + + _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) + + self.bundles_list.add(_action_row) + + self.__register_widgets.append((item["id"], _switcher, _index)) + _index += 1 + + def __next_step(self, widget): + self.__window.next() + + def get_finals(self): + finals = {"vars": {}, "funcs": [x for x in self.__step["final"]]} + + if self.__window.builder.get_temp_finals("packages")["vars"]["flatpak"] == True: + package_manager = "flatpak" + elif self.__window.builder.get_temp_finals("packages")["vars"]["snap"] == True: + try: + package_manager = "snap" + except KeyError: + package_manager = None + else: + package_manager = None + + for _id, switcher, index in self.__register_widgets: + if switcher.get_active() == True: + for app in self.__step["bundles"][index]["applications"]: + if package_manager not in app.keys(): + app["active"] = False + if "active" not in app.keys(): + app["active"] = True + finals["vars"][app["name"]] = app["active"] + else: + for app in self.__step["bundles"][index]["applications"]: + finals["vars"][app["name"]] = False + + return finals \ No newline at end of file diff --git a/vanilla_first_setup/defaults/meson.build b/vanilla_first_setup/defaults/meson.build index 569a130..3552e4e 100644 --- a/vanilla_first_setup/defaults/meson.build +++ b/vanilla_first_setup/defaults/meson.build @@ -5,6 +5,7 @@ sources = [ '__init__.py', 'welcome.py', 'theme.py', + 'applications.py' ] -install_data(sources, install_dir: defaultsdir) \ No newline at end of file +install_data(sources, install_dir: defaultsdir) diff --git a/vanilla_first_setup/defaults/theme.py b/vanilla_first_setup/defaults/theme.py index 0423474..38f9160 100644 --- a/vanilla_first_setup/defaults/theme.py +++ b/vanilla_first_setup/defaults/theme.py @@ -14,8 +14,7 @@ # 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, Gio @Gtk.Template(resource_path='/io/github/vanilla-os/FirstSetup/gtk/default-theme.ui') @@ -38,6 +37,10 @@ class VanillaDefaultTheme(Gtk.Box): self.btn_next.connect("clicked", self.__window.next) self.btn_default.connect('toggled', self.__set_theme, "light") self.btn_dark.connect('toggled', self.__set_theme, "dark") + + @property + def step_id(self): + return self.__key def __build_ui(self): self.btn_dark.set_group(self.btn_default) diff --git a/vanilla_first_setup/defaults/welcome.py b/vanilla_first_setup/defaults/welcome.py index a1217fe..881f156 100644 --- a/vanilla_first_setup/defaults/welcome.py +++ b/vanilla_first_setup/defaults/welcome.py @@ -15,7 +15,7 @@ # along with this program. If not, see . import time -from gi.repository import Gtk, Gio, GLib, Adw +from gi.repository import Gtk, GLib, Adw from vanilla_first_setup.utils.run_async import RunAsync @@ -79,6 +79,10 @@ class VanillaDefaultWelcome(Adw.Bin): # set distro logo self.status_page.set_icon_name(self.__distro_info["logo"]) + + @property + def step_id(self): + return self.__key def __start_welcome_animation(self): def change_langs(): diff --git a/vanilla_first_setup/dialog.py b/vanilla_first_setup/dialog.py index 660052a..d0e1e31 100644 --- a/vanilla_first_setup/dialog.py +++ b/vanilla_first_setup/dialog.py @@ -28,3 +28,10 @@ class VanillaDialog(Adw.Window): self.set_transient_for(window) self.set_title(title) self.label_text.set_text(text) + + 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 diff --git a/vanilla_first_setup/gtk/layout-applications.ui b/vanilla_first_setup/gtk/layout-applications.ui new file mode 100644 index 0000000..152a07d --- /dev/null +++ b/vanilla_first_setup/gtk/layout-applications.ui @@ -0,0 +1,41 @@ + + + + + \ No newline at end of file diff --git a/vanilla_first_setup/layouts/meson.build b/vanilla_first_setup/layouts/meson.build index 964f228..634e2ff 100644 --- a/vanilla_first_setup/layouts/meson.build +++ b/vanilla_first_setup/layouts/meson.build @@ -4,7 +4,7 @@ layoutsdir = join_paths(pkgdatadir, 'vanilla_first_setup/layouts') sources = [ '__init__.py', 'preferences.py', - 'yes_no.py', + 'yes_no.py' ] -install_data(sources, install_dir: layoutsdir) \ No newline at end of file +install_data(sources, install_dir: layoutsdir) diff --git a/vanilla_first_setup/layouts/preferences.py b/vanilla_first_setup/layouts/preferences.py index bcc43a4..dc38a0a 100644 --- a/vanilla_first_setup/layouts/preferences.py +++ b/vanilla_first_setup/layouts/preferences.py @@ -14,10 +14,8 @@ # 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 @@ -40,6 +38,10 @@ class VanillaLayoutPreferences(Adw.Bin): # signals self.btn_next.connect("clicked", self.__next_step) + + @property + def step_id(self): + return self.__key def __build_ui(self): self.status_page.set_icon_name(self.__step["icon"]) diff --git a/vanilla_first_setup/layouts/yes_no.py b/vanilla_first_setup/layouts/yes_no.py index 68aa7fe..11d7526 100644 --- a/vanilla_first_setup/layouts/yes_no.py +++ b/vanilla_first_setup/layouts/yes_no.py @@ -43,6 +43,10 @@ class VanillaLayoutYesNo(Adw.Bin): 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 def __build_ui(self): self.status_page.set_icon_name(self.__step["icon"]) diff --git a/vanilla_first_setup/utils/builder.py b/vanilla_first_setup/utils/builder.py index 1adc551..2c36fa5 100644 --- a/vanilla_first_setup/utils/builder.py +++ b/vanilla_first_setup/utils/builder.py @@ -18,9 +18,6 @@ import os import sys import logging import subprocess -import json - -from gi.repository import Gio from vanilla_first_setup.utils.recipe import RecipeLoader @@ -29,6 +26,7 @@ from vanilla_first_setup.defaults.theme import VanillaDefaultTheme from vanilla_first_setup.layouts.preferences import VanillaLayoutPreferences from vanilla_first_setup.layouts.yes_no import VanillaLayoutYesNo +from vanilla_first_setup.defaults.applications import VanillaLayoutApplications logger = logging.getLogger("FirstSetup::Builder") @@ -38,7 +36,8 @@ templates = { "welcome": VanillaDefaultWelcome, "theme": VanillaDefaultTheme, "preferences": VanillaLayoutPreferences, - "yes-no": VanillaLayoutYesNo + "yes-no": VanillaLayoutYesNo, + "applications": VanillaLayoutApplications } @@ -89,6 +88,13 @@ class Builder: if step["template"] in templates: _widget = templates[step["template"]](self.__window, self.distro_info, key, step) self.__register_widgets.append(_widget) + + def get_temp_finals(self, step_id: str): + for widget in self.__register_widgets: + if widget.step_id == step_id: + return widget.get_finals() + + return None def get_finals(self): self.__register_finals = [] diff --git a/vanilla_first_setup/utils/parser.py b/vanilla_first_setup/utils/parser.py index 211874c..7855b32 100644 --- a/vanilla_first_setup/utils/parser.py +++ b/vanilla_first_setup/utils/parser.py @@ -14,10 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import sys import logging -import json logger = logging.getLogger("FirstSetup::Parser") diff --git a/vanilla_first_setup/utils/processor.py b/vanilla_first_setup/utils/processor.py index a0956a3..268515b 100644 --- a/vanilla_first_setup/utils/processor.py +++ b/vanilla_first_setup/utils/processor.py @@ -30,11 +30,50 @@ class Processor: def run(log_path, pre_run, post_run, commands): commands = pre_run + commands + post_run out_run = "" + next_boot = [] + next_boot_script_path = os.path.expanduser("~/.local/org.vanillaos.FirstSetup.nextBoot") + next_boot_autostart_path = os.path.expanduser("~/.config/autostart/org.vanillaos.FirstSetup.nextBoot.desktop") abroot_bin = shutil.which("abroot") logger.info("processing the following commands: \n%s" % '\n'.join(commands)) + # nextBoot commands are collected in ~/.local/org.vanillaos.FirstSetup.nextBoot + # and executed at the next boot by a desktop entry + for command in commands: + if command.startswith("!nextBoot"): + next_boot.append(command.replace("!nextBoot", "")) + continue + + if len(next_boot) > 0: + with open(next_boot_script_path, "w") 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") + + for command in next_boot: + f.write(f"{command}\n") + + f.write(f"rm -f {next_boot_script_path}\n") + f.write(f"rm -f {next_boot_autostart_path}\n") + f.flush() + f.close() + + # setting the file executable + os.chmod(next_boot_script_path, 0o755) + + # creating the desktop entry + with open(next_boot_autostart_path, "w") as f: + 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=kgx -e bash -c '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") + f.flush() + f.close() + # 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: @@ -43,6 +82,9 @@ class Processor: f.write("# Do not edit this file manually\n\n") for command in commands: + if command.startswith("!nextBoot"): + continue + if command.startswith("!noSudo"): command = command.replace("!noSudo", "sudo -u $USER") diff --git a/vanilla_first_setup/utils/recipe.py b/vanilla_first_setup/utils/recipe.py index 36da611..1b510fc 100644 --- a/vanilla_first_setup/utils/recipe.py +++ b/vanilla_first_setup/utils/recipe.py @@ -19,8 +19,6 @@ import sys import logging import json -from gi.repository import Gio - logger = logging.getLogger("FirstSetup::RecipeLoader") diff --git a/vanilla_first_setup/vanilla-first-setup.gresource.xml b/vanilla_first_setup/vanilla-first-setup.gresource.xml index 0ba114c..6098082 100644 --- a/vanilla_first_setup/vanilla-first-setup.gresource.xml +++ b/vanilla_first_setup/vanilla-first-setup.gresource.xml @@ -12,6 +12,7 @@ gtk/layout-preferences.ui gtk/layout-yes-no.ui + gtk/layout-applications.ui ../data/icons/hicolor/symbolic/actions/vanilla-package-symbolic.svg diff --git a/vanilla_first_setup/window.py b/vanilla_first_setup/window.py index 8ef90ed..8c20809 100644 --- a/vanilla_first_setup/window.py +++ b/vanilla_first_setup/window.py @@ -15,7 +15,7 @@ # 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.builder import Builder from vanilla_first_setup.utils.parser import Parser @@ -52,6 +52,10 @@ class VanillaWindow(Adw.ApplicationWindow): # connect system signals self.__connect_signals() + + @property + def builder(self): + return self.__builder def __connect_signals(self): self.btn_back.connect("clicked", self.back)