#!/bin/bash ################################################################################ # # opensuse-setup.sh # Interactive post-install setup script for OpenSUSE installations # ################################################################################ ################################################################################ # # Constants # ################################################################################ ######################################## # OpenSUSE patterns PATTERNS=( # (Base system) patterns-base-apparmor patterns-base-base patterns-base-enhanced_base # (Base fonts) patterns-fonts-fonts # (Desktop environment) patterns-gnome-gnome_basic ) ######################################## # Zypper packages # To remove RM=( yast2-* nautilus-share nautilus-sendto gnome-console MozillaFirefox-branding-openSUSE opensuse-welcome ) # To remove (and prevent reinstallation of) AL=( gnome-clocks gnome-packagekit gnome-software tigervnc xterm yast2 ) # To install IN=( # (Necessary packages) git-core flatpak opi podman # (Display) papirus-icon-theme fira-code-fonts gdouros-symbola-fonts google-noto-sans-cjk-fonts google-noto-coloremoji-fonts noto-fonts fetchmsttfonts # (Shell utilities) fish fzf ispell zenity fortune toilet cowsay wmctrl libnotify-tools ripgrep jq # (Programs) emacs inkscape gimp shotcut audacity geary nextcloud-desktop steam blender # (Firefox w/o OpenSUSE branding) MozillaFirefox-branding-upstream MozillaFirefox # (GNOME utilities) loupe dconf-editor baobab totem menulibre Fragments rsvg-thumbnailer gnome-session-wayland gnome-characters gnome-disk-utility gnome-font-viewer gnome-dictionary gnome-logs gnome-screenshot gnome-weather # (Dependencies) webp-pixbuf-loader # (Misc.) distribution-logos-openSUSE distribution-logos-openSUSE-icons distribution-logos-openSUSE-Tumbleweed ) ######################################## # Flatpak packages FLATPAK_PACKAGES=( com.discordapp.Discord com.spotify.Client com.github.tchx84.Flatseal org.nickvision.tagger ) ######################################## # GNOME extensions GNOME_EXTENSIONS=( AlphabeticalAppGrid@stuarthayhurst color-picker@tuberry ) ######################################## # Junk files JUNK_FILES=( .inputrc .i18n .bashrc .xim-template bin/ ) ######################################## # URLs PACKMAN_REPO_URL="https://ftp.gwdg.de/pub/linux/misc/packman/suse/openSUSE_Tumbleweed/" FLATHUB_REMOTE_URL="https://flathub.org/repo/flathub.flatpakrepo" REPOS_API_URL="https://git.tty.dog/api/v1/repos/search" ################################################################################ # # Pre-setup prompts # ################################################################################ ######################################## # Pre-setup # Ensure we're not running as root if [[ "${EUID}" == 0 ]]; then echo "DO NOT RUN THIS SCRIPT AS ROOT!" echo "Please run this script as a regular user." exit 1 fi # Ensure PackageKit is not running so that it doesn't interrupt us sudo systemctl stop packagekit # Ensure "dialog" is installed echo "Installing dialog utility..." sudo zypper --non-interactive in dialog ######################################## # Prompt for system chassis system_chassis=$(hostnamectl chassis) function string_eq () { [[ "${1}" == "${2}" ]] && echo ON || echo OFF } function prompt_chassis_type () { entry=$(dialog --title "Chassis Type" --stdout --no-cancel --erase-on-exit \ --radiolist "Confirm the chassis type for this system:" 0 0 0 \ "Desktop" "Stationary form factor" $(string_eq "${system_chassis}" "desktop") \ "Laptop" "Clamshell form factor" $(string_eq "${system_chassis}" "laptop") \ "Tablet" "Touchscreen form factor" $(string_eq "${system_chassis}" "tablet") \ "Convertible" "Hybrid form factor" $(string_eq "${system_chassis}" "convertible")) # Ensure entry is lowercase entry=${entry,,} case "${entry}" in "desktop"|"laptop"|"convertible"|"tablet") if dialog --title "Chassis Type" --defaultno --yesno "Use ${entry} as chassis type?" 0 0 then system_chassis="${entry}" return 0 else return 1 fi ;; *) dialog --title "Chassis Type" --msgbox "Please choose a chassis type for this system." 0 0 return 1 ;; esac } # Repeatedly show the prompt until we receive valid input until prompt_chassis_type; do : ; done # Set chassis type echo "Setting chassis type to ${system_chassis}" sudo hostnamectl chassis "${system_chassis}" ######################################## # Prompt for system name system_name=$(hostnamectl hostname) function prompt_system_name () { entry=$(dialog --title "System Name" --stdout --no-cancel --erase-on-exit \ --inputbox "All lowercase letters, no spaces or symbols:" 0 0 \ "${system_name}") # Filter all non-letter characters out of the user's entry entry=${entry//[^[:alpha:]]/} # Ensure entry is lowercase entry=${entry,,} case "${entry}" in "") dialog --title "Chassis Type" --msgbox "Please enter a name for this system." 0 0 return 1 ;; *) if dialog --title "Chassis Type" --defaultno --yesno "Use ${entry} as system name?" 0 0 then system_name="${entry}" return 0 else return 1 fi ;; esac } # Repeatedly show the prompt until we receive valid input until prompt_system_name; do : ; done # Set hostname and pretty hostname echo "Setting hostname to ${system_name}..." sudo hostnamectl hostname "${system_name}" echo "Setting pretty hostname to ${system_name^}" sudo hostnamectl hostname --pretty "${system_name^}" ######################################## # Prompt for optional additions optional_additions="" function prompt_optional_additions () { ENTRY=$(dialog --title "Optional Additions" --stdout --no-cancel --erase-on-exit --single-quoted --output-separator '|' \ --checklist "Select any additional configuration changes that you would like applied to the system:" 0 0 0 \ "Xournal++" "Install Xournal++ for taking handwritten notes" OFF \ "EasyEffects" "Install EasyEffects for applying effects (e.g. equalizers) to system audio devices" OFF \ "Media Control Key Shortcuts" "Set up media control keyboard shortcuts (for systems without dedicated media control keys)" OFF \ "Power Button Screen Lock Override" "Override the power button's default behavior with a screen lock shortcut (for tablets and some convertibles)" OFF ) case "${ENTRY}" in "") if dialog --title "Chassis Type" --defaultno --yesno "Apply only baseline system configuration?" 0 0 then return 0 else return 1 fi ;; *) if ADDITIONS_LIST=$(echo "${ENTRY}" | sed "s/|/\\\\n * /g" | sed "s/'//g") dialog --title "Chassis Type" --defaultno --yesno "Confirm your selection of the following additions:\n${ADDITIONS_LIST}" 0 0 then optional_additions="${ENTRY}" return 0 else return 1 fi ;; esac } # Repeatedly show the prompt until we receive valid input until prompt_optional_additions; do : ; done ################################################################################ # # Feature detection # ################################################################################ detected_features=() # Mobile system detection case "${system_chassis}" in "latop"|"convertible"|"tablet") detected_features+=('Mobile') ;; esac # x86-64-v3 CPU support detection CPU_FLAGS=$(cat /proc/cpuinfo | grep flags | head -n 1 | cut -d: -f2) if echo ${CPU_FLAGS} | awk '/avx/&&/avx2/&&/bmi1/&&/bmi2/&&/f16c/&&/fma/&&/abm/&&/movbe/&&/xsave/ {found=1} END {exit !found}' then detected_features+=('x86-64-v3') fi # Touchpad detection if cat /proc/bus/input/devices | grep -i Touchpad then detected_features+=('Touchpad') fi # Windows multi-boot detection if sudo grep menuentry /boot/grub2/grub.cfg | grep -Po ".*Windows.*" then detected_features+=('Windows') fi ################################################################################ # # Package setup # ################################################################################ ######################################## # Package operations echo "Setting up packages..." sudo zypper --non-interactive install ${PATTERNS[@]} if [[ "${detected_features[@]}" == *"Mobile"* ]]; then sudo zypper --non-interactive install patterns-desktop-mobile fi if [[ "${detected_features[@]}" == *"x86-64-v3"* ]]; then sudo zypper --non-interactive install patterns-glibc-hwcaps-x86_64_v3 fi sudo zypper --non-interactive remove ${RM[@]} ${AL[@]} sudo zypper --non-interactive addlock ${AL[@]} if ! sudo zypper --non-interactive install ${IN[@]}; then echo "Package installation failed, running interactively..." sudo zypper install ${IN[@]} fi # Install any codecs not hosted by OpenSUSE sudo zypper --non-interactive --gpg-auto-import-keys addrepo -cfp 90 "${PACKMAN_REPO_URL}" packman sudo zypper --non-interactive --gpg-auto-import-keys install --from packman ffmpeg gstreamer-plugins-{good,bad,ugly,libav} libavcodec-full pipewire-aptx sudo zypper --non-interactive --gpg-auto-import-keys dist-upgrade --from packman --allow-vendor-change # FIX: ispell-american doesn't properly install its dictionary files the first time around, for some reason sudo zypper --non-interactive install --force ispell-american ######################################## # Flatpak operations echo "Setting up Flatpak remotes..." sudo flatpak remote-add --if-not-exists flathub "${FLATHUB_REMOTE}" echo "Setting up Flatpak packages..." sudo flatpak install --noninteractive ${FLATPAK_PACKAGES[@]} ######################################## # (OPTIONAL) Extra packages if [[ "${optional_additions}" == *"Xournal++"* ]]; then echo "Installing Xournal++..." sudo zypper --non-interactive addlock texlive sudo zypper --non-interactive install xournalpp fi if [[ "${optional_additions}" == *"EasyEffects"* ]]; then echo "Installing EasyEffects..." sudo zypper --non-interactive install easyeffects fi ################################################################################ # # System configuration # ################################################################################ ######################################## # GRUB configuration # Silence GRUB echo "Configuring GRUB..." echo "# Automatically added by opensuse-personalizer.sh GRUB_TIMEOUT=0 GRUB_HIDDEN_TIMEOUT=0 GRUB_QUIET=true GRUB_HIDDEN_TIMEOUT_QUIET=true" | sudo tee --append /etc/default/grub sudo grub2-mkconfig -o /boot/grub2/grub.cfg ######################################## # PolicyKit configuration # Create a local policy that unlocks basic networking settings for users echo "Configuring PolicyKit..." sudo mkdir -p /etc/polkit/localauthority/50-local.d/ echo "[Allow users to modify system network settings] Identity=unix-group:users Action=org.freedesktop.NetworkManager.settings.modify.system ResultAny=auth_admin_keep ResultInactive=auth_admin_keep ResultActive=yes" | sudo tee /etc/polkit/localauthority/50-local.d/10-NetworkManager.pkla ################################################################################ # # GNOME keyboard shortcuts # ################################################################################ echo "Configuring GNOME keyboard shortcuts..." ######################################## # Navigation gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-left "['Left']" gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-right "['Right']" gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-left "['Left']" gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-right "['Right']" # Swap "switch applications" (grouped alt+tab) for "switch windows" (non-grouped alt+tab) shortcuts gsettings set org.gnome.shell.window-switcher current-workspace-only false gsettings set org.gnome.desktop.wm.keybindings switch-windows "$(gsettings get org.gnome.desktop.wm.keybindings switch-applications)" gsettings set org.gnome.desktop.wm.keybindings switch-windows-backward "$(gsettings get org.gnome.desktop.wm.keybindings switch-applications-backward)" gsettings set org.gnome.desktop.wm.keybindings switch-applications "[]" gsettings set org.gnome.desktop.wm.keybindings switch-applications-backward "[]" ######################################## # (OPTIONAL) Media if [[ "${optional_additions}" == *"Media Control Key Shortcuts"* ]]; then gsettings set org.gnome.settings-daemon.plugins.media-keys mic-mute "['Home']" gsettings set org.gnome.settings-daemon.plugins.media-keys next "['Page_Down']" gsettings set org.gnome.settings-daemon.plugins.media-keys play "['End']" gsettings set org.gnome.settings-daemon.plugins.media-keys previous "['Page_Up']" gsettings set org.gnome.settings-daemon.plugins.media-keys volume-down "['Page_Down']" gsettings set org.gnome.settings-daemon.plugins.media-keys volume-mute "['End']" gsettings set org.gnome.settings-daemon.plugins.media-keys volume-up "['Page_Up']" fi ######################################## # Windows gsettings set org.gnome.desktop.wm.keybindings close "['w']" gsettings set org.gnome.desktop.wm.keybindings toggle-fullscreen "['f']" ######################################## # Custom shortcuts # Helper function (because gsettings doesn't make this simple) function add_custom_gnome_keybinding () { EXISTING_KEYBINDINGS=$(gsettings get org.gnome.settings-daemon.plugins.media-keys custom-keybindings) KEYBINDING_INDEX=$(expr $(echo $EXISTING_KEYBINDINGS | grep -oE '[0-9]+' | sort -n | tail -n 1) + 1) KEYBINDING_PATH=/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom${KEYBINDING_INDEX}/ if [[ "$EXISTING_KEYBINDINGS" == "@as []" ]]; then gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "['${KEYBINDING_PATH}']" else gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "$(echo ${EXISTING_KEYBINDINGS} | sed s/.$//), '${KEYBINDING_PATH}']" fi gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:${KEYBINDING_PATH} name "${1}" gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:${KEYBINDING_PATH} command "${2}" gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:${KEYBINDING_PATH} binding "${3}" } # Add custom shortcuts add_custom_gnome_keybinding "Launch Terminal" "gnome-terminal" "Return" add_custom_gnome_keybinding "Launch System Monitor" "gnome-system-monitor" "Escape" add_custom_gnome_keybinding "Toggle Night Light" "gnome-toggle-night-light" "n" # (OPTIONAL) Add custom shortcuts if [[ "${detected_features[@]}" == *"Touchpad"* ]]; then add_custom_gnome_keybinding "Toggle Touchpad Lock" "gnome-toggle-touchpad-lock" "t" fi if [[ "${optional_additions}" == *"Power Button Screen Lock Override"* ]]; then add_custom_gnome_keybinding "Lock Screen" "gdm-lock-screen" "PowerOff" fi ################################################################################ # # GNOME preferences configuration # ################################################################################ echo "Configuring GNOME preferences..." gsettings set org.gnome.desktop.peripherals.mouse speed -0.995 gsettings set org.gnome.desktop.interface icon-theme "Papirus" gsettings set org.gnome.desktop.interface clock-format '24h' gsettings set org.gnome.desktop.interface clock-show-date true gsettings set org.gnome.desktop.interface clock-show-weekday true gsettings set org.gnome.desktop.interface font-name "Cantarell 10" gsettings set org.gnome.desktop.interface document-font-name "Cantarell 10" gsettings set org.gnome.desktop.interface monospace-font-name "Fira Code 10" gsettings set org.gnome.desktop.wm.preferences auto-raise true gsettings set org.gnome.desktop.wm.preferences action-middle-click-titlebar "minimize" gsettings set org.gnome.desktop.notifications show-in-lock-screen false gsettings set org.gnome.settings-daemon.plugins.color night-light-enabled true gsettings set org.gnome.settings-daemon.plugins.color night-light-temperature 3700 gsettings set org.gnome.settings-daemon.plugins.color night-light-schedule-automatic false gsettings set org.gnome.settings-daemon.plugins.color night-light-schedule-from 22 gsettings set org.gnome.settings-daemon.plugins.color night-light-schedule-to 6 ################################################################################ # # GNOME app folder configuration # ################################################################################ echo "Configuring GNOME app folders..." ######################################## # Favorites gsettings set org.gnome.shell favorite-apps "['org.gnome.Terminal.desktop', 'firefox.desktop', 'emacsclient.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Nautilus.desktop']" ######################################## # Utilities folder UTILITIES_FOLDER_PATH=/org/gnome/desktop/app-folders/folders/Utilities/ gsettings set org.gnome.desktop.app-folders folder-children "['Utilities']" gsettings set org.gnome.desktop.app-folders.folder:${UTILITIES_FOLDER_PATH} name "X-GNOME-Utilities.directory" gsettings set org.gnome.desktop.app-folders.folder:${UTILITIES_FOLDER_PATH} translate true gsettings set org.gnome.desktop.app-folders.folder:${UTILITIES_FOLDER_PATH} categories "[]" gsettings set org.gnome.desktop.app-folders.folder:${UTILITIES_FOLDER_PATH} apps "['org.gnome.FileRoller.desktop', 'org.gnome.Characters.desktop', 'ca.desrt.dconf-editor.desktop', 'org.gnome.Dictionary.desktop', 'org.gnome.baobab.desktop', 'org.gnome.DiskUtility.desktop', 'org.gnome.Evince.desktop', 'com.github.wwmm.easyeffects.desktop', 'org.gnome.Extensions.desktop', 'com.github.tchx84.Flatseal.desktop', 'org.gnome.font-viewer.desktop', 'de.haeckerfelix.Fragments.desktop', 'org.gnome.Loupe.desktop', 'org.gnome.Logs.desktop', 'menulibre.desktop', 'org.gnome.Screenshot.desktop', 'org.gnome.SystemMonitor.desktop', 'org.nickvision.tagger.desktop', 'org.gnome.tweaks.desktop', 'uefi-firmware-settings.desktop', 'org.gnome.Totem.desktop']" ################################################################################ # # GNOME extension setup # ################################################################################ echo "Setting up GNOME extensions..." for EXTENSION in "${GNOME_EXTENSIONS[@]}"; do busctl --user call org.gnome.Shell.Extensions /org/gnome/Shell/Extensions org.gnome.Shell.Extensions InstallRemoteExtension s ${EXTENSION} done ################################################################################ # # Junk file removal # ################################################################################ echo "Removing junk files..." for JUNK_FILE in "${JUNK_FILES[@]}"; do rm -r "${HOME}/${JUNK_FILE}" done ################################################################################ # # Personal configuration files and scripts # ################################################################################ ######################################## # Projects folder setup echo "Setting up Projects folder..." # Ensure ${HOME}/Projects exists PROJECTS_DIR=${HOME}/Projects if [ ! -d ${PROJECTS_DIR} ]; then mkdir ${PROJECTS_DIR} gio set ${PROJECTS_DIR} metadata::custom-icon-name folder-code fi # Configure git git config --global user.name "Jessie Hildebrandt" git config --global user.email "jessieh@jessieh.net" # Perform a blobless clone of all public repos at git.tty.dog for CLONE_URL in $(curl -s "${REPOS_API_URL}?uid=1" | jq -r ".data.[].ssh_url"); do git -C ${PROJECTS_DIR} clone --no-checkout --filter=blob:none "${CLONE_URL}" done ######################################## # Dotfiles echo "Setting up dotfiles..." # Download dotfiles DOTFILES_SOURCE_DIR=${PROJECTS_DIR}/dot-files git -C ${DOTFILES_SOURCE_DIR} checkout # Link application entries mkdir -p ${HOME}/.local/share/applications ln -s ${DOTFILES_SOURCE_DIR}/applications/* ${HOME}/.local/share/applications # Link scripts mkdir -p ${HOME}/.local/bin sudo ln -s ${DOTFILES_SOURCE_DIR}/bin/* /usr/bin # Copy configuration files mkdir -p ${HOME}/.config cp -r ${DOTFILES_SOURCE_DIR}/config/* ${HOME}/.config/ # (OPTIONAL) Remove unneeded files, depending on features selected if [[ "${detected_features[@]}" != *'Touchpad'* ]]; then sudo rm /usr/bin/gnome-toggle-touchpad-lock fi if [[ "${detected_features[@]}" != *'Windows'* ]]; then sudo rm /usr/bin/reboot-windows fi ######################################## # Emacs echo "Setting up Emacs..." # Remove any vendor-provided Emacs configuration files rm ${HOME}/.emacs ${HOME}/.emacs.d/init.el # Download and link Emacs configuration EMACS_CONFIG_DIR=${HOME}/.config/emacs EMACS_SOURCE_DIR=${PROJECTS_DIR}/dot-emacs mkdir -p ${EMACS_CONFIG_DIR} git -C ${EMACS_SOURCE_DIR} checkout ln -s ${EMACS_SOURCE_DIR}/init.el ${EMACS_CONFIG_DIR}/init.el # Download and link Tempel templates for Emacs... TEMPEL_CONFIG_DIR=${HOME}/.config/emacs/templates TEMPEL_SOURCE_DIR=${PROJECTS_DIR}/tempel-templates git -C ${TEMPEL_SOURCE_DIR} checkout ln -s ${TEMPEL_SOURCE_DIR} ${TEMPEL_CONFIG_DIR} # Enable and start Emacs daemon systemctl --user enable emacs.service systemctl --user start emacs.service ######################################## # Fish echo "Setting up Fish..." # Download and link Fish configuration FISH_CONFIG_DIR=${HOME}/.config/fish FISH_SOURCE_DIR=${PROJECTS_DIR}/dot-fish mkdir -p ${FISH_CONFIG_DIR} git -C ${FISH_SOURCE_DIR} checkout ln -s ${FISH_SOURCE_DIR}/config.fish ${FISH_CONFIG_DIR}/config.fish # Set Fish as default shell sudo chsh ${USER} -s $(which fish) ################################################################################ # # Post-setup suggestions # ################################################################################ echo " Personalization complete! Recommended next steps: • Log in to Firefox Sync • Configure Nextcloud client $(if [[ "${optional_additions}" == *"EasyEffects"* ]]; then echo " • Install an audio device profile from"; echo " https://autoeq.app"; echo; fi) • Install a Wireguard VPN profile from https://mullvad.net/en/account/#/wireguard-config/?platform=linux • Configure display settings and run 'gdm-update-display-settings' • Run 'themer' to install/update unofficial libadwaita themes A copy of these recommendations has been saved to ${HOME}/next-steps" | tee ${HOME}/next-steps