diff --git a/dotfiles/.config/hypr/conf/autostart.lua b/dotfiles/.config/hypr/conf/autostart.lua index b35b3bbcf..94ac17b53 100644 --- a/dotfiles/.config/hypr/conf/autostart.lua +++ b/dotfiles/.config/hypr/conf/autostart.lua @@ -31,6 +31,12 @@ hl.on("hyprland.start", function () -- Start polkit daemon hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") + -- Ubuntu ships hyprpolkitagent instead (via packages-ubuntu), but its + -- unit is WantedBy=graphical-session.target, which Hyprland never + -- activates, so it has to be started explicitly. No-op on distros + -- where the unit doesn't exist. + hl.exec_cmd("systemctl --user start hyprpolkitagent.service 2>/dev/null || true") + -- Restore wallpaper (skip for quickshell — handled inside ml4w-autostart) if wallpaper_app ~= "quickshell" then hl.exec_cmd("~/.config/ml4w/scripts/ml4w-wallpaper-app --restore") @@ -42,8 +48,21 @@ hl.on("hyprland.start", function () -- Load GTK settings hl.exec_cmd("~/.config/hypr/scripts/gtk.sh") - -- Start swaync - hl.exec_cmd("swaync") + -- Start swaync -- except on Ubuntu, where it's deliberately left to + -- D-Bus activation instead (org.erikreider.swaync.cc). Confirmed + -- live there: an explicit exec-once here raced with D-Bus activation + -- itself (e.g. from waybar's own swaync-client notification-count + -- module firing around the same time) and lost with "An instance of + -- SwayNotificationCenter is already running!". That's tied to + -- Ubuntu's swaync.service packaging specifically + -- (WantedBy=graphical-session.target, unmasked so D-Bus activation + -- self-heals it) -- other distros' swaync packaging isn't known to + -- have that same setup, so keep the direct launch there. Checking + -- /etc/os-release's ID rather than /etc/debian_version, which also + -- matches Mint/Pop!_OS/other Debian derivatives that don't have this + -- Ubuntu-specific packaging and would otherwise never get swaync + -- started at all. + hl.exec_cmd("grep -q '^ID=ubuntu' /etc/os-release 2>/dev/null || swaync") -- Start hypridle hl.exec_cmd("hypridle") diff --git a/dotfiles/.config/matugen/config.toml b/dotfiles/.config/matugen/config.toml index d02c530a2..0de70fe89 100644 --- a/dotfiles/.config/matugen/config.toml +++ b/dotfiles/.config/matugen/config.toml @@ -23,6 +23,10 @@ input_path = '~/.config/matugen/templates/hyprland-colors.lua' output_path = '~/.config/hypr/colors.lua' post_hook = 'hyprctl reload' +[templates.hyprtoolkit] +input_path = '~/.config/matugen/templates/hyprtoolkit-colors.conf' +output_path = '~/.config/hypr/hyprtoolkit.conf' + [templates.waybar] input_path = '~/.config/matugen/templates/colors.css' output_path = '~/.config/waybar/colors.css' diff --git a/dotfiles/.config/matugen/templates/hyprtoolkit-colors.conf b/dotfiles/.config/matugen/templates/hyprtoolkit-colors.conf new file mode 100644 index 000000000..baed3b035 --- /dev/null +++ b/dotfiles/.config/matugen/templates/hyprtoolkit-colors.conf @@ -0,0 +1,15 @@ +background = 0xff{{colors.surface_dim.default.hex_stripped}} +base = 0xff{{colors.surface_container.default.hex_stripped}} +text = 0xff{{colors.on_surface.default.hex_stripped}} +alternate_base = 0xff{{colors.surface_container_high.default.hex_stripped}} +bright_text = 0xff{{colors.inverse_on_surface.default.hex_stripped}} +link_text = 0xff{{colors.tertiary.default.hex_stripped}} +accent = 0xff{{colors.primary.default.hex_stripped}} +accent_secondary = 0xff{{colors.secondary.default.hex_stripped}} + +# Static (not wallpaper-derived) -- libadwaita's own corner-radius +# convention: 12px for windows/cards, 9px for controls like buttons. +rounding_large = 12 +rounding_small = 9 +font_family = Ubuntu Sans + diff --git a/setup/dependencies/packages-ubuntu b/setup/dependencies/packages-ubuntu new file mode 100644 index 000000000..7fe69dde7 --- /dev/null +++ b/setup/dependencies/packages-ubuntu @@ -0,0 +1,28 @@ +hyprland +hyprland-guiutils +hyprpolkitagent +hyprsunset +nwg-displays +sway-notification-center +fonts-font-awesome +tesseract-ocr +tesseract-ocr-eng +libsecret-1-0 +libnotify-bin +network-manager-gnome +nm-connection-editor +power-profiles-daemon +gnome-themes-extra +gnome-themes-extra-data +gnome-themes-ubuntu +gvfs-backends +qml6-module-qt5compat-graphicaleffects +qml6-module-qt-labs-folderlistmodel +qml6-module-qtquick-effects +qml6-module-qtquick-shapes +qml6-module-qtquick-controls +qml6-module-qtquick-layouts +libgirepository-2.0-dev +lua5.4 +imagemagick +pipx \ No newline at end of file diff --git a/setup/post-ubuntu.sh b/setup/post-ubuntu.sh new file mode 100755 index 000000000..2151576a9 --- /dev/null +++ b/setup/post-ubuntu.sh @@ -0,0 +1,402 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# -------------------------------------------------------------- +# Mask systemd --user units that duplicate autostart.lua's own +# exec-once launching of these daemons. Ubuntu's packaging ships +# "enabled" units for these (WantedBy=graphical-session.target), which +# Hyprland never activates directly, but at least one is triggered by +# other means anyway -- confirmed on the VM: without masking these, +# GDM spams "Failed Units Monitor" alerts. hyprpolkitagent.service is +# intentionally NOT masked here -- it's the intended polkit agent and +# is started explicitly from autostart.lua instead of relying on +# WantedBy=graphical-session.target (which Hyprland never activates). +# +# swaync.service is ALSO intentionally not masked (see below, near +# autostart.lua's former swaync exec-once line, for why) -- it needs +# D-Bus activation to still work so it can restart itself if it dies. +# -------------------------------------------------------------- + +for _svc in waybar.service hypridle.service hyprsunset.service; do + if systemctl --user list-unit-files "$_svc" &>/dev/null; then + systemctl --user mask "$_svc" 2>/dev/null || true + fi +done + +# Undo swaync.service being masked by an earlier version of this script +# (confirmed live: masking it broke D-Bus reactivation permanently -- +# every swaync-client call, including waybar's own notification-count +# module, hangs forever trying to auto-activate a masked unit). +systemctl --user unmask swaync.service 2>/dev/null || true + +# -------------------------------------------------------------- +# snapd-desktop-integration: on GDM-based Ubuntu installs, this snap +# tries to run under every session (including Hyprland) and fails, +# producing a "Failed Units" notification on the GDM greeter. Switching +# to the candidate channel plus a GNOME-only condition drop-in (so it +# still runs normally under GNOME sessions) resolves it. No-op if snap +# isn't installed. +# -------------------------------------------------------------- + +if command -v snap &> /dev/null && snap list snapd-desktop-integration &> /dev/null; then + sudo snap refresh snapd-desktop-integration --channel=candidate >> "$LOG_FILE" 2>&1 || true + + # Gating via ConditionEnvironment (below) rather than masking, since + # this unit is meant to still run under real GNOME sessions -- a + # mask would disable it there too. Relies on XDG_CURRENT_DESKTOP + # being present in the systemd --user manager's own environment + # block by the time this unit tries to start, which on Ubuntu's + # GDM+gnome-session stack is set up by gnome-session's own systemd + # units running `dbus-update-activation-environment --systemd --all` + # early in session startup -- not independently verified here. + _snap_svc="snap.snapd-desktop-integration.snapd-desktop-integration.service" + mkdir -p "$HOME/.config/systemd/user/${_snap_svc}.d" + cat > "$HOME/.config/systemd/user/${_snap_svc}.d/gnome-only.conf" <<-EOF +[Unit] +ConditionEnvironment=XDG_CURRENT_DESKTOP=ubuntu:GNOME +EOF + + systemctl --user daemon-reload 2>/dev/null || true +fi + +# -------------------------------------------------------------- +# mate-polkit: its autostart entry only excludes GNOME and KDE +# (NotShowIn=GNOME;KDE;), so it launches under Hyprland too and fights +# hyprpolkitagent for the polkit authentication agent registration -- +# confirmed live, its dialog was the one showing up instead of +# hyprpolkitagent's. Not a systemd unit, so it can't be masked the +# normal way; hiding it via the standard XDG per-user autostart +# override instead. No-op if mate-polkit isn't installed. +# -------------------------------------------------------------- + +if [ -f /etc/xdg/autostart/polkit-mate-authentication-agent-1.desktop ]; then + mkdir -p "$HOME/.config/autostart" + cat > "$HOME/.config/autostart/polkit-mate-authentication-agent-1.desktop" <<-'EOF' + [Desktop Entry] + Hidden=true + EOF +fi + +# -------------------------------------------------------------- +# Oh My Posh +# -------------------------------------------------------------- + +run_quiet "Installing Oh My Posh" bash -c ' + set -euo pipefail + curl -fsSL https://ohmyposh.dev/install.sh | bash -s -- -d ~/.local/bin +' + +# -------------------------------------------------------------- +# ML4W Settings App +# -------------------------------------------------------------- +# The upstream bootstrap script's distro detection only recognizes +# pacman/dnf/zypper and hard-exits on anything else (same shape as the +# top-level ml4w.com/os/rolling bootstrap and ml4w-dotfiles-installer -- +# none of them know about apt yet). Download it and patch in an apt +# branch rather than piping curl straight to bash, so this doesn't +# silently fail on Ubuntu. +# -------------------------------------------------------------- + +ML4W_SETTINGS_SETUP=$(mktemp -t ml4w-settings-setup-XXXXXX.sh) +curl -fsSL https://raw.githubusercontent.com/mylinuxforwork/ml4w-dotfiles-settings/main/setup.sh -o "$ML4W_SETTINGS_SETUP" + +# sed's own exit status doesn't reflect whether it actually matched +# anything, so upstream changing/removing/duplicating the top-level +# `else` this patch anchors on would otherwise fail silently (either a +# no-op that still hard-exits on Ubuntu, or a patch spliced into the +# wrong place). Check the anchor and the result explicitly instead. +else_count=$(grep -c '^else$' "$ML4W_SETTINGS_SETUP") +if [ "$else_count" -ne 1 ]; then + error "ml4w-dotfiles-settings setup.sh no longer has exactly one" + error "top-level 'else' (found $else_count) -- skipping ML4W Settings App install." +else + sed -i '/^else$/i\ +elif command -v apt-get \&> /dev/null; then\ + DISTRO="ubuntu"\ + info "Ubuntu detected. Installing base dependencies..."\ + sudo apt-get install -y git make jq gawk gum' "$ML4W_SETTINGS_SETUP" + if grep -q 'DISTRO="ubuntu"' "$ML4W_SETTINGS_SETUP"; then + run_quiet "Installing ML4W Settings App" bash "$ML4W_SETTINGS_SETUP" + else + error "Failed to patch ml4w-dotfiles-settings setup.sh for Ubuntu -- skipping install." + fi +fi +rm -f "$ML4W_SETTINGS_SETUP" + +# -------------------------------------------------------------- +# Cargo -- matugen +# -------------------------------------------------------------- + +TARGET_VERSION="4.0.0" + +force_install_matugen() { + run_quiet "Installing matugen" cargo install matugen --force + info "matugen installed." +} + +if ! command -v matugen &> /dev/null; then + info "'matugen' is not currently installed." + force_install_matugen +else + CURRENT_VERSION=$(matugen --version | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1) + LOWEST_VERSION=$(printf "%s\n%s" "$TARGET_VERSION" "$CURRENT_VERSION" | sort -V | head -n1) + if [ "$LOWEST_VERSION" = "$CURRENT_VERSION" ] && [ "$CURRENT_VERSION" != "$TARGET_VERSION" ]; then + info "Current version ($CURRENT_VERSION) is lower than $TARGET_VERSION. Updating..." + force_install_matugen + else + info "matugen is already up to date! (Current version: $CURRENT_VERSION)" + fi +fi + +# -------------------------------------------------------------- +# awww (Wayland wallpaper daemon -- replaces swww; no apt/PPA source) +# -------------------------------------------------------------- + +if [ ! -x /usr/local/bin/awww ] || [ ! -x /usr/local/bin/awww-daemon ]; then + # Checking the actual install target rather than `command -v awww` + # matters here: `cargo install` writes to the persistent ~/.cargo/bin + # first, and awww/awww-daemon are built as two separate targets in one + # `cargo install` call that can succeed independently -- a run where + # awww built but awww-daemon failed (e.g. before libwayland-dev was + # added below) leaves ~/.cargo/bin/awww in place, which `command -v` + # would find on PATH and use to skip this whole block on every + # subsequent run, permanently preventing /usr/local/bin/awww-daemon + # from ever being (re)installed. Confirmed live: awww-daemon ran fine + # (found via ~/.cargo/bin on PATH) while plain `awww` was + # "command not found" in a shell without ~/.cargo/bin on PATH. + # common/build.rs probes for liblz4 via pkg-config; daemon/build.rs + # generates Wayland protocol bindings and needs wayland.xml, which + # comes from wayland-client's pkgdatadir (libwayland-dev). The + # extension protocols it also needs (viewporter, fractional-scale) + # come from wayland-protocols, already guaranteed by hyprland's own + # apt dependency chain earlier in this script. + run_quiet "Building awww from source" bash -c ' + set -e + sudo apt-get install -y liblz4-dev pkg-config libwayland-dev + cargo install --git https://codeberg.org/LGFae/awww --tag v0.12.1 awww awww-daemon --locked + sudo cp "$HOME/.cargo/bin/awww" /usr/local/bin/awww + sudo cp "$HOME/.cargo/bin/awww-daemon" /usr/local/bin/awww-daemon + ' + info "awww installed to /usr/local/bin" +fi + +# Remove hyprpaper -- conflicts with awww, matches the swww-removal +# guard in preflight-ubuntu.sh (same reasoning, different wallpaper +# daemon). +if dpkg -l 2>/dev/null | grep -q "^ii hyprpaper "; then + sudo apt-get remove -y hyprpaper >> "$LOG_FILE" 2>&1 +fi + +# -------------------------------------------------------------- +# Shared scaffold for the from-source builds below: stage into a fresh +# temp dir (passed to the build script as $1), run it under run_quiet, +# and always clean the temp dir up afterwards. +# -------------------------------------------------------------- + +build_from_source() { + local label=$1 tmp_prefix=$2 script=$3 + local src_dir + src_dir=$(mktemp -d -t "${tmp_prefix}-XXXXXX") + run_quiet "Building $label from source" bash -c "$script" _ "$src_dir" + rm -rf "$src_dir" +} + +# -------------------------------------------------------------- +# Quickshell (built from source, not the danklinux PPA's quickshell-git +# package -- pinned to the exact commit that PPA package already builds +# from, since it's the one already validated to work with this repo's +# QML config: 4df562d, which was origin/master HEAD at the time this was +# written). Dependencies verified against BUILD.md and the actual +# find_package()/pkg_check_modules() calls in CMakeLists.txt, not +# assumed -- private Qt headers are only needed pre-Qt-6.10 for +# qt6-wayland (this system has 6.10.2, so that one's skipped), and +# libcpptrace-dev/spirv-tools are already available via the PPAs added +# in preflight-ubuntu.sh. libzstd-dev isn't a quickshell dependency +# directly -- it's cpptrace's crash-handling feature pulling in a +# find_dependency(zstd) that fails the whole cmake configure if it's +# missing. Confirmed live: failed with "Could NOT find zstd" on a clean +# VM where nothing else happened to pull it in already. +# -------------------------------------------------------------- + +if ! command -v qs &> /dev/null; then + build_from_source "quickshell" quickshell-src ' + set -e + sudo apt-get install -y \ + cmake ninja-build pkg-config \ + qt6-base-dev qt6-base-private-dev \ + qt6-declarative-dev qt6-declarative-private-dev \ + qt6-svg-dev qt6-shadertools-dev \ + libcli11-dev \ + libxcb1-dev \ + libdrm-dev libgbm-dev libegl1-mesa-dev \ + libcpptrace-dev libunwind-dev libzstd-dev \ + libwayland-bin libwayland-dev wayland-protocols \ + libglib2.0-dev \ + libpipewire-0.3-dev \ + libjemalloc-dev \ + libvulkan-dev \ + libpolkit-agent-1-dev libpolkit-gobject-1-dev \ + libpam0g-dev \ + spirv-tools + git clone https://github.com/quickshell-mirror/quickshell "$1" + (cd "$1" && git checkout -q 4df562dfb2475a9057f0f33a8db75808efad8670) + cmake -S "$1" -B "$1/build" -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + -DDISTRIBUTOR="ML4W Ubuntu Support (source build)" + cmake --build "$1/build" + sudo cmake --install "$1/build" + ' + info "quickshell installed." +fi + +# -------------------------------------------------------------- +# nwg-dock-hyprland (no apt package; Go build) +# -------------------------------------------------------------- + +if ! command -v nwg-dock-hyprland &> /dev/null; then + build_from_source "nwg-dock-hyprland" nwg-dock ' + set -e + sudo apt-get install -y golang-go libgtk-3-dev libgtk-layer-shell-dev libgtk-4-dev + git clone --depth=1 --branch v0.4.11 https://github.com/nwg-piotr/nwg-dock-hyprland "$1" + cd "$1" && make get && make build && sudo make install + ' + info "nwg-dock-hyprland installed." +fi + +# -------------------------------------------------------------- +# Walker (app launcher, Rust) + Elephant (its provider daemon, Go) +# -------------------------------------------------------------- + +if ! command -v walker &> /dev/null; then + build_from_source "Walker" walker ' + set -e + sudo apt-get install -y protobuf-compiler libgtk-4-dev libgtk4-layer-shell-dev libpoppler-glib-dev libgdk-pixbuf-2.0-dev + git clone --depth=1 --branch v2.16.2 https://github.com/abenz1267/walker "$1" + (cd "$1" && cargo build --release) + sudo cp "$1/target/release/walker" /usr/local/bin/walker + ' + info "Walker installed." +fi + +if [ ! -x /usr/local/bin/elephant ]; then + build_from_source "Elephant" elephant ' + set -e + sudo apt-get install -y golang-go + git clone --depth=1 --branch v2.21.0 https://github.com/abenz1267/elephant "$1" + (cd "$1/cmd/elephant" && go build -o "$HOME/go/bin/elephant" .) + sudo cp "$HOME/go/bin/elephant" /usr/local/bin/elephant + mkdir -p "$HOME/.config/elephant/providers" + for _pdir in "$1/internal/providers"/*/; do + _provider=$(basename "$_pdir") + (cd "$_pdir" && go build -buildmode=plugin -o "$HOME/.config/elephant/providers/${_provider}.so" .) || true + done + ' + info "Elephant and providers installed." +fi + +# -------------------------------------------------------------- +# Grimblast +# -------------------------------------------------------------- +# clean-install-grimblast.sh's Makefile generates a man page from +# grimblast.1.scd via scdoc -- both `make` (default target) and +# `make install` depend on it, so it has to be present before sourcing +# this (shared, unmodified) script. + +run_quiet "Installing grimblast" bash -c ' + set -e + sudo apt-get install -y scdoc + bash "$1" +' _ "$repo_path/setup/clean-install-grimblast.sh" + +# -------------------------------------------------------------- +# Pip +# -------------------------------------------------------------- + +run_quiet "Installing pywalfox" bash -c ' + set -e + sudo apt-get install -y python3-pip pipx + pipx install pywalfox + pipx ensurepath +' + +# -------------------------------------------------------------- +# Cursors +# -------------------------------------------------------------- + +run_quiet "Installing cursors" bash "$repo_path/setup/_cursors.sh" + +# -------------------------------------------------------------- +# Fonts +# -------------------------------------------------------------- + +run_quiet "Installing bundled fonts" bash "$repo_path/setup/_fonts.sh" + +# Shared scaffold for the zip-distributed fonts below: download, unzip, +# and copy $glob matches into $dest. Only creates $dest once matching +# files are confirmed present, so a failed download or an upstream zip +# layout change can't leave an empty $dest behind that would make the +# caller's `[ ! -d "$dest" ]` guard treat the font as already installed +# on every future run. +install_font_zip() { + local label=$1 url=$2 glob=$3 dest=$4 + local tmp + tmp=$(mktemp -d) + if run_quiet "Installing $label" bash -c ' + set -e + curl -fsSL -o "$1/font.zip" "$2" + (cd "$1" && unzip -q font.zip -d extracted) + shopt -s nullglob + files=("$1"/extracted/$4) + [ ${#files[@]} -gt 0 ] + sudo mkdir -p "$3" + sudo cp "${files[@]}" "$3/" + ' _ "$tmp" "$url" "$dest" "$glob"; then + rm -rf "$tmp" + return 0 + fi + rm -rf "$tmp" + return 1 +} + +# JetBrains Mono Nerd Font -- no Ubuntu package, unlike +# ttf-jetbrains-mono-nerd (Arch)/nerd-fonts-JetBrainsMono (Fedora +# copr)/jetbrainsmono-nerd-fonts (openSUSE repo). Used by +# dotfiles/.config/kitty/kitty.conf's font_family. +JBM_DEST="/usr/share/fonts/JetBrainsMonoNerd" +if [ ! -d "$JBM_DEST" ]; then + install_font_zip "JetBrains Mono Nerd Font" \ + "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip" \ + "*.ttf" "$JBM_DEST" || + warn "Failed to download JetBrains Mono Nerd Font; kitty font_family will fall back." +fi + +# Font Awesome 7 (Free + Brands) -- waybar's theme CSS font-family +# chains want the "Font Awesome 7 Free"/"Font Awesome 7 Brands" family +# names specifically (falling back to 6, then the ancient FontAwesome +# name). apt's fonts-font-awesome package is really Font Awesome 4.7 +# rebadged and only registers the plain "FontAwesome" family, so most +# modern icon glyphs (different PUA codepoints per major version) don't +# resolve through it -- confirmed live via fc-list. Kept the apt +# package too (harmless) and added the real thing on top. +FA_DEST="/usr/share/fonts/font-awesome-7" +if [ ! -d "$FA_DEST" ]; then + install_font_zip "Font Awesome 7" \ + "https://github.com/FortAwesome/Font-Awesome/releases/download/7.3.0/fontawesome-free-7.3.0-desktop.zip" \ + "*/otfs/*.otf" "$FA_DEST" || + warn "Failed to install Font Awesome 7; waybar icons may not render." +fi + +sudo fc-cache -f >> "$LOG_FILE" 2>&1 + +# -------------------------------------------------------------- +# Icons +# -------------------------------------------------------------- + +run_quiet "Installing icons" bash "$repo_path/setup/_icons.sh" + +# -------------------------------------------------------------- +# Create XDG Directories +# -------------------------------------------------------------- + +xdg-user-dirs-update diff --git a/setup/preflight-ubuntu.sh b/setup/preflight-ubuntu.sh new file mode 100755 index 000000000..1453d7e67 --- /dev/null +++ b/setup/preflight-ubuntu.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Noisy command output (apt-get, cargo, cmake/ninja, go build, etc.) goes +# here instead of the terminal. `gum spin` shows a spinner and only +# prints a wrapped command's output if it fails. Defined here rather +# than in a top-level install script -- this is the first of the two +# Ubuntu scripts to run, and post-ubuntu.sh depends on both existing. +LOG_FILE="$HOME/.ml4w-install.log" +: > "$LOG_FILE" +export LOG_FILE + +run_quiet() { + local title=$1; shift + echo "=== $title ===" >> "$LOG_FILE" + # Always tee into LOG_FILE, not just show-on-failure -- gum's own + # --show-error only prints to the terminal for a failed step and + # doesn't persist anything, so a compile failure scrolled past (or a + # step that succeeds but is worth checking later) left no record. + if ! gum spin --title "$title" --show-error -- bash -c ' + set -o pipefail + "$@" 2>&1 | tee -a "$LOG_FILE" + ' _ "$@"; then + error "$title -- failed (see $LOG_FILE)" + return 1 + fi +} +export -f run_quiet + +# A long install can easily outlast sudo's credential cache (typically +# 15 min), and a password re-prompt inside a `gum spin`-wrapped step would +# be invisible -- indistinguishable from a genuine hang. Keep the sudo +# timestamp alive for as long as this shell (and anything it sources, +# i.e. post-ubuntu.sh too) is running. +sudo -v +( while kill -0 $$ 2>/dev/null; do sudo -n true; sleep 60; done ) & +SUDO_KEEPALIVE_PID=$! +trap 'kill "$SUDO_KEEPALIVE_PID" 2>/dev/null' EXIT + +# Oh My Posh, pipx, and cargo all install into ~/.local/bin (or expect it +# to already be there) -- on a genuinely fresh account it doesn't exist +# yet. Confirmed live on a clean VM. +mkdir -p "$HOME/.local/bin" + +# -------------------------------------------------------------- +# Repositories +# -------------------------------------------------------------- + +# gum isn't installed yet at this point in a fresh run (it's installed at +# the end of this script), so these early repository-setup steps are +# quieted with plain log redirection rather than `gum spin`. +sudo apt-get install -y software-properties-common >> "$LOG_FILE" 2>&1 +sudo add-apt-repository -y universe >> "$LOG_FILE" 2>&1 +sudo add-apt-repository -y restricted >> "$LOG_FILE" 2>&1 + +# Hyprland core (hyprland, hypridle, hyprlock, hyprpicker, hyprsunset, +# hyprpolkitagent, hyprland-guiutils, xdg-desktop-portal-hyprland) +if ! grep -Rq "cppiber.*hyprland" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null; then + info "Adding PPA: ppa:cppiber/hyprland" + sudo add-apt-repository -y ppa:cppiber/hyprland >> "$LOG_FILE" 2>&1 +else + info "Hyprland PPA already present" +fi + +# cliphist (quickshell is built from source in post-ubuntu.sh instead of +# using this PPA's quickshell-git package, to match the exact commit +# already validated to work) +if ! grep -Rq "avengemedia.*danklinux" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null; then + info "Adding PPA: ppa:avengemedia/danklinux" + sudo add-apt-repository -y ppa:avengemedia/danklinux >> "$LOG_FILE" 2>&1 +else + info "danklinux PPA already present" +fi + +# gum (not available in Ubuntu main/universe). Checked with -s, not -f: a +# curl failure mid-pipeline still leaves gpg's -o output file behind +# (0 bytes) before gpg itself fails, and an existence-only check would +# treat that stale empty file as "already present" forever. +if [ ! -s /etc/apt/keyrings/charm.gpg ]; then + info "Adding Charm apt repo for gum" + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg + echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list > /dev/null +else + info "Charm apt repo already present" +fi + +# Firefox: plain `apt-get install firefox` on Ubuntu installs a +# transitional snap wrapper, not a native .deb (Ubuntu dropped the deb +# from the archive in 22.04+). Add the Mozilla Team PPA and pin it above +# the snap-transitional package so the shared packages file's `firefox` +# entry resolves to a real .deb, matching what Arch/Fedora/openSUSE get. +if ! grep -Rq "mozillateam.*ppa" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null; then + info "Adding PPA: ppa:mozillateam/ppa (native Firefox .deb, not the snap)" + sudo add-apt-repository -y ppa:mozillateam/ppa >> "$LOG_FILE" 2>&1 +fi +sudo tee /etc/apt/preferences.d/mozilla-firefox > /dev/null <<-'EOF' + Package: * + Pin: release o=LP-PPA-mozillateam + Pin-Priority: 1001 + EOF +sudo tee /etc/apt/apt.conf.d/51unattended-upgrades-firefox > /dev/null <<-'EOF' + Unattended-Upgrade::Allowed-Origins:: "LP-PPA-mozillateam:${distro_codename}"; + EOF + +sudo apt-get update >> "$LOG_FILE" 2>&1 + +# gum: only the repo was added above -- install it explicitly now rather +# than leaving it to whatever later step happens to apt-get install it +# first (previously that was the ML4W Settings App's patched bootstrap +# line in post-ubuntu.sh, which meant nothing before that point could +# use `gum spin`/run_quiet for quiet output). +if ! command -v gum &> /dev/null; then + sudo apt-get install -y gum >> "$LOG_FILE" 2>&1 +fi + +# -------------------------------------------------------------- +# Uninstall swww if exists. To be replaced with awww in the next steps +# -------------------------------------------------------------- + +if dpkg -l 2>/dev/null | grep -q "^ii swww "; then + sudo apt-get remove -y swww >> "$LOG_FILE" 2>&1 +fi