1094 lines
33 KiB
Bash
Executable File
1094 lines
33 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
LOG_FILE="$HOME/cachyos-post-install.log"
|
|
|
|
GIT_USER_NAME="Jose Lago"
|
|
GIT_USER_EMAIL="jose@lago.dev"
|
|
|
|
OFFICIAL_PACKAGES=(
|
|
brave-browser
|
|
chromium
|
|
filezilla
|
|
thunderbird
|
|
libreoffice-fresh
|
|
code
|
|
docker
|
|
docker-compose
|
|
telegram-desktop
|
|
signal-desktop
|
|
git
|
|
base-devel
|
|
curl
|
|
wget
|
|
jq
|
|
xdg-utils
|
|
flatpak
|
|
cifs-utils
|
|
smbclient
|
|
ttf-meslo-nerd
|
|
zsh-completions
|
|
ghostty
|
|
fastfetch
|
|
lm_sensors
|
|
pciutils
|
|
hwdata
|
|
variety
|
|
python-pywal
|
|
tela-circle-icon-theme-all
|
|
zsh
|
|
)
|
|
|
|
AUR_PACKAGES=(
|
|
opencode-bin
|
|
ulauncher
|
|
wireguard-gui-bin
|
|
vesktop-bin
|
|
sone
|
|
)
|
|
|
|
FLATPAK_PACKAGES=(
|
|
io.gitlab.adhami3310.Impression
|
|
com.mattjakeman.ExtensionManager
|
|
)
|
|
|
|
NAS_HOST="192.168.178.3"
|
|
NAS_CREDENTIALS_FILE="/etc/samba/credentials-unraid"
|
|
NAS_MOUNT_BASE="/mnt/nas"
|
|
NAS_SHARES=(
|
|
isos
|
|
Data
|
|
)
|
|
|
|
BRAVE_POLICY_DIR="/etc/brave/policies/managed"
|
|
BRAVE_POLICY_FILE="$BRAVE_POLICY_DIR/extensions.json"
|
|
|
|
BITWARDEN_EXTENSION_ID="nngceckbapebfimnlniiiahkandclblb"
|
|
FLOCCUS_EXTENSION_ID="fnaicdffflnofjppbagibeoednhnbjhg"
|
|
CHROME_WEBSTORE_UPDATE_URL="https://clients2.google.com/service/update2/crx"
|
|
|
|
ORCHIS_REPO_URL="https://github.com/vinceliuice/Orchis-theme.git"
|
|
ORCHIS_SOURCE_DIR="$HOME/.cache/Orchis-theme"
|
|
ORCHIS_THEME_NAME="Orchis-Grey-Dark"
|
|
ORCHIS_THEME_VARIANT="grey"
|
|
ORCHIS_COLOR_VARIANT="dark"
|
|
ORCHIS_SIZE_VARIANT="standard"
|
|
|
|
GNOME_ICON_THEME="Adwaita"
|
|
GNOME_CURSOR_THEME="Adwaita"
|
|
GNOME_COLOR_SCHEME="prefer-dark"
|
|
GNOME_ACCENT_COLOR_PRIMARY="slate"
|
|
GNOME_ACCENT_COLOR_FALLBACK="grey"
|
|
|
|
log() {
|
|
echo -e "\n==> $*" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
warn() {
|
|
echo -e "\n[WARN] $*" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
die() {
|
|
echo -e "\n[ERROR] $*" | tee -a "$LOG_FILE"
|
|
exit 1
|
|
}
|
|
|
|
require_not_root() {
|
|
if [[ "${EUID}" -eq 0 ]]; then
|
|
die "Do not run this script as root. Run it as your normal user; the script will use sudo when needed."
|
|
fi
|
|
}
|
|
|
|
check_cachyos() {
|
|
log "Checking system..."
|
|
|
|
if [[ -f /etc/os-release ]]; then
|
|
# shellcheck disable=SC1091
|
|
source /etc/os-release
|
|
echo "Detected OS: ${PRETTY_NAME:-unknown}" | tee -a "$LOG_FILE"
|
|
fi
|
|
|
|
if ! command -v pacman >/dev/null 2>&1; then
|
|
die "pacman is not available. This script is intended for CachyOS/Arch."
|
|
fi
|
|
}
|
|
|
|
full_system_update() {
|
|
log "Synchronizing package databases..."
|
|
sudo pacman -Sy --noconfirm
|
|
|
|
if pacman -Qu >/dev/null 2>&1; then
|
|
log "System updates found. Updating full system..."
|
|
sudo pacman -Syu --noconfirm
|
|
else
|
|
log "No system updates found. Skipping full upgrade."
|
|
fi
|
|
}
|
|
|
|
install_drivers_with_chwd() {
|
|
log "Installing/configuring drivers with CachyOS Hardware Detection..."
|
|
|
|
if ! command -v chwd >/dev/null 2>&1; then
|
|
warn "chwd is not installed. Trying to install it."
|
|
sudo pacman -S --needed --noconfirm chwd || warn "Could not install chwd. Continuing without driver autoconfiguration."
|
|
fi
|
|
|
|
if command -v chwd >/dev/null 2>&1; then
|
|
log "Showing hardware detected by chwd..."
|
|
chwd --list || true
|
|
|
|
log "Running chwd autoconfiguration."
|
|
sudo chwd --autoconfigure || warn "chwd could not autoconfigure something. Review the log: $LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
install_official_packages() {
|
|
log "Checking official packages..."
|
|
|
|
missing_packages=()
|
|
|
|
for package in "${OFFICIAL_PACKAGES[@]}"; do
|
|
if pacman -Q "$package" >/dev/null 2>&1; then
|
|
log "Already installed: $package"
|
|
else
|
|
missing_packages+=("$package")
|
|
fi
|
|
done
|
|
|
|
if (( ${#missing_packages[@]} > 0 )); then
|
|
log "Installing missing official packages: ${missing_packages[*]}"
|
|
sudo pacman -S --needed --noconfirm "${missing_packages[@]}"
|
|
else
|
|
log "All official packages are already installed. Skipping."
|
|
fi
|
|
}
|
|
|
|
ensure_yay() {
|
|
if command -v yay >/dev/null 2>&1; then
|
|
log "yay is already installed."
|
|
return
|
|
fi
|
|
|
|
log "Installing yay from AUR..."
|
|
tmpdir="$(mktemp -d)"
|
|
|
|
git clone https://aur.archlinux.org/yay-bin.git "$tmpdir/yay-bin"
|
|
|
|
pushd "$tmpdir/yay-bin" >/dev/null
|
|
makepkg -si --noconfirm
|
|
popd >/dev/null
|
|
|
|
rm -rf "$tmpdir"
|
|
}
|
|
|
|
install_aur_packages() {
|
|
ensure_yay
|
|
|
|
log "Checking AUR packages..."
|
|
|
|
missing_aur_packages=()
|
|
|
|
for package in "${AUR_PACKAGES[@]}"; do
|
|
if pacman -Q "$package" >/dev/null 2>&1; then
|
|
log "Already installed: $package"
|
|
else
|
|
missing_aur_packages+=("$package")
|
|
fi
|
|
done
|
|
|
|
if (( ${#missing_aur_packages[@]} > 0 )); then
|
|
log "Installing missing AUR packages: ${missing_aur_packages[*]}"
|
|
yay -S --needed --noconfirm "${missing_aur_packages[@]}"
|
|
else
|
|
log "All AUR packages are already installed. Skipping."
|
|
fi
|
|
}
|
|
|
|
|
|
install_flatpak_packages() {
|
|
log "Checking Flatpak and Flathub..."
|
|
|
|
if ! command -v flatpak >/dev/null 2>&1; then
|
|
warn "flatpak is not available. Skipping Flatpak apps."
|
|
return
|
|
fi
|
|
|
|
if ! flatpak remotes --columns=name | grep -qx "flathub"; then
|
|
log "Adding Flathub remote..."
|
|
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
else
|
|
log "Flathub remote already configured."
|
|
fi
|
|
|
|
for flatpak_app in "${FLATPAK_PACKAGES[@]}"; do
|
|
if flatpak info "$flatpak_app" >/dev/null 2>&1; then
|
|
log "Already installed Flatpak: $flatpak_app"
|
|
else
|
|
log "Installing Flatpak: $flatpak_app"
|
|
flatpak install -y flathub "$flatpak_app"
|
|
fi
|
|
done
|
|
}
|
|
|
|
setup_docker() {
|
|
log "Enabling Docker..."
|
|
|
|
sudo systemctl enable --now docker
|
|
|
|
if ! groups "$USER" | grep -q "\bdocker\b"; then
|
|
sudo usermod -aG docker "$USER"
|
|
warn "Added your user to the docker group. Log out and back in, or reboot, to use docker without sudo."
|
|
else
|
|
log "User already belongs to docker group."
|
|
fi
|
|
|
|
docker --version | tee -a "$LOG_FILE" || true
|
|
}
|
|
|
|
set_brave_default() {
|
|
log "Setting Brave as default browser..."
|
|
|
|
if command -v xdg-settings >/dev/null 2>&1; then
|
|
current_browser="$(xdg-settings get default-web-browser 2>/dev/null || true)"
|
|
|
|
if [[ "$current_browser" == "brave-browser.desktop" ]]; then
|
|
log "Brave is already the default browser."
|
|
else
|
|
xdg-settings set default-web-browser brave-browser.desktop || warn "Could not set default browser with xdg-settings."
|
|
fi
|
|
fi
|
|
|
|
xdg-mime default brave-browser.desktop x-scheme-handler/http || true
|
|
xdg-mime default brave-browser.desktop x-scheme-handler/https || true
|
|
xdg-mime default brave-browser.desktop text/html || true
|
|
|
|
log "Brave configured as default browser."
|
|
}
|
|
|
|
install_brave_extensions_policy() {
|
|
log "Checking Brave extension policy..."
|
|
|
|
desired_policy="$(mktemp)"
|
|
|
|
cat > "$desired_policy" <<POLICY
|
|
{
|
|
"ExtensionSettings": {
|
|
"${BITWARDEN_EXTENSION_ID}": {
|
|
"installation_mode": "force_installed",
|
|
"update_url": "${CHROME_WEBSTORE_UPDATE_URL}",
|
|
"toolbar_pin": "force_pinned"
|
|
},
|
|
"${FLOCCUS_EXTENSION_ID}": {
|
|
"installation_mode": "force_installed",
|
|
"update_url": "${CHROME_WEBSTORE_UPDATE_URL}",
|
|
"toolbar_pin": "force_pinned"
|
|
}
|
|
}
|
|
}
|
|
POLICY
|
|
|
|
if [[ -f "$BRAVE_POLICY_FILE" ]] && cmp -s "$desired_policy" "$BRAVE_POLICY_FILE"; then
|
|
log "Brave extension policy already configured. Skipping."
|
|
rm -f "$desired_policy"
|
|
return
|
|
fi
|
|
|
|
log "Applying Brave extension policy without uninstalling existing extensions..."
|
|
sudo mkdir -p "$BRAVE_POLICY_DIR"
|
|
|
|
if [[ -f "$BRAVE_POLICY_FILE" ]]; then
|
|
sudo cp "$BRAVE_POLICY_FILE" "$BRAVE_POLICY_FILE.backup.$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
|
|
sudo cp "$desired_policy" "$BRAVE_POLICY_FILE"
|
|
rm -f "$desired_policy"
|
|
|
|
log "Policy created at: $BRAVE_POLICY_FILE"
|
|
log "Bitwarden will be force-installed and pinned first."
|
|
log "Floccus will be force-installed and pinned second."
|
|
warn "Brave will show these extensions as managed. They apply after opening/restarting Brave. Check brave://policy"
|
|
}
|
|
|
|
install_orchis_theme() {
|
|
log "Installing and applying Orchis theme in grey dark variant..."
|
|
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
warn "git is not available. Skipping Orchis installation."
|
|
return
|
|
fi
|
|
|
|
if [[ -d "$ORCHIS_SOURCE_DIR/.git" ]]; then
|
|
log "Orchis source already exists. Updating repository..."
|
|
git -C "$ORCHIS_SOURCE_DIR" pull --ff-only || warn "Could not update Orchis repository. Continuing with existing copy."
|
|
else
|
|
log "Cloning Orchis theme repository..."
|
|
rm -rf "$ORCHIS_SOURCE_DIR"
|
|
git clone --depth 1 "$ORCHIS_REPO_URL" "$ORCHIS_SOURCE_DIR"
|
|
fi
|
|
|
|
if [[ ! -x "$ORCHIS_SOURCE_DIR/install.sh" ]]; then
|
|
chmod +x "$ORCHIS_SOURCE_DIR/install.sh"
|
|
fi
|
|
|
|
if [[ -d "$HOME/.themes/$ORCHIS_THEME_NAME" ]]; then
|
|
log "Orchis theme already exists: $HOME/.themes/$ORCHIS_THEME_NAME"
|
|
log "Re-running installer only to ensure GTK4/libadwaita links and tweaks are correct."
|
|
fi
|
|
|
|
(
|
|
cd "$ORCHIS_SOURCE_DIR"
|
|
./install.sh \
|
|
--dest "$HOME/.themes" \
|
|
--theme "$ORCHIS_THEME_VARIANT" \
|
|
--color "$ORCHIS_COLOR_VARIANT" \
|
|
--size "$ORCHIS_SIZE_VARIANT" \
|
|
--libadwaita \
|
|
--tweaks solid compact black primary submenu dock
|
|
)
|
|
|
|
log "Orchis installation step completed."
|
|
}
|
|
|
|
apply_gnome_theme_and_accent() {
|
|
log "Applying GNOME theme, shell theme, and accent color..."
|
|
|
|
if ! command -v gsettings >/dev/null 2>&1; then
|
|
warn "gsettings is not available. Skipping GNOME theme configuration."
|
|
return
|
|
fi
|
|
|
|
set_gsetting_if_available() {
|
|
local schema="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
|
|
if gsettings writable "$schema" "$key" >/dev/null 2>&1; then
|
|
current_value="$(gsettings get "$schema" "$key" 2>/dev/null || true)"
|
|
|
|
if [[ "$current_value" == "" ]]; then
|
|
log "Already set: $schema $key = $value"
|
|
else
|
|
log "Setting: $schema $key = $value"
|
|
gsettings set "$schema" "$key" "$value"
|
|
fi
|
|
else
|
|
warn "GSettings key not available or not writable: $schema $key"
|
|
fi
|
|
}
|
|
|
|
set_gsetting_if_available org.gnome.desktop.interface color-scheme "$GNOME_COLOR_SCHEME"
|
|
detected_orchis_theme="$ORCHIS_THEME_NAME"
|
|
|
|
if [[ ! -d "$HOME/.themes/$detected_orchis_theme" ]]; then
|
|
detected_orchis_theme="$(find "$HOME/.themes" -maxdepth 1 -type d -name "Orchis-Grey-Dark*" -printf "%f
|
|
" 2>/dev/null | sort | head -n 1 || true)"
|
|
fi
|
|
|
|
if [[ -z "$detected_orchis_theme" ]]; then
|
|
warn "Could not find an installed Orchis-Grey-Dark theme under $HOME/.themes. Skipping GTK theme application."
|
|
else
|
|
log "Using detected Orchis theme: $detected_orchis_theme"
|
|
set_gsetting_if_available org.gnome.desktop.interface gtk-theme "$detected_orchis_theme"
|
|
set_gsetting_if_available org.gnome.desktop.wm.preferences theme "$detected_orchis_theme"
|
|
fi
|
|
|
|
set_gsetting_if_available org.gnome.desktop.interface icon-theme "$GNOME_ICON_THEME"
|
|
set_gsetting_if_available org.gnome.desktop.interface cursor-theme "$GNOME_CURSOR_THEME"
|
|
|
|
if gsettings writable org.gnome.desktop.interface accent-color >/dev/null 2>&1; then
|
|
accent_range="$(gsettings range org.gnome.desktop.interface accent-color 2>/dev/null || true)"
|
|
|
|
if echo "$accent_range" | grep -qw "$GNOME_ACCENT_COLOR_PRIMARY"; then
|
|
set_gsetting_if_available org.gnome.desktop.interface accent-color "$GNOME_ACCENT_COLOR_PRIMARY"
|
|
elif echo "$accent_range" | grep -qw "$GNOME_ACCENT_COLOR_FALLBACK"; then
|
|
set_gsetting_if_available org.gnome.desktop.interface accent-color "$GNOME_ACCENT_COLOR_FALLBACK"
|
|
else
|
|
warn "GNOME accent color grey/slate is not available on this system. Available range: $accent_range"
|
|
fi
|
|
else
|
|
warn "GNOME accent-color key is not available. Skipping GNOME accent setting."
|
|
fi
|
|
|
|
if command -v gnome-extensions >/dev/null 2>&1; then
|
|
if gnome-extensions list | grep -q "user-theme@gnome-shell-extensions.gcampax.github.com"; then
|
|
gnome-extensions enable user-theme@gnome-shell-extensions.gcampax.github.com || warn "Could not enable User Themes extension."
|
|
|
|
if [[ -n "${detected_orchis_theme:-}" ]]; then
|
|
shell_css="$HOME/.themes/$detected_orchis_theme/gnome-shell/gnome-shell.css"
|
|
|
|
if [[ -f "$shell_css" ]]; then
|
|
set_gsetting_if_available org.gnome.shell.extensions.user-theme name "$detected_orchis_theme"
|
|
warn "GNOME Shell theme is configured. On Wayland, log out and back in, or reboot, to see it fully applied."
|
|
else
|
|
warn "Skipping GNOME Shell theme because $shell_css does not exist."
|
|
fi
|
|
fi
|
|
else
|
|
warn "User Themes extension is not available. GTK theme applied, but GNOME Shell theme may not apply."
|
|
fi
|
|
else
|
|
warn "gnome-extensions command not available. Skipping GNOME Shell theme activation."
|
|
fi
|
|
|
|
if command -v flatpak >/dev/null 2>&1; then
|
|
log "Applying Flatpak GTK theme filesystem overrides..."
|
|
sudo flatpak override --filesystem=xdg-config/gtk-3.0 || warn "Could not apply Flatpak GTK3 override."
|
|
sudo flatpak override --filesystem=xdg-config/gtk-4.0 || warn "Could not apply Flatpak GTK4 override."
|
|
fi
|
|
|
|
log "GNOME theme applied with Orchis grey variant."
|
|
}
|
|
|
|
setup_autologin_for_lago() {
|
|
log "Checking whether user lago exists for GNOME autologin..."
|
|
|
|
if id "lago" >/dev/null 2>&1; then
|
|
log "User lago exists. Enabling GDM autologin for lago."
|
|
|
|
sudo mkdir -p /etc/gdm
|
|
|
|
if [[ ! -f /etc/gdm/custom.conf ]]; then
|
|
sudo tee /etc/gdm/custom.conf >/dev/null <<GDMCONF
|
|
[daemon]
|
|
AutomaticLoginEnable=True
|
|
AutomaticLogin=lago
|
|
GDMCONF
|
|
else
|
|
sudo cp /etc/gdm/custom.conf /etc/gdm/custom.conf.backup.$(date +%Y%m%d%H%M%S)
|
|
|
|
if grep -q "^\[daemon\]" /etc/gdm/custom.conf; then
|
|
sudo sed -i "/^\[daemon\]/,/^\[/ {
|
|
s/^#\?AutomaticLoginEnable=.*/AutomaticLoginEnable=True/
|
|
s/^#\?AutomaticLogin=.*/AutomaticLogin=lago/
|
|
}" /etc/gdm/custom.conf
|
|
|
|
if ! grep -A20 "^\[daemon\]" /etc/gdm/custom.conf | grep -q "^AutomaticLoginEnable="; then
|
|
sudo sed -i "/^\[daemon\]/a AutomaticLoginEnable=True" /etc/gdm/custom.conf
|
|
fi
|
|
|
|
if ! grep -A20 "^\[daemon\]" /etc/gdm/custom.conf | grep -q "^AutomaticLogin="; then
|
|
sudo sed -i "/^\[daemon\]/a AutomaticLogin=lago" /etc/gdm/custom.conf
|
|
fi
|
|
else
|
|
sudo tee -a /etc/gdm/custom.conf >/dev/null <<GDMCONF
|
|
|
|
[daemon]
|
|
AutomaticLoginEnable=True
|
|
AutomaticLogin=lago
|
|
GDMCONF
|
|
fi
|
|
fi
|
|
|
|
log "GDM autologin enabled for lago. It will apply after reboot."
|
|
else
|
|
warn "User lago does not exist. Skipping autologin configuration."
|
|
fi
|
|
}
|
|
|
|
|
|
setup_unraid_nas_mounts() {
|
|
log "Optional: persistent Unraid NAS SMB mounts."
|
|
|
|
if [[ ! -t 0 ]]; then
|
|
warn "Non-interactive shell detected. Skipping NAS mount setup."
|
|
return
|
|
fi
|
|
|
|
read -r -p "Do you want to configure persistent NAS mounts for //${NAS_HOST}/isos and //${NAS_HOST}/Data? [y/N]: " configure_nas
|
|
|
|
case "$configure_nas" in
|
|
y|Y|yes|YES|Yes)
|
|
log "NAS mount setup selected."
|
|
;;
|
|
*)
|
|
log "Skipping NAS mount setup."
|
|
return
|
|
;;
|
|
esac
|
|
|
|
if ! command -v mount.cifs >/dev/null 2>&1; then
|
|
warn "mount.cifs is not available. Make sure cifs-utils is installed. Skipping NAS mount setup."
|
|
return
|
|
fi
|
|
|
|
sudo mkdir -p /etc/samba
|
|
sudo chmod 755 /etc/samba
|
|
|
|
if [[ ! -f "$NAS_CREDENTIALS_FILE" ]]; then
|
|
warn "NAS credentials file does not exist yet: $NAS_CREDENTIALS_FILE"
|
|
warn "Creating a template. Edit it later with: sudo micro $NAS_CREDENTIALS_FILE"
|
|
|
|
sudo tee "$NAS_CREDENTIALS_FILE" >/dev/null <<NASCREDS
|
|
username=CHANGE_ME
|
|
password=CHANGE_ME
|
|
# domain=WORKGROUP
|
|
NASCREDS
|
|
|
|
sudo chown root:root "$NAS_CREDENTIALS_FILE"
|
|
sudo chmod 600 "$NAS_CREDENTIALS_FILE"
|
|
|
|
warn "Credentials template created. NAS mounts were added, but they will not work until username/password are filled in."
|
|
else
|
|
sudo chown root:root "$NAS_CREDENTIALS_FILE"
|
|
sudo chmod 600 "$NAS_CREDENTIALS_FILE"
|
|
log "NAS credentials file already exists and permissions were enforced."
|
|
fi
|
|
|
|
for share in "${NAS_SHARES[@]}"; do
|
|
mount_point="$NAS_MOUNT_BASE/$share"
|
|
remote_share="//$NAS_HOST/$share"
|
|
|
|
if ! sudo mkdir -p "$mount_point" 2>/dev/null; then
|
|
warn "Could not access or create $mount_point. Trying to clear stale mount handle..."
|
|
sudo umount -lf "$mount_point" 2>/dev/null || true
|
|
sudo systemctl reset-failed 2>/dev/null || true
|
|
sudo mkdir -p "$mount_point"
|
|
fi
|
|
|
|
fstab_line="$remote_share $mount_point cifs credentials=$NAS_CREDENTIALS_FILE,uid=$(id -u),gid=$(id -g),iocharset=utf8,vers=3.0,_netdev,nofail,x-systemd.automount,x-systemd.idle-timeout=60,x-systemd.device-timeout=10s 0 0"
|
|
|
|
if grep -qsE "^//$NAS_HOST/$share[[:space:]]+$mount_point[[:space:]]+cifs[[:space:]]" /etc/fstab; then
|
|
log "fstab entry already exists for $remote_share -> $mount_point"
|
|
else
|
|
log "Adding fstab entry for $remote_share -> $mount_point"
|
|
echo "$fstab_line" | sudo tee -a /etc/fstab >/dev/null
|
|
fi
|
|
done
|
|
|
|
sudo systemctl daemon-reload
|
|
|
|
log "NAS automount entries configured."
|
|
warn "After editing credentials, test with: sudo mount -a"
|
|
warn "Mount paths: $NAS_MOUNT_BASE/isos and $NAS_MOUNT_BASE/Data"
|
|
}
|
|
|
|
|
|
|
|
remove_ptyxis_and_konsole_install_ghostty() {
|
|
log "Replacing Ptyxis/Konsole with Ghostty..."
|
|
|
|
if pacman -Q ptyxis >/dev/null 2>&1; then
|
|
log "Ptyxis is installed. Removing it..."
|
|
sudo pacman -Rns --noconfirm ptyxis || warn "Could not remove Ptyxis. It may be required by another package."
|
|
else
|
|
log "Ptyxis is not installed. Skipping removal."
|
|
fi
|
|
|
|
if pacman -Q konsole >/dev/null 2>&1; then
|
|
log "Konsole is installed. Removing it..."
|
|
sudo pacman -Rns --noconfirm konsole || warn "Could not remove Konsole. It may be required by another package."
|
|
else
|
|
log "Konsole is not installed. Skipping removal."
|
|
fi
|
|
|
|
if pacman -Q ghostty >/dev/null 2>&1; then
|
|
log "Ghostty is already installed."
|
|
else
|
|
log "Installing Ghostty..."
|
|
sudo pacman -S --needed --noconfirm ghostty
|
|
fi
|
|
}
|
|
|
|
configure_ghostty() {
|
|
log "Configuring Ghostty..."
|
|
|
|
ghostty_config_dir="$HOME/.config/ghostty"
|
|
ghostty_config="$ghostty_config_dir/config"
|
|
|
|
mkdir -p "$ghostty_config_dir"
|
|
|
|
if [[ -f "$ghostty_config" ]]; then
|
|
cp "$ghostty_config" "$ghostty_config.backup.$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
|
|
cat > "$ghostty_config" <<GHOSTTYCONFIG
|
|
# Jose Lago Ghostty config
|
|
# Reload after changes with Ctrl+Shift+R or restart Ghostty.
|
|
|
|
command = /usr/bin/zsh
|
|
font-family = MesloLGS NF
|
|
font-size = 11
|
|
|
|
copy-on-select = clipboard
|
|
right-click-action = paste
|
|
|
|
# Clipboard
|
|
keybind = ctrl+shift+c=copy_to_clipboard
|
|
keybind = ctrl+shift+v=paste_from_clipboard
|
|
|
|
# Tabs
|
|
keybind = ctrl+shift+t=new_tab
|
|
keybind = ctrl+shift+w=close_surface
|
|
keybind = ctrl+shift+page_up=previous_tab
|
|
keybind = ctrl+shift+page_down=next_tab
|
|
|
|
# Font size
|
|
keybind = ctrl+plus=increase_font_size:1
|
|
keybind = ctrl+minus=decrease_font_size:1
|
|
keybind = ctrl+0=reset_font_size
|
|
|
|
# Config
|
|
keybind = ctrl+shift+comma=open_config
|
|
keybind = ctrl+shift+r=reload_config
|
|
GHOSTTYCONFIG
|
|
|
|
log "Ghostty config written to: $ghostty_config"
|
|
log "Ghostty uses zsh, MesloLGS NF, copy-on-select, and right-click paste."
|
|
}
|
|
|
|
configure_zsh_ohmyzsh_powerlevel10k() {
|
|
log "Configuring Zsh, Oh My Zsh, and Powerlevel10k..."
|
|
|
|
if ! command -v zsh >/dev/null 2>&1; then
|
|
warn "zsh is not available. Skipping shell customization."
|
|
return
|
|
fi
|
|
|
|
if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
|
|
log "Installing Oh My Zsh non-interactively..."
|
|
RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
|
|
else
|
|
log "Oh My Zsh is already installed."
|
|
fi
|
|
|
|
zsh_custom="$HOME/.oh-my-zsh/custom"
|
|
p10k_dir="$zsh_custom/themes/powerlevel10k"
|
|
|
|
mkdir -p "$zsh_custom/themes"
|
|
|
|
if [[ -d "$p10k_dir/.git" ]]; then
|
|
log "Powerlevel10k already exists. Updating..."
|
|
git -C "$p10k_dir" pull --ff-only || warn "Could not update Powerlevel10k. Continuing with existing copy."
|
|
else
|
|
log "Installing Powerlevel10k into $p10k_dir..."
|
|
rm -rf "$p10k_dir"
|
|
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$p10k_dir"
|
|
fi
|
|
|
|
if [[ ! -f "$p10k_dir/powerlevel10k.zsh-theme" ]]; then
|
|
warn "Powerlevel10k theme file was not found after installation: $p10k_dir/powerlevel10k.zsh-theme"
|
|
return
|
|
fi
|
|
|
|
if [[ -f "$HOME/.zshrc" ]]; then
|
|
cp "$HOME/.zshrc" "$HOME/.zshrc.backup.$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
|
|
# Keep the zshrc deterministic. This avoids Oh My Zsh loading before ZSH_CUSTOM
|
|
# is defined, which causes: theme 'powerlevel10k/powerlevel10k' not found.
|
|
cat > "$HOME/.zshrc" <<'ZSHRC'
|
|
# Disable Powerlevel10k instant prompt because this setup intentionally prints
|
|
# Fastfetch system information at terminal startup.
|
|
typeset -g POWERLEVEL9K_INSTANT_PROMPT=off
|
|
|
|
export ZSH="$HOME/.oh-my-zsh"
|
|
ZSH_CUSTOM="$ZSH/custom"
|
|
ZSH_THEME="powerlevel10k/powerlevel10k"
|
|
|
|
plugins=(
|
|
git
|
|
sudo
|
|
)
|
|
|
|
source "$ZSH/oh-my-zsh.sh"
|
|
|
|
# Jose fastfetch terminal summary
|
|
# Disable with: export DISABLE_FASTFETCH=1
|
|
if [[ -o interactive ]] && [[ -z "${DISABLE_FASTFETCH:-}" ]] && [[ -z "${FASTFETCH_SHOWN:-}" ]] && command -v fastfetch >/dev/null 2>&1; then
|
|
export FASTFETCH_SHOWN=1
|
|
fastfetch
|
|
echo
|
|
fi
|
|
|
|
# Powerlevel10k user config.
|
|
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
|
|
ZSHRC
|
|
|
|
current_shell="$(getent passwd "$USER" | cut -d: -f7 || true)"
|
|
zsh_path="$(command -v zsh)"
|
|
|
|
if [[ "$current_shell" != "$zsh_path" ]]; then
|
|
log "Changing default shell to zsh for $USER..."
|
|
chsh -s "$zsh_path" "$USER" || warn "Could not change default shell. You can run: chsh -s $zsh_path"
|
|
else
|
|
log "Default shell is already zsh."
|
|
fi
|
|
|
|
log "Zsh + Oh My Zsh + Powerlevel10k configured."
|
|
warn "Open Ghostty after reboot/login. Run 'p10k configure' if you want to customize the prompt."
|
|
}
|
|
|
|
configure_wireguard_gui_webkit_workaround() {
|
|
log "Configuring WireGuard GUI WebKit renderer workaround..."
|
|
|
|
if ! command -v wireguard-gui >/dev/null 2>&1; then
|
|
warn "wireguard-gui is not installed. Skipping workaround."
|
|
return
|
|
fi
|
|
|
|
mkdir -p "$HOME/.local/bin"
|
|
|
|
cat > "$HOME/.local/bin/wireguard-gui-fixed" <<WIREGUARDWRAPPER
|
|
#!/usr/bin/env bash
|
|
WEBKIT_DISABLE_DMABUF_RENDERER=1 exec wireguard-gui "\$@"
|
|
WIREGUARDWRAPPER
|
|
|
|
chmod +x "$HOME/.local/bin/wireguard-gui-fixed"
|
|
|
|
cat > "$HOME/.local/bin/wire" <<WIREALIAS
|
|
#!/usr/bin/env bash
|
|
WEBKIT_DISABLE_DMABUF_RENDERER=1 exec wireguard-gui "\$@"
|
|
WIREALIAS
|
|
|
|
chmod +x "$HOME/.local/bin/wire"
|
|
|
|
if ! echo "$PATH" | tr ":" "\n" | grep -qx "$HOME/.local/bin"; then
|
|
warn "$HOME/.local/bin is not currently in PATH."
|
|
|
|
if command -v fish >/dev/null 2>&1; then
|
|
log "Adding $HOME/.local/bin to fish user paths..."
|
|
fish -c "fish_add_path -U $HOME/.local/bin" || warn "Could not add ~/.local/bin to fish PATH."
|
|
fi
|
|
|
|
if [[ -f "$HOME/.zshrc" ]] && ! grep -q ".local/bin" "$HOME/.zshrc"; then
|
|
log "Adding $HOME/.local/bin to .zshrc PATH..."
|
|
printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.zshrc"
|
|
fi
|
|
fi
|
|
|
|
mkdir -p "$HOME/.local/share/applications"
|
|
|
|
desktop_file="$(find /usr/share/applications "$HOME/.local/share/applications" -name "*wireguard*gui*.desktop" 2>/dev/null | head -n 1 || true)"
|
|
|
|
if [[ -n "$desktop_file" ]]; then
|
|
cp "$desktop_file" "$HOME/.local/share/applications/wireguard-gui.desktop"
|
|
sed -i "s|^Exec=.*|Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 wireguard-gui|" "$HOME/.local/share/applications/wireguard-gui.desktop"
|
|
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
|
log "WireGuard GUI desktop launcher patched with WEBKIT_DISABLE_DMABUF_RENDERER=1."
|
|
else
|
|
warn "Could not find WireGuard GUI desktop file to patch."
|
|
fi
|
|
|
|
log "WireGuard GUI wrappers created:"
|
|
log " $HOME/.local/bin/wireguard-gui-fixed"
|
|
log " $HOME/.local/bin/wire"
|
|
}
|
|
|
|
|
|
configure_fastfetch_terminal_summary() {
|
|
log "Configuring Fastfetch terminal system summary..."
|
|
|
|
if ! command -v fastfetch >/dev/null 2>&1; then
|
|
warn "fastfetch is not installed. Skipping terminal system summary."
|
|
return
|
|
fi
|
|
|
|
fastfetch_config_dir="$HOME/.config/fastfetch"
|
|
fastfetch_config="$fastfetch_config_dir/config.jsonc"
|
|
|
|
mkdir -p "$fastfetch_config_dir"
|
|
|
|
if [[ -f "$fastfetch_config" ]]; then
|
|
cp "$fastfetch_config" "$fastfetch_config.backup.$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
|
|
cat > "$fastfetch_config" <<'FASTFETCHCONFIG'
|
|
{
|
|
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
|
|
"logo": {
|
|
"type": "auto"
|
|
},
|
|
"modules": [
|
|
"title",
|
|
"separator",
|
|
"os",
|
|
"host",
|
|
"kernel",
|
|
"uptime",
|
|
"shell",
|
|
"terminal",
|
|
"cpu",
|
|
"gpu",
|
|
"memory",
|
|
"swap",
|
|
"disk",
|
|
"localip",
|
|
"battery",
|
|
"packages"
|
|
]
|
|
}
|
|
FASTFETCHCONFIG
|
|
|
|
if [[ -f "$HOME/.zshrc" ]]; then
|
|
if ! grep -q "Jose fastfetch terminal summary" "$HOME/.zshrc"; then
|
|
cat >> "$HOME/.zshrc" <<ZSHFASTFETCH
|
|
|
|
# Jose fastfetch terminal summary
|
|
# Disable with: export DISABLE_FASTFETCH=1
|
|
if [[ -o interactive ]] && [[ -z "\${DISABLE_FASTFETCH:-}" ]] && [[ -z "\${FASTFETCH_SHOWN:-}" ]] && command -v fastfetch >/dev/null 2>&1; then
|
|
export FASTFETCH_SHOWN=1
|
|
fastfetch
|
|
echo
|
|
fi
|
|
ZSHFASTFETCH
|
|
log "Fastfetch startup block added to ~/.zshrc."
|
|
else
|
|
log "Fastfetch startup block already exists in ~/.zshrc."
|
|
fi
|
|
else
|
|
warn "~/.zshrc does not exist. Skipping Fastfetch shell startup block."
|
|
fi
|
|
|
|
log "Fastfetch config written to: $fastfetch_config"
|
|
}
|
|
|
|
|
|
configure_git_identity() {
|
|
log "Configuring global Git identity..."
|
|
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
warn "git is not available. Skipping Git identity configuration."
|
|
return
|
|
fi
|
|
|
|
current_git_name="$(git config --global --get user.name || true)"
|
|
current_git_email="$(git config --global --get user.email || true)"
|
|
|
|
if [[ "$current_git_name" == "$GIT_USER_NAME" ]]; then
|
|
log "Git user.name already set to: $GIT_USER_NAME"
|
|
else
|
|
git config --global user.name "$GIT_USER_NAME"
|
|
log "Git user.name set to: $GIT_USER_NAME"
|
|
fi
|
|
|
|
if [[ "$current_git_email" == "$GIT_USER_EMAIL" ]]; then
|
|
log "Git user.email already set to: $GIT_USER_EMAIL"
|
|
else
|
|
git config --global user.email "$GIT_USER_EMAIL"
|
|
log "Git user.email set to: $GIT_USER_EMAIL"
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
configure_ulauncher_launcher() {
|
|
log "Configuring Ulauncher on Super+Space..."
|
|
|
|
if ! command -v ulauncher >/dev/null 2>&1; then
|
|
warn "ulauncher is not installed. Skipping launcher configuration."
|
|
return
|
|
fi
|
|
|
|
# Free Super+Space from GNOME input-source switching so Ulauncher can use it.
|
|
gsettings set org.gnome.desktop.wm.keybindings switch-input-source "['<Control><Super>space']" || true
|
|
gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward "['<Shift><Control><Super>space']" || true
|
|
|
|
custom_path="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom-ulauncher/"
|
|
existing_bindings="$(gsettings get org.gnome.settings-daemon.plugins.media-keys custom-keybindings 2>/dev/null || echo "@as []")"
|
|
|
|
if ! echo "$existing_bindings" | grep -q "$custom_path"; then
|
|
if [[ "$existing_bindings" == "@as []" ]]; then
|
|
new_bindings="['$custom_path']"
|
|
else
|
|
new_bindings="$(echo "$existing_bindings" | sed "s|]$|, '$custom_path']|")"
|
|
fi
|
|
gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "$new_bindings"
|
|
fi
|
|
|
|
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"$custom_path" name "Ulauncher App Launcher"
|
|
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"$custom_path" command "ulauncher-toggle"
|
|
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"$custom_path" binding "<Super>space"
|
|
|
|
mkdir -p "$HOME/.config/autostart"
|
|
|
|
if [[ -f /usr/share/applications/ulauncher.desktop ]]; then
|
|
cp /usr/share/applications/ulauncher.desktop "$HOME/.config/autostart/ulauncher.desktop"
|
|
else
|
|
cat > "$HOME/.config/autostart/ulauncher.desktop" <<'ULAUNCHERDESKTOP'
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Ulauncher
|
|
Exec=ulauncher --hide-window
|
|
X-GNOME-Autostart-enabled=true
|
|
ULAUNCHERDESKTOP
|
|
fi
|
|
|
|
if ! pgrep -x ulauncher >/dev/null 2>&1; then
|
|
nohup ulauncher --hide-window >/dev/null 2>&1 &
|
|
fi
|
|
|
|
log "Ulauncher configured on Super+Space."
|
|
warn "If Super+Space still changes keyboard input source, change GNOME's input-source shortcut in Settings > Keyboard > Typing."
|
|
}
|
|
|
|
|
|
configure_wallpaper_palette_tools() {
|
|
log "Configuring wallpaper and palette tools..."
|
|
|
|
mkdir -p "$HOME/Pictures/Wallpapers"
|
|
|
|
if command -v variety >/dev/null 2>&1; then
|
|
log "Variety installed. Use it to find and rotate high-quality wallpapers."
|
|
else
|
|
warn "Variety is not available."
|
|
fi
|
|
|
|
if command -v wal >/dev/null 2>&1; then
|
|
log "Pywal installed. Generate a palette with: wal -i ~/Pictures/Wallpapers/example.jpg"
|
|
else
|
|
warn "Pywal/wal is not available."
|
|
fi
|
|
|
|
log "Wallpaper folder prepared: $HOME/Pictures/Wallpapers"
|
|
}
|
|
|
|
|
|
configure_gnome_keyboard_shortcuts() {
|
|
log "Configuring GNOME keyboard shortcuts..."
|
|
|
|
if ! command -v gsettings >/dev/null 2>&1; then
|
|
warn "gsettings is not available. Skipping GNOME shortcuts."
|
|
return
|
|
fi
|
|
|
|
# Keep Super+Space free for Search Light.
|
|
gsettings set org.gnome.desktop.wm.keybindings switch-input-source "['<Control><Super>space']" || true
|
|
gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward "['<Shift><Control><Super>space']" || true
|
|
|
|
# Hyprland-like close shortcut while preserving Alt+F4.
|
|
gsettings set org.gnome.desktop.wm.keybindings close "['<Super>q', '<Alt>F4']" || true
|
|
|
|
python - <<'PYSHORTCUTS'
|
|
import ast
|
|
import subprocess
|
|
|
|
SCHEMA = "org.gnome.settings-daemon.plugins.media-keys"
|
|
BASE = "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/"
|
|
|
|
shortcuts = [
|
|
("custom-ghostty", "Open Ghostty", "ghostty", "<Super>Return"),
|
|
("custom-brave", "Open Brave", "brave-browser", "<Super>b"),
|
|
("custom-vscode", "Open VS Code", "code", "<Super>e"),
|
|
("custom-files", "Open Files", "nautilus", "<Super>f"),
|
|
("custom-mail", "Open Thunderbird", "thunderbird", "<Super>m"),
|
|
("custom-telegram", "Open Telegram", "telegram-desktop", "<Super>t"),
|
|
("custom-signal", "Open Signal", "signal-desktop", "<Super>s"),
|
|
("custom-wireguard", "Open WireGuard GUI", "wireguard-gui-fixed", "<Super>w"),
|
|
("custom-impression", "Open Impression", "flatpak run io.gitlab.adhami3310.Impression", "<Super>i"),
|
|
]
|
|
|
|
def get(cmd):
|
|
return subprocess.check_output(cmd, text=True).strip()
|
|
|
|
def set_value(schema, key, value):
|
|
subprocess.run(["gsettings", "set", schema, key, value], check=True)
|
|
|
|
raw = get(["gsettings", "get", SCHEMA, "custom-keybindings"])
|
|
existing = [] if raw == "@as []" else ast.literal_eval(raw)
|
|
|
|
for shortcut_id, name, command, binding in shortcuts:
|
|
path = f"{BASE}{shortcut_id}/"
|
|
if path not in existing:
|
|
existing.append(path)
|
|
|
|
shortcut_schema = f"{SCHEMA}.custom-keybinding:{path}"
|
|
set_value(shortcut_schema, "name", name)
|
|
set_value(shortcut_schema, "command", command)
|
|
set_value(shortcut_schema, "binding", binding)
|
|
|
|
formatted = "[" + ", ".join(repr(item) for item in existing) + "]"
|
|
set_value(SCHEMA, "custom-keybindings", formatted)
|
|
PYSHORTCUTS
|
|
|
|
log "GNOME keyboard shortcuts configured."
|
|
warn "Search Light should own Super+Space. Clipboard history should own Super+V."
|
|
}
|
|
|
|
|
|
configure_tela_circle_icons() {
|
|
log "Configuring Tela Circle icon theme..."
|
|
|
|
if [[ -d /usr/share/icons/Tela-circle ]] || [[ -d /usr/share/icons/Tela-circle-dark ]]; then
|
|
if gsettings writable org.gnome.desktop.interface icon-theme >/dev/null 2>&1; then
|
|
if [[ -d /usr/share/icons/Tela-circle-dark ]]; then
|
|
gsettings set org.gnome.desktop.interface icon-theme "Tela-circle-dark" || true
|
|
log "GNOME icon theme set to Tela-circle-dark."
|
|
else
|
|
gsettings set org.gnome.desktop.interface icon-theme "Tela-circle" || true
|
|
log "GNOME icon theme set to Tela-circle."
|
|
fi
|
|
else
|
|
warn "GNOME icon-theme setting is not writable."
|
|
fi
|
|
else
|
|
warn "Tela Circle icon theme was not found under /usr/share/icons."
|
|
fi
|
|
}
|
|
|
|
post_checks() {
|
|
log "Quick checks..."
|
|
|
|
echo "Brave:" | tee -a "$LOG_FILE"
|
|
command -v brave-browser | tee -a "$LOG_FILE" || true
|
|
|
|
echo "Chromium:" | tee -a "$LOG_FILE"
|
|
command -v chromium | tee -a "$LOG_FILE" || true
|
|
|
|
echo "VS Code:" | tee -a "$LOG_FILE"
|
|
command -v code | tee -a "$LOG_FILE" || true
|
|
|
|
echo "Docker:" | tee -a "$LOG_FILE"
|
|
docker --version | tee -a "$LOG_FILE" || true
|
|
|
|
echo "Vesktop:" | tee -a "$LOG_FILE"
|
|
command -v vesktop | tee -a "$LOG_FILE" || true
|
|
|
|
echo "Sone:" | tee -a "$LOG_FILE"
|
|
command -v sone | tee -a "$LOG_FILE" || true
|
|
|
|
echo "OpenCode:" | tee -a "$LOG_FILE"
|
|
command -v opencode | tee -a "$LOG_FILE" || true
|
|
|
|
echo "Extension Manager:" | tee -a "$LOG_FILE"
|
|
flatpak info com.mattjakeman.ExtensionManager >/dev/null 2>&1 && echo "com.mattjakeman.ExtensionManager" | tee -a "$LOG_FILE" || true
|
|
|
|
echo "Tela Circle icons:" | tee -a "$LOG_FILE"
|
|
ls -d /usr/share/icons/Tela-circle* 2>/dev/null | head -n 10 | tee -a "$LOG_FILE" || true
|
|
|
|
log "Done. Log saved at: $LOG_FILE"
|
|
}
|
|
|
|
main() {
|
|
require_not_root
|
|
check_cachyos
|
|
full_system_update
|
|
install_drivers_with_chwd
|
|
install_official_packages
|
|
configure_git_identity
|
|
remove_ptyxis_and_konsole_install_ghostty
|
|
configure_ghostty
|
|
configure_zsh_ohmyzsh_powerlevel10k
|
|
configure_fastfetch_terminal_summary
|
|
configure_gnome_keyboard_shortcuts
|
|
configure_tela_circle_icons
|
|
configure_wallpaper_palette_tools
|
|
install_aur_packages
|
|
configure_ulauncher_launcher
|
|
configure_wireguard_gui_webkit_workaround
|
|
install_flatpak_packages
|
|
setup_docker
|
|
setup_unraid_nas_mounts
|
|
set_brave_default
|
|
install_brave_extensions_policy
|
|
setup_autologin_for_lago
|
|
post_checks
|
|
|
|
echo
|
|
echo "✅ Post-install completed."
|
|
echo "🔁 Recommended: reboot to apply kernel, drivers, Docker group membership, Brave policies, GDM autologin, shell changes, and terminal configuration."
|
|
}
|
|
|
|
main "$@"
|