├── debian ├── compat ├── source │ └── format ├── vanilla-first-setup.install ├── debhelper-build-stamp ├── vanilla-first-setup.substvars ├── rules ├── copyright ├── control └── changelog ├── vanilla_first_setup ├── __init__.py ├── core │ ├── __init__.py │ ├── meson.build │ ├── applications.py │ ├── languages.py │ ├── keyboard.py │ └── backend.py ├── views │ ├── __init__.py │ ├── meson.build │ ├── language.py │ ├── keyboard.py │ ├── welcome_user.py │ ├── done.py │ ├── timezone.py │ ├── hostname.py │ ├── theme.py │ ├── logout.py │ ├── conn_check.py │ ├── welcome.py │ ├── progress.py │ ├── user.py │ └── applications.py ├── assets │ ├── background_replacement.png │ ├── symbolic │ │ ├── actions │ │ │ ├── computer-symbolic.svg │ │ │ ├── go-next-symbolic.svg │ │ │ ├── go-previous-symbolic.svg │ │ │ ├── dialog-warning-symbolic.svg │ │ │ ├── emblem-default-symbolic.svg │ │ │ ├── preferences-system-time-symbolic.svg │ │ │ ├── network-wired-acquiring-symbolic.svg │ │ │ ├── network-wired-disconnected-symbolic.svg │ │ │ ├── preferences-desktop-locale-symbolic.svg │ │ │ └── input-keyboard-symbolic.svg │ │ └── apps │ │ │ └── org.gnome.Software-symbolic.svg │ └── theme-default.svg ├── scripts │ ├── logout │ ├── flatpak │ ├── hostname │ ├── open-network-settings │ ├── open-accessibility-settings │ ├── setup-system │ ├── locale │ ├── remove-autostart-file │ ├── setup-flatpak-remote │ ├── user │ ├── disable-lockscreen │ ├── timezone │ ├── keyboard │ ├── live-keyboard │ ├── theme │ ├── meson.build │ └── remove-first-setup-user ├── gtk │ ├── language.ui │ ├── widget-keyboard.ui │ ├── widget-location-list-page.ui │ ├── widget-timezone-list-page.ui │ ├── progress.ui │ ├── conn-check.ui │ ├── keyboard.ui │ ├── location.ui │ ├── dialog.ui │ ├── applications-dialog.ui │ ├── hostname.ui │ ├── timezone.ui │ ├── welcome-user.ui │ ├── logout.ui │ ├── user.ui │ ├── welcome.ui │ ├── done.ui │ ├── window.ui │ ├── layout-applications.ui │ └── theme.ui ├── style.css ├── vanilla-first-setup.in ├── meson.build ├── dialog.py ├── main.py ├── vanilla-first-setup.gresource.xml ├── apps.json ├── application.py └── window.py ├── .gitattributes ├── po ├── meson.build ├── LINGUAS └── POTFILES ├── data ├── screenshots │ └── welcome-page.png ├── org.vanillaos.FirstSetup.desktop.in ├── remove-first-setup-user.service ├── firstsetup-session-mode.json ├── org.vanillaos.FirstSetup.rules ├── firstsetup-session.desktop ├── remove-first-setup-user ├── icons │ ├── meson.build │ └── hicolor │ │ ├── symbolic │ │ └── apps │ │ │ └── org.vanillaos.FirstSetup-symbolic.svg │ │ └── scalable │ │ └── apps │ │ ├── org.vanillaos.FirstSetup.svg │ │ └── org.vanillaos.FirstSetup-flower.svg ├── meson.build └── org.vanillaos.FirstSetup.policy ├── .gitignore ├── meson.build ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── test.py └── README.md /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /vanilla_first_setup/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/vanilla-first-setup.install: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vanilla_first_setup/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/debhelper-build-stamp: -------------------------------------------------------------------------------- 1 | vanilla-first-setup 2 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('vanilla-first-setup', preset: 'glib') -------------------------------------------------------------------------------- /debian/vanilla-first-setup.substvars: -------------------------------------------------------------------------------- 1 | misc:Depends= 2 | misc:Pre-Depends= 3 | -------------------------------------------------------------------------------- /data/screenshots/welcome-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vanilla-OS/first-setup/HEAD/data/screenshots/welcome-page.png -------------------------------------------------------------------------------- /vanilla_first_setup/assets/background_replacement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vanilla-OS/first-setup/HEAD/vanilla_first_setup/assets/background_replacement.png -------------------------------------------------------------------------------- /data/org.vanillaos.FirstSetup.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Vanilla OS First Setup 3 | Exec=vanilla-first-setup 4 | Icon=org.vanillaos.FirstSetup 5 | Terminal=false 6 | Type=Application 7 | Categories=System;GTK; 8 | StartupNotify=true 9 | NoDisplay=true 10 | -------------------------------------------------------------------------------- /data/remove-first-setup-user.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Remove the first-setup user from the system 3 | 4 | [Service] 5 | ExecStart=/usr/libexec/remove-first-setup-user 6 | Type=oneshot 7 | Restart=on-failure 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | mesonbuild/ 3 | build/ 4 | debian/files 5 | debian/vanilla-first-setup.debhelper.log 6 | debian/vanilla-first-setup 7 | debian/.debhelper 8 | */__pycache__ 9 | *.pyc 10 | vanilla_first_setup/vanilla-first-setup.gresource 11 | .buildconfig 12 | install/ 13 | localegen/ 14 | -------------------------------------------------------------------------------- /data/firstsetup-session-mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "parentMode": "user", 3 | "hasOverview": false, 4 | "showWelcomeDialog": false, 5 | "panel": { "left": [], 6 | "center": ["dateMenu"], 7 | "right": ["a11y", "keyboard", "quickSettings"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/logout: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "logout" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | loginctl terminate-user "$USER" 14 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/flatpak: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "flatpak " 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | flatpak install --user -y "$1" 14 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/hostname: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "hostname " 5 | exit 5 6 | fi 7 | 8 | if ! [ "$UID" == "0" ]; then 9 | echo "this script must be run with super user privileges" 10 | exit 6 11 | fi 12 | 13 | echo "$1" > /etc/hostname 14 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/open-network-settings: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "open-network-settings" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | gnome-control-center wifi 14 | -------------------------------------------------------------------------------- /data/org.vanillaos.FirstSetup.rules: -------------------------------------------------------------------------------- 1 | polkit.addRule(function(action, subject) { 2 | if (action.id.startsWith("org.vanillaos.FirstSetup.scripts") && subject.isInGroup("vanilla-first-setup")) { 3 | polkit.log("action=" + action); 4 | polkit.log("subject=" + subject); 5 | return polkit.Result.YES; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/open-accessibility-settings: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "open-accessibility-settings" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | gnome-control-center universal-access 14 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/setup-system: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "setup-system" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | vso pico-init 14 | vso run echo vso subsystem set up successfully 15 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/locale: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "locale " 5 | exit 5 6 | fi 7 | 8 | if ! [ "$UID" == "0" ]; then 9 | echo "this script must be run with super user privileges" 10 | exit 6 11 | fi 12 | 13 | echo "LC_ALL=$1" > /etc/locale.conf 14 | echo "LANG=$1" >> /etc/locale.conf 15 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/remove-autostart-file: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "remove-autostart-file" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | rm ~/.config/autostart/org.vanillaos.FirstSetup.autostart.desktop 14 | -------------------------------------------------------------------------------- /data/firstsetup-session.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=FirstSetup 3 | Comment=This session provides a First Setup interface 4 | Exec=env GNOME_SHELL_SESSION_MODE=firstsetup /usr/bin/gnome-session 5 | TryExec=/usr/bin/gnome-session 6 | Type=Application 7 | DesktopNames=vanilla:GNOME 8 | X-GDM-SessionRegisters=true 9 | X-Ubuntu-Gettext-Domain=gnome-session-3.0 10 | NoDisplay=true 11 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/setup-flatpak-remote: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "setup-flatpak-remote" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo 14 | -------------------------------------------------------------------------------- /vanilla_first_setup/core/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | coredir = join_paths(pkgdatadir, 'vanilla_first_setup/core') 3 | 4 | sources = [ 5 | '__init__.py', 6 | 'applications.py', 7 | 'backend.py', 8 | 'keyboard.py', 9 | 'languages.py', 10 | 'timezones.py', 11 | ] 12 | 13 | install_data(sources, install_dir: coredir) -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | ar 2 | bg 3 | ca 4 | cs 5 | da 6 | de 7 | el 8 | en_GB 9 | eo 10 | es 11 | et 12 | fa 13 | fr 14 | fr_CA 15 | gl 16 | he 17 | hi 18 | hu 19 | id 20 | it 21 | ja 22 | ka 23 | ko 24 | lt 25 | ml 26 | ms 27 | nb_NO 28 | ne 29 | nl 30 | oc 31 | pl 32 | pt 33 | pt_BR 34 | ro 35 | ru 36 | sk 37 | sq 38 | sv 39 | ta 40 | th 41 | tl 42 | tr 43 | uk 44 | ur 45 | vi 46 | zh_Hans 47 | zh_Hant 48 | be 49 | fi 50 | ga 51 | kab 52 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/user: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ] || [ -z "$2" ]; then 3 | echo "usage:" 4 | echo "user " 5 | exit 5 6 | fi 7 | 8 | if ! [ "$UID" == "0" ]; then 9 | echo "this script must be run with super user privileges" 10 | exit 6 11 | fi 12 | 13 | adduser --quiet --disabled-password --shell /bin/bash --gecos "$2" "$1" 14 | passwd --stdin "$1" 15 | usermod -a -G sudo,adm,lpadmin "$1" 16 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/disable-lockscreen: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "disable-lockscreen" 5 | exit 5 6 | fi 7 | 8 | if [ "$UID" == "0" ]; then 9 | echo "this script must be run as a regular user" 10 | exit 7 11 | fi 12 | 13 | /usr/bin/gsettings set "org.gnome.desktop.lockdown" "disable-lock-screen" "true" 14 | /usr/bin/gsettings set "org.gnome.desktop.screensaver" "lock-enabled" "false" 15 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/timezone: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "timezone " 5 | exit 5 6 | fi 7 | 8 | if ! [ "$UID" == "0" ]; then 9 | echo "this script must be run with super user privileges" 10 | exit 6 11 | fi 12 | 13 | echo "$1" > /etc/timezone 14 | CODE_ONE="$?" 15 | rm /etc/localtime 16 | ln -sf "/usr/share/zoneinfo/$1" /etc/localtime 17 | CODE_TWO="$?" 18 | 19 | exit $(($CODE_ONE + $CODE_TWO)) 20 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/keyboard: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ] || [ -z "$2" ]; then 3 | echo "usage:" 4 | echo "keyboard [variant]" 5 | exit 5 6 | fi 7 | 8 | if ! [ "$UID" == "0" ]; then 9 | echo "this script must be run with super user privileges" 10 | exit 6 11 | fi 12 | 13 | echo "XKBMODEL=$1" > /etc/vconsole.conf 14 | echo "XKBLAYOUT=$2" >> /etc/vconsole.conf 15 | echo "XKBVARIANT=$3" >> /etc/vconsole.conf 16 | echo "BACKSPACE=guess" >> /etc/vconsole.conf 17 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ --buildsystem=meson 14 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('org.vanillaos.FirstSetup', 2 | version: 'VTESTING', 3 | meson_version: '>= 0.59.0', 4 | default_options: [ 'warning_level=2', 5 | 'werror=false', 6 | ], 7 | ) 8 | 9 | i18n = import('i18n') 10 | 11 | gnome = import('gnome') 12 | 13 | subdir('data') 14 | subdir('vanilla_first_setup') 15 | subdir('po') 16 | 17 | gnome.post_install( 18 | glib_compile_schemas: false, 19 | gtk_update_icon_cache: true, 20 | update_desktop_database: true, 21 | ) 22 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/live-keyboard: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This sets the currently active keyboard in gnome, not the persistant system keyboard configuration 3 | if [ -z "$1" ]; then 4 | echo "usage:" 5 | echo "live-keyboard [variant]" 6 | exit 5 7 | fi 8 | 9 | if [ "$UID" == "0" ]; then 10 | echo "this script must be run as a regular user" 11 | exit 7 12 | fi 13 | 14 | layout="$1" 15 | 16 | if ! [ -z "$2" ]; then 17 | layout="$layout+$2" 18 | fi 19 | 20 | gsettings set org.gnome.desktop.input-sources sources "[('xkb', '$layout')]" 21 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | viewsdir = join_paths(pkgdatadir, 'vanilla_first_setup/views') 3 | 4 | sources = [ 5 | '__init__.py', 6 | 'applications.py', 7 | 'conn_check.py', 8 | 'done.py', 9 | 'hostname.py', 10 | 'keyboard.py', 11 | 'language.py', 12 | 'locations.py', 13 | 'logout.py', 14 | 'progress.py', 15 | 'theme.py', 16 | 'timezone.py', 17 | 'user.py', 18 | 'welcome_user.py', 19 | 'welcome.py', 20 | ] 21 | 22 | install_data(sources, install_dir: viewsdir) 23 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/computer-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/remove-first-setup-user: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | USERS_IN_FIRST_SETUP_GROUP=$(getent group "vanilla-first-setup" | awk -F: '{print $4}' | tr ',' ' ') 3 | 4 | for USER in $USERS_IN_FIRST_SETUP_GROUP; do 5 | if id "$USER" &>/dev/null; then 6 | echo "Deleting first-setup user: $USER" 7 | userdel -r "$USER" 8 | if ! [ "$?" == 0 ]; then 9 | exit 1 10 | fi 11 | rm "/var/lib/AccountsService/users/$USER" 12 | else 13 | echo "User $USER does not exist." 14 | fi 15 | done 16 | 17 | rm /etc/systemd/system/multi-user.target.wants/remove-first-setup-user.service 18 | exit 0 19 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/theme: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$UID" == "0" ]; then 3 | echo "this script must be run as a regular user" 4 | exit 7 5 | fi 6 | 7 | if [ "$1" == "dark" ]; then 8 | 9 | gsettings set org.gnome.desktop.interface color-scheme "prefer-dark" 10 | gsettings set org.gnome.desktop.interface gtk-theme "Adwaita-dark" 11 | 12 | elif [ "$1" == "light" ]; then 13 | 14 | gsettings set org.gnome.desktop.interface color-scheme "prefer-light" 15 | gsettings set org.gnome.desktop.interface gtk-theme "Adwaita" 16 | 17 | else 18 | echo "usage:" 19 | echo "theme " 20 | exit 5 21 | fi 22 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | scriptsdir = join_paths(pkgdatadir, 'vanilla_first_setup/scripts') 3 | 4 | sources = [ 5 | 'disable-lockscreen', 6 | 'keyboard', 7 | 'logout', 8 | 'open-network-settings', 9 | 'theme', 10 | 'flatpak', 11 | 'live-keyboard', 12 | 'setup-flatpak-remote', 13 | 'timezone', 14 | 'hostname', 15 | 'locale', 16 | 'open-accessibility-settings', 17 | 'setup-system', 18 | 'user', 19 | 'remove-first-setup-user', 20 | 'remove-autostart-file', 21 | ] 22 | 23 | install_data(sources, install_dir: scriptsdir) 24 | -------------------------------------------------------------------------------- /vanilla_first_setup/scripts/remove-first-setup-user: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ -z "$1" ]; then 3 | echo "usage:" 4 | echo "remove-first-setup-user" 5 | exit 5 6 | fi 7 | 8 | if ! [ "$UID" == "0" ]; then 9 | echo "this script must be run with super user privileges" 10 | exit 6 11 | fi 12 | 13 | echo -e '[User]\nSession=firstsetup\nSystemAccount=true' > /var/lib/AccountsService/users/$(id -nu $PKEXEC_UID || echo invaliduser) 14 | systemctl restart accounts-daemon.service 15 | cp /usr/share/org.vanillaos.FirstSetup/remove-first-setup-user.service /etc/systemd/system/remove-first-setup-user.service 16 | systemctl enable remove-first-setup-user.service 17 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | icons_dir = join_paths(get_option('datadir'), 'icons') 2 | 3 | scalable_dir = join_paths('hicolor', 'scalable', 'apps') 4 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') 5 | 6 | 7 | install_data( 8 | join_paths(scalable_dir, 'org.vanillaos.FirstSetup.svg'), 9 | install_dir: join_paths(icons_dir, scalable_dir) 10 | ) 11 | 12 | install_data( 13 | join_paths(scalable_dir, 'org.vanillaos.FirstSetup-flower.svg'), 14 | install_dir: join_paths(icons_dir, scalable_dir) 15 | ) 16 | 17 | install_data( 18 | join_paths(symbolic_dir, 'org.vanillaos.FirstSetup-symbolic.svg'), 19 | install_dir: join_paths(icons_dir, symbolic_dir) 20 | ) 21 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/go-next-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/go-previous-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/dialog-warning-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/language.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | -------------------------------------------------------------------------------- /vanilla_first_setup/style.css: -------------------------------------------------------------------------------- 1 | /* CSS inspired from: Settings Appearance Panel */ 2 | 3 | .background-preview-button { 4 | background: none; 5 | border-radius: 9px; 6 | padding: 3px; 7 | box-shadow: none; 8 | outline: none; 9 | } 10 | 11 | .background-preview-button:checked { 12 | box-shadow: 0 0 0 3px @accent_color; 13 | } 14 | 15 | .background-preview-button:focus:focus-visible { 16 | box-shadow: 0 0 0 3px alpha(@accent_color, .3); 17 | } 18 | 19 | .background-preview-button:checked:focus:focus-visible { 20 | box-shadow: 0 0 0 3px @accent_color, 0 0 0 6px alpha(@accent_color, .3); 21 | } 22 | 23 | .appearance-thumbnail { 24 | border-radius: 6px; 25 | } 26 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/apps/org.gnome.Software-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/emblem-default-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/widget-keyboard.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/preferences-system-time-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/widget-location-list-page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/widget-timezone-list-page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://dep.debian.net/deps/dep5 2 | Upstream-Name: Vanilla OS First Setup 3 | Source: https://github.com/mirko-brombin/vanilla-first-setup 4 | 5 | Files: * 6 | Copyright: 2023 Mirko Brombin 7 | License: GPL-3.0 8 | 9 | License: GPL-3.0 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License. 13 | . 14 | This package is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | . 19 | You should have received a copy of the GNU General Public License 20 | along with this program. If not, see . 21 | . 22 | On Debian systems, the complete text of the GNU General 23 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". 24 | -------------------------------------------------------------------------------- /vanilla_first_setup/vanilla-first-setup.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # vanilla-first-setup.in 4 | # 5 | # Copyright 2023 mirkobrombin 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundationat version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import sys 20 | 21 | VERSION = '@VERSION@' 22 | pkgdatadir = '@pkgdatadir@' 23 | localedir = '@localedir@' 24 | moduledir = '@moduledir@' 25 | 26 | sys.path.insert(1, pkgdatadir) 27 | 28 | if __name__ == '__main__': 29 | from vanilla_first_setup import main 30 | sys.exit(main.main(VERSION, moduledir, localedir)) 31 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/progress.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 28 | 29 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/network-wired-acquiring-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: vanilla-first-setup 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Mirko Brombin 5 | Build-Depends: 6 | build-essential, 7 | debhelper, 8 | python3, 9 | meson, 10 | gettext, 11 | desktop-file-utils, 12 | make, 13 | ninja-build, 14 | pkgconf, 15 | libgio-2.0-dev, 16 | libgio-2.0-dev-bin, 17 | libxml2-utils, 18 | gtk-update-icon-cache 19 | Homepage: https://github.com/Vanilla-OS/first-setup/ 20 | Vcs-Browser: https://github.com/Vanilla-OS/first-setup 21 | Vcs-Git: https://github.com/Vanilla-OS/first-setup.git 22 | Rules-Requires-Root: no 23 | 24 | Package: vanilla-first-setup 25 | Architecture: all 26 | Depends: python3, 27 | python3-gi, 28 | python3-tz, 29 | python3-requests, 30 | libadwaita-1-0, 31 | gir1.2-gtk-4.0, 32 | gir1.2-adw-1, 33 | gir1.2-gweather-4.0, 34 | gir1.2-gnomedesktop-4.0, 35 | pkexec 36 | Description: This utility is meant to be used in Vanilla GNOME as a first-setup wizard. 37 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/network-wired-disconnected-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /vanilla_first_setup/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | moduledir = join_paths(pkgdatadir, 'vanilla_first_setup') 3 | gnome = import('gnome') 4 | 5 | gnome.compile_resources('vanilla-first-setup', 6 | 'vanilla-first-setup.gresource.xml', 7 | gresource_bundle: true, 8 | install: true, 9 | install_dir: moduledir, 10 | ) 11 | 12 | python = import('python') 13 | 14 | conf = configuration_data() 15 | conf.set('PYTHON', python.find_installation('python3').full_path()) 16 | conf.set('VERSION', meson.project_version()) 17 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 18 | conf.set('pkgdatadir', pkgdatadir) 19 | conf.set('moduledir', moduledir) 20 | 21 | configure_file( 22 | input: 'vanilla-first-setup.in', 23 | output: 'vanilla-first-setup', 24 | install_mode: 'rwxr-xr-x', 25 | configuration: conf, 26 | install: true, 27 | install_dir: get_option('bindir'), 28 | ) 29 | 30 | subdir('core') 31 | subdir('scripts') 32 | subdir('views') 33 | 34 | vanilla_first_setup_sources = [ 35 | '__init__.py', 36 | 'application.py', 37 | 'main.py', 38 | 'window.py', 39 | 'dialog.py', 40 | 'apps.json' 41 | ] 42 | 43 | install_data(vanilla_first_setup_sources, install_dir: moduledir) 44 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/preferences-desktop-locale-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/conn-check.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 27 | 28 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/theme-default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: ghcr.io/vanilla-os/pico:main 13 | volumes: 14 | - /proc:/proc 15 | - /:/run/host 16 | options: --privileged -it 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install build dependencies 22 | run: | 23 | apt-get update 24 | apt-get build-dep -y . 25 | 26 | - name: Build .deb package 27 | run: | 28 | dpkg-buildpackage 29 | mv ../vanilla-first-setup_*.deb vanilla-first-setup.deb 30 | 31 | - name: Calculate and Save Checksums 32 | run: | 33 | sha256sum vanilla-first-setup.deb >> checksums.txt 34 | 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: first-setup 38 | path: | 39 | checksums.txt 40 | vanilla-first-setup.deb 41 | 42 | - uses: softprops/action-gh-release@v2 43 | if: github.ref == 'refs/heads/main' 44 | with: 45 | token: "${{ secrets.GITHUB_TOKEN }}" 46 | tag_name: "continuous" 47 | prerelease: true 48 | name: "Continuous Build" 49 | files: | 50 | checksums.txt 51 | vanilla-first-setup.deb 52 | -------------------------------------------------------------------------------- /vanilla_first_setup/dialog.py: -------------------------------------------------------------------------------- 1 | # dialog.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from gi.repository import Gtk, Adw 18 | 19 | 20 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/dialog.ui") 21 | class VanillaDialog(Adw.Window): 22 | __gtype_name__ = "VanillaDialog" 23 | 24 | label_text = Gtk.Template.Child() 25 | 26 | def __init__(self, window, title, text, **kwargs): 27 | super().__init__(**kwargs) 28 | self.set_transient_for(window) 29 | self.set_title(title) 30 | self.label_text.set_text(text) 31 | 32 | def hide(action, callback=None): 33 | self.hide() 34 | 35 | shortcut_controller = Gtk.ShortcutController.new() 36 | shortcut_controller.add_shortcut( 37 | Gtk.Shortcut.new( 38 | Gtk.ShortcutTrigger.parse_string("Escape"), Gtk.CallbackAction.new(hide) 39 | ) 40 | ) 41 | self.add_controller(shortcut_controller) 42 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/keyboard.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 39 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/location.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 38 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/org.vanillaos.FirstSetup-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /vanilla_first_setup/main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | # 3 | # Copyright 2025 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import sys 18 | import os 19 | import signal 20 | import locale 21 | import gettext 22 | 23 | from gi.repository import Gio 24 | 25 | def main(version, moduledir: str, localedir: str): 26 | """The application's entry point.""" 27 | if moduledir == "": 28 | print("Can't continue without a data directory.") 29 | sys.exit(1) 30 | return 31 | 32 | signal.signal(signal.SIGINT, signal.SIG_DFL) 33 | locale.bindtextdomain('vanilla-first-setup', localedir) 34 | locale.textdomain('vanilla-first-setup') 35 | gettext.install('vanilla-first-setup', localedir) 36 | 37 | resource = Gio.Resource.load(os.path.join(moduledir, 'vanilla-first-setup.gresource')) 38 | resource._register() 39 | 40 | import vanilla_first_setup.core.backend as backend 41 | from vanilla_first_setup.application import FirstSetupApplication 42 | 43 | backend.set_script_path(os.path.join(moduledir, "scripts")) 44 | app = FirstSetupApplication(moduledir) 45 | return app.run(sys.argv) 46 | -------------------------------------------------------------------------------- /vanilla_first_setup/core/applications.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import threading 3 | 4 | from gi.repository import GdkPixbuf, Gio, GLib 5 | 6 | icon_url_cache = {} 7 | 8 | def fetch_icon_url_from_id(id: str) -> str: 9 | if id in icon_url_cache: 10 | return icon_url_cache[id] 11 | 12 | response = requests.get(f'https://flathub.org/api/v2/appstream/{ id }') 13 | if response.status_code != 200: 14 | raise ValueError(f"Failed to retrieve icon for {id}, status code {response.status_code}") 15 | 16 | data = response.json() 17 | if "icon" not in data: 18 | raise ValueError(f"Server returned unexpected output for {id}: {data}") 19 | 20 | icon_url = data["icon"] 21 | 22 | icon_url_cache[id] = icon_url 23 | return icon_url 24 | 25 | pixbuf_cache = {} 26 | 27 | def fetch_pixbuf_from_url(url: str): 28 | if url in pixbuf_cache: 29 | return pixbuf_cache[url] 30 | 31 | response = requests.get(url) 32 | if response.status_code != 200: 33 | raise ValueError(f"Failed to download {url}, server returned status code {response.status_code}") 34 | 35 | img_data = Gio.MemoryInputStream.new_from_data(response.content) 36 | pixbuf = GdkPixbuf.Pixbuf.new_from_stream(img_data, None) 37 | 38 | pixbuf_cache[url] = pixbuf 39 | return pixbuf 40 | 41 | def set_app_icon_from_id_async(icon_widget, id: str): 42 | def fetch_icon_thread(widget, id: str): 43 | try: 44 | icon_url = fetch_icon_url_from_id(id) 45 | pixbuf = fetch_pixbuf_from_url(icon_url) 46 | GLib.idle_add(widget.set_from_pixbuf, pixbuf) 47 | except Exception as e: 48 | print(f"{e}") 49 | 50 | thread = threading.Thread(target=fetch_icon_thread, args=(icon_widget, id)) 51 | thread.start() 52 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 37 | 38 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/org.vanillaos.FirstSetup.desktop.in 2 | 3 | vanilla_first_setup/gtk/window.ui 4 | vanilla_first_setup/gtk/logout.ui 5 | vanilla_first_setup/gtk/done.ui 6 | vanilla_first_setup/gtk/progress.ui 7 | vanilla_first_setup/gtk/dialog.ui 8 | vanilla_first_setup/gtk/conn-check.ui 9 | vanilla_first_setup/gtk/theme.ui 10 | vanilla_first_setup/gtk/welcome.ui 11 | vanilla_first_setup/gtk/welcome-user.ui 12 | vanilla_first_setup/gtk/user.ui 13 | vanilla_first_setup/gtk/hostname.ui 14 | vanilla_first_setup/gtk/language.ui 15 | vanilla_first_setup/gtk/location.ui 16 | vanilla_first_setup/gtk/timezone.ui 17 | vanilla_first_setup/gtk/keyboard.ui 18 | vanilla_first_setup/gtk/widget-location-list-page.ui 19 | vanilla_first_setup/gtk/layout-applications.ui 20 | vanilla_first_setup/gtk/applications-dialog.ui 21 | 22 | 23 | vanilla_first_setup/__init__.py 24 | vanilla_first_setup/window.py 25 | vanilla_first_setup/dialog.py 26 | vanilla_first_setup/application.py 27 | vanilla_first_setup/main.py 28 | 29 | vanilla_first_setup/views/__init__.py 30 | vanilla_first_setup/views/applications.py 31 | vanilla_first_setup/views/language.py 32 | vanilla_first_setup/views/theme.py 33 | vanilla_first_setup/views/conn_check.py 34 | vanilla_first_setup/views/keyboard.py 35 | vanilla_first_setup/views/hostname.py 36 | vanilla_first_setup/views/user.py 37 | vanilla_first_setup/views/locations.py 38 | vanilla_first_setup/views/timezone.py 39 | vanilla_first_setup/views/welcome_user.py 40 | vanilla_first_setup/views/welcome.py 41 | vanilla_first_setup/views/progress.py 42 | vanilla_first_setup/views/logout.py 43 | vanilla_first_setup/views/done.py 44 | 45 | vanilla_first_setup/core/__init__.py 46 | vanilla_first_setup/core/languages.py 47 | vanilla_first_setup/core/timezones.py 48 | vanilla_first_setup/core/keyboard.py 49 | vanilla_first_setup/core/backend.py 50 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/language.py: -------------------------------------------------------------------------------- 1 | # language.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | _ = __builtins__["_"] 18 | 19 | from gi.repository import Adw, Gtk 20 | 21 | from vanilla_first_setup.views.locations import VanillaLocation 22 | 23 | import vanilla_first_setup.core.languages as lang 24 | import vanilla_first_setup.core.backend as backend 25 | 26 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/language.ui") 27 | class VanillaLanguage(Adw.Bin): 28 | __gtype_name__ = "VanillaLanguage" 29 | 30 | status_page = Gtk.Template.Child() 31 | 32 | def __init__(self, window, **kwargs): 33 | super().__init__(**kwargs) 34 | self.__window = window 35 | 36 | self.__location_page = VanillaLocation(window, _("Language"), lang.LanguagesDataSource()) 37 | self.status_page.set_child(self.__location_page) 38 | 39 | def set_page_active(self): 40 | self.__location_page.set_page_active() 41 | return 42 | 43 | def set_page_inactive(self): 44 | self.__location_page.set_page_inactive() 45 | return 46 | 47 | def finish(self): 48 | self.__location_page.finish() 49 | language = self.__location_page.selected_special 50 | return backend.set_locale(language) 51 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/applications-dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 38 | 39 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # vanilla-first-setup.in 2 | # 3 | # Copyright 2025 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import sys 19 | import subprocess 20 | 21 | VERSION = 'testing' 22 | 23 | path_of_this_file = os.path.dirname(os.path.realpath(__file__)) 24 | pkgdatadir = os.path.join(path_of_this_file) 25 | localedir = os.path.join(path_of_this_file, "localegen") 26 | moduledir = os.path.join(path_of_this_file, "vanilla_first_setup") 27 | 28 | def setup_translations(): 29 | import pathlib 30 | po_file_path = pathlib.Path(os.path.join(pkgdatadir, "po")) 31 | 32 | for po_file in po_file_path.glob("*.po"): 33 | lang = po_file.stem 34 | mo_path = os.path.join(localedir, lang, "LC_MESSAGES") 35 | os.makedirs(mo_path, exist_ok=True) 36 | mo_file_path = os.path.join(mo_path, "vanilla-first-setup.mo") 37 | subprocess.run(["msgfmt", "-o", mo_file_path, po_file.absolute()]) 38 | 39 | def setup_gresource(): 40 | resource_file = os.path.join(moduledir, "vanilla-first-setup.gresource") 41 | command = ["glib-compile-resources", f"--sourcedir={moduledir}", f"--target={resource_file}", f"{resource_file}.xml"] 42 | subprocess.run(command, check=True) 43 | 44 | if __name__ == '__main__': 45 | setup_translations() 46 | setup_gresource() 47 | 48 | from vanilla_first_setup import main 49 | sys.exit(main.main(VERSION, moduledir, localedir)) 50 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/hostname.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 44 | 45 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/keyboard.py: -------------------------------------------------------------------------------- 1 | # keyboard.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from gi.repository import Adw, Gtk 18 | 19 | from vanilla_first_setup.views.locations import VanillaLocation 20 | 21 | import vanilla_first_setup.core.keyboard as kbd 22 | import vanilla_first_setup.core.timezones as tz 23 | import vanilla_first_setup.core.backend as backend 24 | 25 | _ = __builtins__["_"] 26 | 27 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/keyboard.ui") 28 | class VanillaKeyboard(Adw.Bin): 29 | __gtype_name__ = "VanillaKeyboard" 30 | 31 | status_page = Gtk.Template.Child() 32 | 33 | def __init__(self, window, **kwargs): 34 | super().__init__(**kwargs) 35 | self.__window = window 36 | 37 | self.__location_page = VanillaLocation(window, _("Keyboard"), kbd.KeyboardsDataSource()) 38 | self.status_page.set_child(self.__location_page) 39 | 40 | def set_page_active(self): 41 | self.__location_page.set_page_active() 42 | return 43 | 44 | def set_page_inactive(self): 45 | self.__location_page.set_page_inactive() 46 | return 47 | 48 | def finish(self): 49 | self.__location_page.finish() 50 | keyboard = self.__location_page.selected_special 51 | success = backend.set_live_keyboard(keyboard) 52 | if not success: 53 | return False 54 | success = backend.set_keyboard(keyboard) 55 | if not success: 56 | return False 57 | return True 58 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/timezone.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 50 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | 'firstsetup-session.desktop', 3 | rename: 'firstsetup.desktop', 4 | install_dir: join_paths(get_option('datadir'), 'xsessions') 5 | ) 6 | 7 | install_data( 8 | 'firstsetup-session.desktop', 9 | rename: 'firstsetup.desktop', 10 | install_dir: join_paths(get_option('datadir'), 'wayland-sessions') 11 | ) 12 | 13 | install_data( 14 | 'firstsetup-session.desktop', 15 | rename: 'firstsetup.desktop', 16 | install_dir: join_paths(get_option('datadir'), 'gnome-session', 'sessions') 17 | ) 18 | 19 | install_data( 20 | 'firstsetup-session-mode.json', 21 | rename: 'firstsetup.json', 22 | install_dir: join_paths(get_option('datadir'), 'gnome-shell', 'modes') 23 | ) 24 | 25 | install_data( 26 | 'org.vanillaos.FirstSetup.policy', 27 | install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'polkit-1', 'actions') 28 | ) 29 | 30 | install_data( 31 | 'org.vanillaos.FirstSetup.rules', 32 | install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'polkit-1', 'rules.d') 33 | ) 34 | 35 | desktop_file = i18n.merge_file( 36 | input: 'org.vanillaos.FirstSetup.desktop.in', 37 | output: 'org.vanillaos.FirstSetup.desktop', 38 | type: 'desktop', 39 | po_dir: '../po', 40 | install: true, 41 | install_dir: join_paths(get_option('datadir'), 'applications'), 42 | ) 43 | 44 | custom_target( 45 | 'install-autostart-desktop', 46 | input: desktop_file, 47 | output: 'org.vanillaos.FirstSetup.autostart.desktop', 48 | command: ['cp', '@INPUT@', '@OUTPUT@'], 49 | install_dir: join_paths(get_option('sysconfdir'), 'skel', '.config', 'autostart'), 50 | build_by_default: true, 51 | install : true, 52 | ) 53 | 54 | desktop_utils = find_program('desktop-file-validate', required: false) 55 | if desktop_utils.found() 56 | test('Validate desktop file', desktop_utils, 57 | args: [desktop_file] 58 | ) 59 | endif 60 | 61 | install_data( 62 | 'remove-first-setup-user.service', 63 | install_dir: join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 64 | ) 65 | 66 | install_data( 67 | 'remove-first-setup-user', 68 | install_dir: join_paths(get_option('prefix'), 'libexec') 69 | ) 70 | 71 | subdir('icons') 72 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/welcome_user.py: -------------------------------------------------------------------------------- 1 | # welcome.py 2 | # 3 | # Copyright 2024 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import threading 18 | import os 19 | import pwd 20 | 21 | _ = __builtins__["_"] 22 | 23 | from gi.repository import Gtk, GLib, Adw 24 | 25 | import vanilla_first_setup.core.backend as backend 26 | 27 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/welcome-user.ui") 28 | class VanillaWelcomeUser(Adw.Bin): 29 | __gtype_name__ = "VanillaWelcomeUser" 30 | 31 | btn_next = Gtk.Template.Child() 32 | btn_access = Gtk.Template.Child() 33 | status_page = Gtk.Template.Child() 34 | 35 | def __init__(self, window, **kwargs): 36 | super().__init__(**kwargs) 37 | self.__window = window 38 | 39 | self.btn_next.connect("clicked", self.__on_btn_next_clicked) 40 | self.btn_access.connect("clicked", self.__on_btn_access_clicked) 41 | 42 | username = os.getlogin() 43 | full_name = pwd.getpwnam(username).pw_gecos.split(",")[0] 44 | 45 | message = _("Hello {}!").format(full_name) 46 | 47 | self.status_page.set_title(message) 48 | 49 | def set_page_active(self): 50 | self.__window.set_ready(True) 51 | self.btn_next.grab_focus() 52 | 53 | def set_page_inactive(self): 54 | return 55 | 56 | def finish(self): 57 | return True 58 | 59 | def __on_btn_next_clicked(self, widget): 60 | self.__window.finish_step() 61 | 62 | def __on_btn_access_clicked(self, widget): 63 | thread = threading.Thread(target=backend.open_accessibility_settings) 64 | thread.start() 65 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/welcome-user.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 43 | 44 | -------------------------------------------------------------------------------- /vanilla_first_setup/assets/symbolic/actions/input-keyboard-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build-artifacts: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: ghcr.io/vanilla-os/pico:main 13 | volumes: 14 | - /proc:/proc 15 | - /:/run/host 16 | options: --privileged -it 17 | permissions: 18 | contents: read 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set meson version by tag 24 | id: set_version 25 | run: | 26 | sed -i 's/VTESTING/${{ github.ref_name }}/g' meson.build 27 | 28 | - name: Install build dependencies 29 | run: | 30 | apt-get update 31 | apt-get build-dep -y . 32 | 33 | - name: Build .deb package 34 | run: | 35 | dpkg-buildpackage 36 | mv ../vanilla-first-setup_*.deb vanilla-first-setup.deb 37 | 38 | - name: Calculate and Save Checksums 39 | run: | 40 | sha256sum vanilla-first-setup.deb >> checksums.txt 41 | 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: first-setup 45 | path: | 46 | checksums.txt 47 | vanilla-first-setup.deb 48 | 49 | release: 50 | runs-on: ubuntu-latest 51 | needs: build-artifacts 52 | permissions: 53 | contents: write # to create and upload assets to releases 54 | attestations: write # to upload assets attestation for build provenance 55 | id-token: write # grant additional permission to attestation action to mint the OIDC token permission 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 62 | 63 | - name: Download Artifact 64 | uses: actions/download-artifact@v4 65 | with: 66 | name: first-setup 67 | 68 | - name: Create Release 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | run: gh release create "${{ github.ref_name }}" --generate-notes vanilla-first-setup.deb checksums.txt 72 | 73 | - name: Attest Release Files 74 | id: attest 75 | uses: actions/attest-build-provenance@v1 76 | with: 77 | subject-path: 'vanilla-first-setup.deb, checksums.txt' 78 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/done.py: -------------------------------------------------------------------------------- 1 | # done.py 2 | # 3 | # Copyright 2024 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import subprocess 18 | 19 | _ = __builtins__["_"] 20 | from gi.repository import Gtk, Adw 21 | 22 | import vanilla_first_setup.core.backend as backend 23 | 24 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/done.ui") 25 | class VanillaDone(Adw.Bin): 26 | __gtype_name__ = "VanillaDone" 27 | 28 | status_page = Gtk.Template.Child() 29 | btn_tour = Gtk.Template.Child() 30 | btn_exit = Gtk.Template.Child() 31 | btn_logs = Gtk.Template.Child() 32 | log_box = Gtk.Template.Child() 33 | log_output = Gtk.Template.Child() 34 | 35 | def __init__( 36 | self, 37 | window, 38 | **kwargs, 39 | ): 40 | super().__init__(**kwargs) 41 | self.__window = window 42 | 43 | self.btn_logs.connect("clicked", self.__on_logs_clicked) 44 | self.btn_exit.connect("clicked", self.__on_exit_clicked) 45 | self.btn_tour.connect("clicked", self.__on_tour_clicked) 46 | 47 | def set_page_active(self): 48 | has_errors = len(backend.errors) > 0 49 | self.btn_logs.set_visible(has_errors) 50 | self.btn_tour.grab_focus() 51 | 52 | def set_page_inactive(self): 53 | return 54 | 55 | def __on_tour_clicked(self, *args): 56 | subprocess.Popen(["/usr/bin/vanilla-tour"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True) 57 | self.__window.close() 58 | 59 | def __on_exit_clicked(self, *args): 60 | self.__window.close() 61 | 62 | def __on_logs_clicked(self, *args): 63 | self.btn_logs.set_visible(False) 64 | self.log_box.set_visible(True) 65 | logs_text = "\n\n".join(backend.errors) 66 | self.log_output.set_label(logs_text) 67 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/timezone.py: -------------------------------------------------------------------------------- 1 | # timezone.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | _ = __builtins__["_"] 18 | 19 | from gi.repository import Adw, Gtk 20 | 21 | from vanilla_first_setup.views.locations import VanillaLocation 22 | 23 | import vanilla_first_setup.core.timezones as tz 24 | import vanilla_first_setup.core.backend as backend 25 | 26 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/timezone.ui") 27 | class VanillaTimezone(Adw.Bin): 28 | __gtype_name__ = "VanillaTimezone" 29 | 30 | status_page = Gtk.Template.Child() 31 | footer = Gtk.Template.Child() 32 | current_timezone_label = Gtk.Template.Child() 33 | current_time_label = Gtk.Template.Child() 34 | 35 | def __init__(self, window, **kwargs): 36 | super().__init__(**kwargs) 37 | self.__window = window 38 | 39 | self.__location_page = VanillaLocation(window, _("Timezone"), tz.TimezonesDataSource()) 40 | self.status_page.set_child(self.__location_page) 41 | 42 | def set_page_active(self): 43 | self.__location_page.set_page_active() 44 | 45 | selected_timezone = self.__location_page.selected_special 46 | if not selected_timezone: 47 | try: 48 | with open('/etc/timezone', 'r') as file: 49 | selected_timezone = file.read().split("\n")[0] 50 | except Exception as e: 51 | print(e) 52 | 53 | if selected_timezone: 54 | time_string, with_date = tz.get_timezone_preview(selected_timezone) 55 | self.current_time_label.set_label(time_string) 56 | self.current_timezone_label.set_label(selected_timezone) 57 | 58 | def set_page_inactive(self): 59 | self.__location_page.set_page_inactive() 60 | 61 | def finish(self): 62 | self.__location_page.finish() 63 | timezone = self.__location_page.selected_special 64 | return backend.set_timezone(timezone) 65 | -------------------------------------------------------------------------------- /vanilla_first_setup/vanilla-first-setup.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style.css 5 | gtk/window.ui 6 | gtk/logout.ui 7 | gtk/done.ui 8 | gtk/progress.ui 9 | gtk/dialog.ui 10 | 11 | gtk/conn-check.ui 12 | gtk/theme.ui 13 | gtk/welcome.ui 14 | gtk/welcome-user.ui 15 | gtk/user.ui 16 | gtk/hostname.ui 17 | gtk/language.ui 18 | gtk/location.ui 19 | gtk/timezone.ui 20 | gtk/keyboard.ui 21 | gtk/widget-location-list-page.ui 22 | 23 | gtk/layout-applications.ui 24 | gtk/applications-dialog.ui 25 | 26 | assets/theme-default.svg 27 | assets/theme-dark.svg 28 | assets/background_replacement.png 29 | 30 | 31 | assets/symbolic/apps/org.gnome.Software-symbolic.svg 32 | 33 | 34 | assets/symbolic/actions/emblem-default-symbolic.svg 35 | assets/symbolic/actions/network-wired-acquiring-symbolic.svg 36 | assets/symbolic/actions/computer-symbolic.svg 37 | assets/symbolic/actions/input-keyboard-symbolic.svg 38 | assets/symbolic/actions/preferences-desktop-locale-symbolic.svg 39 | assets/symbolic/actions/go-next-symbolic.svg 40 | assets/symbolic/actions/preferences-system-time-symbolic.svg 41 | assets/symbolic/actions/go-previous-symbolic.svg 42 | assets/symbolic/actions/network-wired-disconnected-symbolic.svg 43 | assets/symbolic/actions/dialog-warning-symbolic.svg 44 | 45 | 46 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/org.vanillaos.FirstSetup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/hostname.py: -------------------------------------------------------------------------------- 1 | # hostname.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundationat version 3 of the License. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import re 19 | from gi.repository import Gtk, Adw 20 | 21 | import vanilla_first_setup.core.backend as backend 22 | 23 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/hostname.ui") 24 | class VanillaHostname(Adw.Bin): 25 | __gtype_name__ = "VanillaHostname" 26 | 27 | hostname_entry = Gtk.Template.Child() 28 | hostname_error = Gtk.Template.Child() 29 | 30 | hostname = "" 31 | 32 | def __init__(self, window, **kwargs): 33 | super().__init__(**kwargs) 34 | self.__window = window 35 | 36 | # signals 37 | self.hostname_entry.connect("changed", self.__on_hostname_entry_changed) 38 | self.hostname_entry.connect("entry-activated", self.__on_activate) 39 | 40 | def set_page_active(self): 41 | self.hostname_entry.grab_focus() 42 | self.__verify_continue() 43 | 44 | def set_page_inactive(self): 45 | return 46 | 47 | def finish(self): 48 | return backend.set_hostname(self.hostname) 49 | 50 | def __on_activate(self, widget): 51 | self.__window.finish_step() 52 | 53 | def __on_hostname_entry_changed(self, *args): 54 | _hostname = self.hostname_entry.get_text() 55 | 56 | if self.__validate_hostname(_hostname): 57 | self.hostname = _hostname 58 | self.hostname_entry.remove_css_class("error") 59 | self.hostname_error.set_opacity(0.0) 60 | self.__verify_continue() 61 | return 62 | 63 | self.hostname_entry.add_css_class("error") 64 | self.hostname = "" 65 | self.hostname_error.set_opacity(1.0) 66 | self.__verify_continue() 67 | 68 | def __validate_hostname(self, hostname): 69 | if len(hostname) > 64: 70 | return False 71 | 72 | lower_ascii = re.compile(r"[a-z0-9]+$") 73 | 74 | hyphen_parts = hostname.split("-") 75 | for hyphen_part in hyphen_parts: 76 | if not lower_ascii.match(hyphen_part): 77 | return False 78 | 79 | return True 80 | 81 | def __verify_continue(self): 82 | ready = self.hostname != "" 83 | self.__window.set_ready(ready) 84 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/theme.py: -------------------------------------------------------------------------------- 1 | # theme.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | from gi.repository import Gtk, Gio, Adw, GdkPixbuf 20 | 21 | import vanilla_first_setup.core.backend as backend 22 | 23 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/theme.ui") 24 | class VanillaTheme(Adw.Bin): 25 | __gtype_name__ = "VanillaTheme" 26 | 27 | default_image = Gtk.Template.Child() 28 | dark_image = Gtk.Template.Child() 29 | btn_default = Gtk.Template.Child() 30 | btn_dark = Gtk.Template.Child() 31 | 32 | def __init__(self, window, **kwargs): 33 | super().__init__(**kwargs) 34 | self.__window = window 35 | self.__style_manager = self.__window.style_manager 36 | 37 | self.btn_default.set_active(not self.__style_manager.get_dark()) 38 | self.btn_dark.set_active(self.__style_manager.get_dark()) 39 | 40 | self.__set_wallpaper_assets() 41 | 42 | self.btn_default.connect("toggled", self.__set_theme, "light") 43 | self.btn_dark.connect("toggled", self.__set_theme, "dark") 44 | 45 | def set_page_active(self): 46 | self.__window.set_ready() 47 | self.__window.set_focus_on_next() 48 | 49 | def set_page_inactive(self): 50 | return 51 | 52 | def finish(self): 53 | return True 54 | 55 | def __set_theme(self, widget, theme: str): 56 | if widget.get_active(): 57 | backend.set_theme(theme) 58 | 59 | def __set_wallpaper_assets(self): 60 | wallpaper_schema = Gio.Settings.new("org.gnome.desktop.background") 61 | 62 | try: 63 | default_pixbuf = GdkPixbuf.Pixbuf.new_from_file(wallpaper_schema.get_string("picture-uri").split("file://")[1]) 64 | dark_pixbuf = GdkPixbuf.Pixbuf.new_from_file(wallpaper_schema.get_string("picture-uri-dark").split("file://")[1]) 65 | except: 66 | default_pixbuf = GdkPixbuf.Pixbuf.new_from_resource("/org/vanillaos/FirstSetup/assets/background_replacement.png") 67 | dark_pixbuf = default_pixbuf 68 | logging.warning("could not load background, falling back to replacement image") 69 | 70 | self.default_image.set_pixbuf(default_pixbuf.scale_simple(180, 120, GdkPixbuf.InterpType.BILINEAR)) 71 | self.dark_image.set_pixbuf(dark_pixbuf.scale_simple(180, 120, GdkPixbuf.InterpType.BILINEAR)) 72 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/logout.py: -------------------------------------------------------------------------------- 1 | # done.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | _ = __builtins__["_"] 18 | from gi.repository import Gtk, Adw 19 | 20 | import vanilla_first_setup.core.backend as backend 21 | 22 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/logout.ui") 23 | class VanillaLogout(Adw.Bin): 24 | __gtype_name__ = "VanillaLogout" 25 | 26 | status_page = Gtk.Template.Child() 27 | btn_login = Gtk.Template.Child() 28 | btn_logs = Gtk.Template.Child() 29 | log_box = Gtk.Template.Child() 30 | log_output = Gtk.Template.Child() 31 | 32 | def __init__( 33 | self, 34 | window, 35 | **kwargs, 36 | ): 37 | super().__init__(**kwargs) 38 | self.__window = window 39 | 40 | self.btn_logs.connect("clicked", self.__on_logs_clicked) 41 | self.btn_login.connect("clicked", self.__on_login_clicked) 42 | 43 | def set_page_active(self): 44 | backend.remove_first_setup_user() 45 | has_errors = len(backend.errors) > 0 46 | self.btn_logs.set_visible(has_errors) 47 | self.btn_login.grab_focus() 48 | 49 | def set_page_inactive(self): 50 | return 51 | 52 | __already_subscribed = False 53 | __deferred_actions_succeeded = True 54 | __currently_running = False 55 | 56 | def __on_login_clicked(self, *args): 57 | if not self.__already_subscribed: 58 | backend.subscribe_progress(self.__deferred_progress_callback) 59 | self.__already_subscribed = True 60 | if not self.__currently_running: 61 | self.__currently_running = True 62 | backend.start_deferred_actions() 63 | self.__deferred_actions_succeeded = True 64 | 65 | def __deferred_progress_callback(self, id: str, uid: str, state: backend.ProgressState, info = None): 66 | if state == backend.ProgressState.Failed: 67 | self.__deferred_actions_succeeded = False 68 | if uid == "all_actions" and state == backend.ProgressState.Finished: 69 | self.__currently_running = False 70 | if self.__deferred_actions_succeeded: 71 | backend.logout() 72 | 73 | def __on_logs_clicked(self, *args): 74 | self.btn_logs.set_visible(False) 75 | self.log_box.set_visible(True) 76 | logs_text = "\n\n".join(backend.errors) 77 | self.log_output.set_label(logs_text) 78 | -------------------------------------------------------------------------------- /data/org.vanillaos.FirstSetup.policy: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | FirstSetup hostname script 10 | Set the hostname of the system 11 | 12 | no 13 | no 14 | auth_admin 15 | 16 | /usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/hostname 17 | 18 | 19 | 20 | FirstSetup keyboard script 21 | Set the keyboard of the system 22 | 23 | no 24 | no 25 | auth_admin 26 | 27 | /usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/keyboard 28 | 29 | 30 | 31 | FirstSetup locale script 32 | Set the locale of the system 33 | 34 | no 35 | no 36 | auth_admin 37 | 38 | /usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/locale 39 | 40 | 41 | 42 | FirstSetup timezone script 43 | Set the timezone of the system 44 | 45 | no 46 | no 47 | auth_admin 48 | 49 | /usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/timezone 50 | 51 | 52 | 53 | FirstSetup user script 54 | Add a user to the system 55 | 56 | no 57 | no 58 | auth_admin 59 | 60 | /usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/user 61 | 62 | 63 | 64 | FirstSetup remove FirstSetup user script 65 | Remove FirstSetup user from the system 66 | 67 | no 68 | no 69 | auth_admin 70 | 71 | /usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/remove-first-setup-user 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/logout.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 74 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/conn_check.py: -------------------------------------------------------------------------------- 1 | # conn_check.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import threading 18 | import logging 19 | _ = __builtins__["_"] 20 | 21 | from gi.repository import Adw, Gtk, Gio, GLib 22 | 23 | import vanilla_first_setup.core.backend as backend 24 | 25 | logger = logging.getLogger("FirstSetup::Conn_Check") 26 | 27 | 28 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/conn-check.ui") 29 | class VanillaConnCheck(Adw.Bin): 30 | __gtype_name__ = "VanillaConnCheck" 31 | 32 | status_page = Gtk.Template.Child() 33 | btn_settings = Gtk.Template.Child() 34 | 35 | __network_monitor = None 36 | __active = False 37 | __already_skipped = False 38 | 39 | def __init__(self, window, **kwargs): 40 | super().__init__(**kwargs) 41 | self.__window = window 42 | 43 | self.__network_monitor = Gio.NetworkMonitor.get_default() 44 | 45 | self.__network_monitor.connect("network-changed", self.__check_network_status) 46 | self.btn_settings.connect("clicked", self.__on_btn_settings_clicked) 47 | 48 | def set_page_active(self): 49 | self.__active = True 50 | self.__check_network_status() 51 | 52 | def set_page_inactive(self): 53 | self.__active = False 54 | 55 | def finish(self): 56 | return True 57 | 58 | def __check_network_status(self, *args): 59 | if not self.__active: 60 | return 61 | 62 | if self.__network_monitor.get_connectivity() == Gio.NetworkConnectivity.FULL: 63 | self.__set_network_connected() 64 | self.__window.set_ready(True) 65 | else: 66 | self.__set_network_disconnected() 67 | self.__window.set_ready(False) 68 | 69 | def __set_network_disconnected(self): 70 | logger.info("Internet connection available.") 71 | self.status_page.set_icon_name("network-wired-disconnected-symbolic") 72 | self.status_page.set_title(_("No Internet Connection!")) 73 | self.status_page.set_description(_("First Setup requires an active internet connection")) 74 | self.btn_settings.set_visible(True) 75 | 76 | def __set_network_connected(self): 77 | logger.info("Internet connection not avaiable.") 78 | self.status_page.set_icon_name("emblem-default-symbolic") 79 | self.status_page.set_title(_("Connection available")) 80 | self.status_page.set_description(_("You have a working internet connection")) 81 | self.btn_settings.set_visible(False) 82 | if not self.__already_skipped: 83 | self.__already_skipped = True 84 | GLib.idle_add(self.__window.finish_step) 85 | 86 | def __on_btn_settings_clicked(self, widget): 87 | thread = threading.Thread(target=backend.open_network_settings) 88 | thread.start() 89 | return 90 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/user.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 74 | 75 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/welcome.py: -------------------------------------------------------------------------------- 1 | # welcome.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import time 18 | import threading 19 | import random 20 | 21 | from gi.repository import Gtk, GLib, Adw 22 | 23 | import vanilla_first_setup.core.backend as backend 24 | 25 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/welcome.ui") 26 | class VanillaWelcome(Adw.Bin): 27 | __gtype_name__ = "VanillaWelcome" 28 | 29 | btn_next = Gtk.Template.Child() 30 | btn_access = Gtk.Template.Child() 31 | title_label = Gtk.Template.Child() 32 | 33 | __stop_animation = True 34 | 35 | welcome = [ 36 | "Welcome", 37 | "Benvenuto", 38 | "Bienvenido", 39 | "Bienvenue", 40 | "Willkommen", 41 | "Bem-vindo", 42 | "Добро пожаловать", 43 | "欢迎", 44 | "ようこそ", 45 | "환영합니다", 46 | "أهلا بك", 47 | "ברוך הבא", 48 | "Καλώς ήρθατε", 49 | "Hoşgeldiniz", 50 | "Welkom", 51 | "Witamy", 52 | "Välkommen", 53 | "Tervetuloa", 54 | "Vítejte", 55 | "Üdvözöljük", 56 | "Bun venit", 57 | "Vitajte", 58 | "Tere tulemast", 59 | "Sveiki atvykę", 60 | "Dobrodošli", 61 | "خوش آمدید", 62 | "आपका स्वागत है", 63 | "স্বাগতম", 64 | "வரவேற்கிறோம்", 65 | "స్వాగతం", 66 | "मुबारक हो", 67 | "સુસ્વાગત છે", 68 | "ಸುಸ್ವಾಗತ", 69 | "സ്വാഗതം", 70 | ] 71 | current_welcome_text = 0 72 | 73 | def __init__(self, window, **kwargs): 74 | super().__init__(**kwargs) 75 | self.__window = window 76 | 77 | random.shuffle(self.welcome) 78 | 79 | self.btn_next.connect("clicked", self.__on_btn_next_clicked) 80 | self.btn_access.connect("clicked", self.__on_btn_access_clicked) 81 | 82 | def set_page_active(self): 83 | self.__window.set_ready(True) 84 | self.btn_next.grab_focus() 85 | 86 | self.__stop_animation = False 87 | self.__start_welcome_animation() 88 | 89 | def set_page_inactive(self): 90 | self.__stop_animation = True 91 | 92 | def finish(self): 93 | return True 94 | 95 | def __start_welcome_animation(self): 96 | def change_langs_thread(): 97 | while not self.__stop_animation: 98 | time.sleep(1.2) 99 | lang = self.welcome[self.current_welcome_text] 100 | GLib.idle_add(self.title_label.set_text, lang) 101 | 102 | self.current_welcome_text += 1 103 | if self.current_welcome_text > len(self.welcome)-1: 104 | self.current_welcome_text = 0 105 | 106 | welcome_animation_thread = threading.Thread(target=change_langs_thread) 107 | welcome_animation_thread.start() 108 | 109 | def __on_btn_next_clicked(self, widget): 110 | self.__window.finish_step() 111 | 112 | def __on_btn_access_clicked(self, widget): 113 | thread = threading.Thread(target=backend.open_accessibility_settings) 114 | thread.start() 115 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/welcome.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 76 | 77 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/done.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 91 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/progress.py: -------------------------------------------------------------------------------- 1 | # progress.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import threading 18 | import vanilla_first_setup.core.backend as backend 19 | import vanilla_first_setup.core.applications as applications 20 | 21 | from gi.repository import Gtk, Adw, GLib 22 | 23 | _ = __builtins__["_"] 24 | 25 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/progress.ui") 26 | class VanillaProgress(Adw.Bin): 27 | __gtype_name__ = "VanillaProgress" 28 | 29 | action_list = Gtk.Template.Child() 30 | 31 | actions = {} 32 | 33 | __not_started = True 34 | __finished = False 35 | __already_skipped = False 36 | __already_removed_autostart_file = False 37 | 38 | def __init__(self, window, **kwargs): 39 | super().__init__(**kwargs) 40 | 41 | self.__window = window 42 | 43 | def set_page_active(self): 44 | self.__window.set_ready(self.__finished) 45 | if self.__not_started: 46 | self.__not_started = False 47 | backend.subscribe_progress(self.__on_items_changed_thread) 48 | thread = threading.Thread(target=backend.start_deferred_actions) 49 | thread.start() 50 | 51 | def set_page_inactive(self): 52 | return 53 | 54 | def finish(self): 55 | if not self.__already_removed_autostart_file: 56 | self.__already_removed_autostart_file = backend.remove_autostart_file() 57 | return True 58 | 59 | def __on_items_changed_thread(self, id: str, uid: str, state: backend.ProgressState, info: dict): 60 | GLib.idle_add(self.__on_items_changed, id, uid, state, info) 61 | 62 | def __on_items_changed(self, id: str, uid: str, state: backend.ProgressState, info: dict): 63 | if id == "all_actions": 64 | if state == backend.ProgressState.Finished: 65 | self.__window.set_ready(True) 66 | self.__finished = True 67 | self.__skip_page_once() 68 | return 69 | 70 | if state == backend.ProgressState.Initialized: 71 | self.__add_new_action(id, uid, info) 72 | return 73 | 74 | status_suffix = None 75 | if state == backend.ProgressState.Running: 76 | status_suffix = Adw.Spinner() 77 | elif state == backend.ProgressState.Finished: 78 | status_suffix = Gtk.Image.new_from_icon_name("emblem-default-symbolic") 79 | status_suffix.add_css_class("success") 80 | elif state == backend.ProgressState.Failed: 81 | status_suffix = Gtk.Image.new_from_icon_name("dialog-warning-symbolic") 82 | status_suffix.add_css_class("error") 83 | 84 | if "suffix" in self.actions[uid]: 85 | self.actions[uid]["suffix"].set_visible(False) 86 | 87 | self.actions[uid]["widget"].add_suffix(status_suffix) 88 | self.actions[uid]["suffix"] = status_suffix 89 | 90 | def __add_new_action(self, id: str, uid: str, info: dict): 91 | title = "" 92 | icon = None 93 | if id == "setup_system": 94 | icon = Gtk.Image.new_from_icon_name("computer-symbolic") 95 | title = _("Setting up the system") 96 | elif id == "install_flatpak": 97 | icon = Gtk.Image.new_from_icon_name(info["app_id"]) 98 | applications.set_app_icon_from_id_async(icon, info["app_id"]) 99 | title = _("Installing") + " " + info["app_name"] 100 | 101 | row = Adw.ActionRow() 102 | row.set_title(title) 103 | icon.add_css_class("lowres-icon") 104 | icon.set_icon_size(Gtk.IconSize.LARGE) 105 | 106 | row.add_prefix(icon) 107 | 108 | self.action_list.add(row) 109 | self.actions[uid] = {"id": id, "info": info, "widget": row} 110 | 111 | def __skip_page_once(self): 112 | if not self.__already_skipped: 113 | self.__already_skipped = True 114 | self.__window.finish_step() 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Vanilla OS First Setup

4 |

This utility is meant to be used in Vanilla OS 5 | as a first-setup wizard. Its purpose is to help the user to configure the 6 | system to their needs, e.g. by configuring hostname, theme, flatpak apps, etc.

7 |
8 | 9 | Translation status 10 | 11 |
12 | 13 |
14 | 15 | ## Run without building for Testing 16 | 17 | > [!IMPORTANT] 18 | > You need to install all build and run dependencies first 19 | 20 | ```bash 21 | python3 test.py -d 22 | ``` 23 | 24 | The "-d" option is the dry-run mode, without it, first-setup will make changes to your system. 25 | 26 | Pass the "-c" flag to force the configure system mode. 27 | 28 | ### Test translations: 29 | 30 | You can change the used language like this: 31 | ```bash 32 | LANGUAGE=de python3 test.py -d 33 | ``` 34 | 35 | ## Build 36 | 37 | ### Installing build dependencies 38 | ```bash 39 | sudo apt-get update 40 | sudo apt-get build-dep . 41 | ``` 42 | 43 | If you want to install the build dependencies manually, have a look in: 44 | [debian/control](https://github.com/Vanilla-OS/first-setup/blob/main/debian/control) 45 | 46 | ### Building 47 | 48 | > [!WARNING] 49 | > dpkg-buildpackage places it's output files (Like the .deb file) into the parent folder. 50 | 51 | ```bash 52 | dpkg-buildpackage 53 | ``` 54 | 55 | or manually with meson: 56 | 57 | ```bash 58 | meson setup build 59 | meson compile -C build 60 | ``` 61 | 62 | Here you can change the install folder (default is /usr/local), for example: 63 | ```bash 64 | meson setup --prefix="$(pwd)/install" build 65 | ``` 66 | 67 | ## Install 68 | 69 | ### Installing runtime dependencies 70 | These can be found here: 71 | [debian/control](https://github.com/Vanilla-OS/first-setup/blob/main/debian/control) 72 | 73 | > [!TIP] 74 | > If you use apt-get to install the .deb file it will automatically install the dependencies. 75 | 76 | ### Installing 77 | 78 | ```bash 79 | sudo apt-get install ./vanilla-first-setup*.deb 80 | ``` 81 | 82 | or manually with meson: 83 | 84 | ```bash 85 | meson install -C build 86 | ``` 87 | 88 | ## Run 89 | 90 | ### Creating initial user 91 | 92 | A special user is needed to run the initial setup for hostname, user-creation, locale, etc. 93 | 94 | 1. Create a user 95 | 2. Create the group vanilla-first-setup (Changing the gid is recommended to avoid messing with user groups) 96 | 3. Add the user to group vanilla-first-setup 97 | 4. Create the file `/var/lib/AccountsService/users/your_user` 98 | ```ini 99 | [User] 100 | Session=firstsetup 101 | ``` 102 | 5. Create the file `/etc/gdm3/daemon.conf` (replace your_user) 103 | ```ini 104 | [daemon] 105 | AutomaticLogin=your_user 106 | AutomaticLoginEnable=True 107 | ``` 108 | 109 | > [!WARNING] 110 | > All users in this group will be deleted on the first reboot after a successful first setup. 111 | 112 | ### Running 113 | ```bash 114 | vanilla-first-setup 115 | ``` 116 | 117 | #### Flags: 118 | 119 | - `--dry-run (-d)`: Don't make any changes to the system. 120 | - `--force-configure-mode (-c)`: Force the configure system mode, independant of group. 121 | - `--force-regular-mode (-r)`: Force the regular mode, independant of group. 122 | - `--oem-mode (-o)`: Use the original equipment manufacturer mode with language, keyboard and timezone selection. 123 | 124 | ## Update translation file 125 | 126 | To update the .pot file with newly added translation strings, run: 127 | 128 | ```bash 129 | meson compile -C build vanilla-first-setup-pot 130 | ``` 131 | 132 | ## Adjust for Custom Image 133 | 134 | ### Adjusting the scripts 135 | 136 | The scripts which are used to modify the system can be found in `/usr/share/org.vanillaos.FirstSetup/vanilla_first_setup/scripts/`. 137 | 138 | Please adjust (overwrite) them to your needs in your image. 139 | 140 | ### Non-GNOME desktops 141 | 142 | If you are using a different desktop than GNOME, you will have to adjust `/usr/share/xsessions/firstsetup.desktop` and `/usr/share/wayland-sessions/firstsetup.desktop`. 143 | 144 | This session is only used to create a new user account. It should be a restricted shell to prevent the user from making changes to the system that will be lost when logging into their own user account. 145 | 146 | If your Desktop doesn't offer this feature, just copy the session of your desktop to firstsetup.desktop. 147 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 96 | -------------------------------------------------------------------------------- /vanilla_first_setup/core/languages.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import vanilla_first_setup.core.timezones as tz 4 | 5 | all_regions = [] 6 | all_country_codes = [] 7 | all_locales = [] 8 | country_codes_by_region = {} 9 | locales_by_country_code = {} 10 | locale_name_by_locale = {} 11 | 12 | with open('/usr/share/i18n/SUPPORTED', 'r') as file: 13 | lines = file.readlines() 14 | for line in lines: 15 | parts = line.split(" ") 16 | if len(parts) < 2 or "UTF-8" not in parts[1]: 17 | continue 18 | loc = parts[0] 19 | lang_with_country = loc.split(".")[0].split("_") 20 | lang = lang_with_country[0] 21 | if len(lang_with_country) < 2: 22 | continue 23 | country_code = lang_with_country[1].split("@")[0] 24 | if country_code not in tz.all_country_codes: 25 | continue 26 | 27 | region = tz.region_from_country_code(country_code) 28 | if region not in all_regions: 29 | all_regions.append(region) 30 | country_codes_by_region[region] = [] 31 | if country_code not in country_codes_by_region[region]: 32 | country_codes_by_region[region].append(country_code) 33 | 34 | if country_code not in all_country_codes: 35 | all_country_codes.append(country_code) 36 | locales_by_country_code[country_code] = [] 37 | if loc not in locales_by_country_code[country_code]: 38 | locales_by_country_code[country_code].append(loc) 39 | 40 | all_locales.append(loc) 41 | 42 | def country_code_from_locale(loc): 43 | for country_code, loc_list in locales_by_country_code.items(): 44 | if loc in loc_list: 45 | return country_code 46 | return "" 47 | 48 | def region_from_locale(loc): 49 | country_code = country_code_from_locale(loc) 50 | for region, country_code_list in country_codes_by_region.items(): 51 | if country_code in country_code_list: 52 | return region 53 | return "" 54 | 55 | for loc in all_locales: 56 | loc_first_part = loc.split(".")[0] 57 | filename = os.path.join('/usr/share/i18n/locales/', loc_first_part) 58 | if not os.path.isfile(filename): 59 | locale_name_by_locale[loc] = loc 60 | continue 61 | lang_name = "" 62 | with open(filename, 'r') as file: 63 | lines = file.readlines() 64 | for line in lines: 65 | if line.startswith("lang_name"): 66 | split_line = line.split() 67 | if len(split_line) >= 2: 68 | lang_name = split_line[1].replace("\"", "").replace("\n", "") 69 | if lang_name == "": 70 | lang_name = loc 71 | locale_name_by_locale[loc] = lang_name + " (" + tz.all_country_names_by_code[country_code_from_locale(loc)] + ")" 72 | 73 | 74 | def search_locales(search_term: str, limit: int) -> tuple[list[str], bool]: 75 | clean_search_term = search_term.lower() 76 | 77 | locales_filtered = [] 78 | list_shortened = False 79 | for loc in all_locales: 80 | locale_name = locale_name_by_locale[loc] 81 | if len(locales_filtered) >= limit: 82 | list_shortened = True 83 | break 84 | does_match = True 85 | for search_term_part in clean_search_term.split(): 86 | if search_term_part not in locale_name.lower(): 87 | does_match = False 88 | if does_match: 89 | locales_filtered.append(loc) 90 | return (locales_filtered, list_shortened) 91 | 92 | class LanguagesDataSource(): 93 | def get_all_regions(self) -> list[str]: 94 | return all_regions 95 | 96 | def find_name_for_region(self, region: str) -> str: 97 | index = tz.all_regions.index(region) 98 | return tz.all_region_names[index] 99 | 100 | def get_all_country_codes(self) -> list[str]: 101 | return all_country_codes 102 | 103 | def get_all_country_codes_by_region(self, region: str) -> list[str]: 104 | return country_codes_by_region[region] 105 | 106 | def find_name_for_country_code(self, country_code: str) -> str: 107 | return tz.all_country_names_by_code[country_code] 108 | 109 | def get_specials_by_country_code(self, country_code: str) -> list[str]: 110 | return locales_by_country_code[country_code] 111 | 112 | def country_code_from_special(self, special: str) -> str: 113 | return country_code_from_locale(special) 114 | 115 | def region_from_special(self, special: str) -> str: 116 | return region_from_locale(special) 117 | 118 | def search_specials(self, search_term: str, max_results: int) -> tuple[list[str], bool]: 119 | locales_filtered, shortened = search_locales(search_term, max_results) 120 | 121 | return locales_filtered, shortened 122 | 123 | def find_name_for_special(self, special: str) -> str|None: 124 | return locale_name_by_locale[special] 125 | 126 | def find_description_for_special(self, special: str) -> str|None: 127 | return special -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | vanilla-first-setup (3.1.0) unstable; urgency=medium 2 | 3 | * Added Decibles, OnlyOffice, Thunderbird and more Browsers to apps 4 | * Removed Gnome Music from apps 5 | * Downloads icons from Flathub instead of bundling them 6 | * Improved hostname validation 7 | 8 | -- Tau Tue, 12 Mar 2025 17:27:00 +0000 9 | 10 | vanilla-first-setup (3.0.0) unstable; urgency=high 11 | 12 | * Completely reworked all components 13 | * Severily improved accessibility 14 | 15 | -- Tau Tue, 18 Feb 2025 03:28:00 +0000 16 | 17 | vanilla-first-setup (2.2.4) unstable; urgency=critical 18 | 19 | * Improved network and connection checks 20 | * Fix logging in recipe 21 | * New translation updates 22 | 23 | -- Dharun Krishna Thu, 24 Oct 2024 10:52:00 -0300 24 | 25 | vanilla-first-setup (2.2.3) unstable; urgency=critical 26 | 27 | * Fixed multi-user support 28 | 29 | -- Tau Wed, 31 Jul 2024 10:52:00 -0300 30 | 31 | vanilla-first-setup (2.2.0) unstable; urgency=critical 32 | 33 | * Fine tune permissions 34 | 35 | -- Mateus Melchiades Fri, 26 Jul 2023 10:52:00 -0300 36 | 37 | vanilla-first-setup (2.1.0) unstable; urgency=critical 38 | 39 | * Add network step 40 | 41 | -- Mateus Melchiades Fri, 26 Jul 2023 10:52:00 -0300 42 | 43 | vanilla-first-setup (2.0.7) unstable; urgency=critical 44 | 45 | * Correctly use log_file variable from recipe 46 | 47 | -- Mateus Melchiades Mon, 30 Jul 2023 19:18:00 -0300 48 | 49 | vanilla-first-setup (2.0.6) unstable; urgency=critical 50 | 51 | * Post script fixes 52 | 53 | -- Mateus Melchiades Mon, 27 Jul 2023 10:35:00 -0300 54 | 55 | vanilla-first-setup (2.0.5) unstable; urgency=critical 56 | 57 | * Default user cleanup fixes 58 | 59 | -- Mateus Melchiades Mon, 27 Jul 2023 09:04:00 -0300 60 | 61 | vanilla-first-setup (2.0.3) unstable; urgency=critical 62 | 63 | * Change log font 64 | * Use polkit for handling cleanup 65 | 66 | -- Mateus Melchiades Mon, 26 Jul 2023 19:58:00 -0300 67 | 68 | vanilla-first-setup (2.0.2) unstable; urgency=critical 69 | 70 | * Complete Orchid port 71 | * Replace eog with Loupe 72 | * Add Epiplany as default browser 73 | 74 | -- Mateus Melchiades Mon, 24 Jul 2023 18:34:00 -0300 75 | 76 | vanilla-first-setup (2.0.1) unstable; urgency=critical 77 | 78 | * Remove ubuntu-drivers-common dependency 79 | * Update recipe to use Debian packages 80 | 81 | -- Mateus Melchiades Mon, 20 Mar 2023 19:44:00 -0300 82 | 83 | vanilla-first-setup (2.0.0) unstable; urgency=critical 84 | 85 | * Express and Advanced mode 86 | * Custom GNOME session 87 | 88 | -- Mirko Brombin Mon, 27 Feb 2023 09:06:00 +0000 89 | 90 | vanilla-first-setup (1.7.4-2) lunar; urgency=critical 91 | 92 | * Fix CSS-provider getting too many arguments 93 | 94 | -- Mateus Melchiades Tue, 28 Feb 2023 19:21:00 -0300 95 | 96 | vanilla-first-setup (1.7.4) lunar; urgency=critical 97 | 98 | * gtk4 changes 99 | 100 | -- Mirko Brombin Mon, 27 Feb 2023 09:06:00 +0000 101 | 102 | vanilla-first-setup (1.7.3-1) lunar; urgency=critical 103 | 104 | * Updated translations 105 | 106 | -- Mirko Brombin Sat, 25 Feb 2023 23:04:00 +0000 107 | 108 | vanilla-first-setup (1.7.3) kinetic; urgency=critical 109 | 110 | * Updated translations 111 | 112 | -- Mirko Brombin Sat, 25 Feb 2023 23:04:00 +0000 113 | 114 | vanilla-first-setup (1.7.2-1) kinetic; urgency=critical 115 | 116 | * Log first setup completion 117 | 118 | -- Mirko Brombin Mon, 13 Feb 2023 22:46:00 +0000 119 | 120 | vanilla-first-setup (1.7.1) kinetic; urgency=critical 121 | 122 | * Make gnome-software mandatory 123 | 124 | -- Mirko Brombin Fri, 20 Jan 2023 21:06:00 +0000 125 | 126 | vanilla-first-setup (1.7.0-3) kinetic; urgency=critical 127 | 128 | * Transaction now runs in a Vte terminal 129 | * Ability to display the output while the transaction is running 130 | * Connection check is now part of the recipe 131 | 132 | -- Mirko Brombin Fri, 20 Jan 2023 21:06:00 +0000 133 | 134 | vanilla-first-setup (1.6.4-1) kinetic; urgency=high 135 | 136 | * Add Lutris and Heroic Games Launcher 137 | * Adapt to ABRoot 1.3.1 138 | 139 | -- Mirko Brombin Fri, 20 Jan 2023 21:06:00 +0000 140 | 141 | vanilla-first-setup (1.6.3) kinetic; urgency=critical 142 | 143 | * Better internet check 144 | * Add nvidia-prime for NVIDIA Optimus support 145 | * Update translations 146 | * Disable autostart on success 147 | * Hide desktop entry on success 148 | 149 | -- Mirko Brombin Thu, 29 Dec 2022 16:29:00 +0000 150 | 151 | vanilla-first-setup (1.6.0) kinetic; urgency=critical 152 | 153 | * Skip applications view with no pkg manager selected 154 | 155 | -- Mirko Brombin Thu, 29 Dec 2022 16:29:00 +0000 156 | 157 | vanilla-first-setup (1.5.9) kinetic; urgency=critical 158 | 159 | * Fix reboot button not being hidden 160 | * Add open-vm-tools-desktop to recipe.json 161 | 162 | -- Mirko Brombin Thu, 29 Dec 2022 14:35:00 +0000 163 | -------------------------------------------------------------------------------- /vanilla_first_setup/apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": [ 3 | { 4 | "name": "Audio Player", 5 | "id": "org.gnome.Decibels" 6 | }, 7 | { 8 | "name": "Calculator", 9 | "id": "org.gnome.Calculator" 10 | }, 11 | { 12 | "name": "Calendar", 13 | "id": "org.gnome.Calendar" 14 | }, 15 | { 16 | "name": "Characters", 17 | "id": "org.gnome.Characters" 18 | }, 19 | { 20 | "name": "Clocks", 21 | "id": "org.gnome.clocks" 22 | }, 23 | { 24 | "name": "Connections", 25 | "id": "org.gnome.Connections" 26 | }, 27 | { 28 | "name": "Contacts", 29 | "id": "org.gnome.Contacts" 30 | }, 31 | { 32 | "name": "Disk Usage Analyzer", 33 | "id": "org.gnome.baobab" 34 | }, 35 | { 36 | "name": "Document Scanner", 37 | "id": "org.gnome.SimpleScan" 38 | }, 39 | { 40 | "name": "Document Viewer", 41 | "id": "org.gnome.Papers" 42 | }, 43 | { 44 | "name": "File Roller", 45 | "id": "org.gnome.FileRoller" 46 | }, 47 | { 48 | "name": "Fonts", 49 | "id": "org.gnome.font-viewer" 50 | }, 51 | { 52 | "name": "Image Viewer", 53 | "id": "org.gnome.Loupe" 54 | }, 55 | { 56 | "name": "Logs", 57 | "id": "org.gnome.Logs" 58 | }, 59 | { 60 | "name": "Maps", 61 | "id": "org.gnome.Maps" 62 | }, 63 | { 64 | "name": "Photos", 65 | "id": "org.gnome.Photos" 66 | }, 67 | { 68 | "name": "Snapshot", 69 | "id": "org.gnome.Snapshot" 70 | }, 71 | { 72 | "name": "Text Editor", 73 | "id": "org.gnome.TextEditor" 74 | }, 75 | { 76 | "name": "Videos", 77 | "id": "org.gnome.Showtime" 78 | }, 79 | { 80 | "name": "Weather", 81 | "id": "org.gnome.Weather" 82 | } 83 | ], 84 | "office": [ 85 | { 86 | "name": "LibreOffice", 87 | "id": "org.libreoffice.LibreOffice" 88 | }, 89 | { 90 | "name": "OnlyOffice", 91 | "id": "org.onlyoffice.desktopeditors", 92 | "active": false 93 | } 94 | ], 95 | "utilities": [ 96 | { 97 | "name": "Bottles", 98 | "id": "com.usebottles.bottles" 99 | }, 100 | { 101 | "name": "Extension Manager", 102 | "id": "com.mattjakeman.ExtensionManager" 103 | }, 104 | { 105 | "name": "Heroic Games Launcher", 106 | "id": "com.heroicgameslauncher.hgl" 107 | }, 108 | { 109 | "name": "Lutris", 110 | "id": "net.lutris.Lutris" 111 | }, 112 | { 113 | "name": "Boxes", 114 | "id": "org.gnome.Boxes" 115 | }, 116 | { 117 | "name": "D\u00e9j\u00e0 Dup Backups", 118 | "id": "org.gnome.DejaDup" 119 | }, 120 | { 121 | "name": "Flatseal", 122 | "id": "com.github.tchx84.Flatseal" 123 | }, 124 | { 125 | "name": "Refine", 126 | "id": "page.tesk.Refine" 127 | }, 128 | { 129 | "name": "Metadata Cleaner", 130 | "id": "fr.romainvigier.MetadataCleaner" 131 | }, 132 | { 133 | "name": "Rnote", 134 | "id": "com.github.flxzt.rnote" 135 | }, 136 | { 137 | "name": "Shortwave", 138 | "id": "de.haeckerfelix.Shortwave" 139 | }, 140 | { 141 | "name": "Sound Recorder", 142 | "id": "org.gnome.SoundRecorder" 143 | }, 144 | { 145 | "name": "Warehouse", 146 | "id": "io.github.flattool.Warehouse" 147 | }, 148 | { 149 | "name": "Thunderbird", 150 | "id": "org.mozilla.Thunderbird" 151 | } 152 | ], 153 | "browsers": [ 154 | { 155 | "name": "Firefox", 156 | "id": "org.mozilla.firefox" 157 | }, 158 | { 159 | "name": "Google Chrome", 160 | "id": "com.google.Chrome", 161 | "active": false 162 | }, 163 | { 164 | "name": "Chromium", 165 | "id": "org.chromium.Chromium", 166 | "active": false 167 | }, 168 | { 169 | "name": "Brave Browser", 170 | "id": "com.brave.Browser", 171 | "active": false 172 | }, 173 | { 174 | "name": "Floorp", 175 | "id": "one.ablaze.floorp", 176 | "active": false 177 | }, 178 | { 179 | "name": "LibreWolf", 180 | "id": "io.gitlab.librewolf-community", 181 | "active": false 182 | }, 183 | { 184 | "name": "Zen", 185 | "id": "app.zen_browser.zen", 186 | "active": false 187 | }, 188 | { 189 | "name": "Microsoft Edge", 190 | "id": "com.microsoft.Edge", 191 | "active": false 192 | }, 193 | { 194 | "name": "Vivaldi", 195 | "id": "com.vivaldi.Vivaldi", 196 | "active": false 197 | }, 198 | { 199 | "name": "GNOME Web", 200 | "id": "org.gnome.Epiphany", 201 | "active": false 202 | } 203 | ] 204 | } 205 | -------------------------------------------------------------------------------- /vanilla_first_setup/application.py: -------------------------------------------------------------------------------- 1 | # application.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import gi 19 | 20 | gi.require_version("Gtk", "4.0") 21 | gi.require_version("Adw", "1") 22 | 23 | from gi.repository import Gtk, Gdk, Gio, GLib, Adw 24 | 25 | import os 26 | import sys 27 | import logging 28 | import grp 29 | _ = __builtins__["_"] 30 | from vanilla_first_setup.window import VanillaWindow 31 | import vanilla_first_setup.core.backend as backend 32 | 33 | logger = logging.getLogger("FirstSetup::Main") 34 | 35 | class FirstSetupApplication(Adw.Application): 36 | """The main application singleton class.""" 37 | 38 | moduledir = "" 39 | 40 | def __init__(self, moduledir: str, *args, **kwargs): 41 | 42 | log_path = "/tmp/first-setup.log" 43 | 44 | self.moduledir = moduledir 45 | 46 | if not os.path.exists(log_path): 47 | try: 48 | open(log_path, "a").close() 49 | os.chmod(log_path, 0o666) 50 | logging.basicConfig(level=logging.DEBUG, 51 | filename=log_path, 52 | filemode='a', 53 | ) 54 | except OSError as e: 55 | logger.warning(f"failed to create log file: {log_path}: {e}") 56 | logging.warning("No log will be stored.") 57 | 58 | 59 | super().__init__( 60 | *args, 61 | application_id="org.vanillaos.FirstSetup", 62 | flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, 63 | **kwargs 64 | ) 65 | self.dry_run = True 66 | 67 | self.__register_arguments() 68 | 69 | def __register_arguments(self): 70 | """Register the command line arguments.""" 71 | self.add_main_option( 72 | "dry-run", 73 | ord("d"), 74 | GLib.OptionFlags.NONE, 75 | GLib.OptionArg.NONE, 76 | _("Don't make any changes to the system."), 77 | None, 78 | ) 79 | self.add_main_option( 80 | "force-configure-mode", 81 | ord("c"), 82 | GLib.OptionFlags.NONE, 83 | GLib.OptionArg.NONE, 84 | _("Force the configure system mode, independant of group."), 85 | None, 86 | ) 87 | self.add_main_option( 88 | "force-regular-mode", 89 | ord("r"), 90 | GLib.OptionFlags.NONE, 91 | GLib.OptionArg.NONE, 92 | _("Force the regular mode, independant of group."), 93 | None, 94 | ) 95 | self.add_main_option( 96 | "oem-mode", 97 | ord("o"), 98 | GLib.OptionFlags.NONE, 99 | GLib.OptionArg.NONE, 100 | _("Use the original equipment manufacturer mode with language, keyboard and timezone selection."), 101 | None, 102 | ) 103 | 104 | def do_command_line(self, command_line): 105 | """Handle command line arguments.""" 106 | options = command_line.get_options_dict() 107 | 108 | if options.lookup_value("dry-run"): 109 | logger.info("Running in dry-run mode.") 110 | self.dry_run = True 111 | else: 112 | self.dry_run = False 113 | 114 | self.force_configure = bool(options.lookup_value("force-configure-mode")) 115 | self.force_regular = bool(options.lookup_value("force-regular-mode")) 116 | self.oem_mode = bool(options.lookup_value("oem-mode")) 117 | 118 | backend.set_dry_run(self.dry_run) 119 | 120 | self.activate() 121 | return 0 122 | 123 | def do_activate(self): 124 | """ 125 | Called when the application is activated. 126 | We raise the application's main window, creating it if 127 | necessary. 128 | """ 129 | all_groups = [g.gr_name for g in grp.getgrall()] 130 | configure_system_mode = False 131 | if "vanilla-first-setup" in all_groups and os.getlogin() in grp.getgrnam("vanilla-first-setup").gr_mem: 132 | configure_system_mode = True 133 | 134 | if self.force_configure: 135 | configure_system_mode = True 136 | elif self.force_regular: 137 | configure_system_mode = False 138 | 139 | if configure_system_mode: 140 | print("Running in configure system mode.") 141 | backend.disable_lockscreen() 142 | else: 143 | print("Running in regular mode.") 144 | backend.setup_system_deferred() 145 | 146 | provider = Gtk.CssProvider() 147 | provider.load_from_resource("/org/vanillaos/FirstSetup/style.css") 148 | Gtk.StyleContext.add_provider_for_display( 149 | display=Gdk.Display.get_default(), 150 | provider=provider, 151 | priority=Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, 152 | ) 153 | win = self.props.active_window 154 | if not win: 155 | win = VanillaWindow( 156 | application=self, 157 | moduledir=self.moduledir, 158 | configure_system_mode=configure_system_mode, 159 | oem_mode=self.oem_mode, 160 | ) 161 | win.present() 162 | 163 | def close(self, *args): 164 | """Close the application.""" 165 | self.quit() 166 | 167 | -------------------------------------------------------------------------------- /vanilla_first_setup/core/keyboard.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import copy 3 | 4 | import gi 5 | gi.require_version("GnomeDesktop", "4.0") 6 | from gi.repository import GnomeDesktop 7 | 8 | import vanilla_first_setup.core.timezones as tz 9 | 10 | logger = logging.getLogger("FirstSetup::Keyboard") 11 | 12 | xkb = GnomeDesktop.XkbInfo() 13 | 14 | all_regions: list[str] = [] 15 | all_region_names: list[str] = [] 16 | all_country_codes: list[str] = [] 17 | all_country_codes_by_region: dict[str, list[str]] = {} 18 | all_keyboard_layouts: list[str] = [] 19 | all_keyboard_layout_names: list[str] = [] 20 | all_keyboard_layouts_by_country_code: dict[str, list[str]] = {} 21 | all_keyboard_layout_names_by_country_code: dict[str, list[str]] = {} 22 | 23 | def region_from_keyboard(keyboard) -> str: 24 | return tz.region_from_country_code(country_code_from_keyboard(keyboard)) 25 | 26 | def country_code_from_keyboard(keyboard) -> str: 27 | for country_code, e_keyboards in all_keyboard_layouts_by_country_code.items(): 28 | for e_keyboard in e_keyboards: 29 | if keyboard == e_keyboard: 30 | return country_code 31 | return "" 32 | 33 | def retrieve_country_names_by_region(region) -> list[str]: 34 | country_codes = all_country_codes_by_region[region] 35 | countries = copy.deepcopy(country_codes) 36 | for idx, country_code in enumerate(countries): 37 | countries[idx] = tz.all_country_names_by_code[country_code] 38 | return countries 39 | 40 | def search_keyboards(search_term: str, limit: int) -> tuple[list[str], bool]: 41 | ''' 42 | search_keyboards looks for all keyboard names with substring search_term 43 | 44 | search is not case sensitive 45 | 46 | returns a list of all matching keyboard names and a bool if the list is shortened due to the limit 47 | ''' 48 | clean_search_term = search_term.lower() 49 | 50 | keyboards_filtered = [] 51 | list_shortened = False 52 | for index, keyboard_layout_name in enumerate(all_keyboard_layout_names): 53 | if len(keyboards_filtered) >= limit: 54 | list_shortened = True 55 | break 56 | does_match = True 57 | for search_term_part in clean_search_term.split(" "): 58 | if search_term_part not in keyboard_layout_name.lower(): 59 | does_match = False 60 | if does_match: 61 | keyboards_filtered.append(all_keyboard_layouts[index]) 62 | return (keyboards_filtered, list_shortened) 63 | 64 | def find_keyboard_layout_name_for_keyboard(keyboard: str) -> str: 65 | index = all_keyboard_layouts.index(keyboard) 66 | return all_keyboard_layout_names[index] 67 | 68 | def is_variant_of_same_layout(keyboard_layout_a: str, keyboard_layout_b: str) -> bool: 69 | info_a = xkb.get_layout_info(keyboard_layout_a) 70 | info_b = xkb.get_layout_info(keyboard_layout_b) 71 | 72 | same_layout = info_a.xkb_layout == info_b.xkb_layout 73 | return same_layout 74 | 75 | for country_code in tz.all_country_codes: 76 | layouts = xkb.get_layouts_for_country(country_code) 77 | layouts.sort(key=len) 78 | 79 | if len(layouts) == 0: 80 | continue 81 | 82 | names = [] 83 | for layout in layouts: 84 | info = xkb.get_layout_info(layout) 85 | names.append(info.display_name) 86 | 87 | region = tz.region_from_country_code(country_code) 88 | if region not in all_country_codes_by_region: 89 | all_country_codes_by_region[region] = [] 90 | 91 | all_country_codes.append(country_code) 92 | all_country_codes_by_region[region].append(country_code) 93 | all_keyboard_layouts_by_country_code[country_code] = layouts 94 | all_keyboard_layout_names_by_country_code[country_code] = names 95 | 96 | for layout in layouts: 97 | if layout not in all_keyboard_layouts: 98 | all_keyboard_layouts.append(layout) 99 | 100 | for country_code in all_country_codes: 101 | region = tz.region_from_country_code(country_code) 102 | if region not in all_regions: 103 | all_regions.append(region) 104 | 105 | all_regions.sort() 106 | for region in all_regions: 107 | index_in_tz = tz.all_regions.index(region) 108 | region_name = tz.all_region_names[index_in_tz] 109 | all_region_names.append(region_name) 110 | 111 | all_keyboard_layouts.sort(key=len) 112 | for keyboard_layout in all_keyboard_layouts: 113 | info = xkb.get_layout_info(keyboard_layout) 114 | all_keyboard_layout_names.append(info.display_name) 115 | 116 | class KeyboardsDataSource(): 117 | def get_all_regions(self) -> list[str]: 118 | return all_regions 119 | 120 | def find_name_for_region(self, region: str) -> str: 121 | index = all_regions.index(region) 122 | return all_region_names[index] 123 | 124 | def get_all_country_codes(self) -> list[str]: 125 | return all_country_codes 126 | 127 | def get_all_country_codes_by_region(self, region: str) -> list[str]: 128 | return all_country_codes_by_region[region] 129 | 130 | def find_name_for_country_code(self, country_code: str) -> str: 131 | return tz.all_country_names_by_code[country_code] 132 | 133 | def get_specials_by_country_code(self, country_code: str) -> list[str]: 134 | return all_keyboard_layouts_by_country_code[country_code] 135 | 136 | def country_code_from_special(self, special: str) -> str: 137 | return country_code_from_keyboard(special) 138 | 139 | def region_from_special(self, special: str) -> str: 140 | return region_from_keyboard(special) 141 | 142 | def search_specials(self, search_term: str, max_results: int) -> tuple[list[str], bool]: 143 | timezones_filtered, shortened = search_keyboards(search_term, max_results) 144 | 145 | return timezones_filtered, shortened 146 | 147 | def find_name_for_special(self, special: str) -> str|None: 148 | return find_keyboard_layout_name_for_keyboard(special) 149 | 150 | def find_description_for_special(self, special: str) -> str|None: 151 | return special 152 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/user.py: -------------------------------------------------------------------------------- 1 | # user.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # Copyright 2023 muqtadir 5 | # 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundationat version 3 of the License. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import re 20 | import subprocess 21 | from gi.repository import Gtk, Adw 22 | _ = __builtins__["_"] 23 | 24 | import vanilla_first_setup.core.backend as backend 25 | 26 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/user.ui") 27 | class VanillaUser(Adw.Bin): 28 | __gtype_name__ = "VanillaUser" 29 | 30 | fullname_entry = Gtk.Template.Child() 31 | username_entry = Gtk.Template.Child() 32 | error = Gtk.Template.Child() 33 | password_entry = Gtk.Template.Child() 34 | password_confirmation = Gtk.Template.Child() 35 | 36 | username = "" 37 | __user_changed_username = False 38 | 39 | fullname = "" 40 | 41 | 42 | __automatic_username = "" 43 | 44 | def __init__(self, window, **kwargs): 45 | super().__init__(**kwargs) 46 | self.__window = window 47 | 48 | self.fullname_entry.connect("changed", self.__on_fullname_entry_changed) 49 | self.username_entry.connect("changed", self.__on_username_entry_changed) 50 | self.fullname_entry.connect("entry-activated", self.__on_activate) 51 | self.username_entry.connect("entry-activated", self.__on_activate) 52 | self.password_entry.connect("changed", self.__on_password_entry_changed) 53 | self.password_confirmation.connect("changed", self.__on_password_confirmation_changed) 54 | 55 | self.existing_users = subprocess.Popen("getent passwd | cut -d: -f1", shell=True, 56 | stdout=subprocess.PIPE).stdout.read().decode().splitlines() 57 | 58 | def set_page_active(self): 59 | self.fullname_entry.grab_focus() 60 | self.__verify_continue() 61 | 62 | def set_page_inactive(self): 63 | return 64 | 65 | def finish(self): 66 | backend.add_user_deferred(self.username, self.fullname, self.password) 67 | return True 68 | 69 | def __on_activate(self, widget): 70 | self.__window.finish_step() 71 | 72 | def __on_fullname_entry_changed(self, *args): 73 | fullname = self.fullname_entry.get_text() 74 | 75 | self.fullname = fullname 76 | self.__verify_continue() 77 | 78 | self.__generate_username_from_fullname() 79 | 80 | def __on_username_entry_changed(self, *args): 81 | entry_text = self.username_entry.get_text() 82 | if entry_text != "" and entry_text != self.__automatic_username: 83 | self.__user_changed_username = True 84 | 85 | err = self.__verify_username() 86 | 87 | if err != "": 88 | self.username = "" 89 | self.username_entry.add_css_class("error") 90 | self.error.set_label(err) 91 | self.error.set_opacity(1) 92 | self.__verify_continue() 93 | return 94 | 95 | self.username = entry_text 96 | self.username_entry.remove_css_class("error") 97 | self.error.set_opacity(0) 98 | self.__verify_continue() 99 | 100 | def __generate_username_from_fullname(self): 101 | if self.__user_changed_username: 102 | return 103 | 104 | if self.fullname == "": 105 | return 106 | 107 | username_stripped = self.fullname.strip() 108 | username_no_whitespace = "-".join(username_stripped.split()) 109 | username_lowercase = username_no_whitespace.lower() 110 | 111 | self.__automatic_username = username_lowercase 112 | self.username_entry.set_text(username_lowercase) 113 | 114 | def __verify_continue(self): 115 | password_err = self.__verify_password() 116 | ready = self.username != "" and self.fullname != "" and password_err == "" 117 | self.__window.set_ready(ready) 118 | 119 | def __verify_username(self) -> str: 120 | input = self.username_entry.get_text() 121 | 122 | if not input: 123 | return _("Username cannot be empty.") 124 | 125 | if len(input) > 32: 126 | return _("Username cannot be longer than 32 characters.") 127 | 128 | if re.search(r"[^a-z0-9_-]", input): 129 | return _("Username cannot contain special characters or uppercase letters.") 130 | 131 | if input in self.existing_users: 132 | _status = False 133 | return _("This username is already in use.") 134 | 135 | return "" 136 | 137 | def __verify_password(self) -> str: 138 | password = self.password_entry.get_text() 139 | confirmation = self.password_confirmation.get_text() 140 | 141 | if not password: 142 | return _("Password cannot be empty.") 143 | 144 | if password != confirmation: 145 | return _("Passwords do not match.") 146 | 147 | return "" 148 | 149 | def __on_password_entry_changed(self, *args): 150 | self.password = self.password_entry.get_text() 151 | self.__update_password_styles() 152 | self.__verify_continue() 153 | 154 | def __on_password_confirmation_changed(self, *args): 155 | self.__update_password_styles() 156 | self.__verify_continue() 157 | 158 | def __update_password_styles(self): 159 | err = self.__verify_password() 160 | if err != "": 161 | self.password_entry.add_css_class("error") 162 | self.password_confirmation.add_css_class("error") 163 | self.error.set_text(err) 164 | self.error.set_opacity(1) 165 | else: 166 | self.password_entry.remove_css_class("error") 167 | self.password_confirmation.remove_css_class("error") 168 | self.error.set_opacity(0) 169 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/org.vanillaos.FirstSetup-flower.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /vanilla_first_setup/core/backend.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import time 4 | import os 5 | import subprocess 6 | 7 | script_base_path = None 8 | 9 | dry_run = True 10 | 11 | _progress_subscribers = [] 12 | _error_subscribers = [] 13 | 14 | errors = [] 15 | 16 | class ProgressState(Enum): 17 | Initialized = 1 18 | Running = 2 19 | Finished = 3 20 | Failed = 4 21 | 22 | def set_keyboard(keyboard: str): 23 | keyboard_parts = keyboard.split("+", 1) 24 | layout = keyboard_parts[0] 25 | variant = "" 26 | if len(keyboard_parts) > 1: 27 | variant = keyboard_parts[1] 28 | return run_script("keyboard", ["pc105", layout, variant], root=True) 29 | 30 | # sets the currently used keyboard of the desktop environment 31 | def set_live_keyboard(keyboard: str): 32 | return run_script("live-keyboard", [keyboard]) 33 | 34 | def set_locale(locale: str): 35 | return run_script("locale", [locale], root=True) 36 | 37 | def set_timezone(timezone: str): 38 | return run_script("timezone", [timezone], root=True) 39 | 40 | def set_hostname(hostname: str): 41 | return run_script("hostname", [hostname], root=True) 42 | 43 | def set_theme(theme: str) -> str|None: 44 | return run_script("theme", [theme]) 45 | 46 | def _add_user(username: str, full_name: str, password: str): 47 | return run_script("user", [username, full_name], root=True, input_data=password) 48 | 49 | def logout(): 50 | return run_script("logout", []) 51 | 52 | def open_network_settings(): 53 | return run_script("open-network-settings", []) 54 | 55 | def open_accessibility_settings(): 56 | return run_script("open-accessibility-settings", []) 57 | 58 | def disable_lockscreen(): 59 | return run_script("disable-lockscreen", []) 60 | 61 | def setup_flatpak_remote(): 62 | return run_script("setup-flatpak-remote", []) 63 | 64 | def remove_first_setup_user(): 65 | return run_script("remove-first-setup-user", [], root=True) 66 | 67 | def remove_autostart_file(): 68 | return run_script("remove-autostart-file", []) 69 | 70 | def _setup_system(): 71 | return run_script("setup-system", []) 72 | 73 | def _install_flatpak(id: str): 74 | return run_script("flatpak", [id]) 75 | 76 | 77 | def run_script(name: str, args: list[str], root: bool = False, input_data: str = None) -> bool: 78 | if dry_run: 79 | print("dry-run", name, args) 80 | time.sleep(0.3) 81 | return True 82 | if script_base_path == None: 83 | print("Could not run operation", name, args, "due to missing script base path") 84 | return True 85 | script_path = os.path.join(script_base_path, name) 86 | command = [script_path] + args 87 | if root: 88 | command = ["pkexec"] + command 89 | process = subprocess.Popen( 90 | command, 91 | stdout=subprocess.PIPE, 92 | stderr=subprocess.STDOUT, 93 | text=True, 94 | stdin=subprocess.PIPE 95 | ) 96 | 97 | result = "" 98 | try: 99 | result, _ = process.communicate(input=input_data, timeout=300) 100 | except subprocess.TimeoutExpired: 101 | process.kill() 102 | result, _ = process.communicate() 103 | 104 | if process.returncode != 0: 105 | report_error(name, command, result) 106 | print(name, args, "returned an error:") 107 | print(result) 108 | return False 109 | 110 | return True 111 | 112 | _error_count = 0 113 | _lock_error_count = False 114 | 115 | def report_error(script_name: str, command: list[str], message: str): 116 | global _error_count 117 | global _lock_error_count 118 | while(_lock_error_count): 119 | time.sleep(0.5) 120 | _lock_error_count = True 121 | 122 | errors.append(message) 123 | 124 | for callback in _error_subscribers: 125 | callback(script_name, command, _error_count) 126 | 127 | _error_count = _error_count + 1 128 | _lock_error_count = False 129 | 130 | _deferred_actions = {} 131 | 132 | def setup_system_deferred(): 133 | global _deferred_actions 134 | action_id = "setup_system" 135 | uid = action_id 136 | def setup_system(): 137 | _run_function_with_progress(action_id, uid, None, _setup_system) 138 | _deferred_actions[uid] = {"action_id": action_id, "uid": uid, "callback": setup_system} 139 | report_progress(action_id, uid, ProgressState.Initialized) 140 | 141 | def add_user_deferred(username: str, full_name: str, password: str): 142 | global _deferred_actions 143 | action_id = "add_user" 144 | uid = action_id 145 | action_info = {"username": username, "full_name": full_name} 146 | def add_user(): 147 | _run_function_with_progress(action_id, uid, action_info, _add_user, username, full_name, password) 148 | _deferred_actions[uid] = {"action_id": action_id, "callback": add_user, "info": action_info} 149 | report_progress(action_id, uid, ProgressState.Initialized, action_info) 150 | 151 | def install_flatpak_deferred(id: str, name: str): 152 | global _deferred_actions 153 | action_id = "install_flatpak" 154 | uid = action_id+id 155 | action_info = {"app_id": id, "app_name": name} 156 | def install_flatpak(): 157 | _run_function_with_progress(action_id, uid, action_info, _install_flatpak, id) 158 | _deferred_actions[uid] = {"action_id": action_id, "callback": install_flatpak, "info": action_info} 159 | report_progress(action_id, uid, ProgressState.Initialized, action_info) 160 | 161 | def _run_function_with_progress(action_id: str, uid: str, action_info: dict, function, *args): 162 | report_progress(action_id, uid, ProgressState.Running, action_info) 163 | success = function(*args) 164 | if not success: 165 | report_progress(action_id, uid, ProgressState.Failed, action_info) 166 | else: 167 | report_progress(action_id, uid, ProgressState.Finished, action_info) 168 | 169 | def clear_flatpak_deferred(): 170 | global _deferred_actions 171 | new_list = {} 172 | for uid, action in _deferred_actions.items(): 173 | if action["action_id"] != "install_flatpak": 174 | new_list[uid] = action 175 | _deferred_actions = new_list 176 | 177 | def start_deferred_actions(): 178 | global _deferred_actions 179 | for _, action in _deferred_actions.items(): 180 | action["callback"]() 181 | id = "all_actions" 182 | report_progress(id, id, ProgressState.Finished) 183 | 184 | def subscribe_progress(callback): 185 | global _deferred_actions 186 | global _progress_subscribers 187 | _progress_subscribers.append(callback) 188 | for uid, deferred_action in _deferred_actions.items(): 189 | info = None 190 | if "info" in deferred_action: 191 | info = deferred_action["info"] 192 | callback(deferred_action["action_id"], uid, ProgressState.Initialized, info) 193 | 194 | def report_progress(id: str, uid: str, state: ProgressState, info = None): 195 | global _progress_subscribers 196 | for subscriber in _progress_subscribers: 197 | subscriber(id, uid, state, info) 198 | 199 | def set_script_path(path: str): 200 | global script_base_path 201 | script_base_path = path 202 | 203 | def set_dry_run(dry: bool): 204 | global dry_run 205 | dry_run = dry 206 | 207 | def subscribe_errors(callback): 208 | global _error_subscribers 209 | _error_subscribers.append(callback) 210 | -------------------------------------------------------------------------------- /vanilla_first_setup/views/applications.py: -------------------------------------------------------------------------------- 1 | # applications.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import copy 18 | import json 19 | import os 20 | 21 | from gi.repository import Gtk, Adw 22 | _ = __builtins__["_"] 23 | 24 | import vanilla_first_setup.core.backend as backend 25 | import vanilla_first_setup.core.applications as applications 26 | 27 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/applications-dialog.ui") 28 | class VanillaApplicationsDialog(Adw.Window): 29 | __gtype_name__ = "VanillaApplicationsDialog" 30 | 31 | apply_button = Gtk.Template.Child() 32 | applications_group = Gtk.Template.Child() 33 | 34 | __apps = {} 35 | __category = "" 36 | 37 | __finish_callback = None 38 | 39 | def __init__(self, window, apps, category: str, finish_callback, **kwargs): 40 | super().__init__(**kwargs) 41 | self.set_transient_for(window) 42 | 43 | self.__apps = copy.deepcopy(apps) 44 | self.__category = category 45 | self.__finish_callback = finish_callback 46 | 47 | self.apply_button.connect("clicked", self.__on_apply_button_clicked) 48 | 49 | shortcut_controller = Gtk.ShortcutController.new() 50 | shortcut_controller.add_shortcut( 51 | Gtk.Shortcut.new( 52 | Gtk.ShortcutTrigger.parse_string("Escape"), Gtk.CallbackAction.new(self.__on_escape_key) 53 | ) 54 | ) 55 | self.add_controller(shortcut_controller) 56 | 57 | self.__build_apps() 58 | self.set_visible(True) 59 | 60 | def __on_apply_button_clicked(self, widget): 61 | self.set_visible(False) 62 | self.__finish_callback(self.__apps) 63 | 64 | def __on_escape_key(self, action, callback=None): 65 | self.set_visible(False) 66 | self.__finish_callback(self.__apps) 67 | 68 | def __build_apps(self): 69 | for app in self.__apps[self.__category]: 70 | apps_action_row = Adw.ActionRow( 71 | title=app["name"], 72 | ) 73 | app_icon = Gtk.Image.new_from_icon_name(app["id"]) 74 | app_icon.set_icon_size(Gtk.IconSize.LARGE) 75 | app_icon.add_css_class("lowres-icon") 76 | applications.set_app_icon_from_id_async(app_icon, app["id"]) 77 | 78 | apps_action_row.add_prefix(app_icon) 79 | 80 | app_switch = Gtk.Switch() 81 | app_switch.set_active(True) 82 | if "active" in app: 83 | app_switch.set_active(app["active"]) 84 | app_switch.set_valign(Gtk.Align.CENTER) 85 | app_switch.set_focusable(False) 86 | app_switch.connect("state-set", self.__on_switch_state_change, app["id"]) 87 | 88 | apps_action_row.add_suffix(app_switch) 89 | apps_action_row.set_activatable_widget(app_switch) 90 | 91 | self.applications_group.add(apps_action_row) 92 | 93 | def __on_switch_state_change(self, widget, state, id): 94 | for app in self.__apps[self.__category]: 95 | if app["id"] == id: 96 | app["active"] = state 97 | break 98 | 99 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/layout-applications.ui") 100 | class VanillaLayoutApplications(Adw.Bin): 101 | __gtype_name__ = "VanillaLayoutApplications" 102 | 103 | bundles_list = Gtk.Template.Child() 104 | core_switch = Gtk.Template.Child() 105 | core_button = Gtk.Template.Child() 106 | browsers_switch = Gtk.Template.Child() 107 | browsers_button = Gtk.Template.Child() 108 | utilities_switch = Gtk.Template.Child() 109 | utilities_button = Gtk.Template.Child() 110 | office_switch = Gtk.Template.Child() 111 | office_button = Gtk.Template.Child() 112 | 113 | __apps = {} 114 | 115 | __already_setup_remote = False 116 | 117 | def __init__(self, window, **kwargs): 118 | super().__init__(**kwargs) 119 | self.__window = window 120 | 121 | apps_file_path = os.path.join(window.moduledir, "apps.json") 122 | with open(apps_file_path) as file: 123 | self.__apps = json.load(file) 124 | 125 | self.core_switch.connect("state-set", self.__on_core_switch_state_change) 126 | self.browsers_switch.connect("state-set", self.__on_browsers_switch_state_change) 127 | self.utilities_switch.connect("state-set", self.__on_utilities_switch_state_change) 128 | self.office_switch.connect("state-set", self.__on_office_switch_state_change) 129 | 130 | self.core_button.connect("clicked", self.__on_customize_button_clicked, "core") 131 | self.browsers_button.connect("clicked", self.__on_customize_button_clicked, "browsers") 132 | self.utilities_button.connect("clicked", self.__on_customize_button_clicked, "utilities") 133 | self.office_button.connect("clicked", self.__on_customize_button_clicked, "office") 134 | 135 | def set_page_active(self): 136 | if not self.__already_setup_remote: 137 | success = backend.setup_flatpak_remote() 138 | self.__already_setup_remote = success 139 | self.__window.set_ready(True) 140 | self.__window.set_focus_on_next() 141 | 142 | def set_page_inactive(self): 143 | return 144 | 145 | def finish(self): 146 | enabled_categories = [] 147 | if self.core_switch.get_active(): 148 | enabled_categories.append("core") 149 | if self.browsers_switch.get_active(): 150 | enabled_categories.append("browsers") 151 | if self.utilities_switch.get_active(): 152 | enabled_categories.append("utilities") 153 | if self.office_switch.get_active(): 154 | enabled_categories.append("office") 155 | 156 | backend.clear_flatpak_deferred() 157 | for category in enabled_categories: 158 | for app in self.__apps[category]: 159 | if "active" not in app or app["active"]: 160 | app_id = app["id"] 161 | app_name = app["name"] 162 | backend.install_flatpak_deferred(app_id, app_name) 163 | return True 164 | 165 | 166 | def __on_core_switch_state_change(self, widget, state): 167 | self.core_button.set_sensitive(state) 168 | 169 | def __on_browsers_switch_state_change(self, widget, state): 170 | self.browsers_button.set_sensitive(state) 171 | 172 | def __on_utilities_switch_state_change(self, widget, state): 173 | self.utilities_button.set_sensitive(state) 174 | 175 | def __on_office_switch_state_change(self, widget, state): 176 | self.office_button.set_sensitive(state) 177 | 178 | def __on_customize_button_clicked(self, widget, app_type: str): 179 | dialog = None 180 | 181 | def update_apps(apps): 182 | self.__apps = apps 183 | dialog.destroy() 184 | return 185 | 186 | dialog = VanillaApplicationsDialog(self.__window, self.__apps, app_type, update_apps) 187 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/layout-applications.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 139 | 140 | -------------------------------------------------------------------------------- /vanilla_first_setup/gtk/theme.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 149 | 150 | -------------------------------------------------------------------------------- /vanilla_first_setup/window.py: -------------------------------------------------------------------------------- 1 | # window.py 2 | # 3 | # Copyright 2023 mirkobrombin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundationat version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import threading 18 | 19 | from gi.repository import Gtk, Adw, GLib 20 | 21 | import vanilla_first_setup.core.backend as backend 22 | 23 | from vanilla_first_setup.dialog import VanillaDialog 24 | 25 | _ = __builtins__["_"] 26 | 27 | @Gtk.Template(resource_path="/org/vanillaos/FirstSetup/gtk/window.ui") 28 | class VanillaWindow(Adw.ApplicationWindow): 29 | __gtype_name__ = "VanillaWindow" 30 | 31 | stack = Gtk.Template.Child() 32 | btn_back = Gtk.Template.Child() 33 | btn_next = Gtk.Template.Child() 34 | btn_next_spinner = Gtk.Template.Child() 35 | toasts = Gtk.Template.Child() 36 | style_manager = Adw.StyleManager().get_default() 37 | 38 | can_continue = False 39 | 40 | __is_finishing_step = False 41 | 42 | pages = [] 43 | __current_page_index = 0 44 | 45 | def __init__(self, moduledir: str, configure_system_mode: bool, oem_mode: bool = False, **kwargs): 46 | super().__init__(**kwargs) 47 | 48 | self.moduledir = moduledir 49 | self.configure_system_mode = configure_system_mode 50 | self.oem_mode = oem_mode 51 | 52 | self.__build_ui(configure_system_mode) 53 | self.__connect_signals() 54 | 55 | backend.subscribe_errors(self.__error_received) 56 | 57 | def set_ready(self, ready: bool = True): 58 | self.__loading_indicator(False) 59 | self.can_continue = ready 60 | self.btn_next.set_sensitive(ready) 61 | 62 | def finish_step(self): 63 | if not self.can_continue or self.__is_finishing_step: 64 | return 65 | self.can_continue = False 66 | self.__is_finishing_step = True 67 | 68 | self.__loading_indicator() 69 | 70 | thread = threading.Thread(target=self.__finish_step_thread) 71 | thread.start() 72 | 73 | def __error_received(self, script_name: str, command: list[str], id: int): 74 | GLib.idle_add(self.__error_toast, _("Setup failed: ") + script_name, id) 75 | 76 | def __error_toast(self, message: str, id: int): 77 | toast = Adw.Toast.new(message) 78 | toast.props.timeout = 0 79 | toast.props.button_label = _("Details") 80 | toast.connect("button-clicked", self.__error_toast_clicked, id) 81 | self.toasts.add_toast(toast) 82 | 83 | def __error_toast_clicked(self, widget, id: int): 84 | message = backend.errors[id] 85 | dialog = VanillaDialog(self, _("Error log"), message) 86 | dialog.present() 87 | 88 | def set_focus_on_next(self): 89 | self.btn_next.grab_focus() 90 | 91 | def __connect_signals(self): 92 | self.btn_back.connect("clicked", self.__on_btn_back_clicked) 93 | self.btn_next.connect("clicked", self.__on_btn_next_clicked) 94 | return 95 | 96 | def __build_ui(self, configure_system_mode: bool): 97 | 98 | if configure_system_mode: 99 | from vanilla_first_setup.views.welcome import VanillaWelcome 100 | if self.oem_mode: 101 | from vanilla_first_setup.views.language import VanillaLanguage 102 | from vanilla_first_setup.views.keyboard import VanillaKeyboard 103 | from vanilla_first_setup.views.timezone import VanillaTimezone 104 | from vanilla_first_setup.views.hostname import VanillaHostname 105 | from vanilla_first_setup.views.user import VanillaUser 106 | from vanilla_first_setup.views.logout import VanillaLogout 107 | 108 | self.__view_welcome = VanillaWelcome(self) 109 | self.__view_welcome.no_next_button = True 110 | self.__view_welcome.no_back_button = True 111 | if self.oem_mode: 112 | self.__view_language = VanillaLanguage(self) 113 | self.__view_language.no_back_button = True 114 | self.__view_keyboard = VanillaKeyboard(self) 115 | self.__view_timezone = VanillaTimezone(self) 116 | self.__view_hostname = VanillaHostname(self) 117 | else: 118 | self.__view_hostname = VanillaHostname(self) 119 | self.__view_hostname.no_back_button = True 120 | self.__view_user = VanillaUser(self) 121 | self.__view_logout = VanillaLogout(self) 122 | self.__view_logout.no_next_button = True 123 | 124 | self.pages.append(self.__view_welcome) 125 | if self.oem_mode: 126 | self.pages.append(self.__view_language) 127 | self.pages.append(self.__view_keyboard) 128 | self.pages.append(self.__view_timezone) 129 | self.pages.append(self.__view_hostname) 130 | self.pages.append(self.__view_user) 131 | self.pages.append(self.__view_logout) 132 | else: 133 | from vanilla_first_setup.views.welcome_user import VanillaWelcomeUser 134 | from vanilla_first_setup.views.conn_check import VanillaConnCheck 135 | from vanilla_first_setup.views.theme import VanillaTheme 136 | from vanilla_first_setup.views.applications import VanillaLayoutApplications 137 | from vanilla_first_setup.views.progress import VanillaProgress 138 | from vanilla_first_setup.views.done import VanillaDone 139 | 140 | self.__view_welcome = VanillaWelcomeUser(self) 141 | self.__view_welcome.no_next_button = True 142 | self.__view_welcome.no_back_button = True 143 | self.__view_conn_check = VanillaConnCheck(self) 144 | self.__view_conn_check.no_back_button = True 145 | self.__view_theme = VanillaTheme(self) 146 | self.__view_theme.no_back_button = True 147 | self.__view_apps = VanillaLayoutApplications(self) 148 | self.__view_progress = VanillaProgress(self) 149 | self.__view_progress.no_back_button = True 150 | self.__view_done = VanillaDone(self) 151 | self.__view_done.no_next_button = True 152 | 153 | self.pages.append(self.__view_welcome) 154 | self.pages.append(self.__view_conn_check) 155 | self.pages.append(self.__view_theme) 156 | self.pages.append(self.__view_apps) 157 | self.pages.append(self.__view_progress) 158 | self.pages.append(self.__view_done) 159 | 160 | for page in self.pages: 161 | self.stack.add_child(page) 162 | 163 | self.stack.set_visible_child(self.__view_welcome) 164 | 165 | self.__update_button_visibility(self.pages[0]) 166 | self.__on_page_changed() 167 | 168 | def __on_page_changed(self, *args): 169 | current_page = self.stack.get_visible_child() 170 | current_page.set_page_active() 171 | 172 | def __on_btn_next_clicked(self, widget): 173 | self.finish_step() 174 | 175 | def __on_btn_back_clicked(self, widget): 176 | if self.__is_finishing_step: 177 | return 178 | self.__last_page() 179 | 180 | def __loading_indicator(self, waiting: bool = True): 181 | if self.__current_page_index == 0: 182 | self.btn_next.set_visible(False) 183 | self.btn_next_spinner.set_visible(False) 184 | return 185 | 186 | self.btn_next.set_visible(not waiting) 187 | self.btn_next_spinner.set_visible(waiting) 188 | 189 | def __finish_step_thread(self): 190 | success = self.stack.get_visible_child().finish() 191 | if success: 192 | GLib.idle_add(self.__next_page) 193 | else: 194 | GLib.idle_add(self.__fail) 195 | 196 | def __fail(self): 197 | self.__is_finishing_step = False 198 | self.__loading_indicator(False) 199 | self.set_ready(False) 200 | 201 | def __next_page(self): 202 | target_index = self.__current_page_index + 1 203 | self.__scroll_page(target_index) 204 | self.__is_finishing_step = False 205 | 206 | def __last_page(self): 207 | target_index = self.__current_page_index - 1 208 | self.__scroll_page(target_index) 209 | 210 | def __scroll_page(self, target_index: int): 211 | self.set_ready(False) 212 | 213 | old_current_page = self.stack.get_visible_child() 214 | target_page = self.pages[target_index] 215 | 216 | self.__update_button_visibility(target_page) 217 | 218 | self.stack.set_visible_child(target_page) 219 | self.__current_page_index = target_index 220 | 221 | old_current_page.set_page_inactive() 222 | self.__on_page_changed() 223 | 224 | def __update_button_visibility(self, current_page): 225 | no_back = getattr(current_page, "no_back_button", False) 226 | no_next = getattr(current_page, "no_next_button", False) 227 | 228 | self.btn_back.set_visible(not no_back) 229 | self.btn_next.set_visible(not no_next) 230 | 231 | --------------------------------------------------------------------------------