#!/usr/bin/env bash # ReadyLinux — install # # Supported: # Ubuntu GNOME, Zorin, Linux Mint Cinnamon / MATE / Xfce, # Pop!_OS COSMIC, Fedora Workstation. # # Run: # wget -qO- https://raw.githubusercontent.com/robbraxman/readylinux/main/install.sh | bash # # What is the same on every distro: # Packages: Brave, Chrome, Firefox, Thunderbird, KeePassXC, KolourPaint, VLC. # Folders: ~/Media/Disks and ~/Documents/AppFiles, plus a sidebar bookmark. # USB watcher: refreshes Media when a disk is plugged in. # Find my file, Brax.me launcher, AppImage click-to-install. # Defaults: Brave for web, Thunderbird for mail, VLC for video. # IPv4 preferred, IPv6 left on. Lid sleeps on battery, stays awake on AC. # # What changes by distro or desktop: # Packages: apt on Ubuntu-family, dnf on Fedora. Fedora also installs Dash to Dock. # Dock: GNOME favorite-apps on Ubuntu, Zorin, Fedora. Cinnamon, MATE, and Xfce # use their own panel. COSMIC uses its own favorites file. Zorin’s layout # is not moved. Xfce pins are inserted, never a full panel rewrite. # Fedora has no dock until Dash to Dock is enabled. Icons are 40px. # Wallpaper: GNOME picture-uri, MATE picture-filename, Xfce per-monitor backdrop. # Terminal: Ptyxis on new Ubuntu, GNOME Terminal on Zorin and Mint, xfce4-terminal # on Xfce. Size 110x34, or 120x36 on Xfce. Colors are not forced. # Editor: GNOME Text Editor, or Xed on Mint. Font 12pt. # Scale: 125% on a laptop or 1080p. 100% on a 1440p or 4K desktop. # File manager: Nautilus, Nemo, Caja, Thunar, or COSMIC Files. Bookmarks are GTK. # # Known limits: # Zorin: panel layout is not touched. Pins are added only. Moving Zorin's # panel or taskbar breaks its Windows-style setup. # Xfce: the panel list is never rewritten. Replacing plugin-ids moves the # tray and leaves dead Show Apps icons. Pins are inserted if the # panel is already a normal launcher panel. Thunar does not remember # window position. Wallpaper is set per monitor and can miss if the # backdrop path is not the usual screen0/monitor path. # Fedora: custom partitioning is an Anaconda bug. Use a whole disk. # There is no dock until Dash to Dock is enabled. # GNOME: dock clicks can freeze until a logout after pins are written. # Colors are not forced on any terminal. The user keeps their palette. # # Mint's file manager may run this with sh. That fails on lines 9, 17, and 23. if [ -z "${BASH_VERSION:-}" ]; then exec bash "$0" "$@" fi # Double-click in Files does nothing useful. Use install.desktop, or run in a terminal. if [[ ! -t 1 ]]; then zenity --question --width=420 --title="ReadyLinux" \ --text="This installs KeePassXC, KolourPaint, Chrome, VLC, Thunderbird, GDebi, Disks, and Flatpak.\n\nIt also sets folders, the dock, the lid rule, and Find my file.\n\nA terminal will open so you can enter your password. Continue?" \ || exit 0 term="$(command -v gnome-terminal || command -v x-terminal-emulator || true)" [[ -n "$term" ]] || { zenity --error --text="No terminal found."; exit 1; } exec "$term" -- bash -c "\"$0\"; echo; read -r -p 'Press Enter to close.'" fi set -euo pipefail SCALE="1.25" WALLPAPER_URL="https://brax.me/readylinux/wallpaper.jpg" BRAX_ICON_URL="https://brax.me/img/logo-b1a.png" BRAX_URL="https://brax.me" log() { printf '\n==> %s\n' "$*"; } warn() { printf '!! %s\n' "$*" >&2; } need_cmd() { command -v "$1" >/dev/null 2>&1; } if [[ "$(id -u)" -eq 0 ]]; then warn "Run as your user, not root. The script will ask for sudo." exit 1 fi desktop_session() { printf '%s\n' "${XDG_CURRENT_DESKTOP:-${DESKTOP_SESSION:-unknown}}" } # --------------------------------------------------------------------------- # System packages # Fedora uses dnf. Everything else uses apt. # Chrome and Brave are added from their own repos. Fedora 41+ needs the # dnf5 repo command. Cinnamon also gets Xed. Fedora gets Dash to Dock. # --------------------------------------------------------------------------- log "Installing packages" . /etc/os-release case "${ID:-}" in fedora) sudo dnf install -y \ curl ca-certificates wget gnupg2 \ zenity flatpak \ keepassxc kolourpaint thunderbird firefox vlc gnome-text-editor gnome-disk-utility \ gnome-shell-extension-dash-to-dock if ! rpm -q google-chrome-stable >/dev/null 2>&1; then log "Adding Google Chrome" sudo dnf install -y https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm fi if ! rpm -q brave-browser >/dev/null 2>&1; then log "Adding Brave" sudo dnf install -y dnf-plugins-core if dnf config-manager --help 2>&1 | grep -q addrepo; then sudo dnf config-manager addrepo --from-repofile=https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo else sudo dnf config-manager --add-repo https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo fi sudo dnf install -y brave-browser fi ;; *) sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ curl ca-certificates wget gnupg \ zenity flatpak gdebi gnome-disk-utility \ keepassxc kolourpaint thunderbird firefox vlc sudo DEBIAN_FRONTEND=noninteractive apt-get install -y gnome-text-editor 2>/dev/null \ || sudo DEBIAN_FRONTEND=noninteractive apt-get install -y gedit 2>/dev/null \ || true if [[ "${XDG_CURRENT_DESKTOP:-}${DESKTOP_SESSION:-}" == *[Cc]innamon* ]]; then sudo DEBIAN_FRONTEND=noninteractive apt-get install -y xed || true fi if ! dpkg -s google-chrome-stable >/dev/null 2>&1; then log "Adding Google Chrome" sudo mkdir -p /usr/share/keyrings /tmp curl -fsSL -o /tmp/google-chrome.pub https://dl.google.com/linux/linux_signing_key.pub sudo rm -f /usr/share/keyrings/google-chrome.gpg sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/google-chrome.gpg /tmp/google-chrome.pub echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \ | sudo tee /etc/apt/sources.list.d/google-chrome.list >/dev/null sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y google-chrome-stable fi if ! dpkg -s brave-browser >/dev/null 2>&1; then log "Adding Brave" sudo mkdir -p /usr/share/keyrings curl -fsSL -o /tmp/brave-browser-archive-keyring.gpg \ https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg sudo cp /tmp/brave-browser-archive-keyring.gpg /usr/share/keyrings/brave-browser-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" \ | sudo tee /etc/apt/sources.list.d/brave-browser-release.list >/dev/null sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y brave-browser fi ;; esac if ! flatpak remotes | grep -q flathub; then sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo fi # Ubuntu Snap Thunderbird stores mail outside the apt profile. # Copy it when the apt profile is missing or still blank. Do not overwrite accounts. snap_tb="$HOME/snap/thunderbird/common/.thunderbird" if [[ -f "$snap_tb/profiles.ini" ]]; then blank=1 if [[ -f "$HOME/.thunderbird/profiles.ini" ]]; then if grep -Rqs 'mail.accountmanager.accounts' "$HOME/.thunderbird" 2>/dev/null \ && grep -Rqs 'mail.accountmanager.accounts=.*[a-zA-Z0-9]' "$HOME/.thunderbird" 2>/dev/null; then blank=0 fi fi if [[ "$blank" == "1" ]]; then log "Copying Snap Thunderbird profile into the blank apt profile" mkdir -p "$HOME/.thunderbird" cp -a "$snap_tb"/. "$HOME/.thunderbird"/ fi fi if [[ -f /etc/gai.conf ]] && grep -q 'human-linux-ipv4' /etc/gai.conf; then log "IPv4 preference already set" else log "Prefer IPv4, leave IPv6 available" echo "precedence ::ffff:0:0/96 100 # human-linux-ipv4" | sudo tee -a /etc/gai.conf >/dev/null fi # Lid: suspend on battery, awake on AC and dock. Do not restart logind. log "Lid close rule (applies on next reboot)" sudo mkdir -p /etc/systemd/logind.conf.d sudo tee /etc/systemd/logind.conf.d/human-linux-lid.conf >/dev/null <<'EOF' [Login] HandleLidSwitch=suspend HandleLidSwitchExternalPower=ignore HandleLidSwitchDocked=ignore EOF # --------------------------------------------------------------------------- # Folders and sidebar # Same on every distro. ~/Media/Disks links extra disks and USB. # ~/Documents/AppFiles links Snap, Flatpak, debs, and rpms. # Bookmarks are GTK, so Nautilus, Nemo, Caja, and Thunar all see them. # KDE places are written later, in the desktop branch. # --------------------------------------------------------------------------- refresh_media_links() { local user src d gvfs mtp line dev mp fstype user="$(id -un)" mkdir -p "$HOME/Media/Disks" # Mount extra data partitions (NVMe, internal). Skip system, swap, and optical. if command -v lsblk >/dev/null && command -v udisksctl >/dev/null; then while read -r dev fstype label mp; do [[ -n "$dev" && -n "$fstype" ]] || continue [[ "$fstype" == "swap" || "$fstype" == "iso9660" ]] && continue case "$mp" in /|/boot|/boot/efi|/home) continue ;; esac [[ -n "$mp" ]] && continue case "${label,,}" in boot|efi|esp|system|recovery|winre|winre_drv|"microsoft reserved"|reserved) continue ;; esac udisksctl mount -b "$dev" >/dev/null 2>&1 || true done < <(lsblk -pnr -o PATH,FSTYPE,LABEL,MOUNTPOINT) fi find "$HOME/Media/Disks" -maxdepth 1 -type l -delete 2>/dev/null || true find "$HOME/Media/USB" -maxdepth 1 -type l -delete 2>/dev/null || true rmdir "$HOME/Media/USB" 2>/dev/null || true for src in "/media/$user" "/run/media/$user"; do [[ -d "$src" ]] || continue for d in "$src"/*; do [[ -e "$d" ]] || continue case "$(basename "$d" | tr '[:upper:]' '[:lower:]')" in boot|efi|esp|system|recovery|winre|winre_drv) continue ;; esac ln -sfn "$d" "$HOME/Media/Disks/$(basename "$d")" done done gvfs="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gvfs" rm -f "$HOME/Media/Phone" if [[ -d "$gvfs" ]]; then mtp="$(find "$gvfs" -maxdepth 1 \( -iname '*mtp*' -o -iname '*gphoto*' -o -iname '*afc*' \) 2>/dev/null | head -n1 || true)" if [[ -n "${mtp:-}" && -e "$mtp" ]]; then ln -sfn "$mtp" "$HOME/Media/Phone" fi fi } refresh_appfiles() { local dest="$HOME/Documents/AppFiles" d name mkdir -p "$dest" "$dest/debs" "$dest/rpms" "$dest/appimages" if [[ -d "$HOME/snap" ]]; then for d in "$HOME/snap"/*; do [[ -d "$d" ]] || continue name="$(basename "$d")" [[ "$name" == "README" ]] && continue [[ -e "$d/current" ]] && ln -sfn "$d/current" "$dest/${name}" [[ -d "$d/common" ]] && ln -sfn "$d/common" "$dest/${name}-common" done fi if [[ -d "$HOME/.var/app" ]]; then for d in "$HOME/.var/app"/*; do [[ -d "$d" ]] || continue name="$(basename "$d")" ln -sfn "$d" "$dest/flatpak-${name}" done fi if [[ -d "$HOME/Downloads" ]]; then for d in "$HOME/Downloads"/*.deb "$HOME/Downloads"/*.Deb; do [[ -e "$d" ]] || continue ln -sfn "$d" "$dest/debs/$(basename "$d")" done for d in "$HOME/Downloads"/*.rpm "$HOME/Downloads"/*.RPM; do [[ -e "$d" ]] || continue ln -sfn "$d" "$dest/rpms/$(basename "$d")" done fi } bookmark() { local uri="$1" label="$2" f for f in "$HOME/.config/gtk-3.0/bookmarks" "$HOME/.config/gtk-4.0/bookmarks"; do mkdir -p "$(dirname "$f")" touch "$f" grep -qF "$uri" "$f" || echo "$uri $label" >> "$f" done } log "Folders and sidebar" xdg-user-dirs-update || true mkdir -p "$HOME/Media/Disks" "$HOME/Videos/OBS" "$HOME/Documents/AppFiles" refresh_media_links refresh_appfiles bookmark "file://$HOME/Media" "Media" bookmark "file://$HOME/Documents/AppFiles" "AppFiles" bookmark "file:///" "File System" # --------------------------------------------------------------------------- # USB watcher # User systemd service. Refreshes Media on udisks events and every 5 seconds. # Same on every distro. Needs a desktop login, not a raw TTY. # --------------------------------------------------------------------------- log "USB watcher" mkdir -p "$HOME/.local/bin" "$HOME/.config/systemd/user" cat > "$HOME/.local/bin/human-linux-usb-watch" <<'EOF' #!/usr/bin/env bash refresh() { local user src d gvfs mtp dev mp fstype user="$(id -un)" mkdir -p "$HOME/Media/Disks" if command -v lsblk >/dev/null && command -v udisksctl >/dev/null; then while read -r dev fstype label mp; do [[ -n "$dev" && -n "$fstype" ]] || continue [[ "$fstype" == "swap" || "$fstype" == "iso9660" ]] && continue case "$mp" in /|/boot|/boot/efi|/home) continue ;; esac [[ -n "$mp" ]] && continue case "${label,,}" in boot|efi|esp|system|recovery|winre|winre_drv|"microsoft reserved"|reserved) continue ;; esac udisksctl mount -b "$dev" >/dev/null 2>&1 || true done < <(lsblk -pnr -o PATH,FSTYPE,LABEL,MOUNTPOINT) fi find "$HOME/Media/Disks" -maxdepth 1 -type l -delete 2>/dev/null || true find "$HOME/Media/USB" -maxdepth 1 -type l -delete 2>/dev/null || true rmdir "$HOME/Media/USB" 2>/dev/null || true for src in "/media/$user" "/run/media/$user"; do [[ -d "$src" ]] || continue for d in "$src"/*; do [[ -e "$d" ]] || continue case "$(basename "$d" | tr '[:upper:]' '[:lower:]')" in boot|efi|esp|system|recovery|winre|winre_drv) continue ;; esac ln -sfn "$d" "$HOME/Media/Disks/$(basename "$d")" done done gvfs="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gvfs" rm -f "$HOME/Media/Phone" if [[ -d "$gvfs" ]]; then mtp="$(find "$gvfs" -maxdepth 1 \( -iname '*mtp*' -o -iname '*gphoto*' -o -iname '*afc*' \) 2>/dev/null | head -n1 || true)" if [[ -n "${mtp:-}" && -e "$mtp" ]]; then ln -sfn "$mtp" "$HOME/Media/Phone" fi fi } refresh # udisks events, plus a poll, so a missed mount still shows up. ( while true; do sleep 5; refresh; done ) & udisksctl monitor | while read -r line; do case "$line" in *Added*|*Removed*|*mounted*|*unmounted*|*Properties\ Changed*) refresh ;; esac done EOF chmod +x "$HOME/.local/bin/human-linux-usb-watch" cat > "$HOME/.config/systemd/user/human-linux-usb-watch.service" </dev/null || true rm -f "$HOME/.config/systemd/user/usb-watch.service" "$HOME/.local/bin/usb-watch" systemctl --user daemon-reload || true systemctl --user enable human-linux-usb-watch.service || true systemctl --user restart human-linux-usb-watch.service || warn "Disk watcher not started. Log in on the desktop and rerun." # --------------------------------------------------------------------------- # Find my file # A small search GUI in Show Apps. Same on every distro. # --------------------------------------------------------------------------- log "Find my file" cat > "$HOME/.local/bin/where-is-my-file" <<'EOF' #!/usr/bin/env bash q="${1:-}" [[ -n "$q" ]] || { echo "usage: where-is-my-file "; exit 1; } roots=( "$HOME/Documents" "$HOME/Downloads" "$HOME/Videos" "$HOME/Pictures" "$HOME/Desktop" "$HOME/Media" "$HOME/Documents/AppFiles" "$HOME/snap" "$HOME/.var/app" ) find "${roots[@]}" -iname "*${q}*" 2>/dev/null | head -n 50 EOF chmod +x "$HOME/.local/bin/where-is-my-file" cat > "$HOME/.local/bin/where-is-my-file-gui" <<'EOF' #!/usr/bin/env bash q="$(zenity --entry --title="Find my file" --text="Name or part of a name" --ok-label="Search" --cancel-label="Cancel")" || exit 0 [[ -z "$q" ]] && exit 0 hits="$("$HOME/.local/bin/where-is-my-file" "$q")" if [[ -z "$hits" ]]; then zenity --info --title="Find my file" --text="No matches for: $q" --ok-label="OK" exit 0 fi zenity --info --title="Find my file: $q" --ok-label="OK" --width=700 --text="$hits" EOF chmod +x "$HOME/.local/bin/where-is-my-file-gui" mkdir -p "$HOME/.local/share/applications" cat > "$HOME/.local/share/applications/where-is-my-file.desktop" < "$desk" </dev/null || true ( cd /tmp rm -rf squashfs-root "$dest" --appimage-extract >/dev/null 2>&1 || true found="" if [[ -e squashfs-root/.DirIcon ]]; then found="$(readlink -f squashfs-root/.DirIcon)" fi if [[ ! -s "$found" ]]; then found="$(find squashfs-root -type f \( -iname '*.png' -o -iname '*.svg' \) 2>/dev/null | head -n1)" fi if [[ -s "$found" ]]; then cp -f "$found" "$icon" sed -i "s|^Icon=.*|Icon=$icon|" "$HOME/.local/share/applications/human-linux-appimage-${name}.desktop" fi rm -rf squashfs-root ) || true echo "integrated $base" } shopt -s nullglob for d in "$HOME/Downloads"/*.AppImage "$HOME/Downloads"/*.appimage \ "$HOME/Applications"/*.AppImage "$HOME/Applications"/*.appimage; do integrate_appimage "$d" done shopt -u nullglob # --------------------------------------------------------------------------- # Brax.me # A site-as-app launcher. Same on every distro that has Brave or Chrome. # --------------------------------------------------------------------------- log "Brax.me launcher" browser="$(command -v brave-browser || command -v google-chrome-stable || command -v google-chrome || true)" if [[ -n "$browser" ]]; then mkdir -p "$HOME/.local/share/icons" curl -fsSL -o "$HOME/.local/share/icons/brax-me.png" "$BRAX_ICON_URL" || warn "Brax icon download failed" cat > "$HOME/.local/share/applications/brax-me.desktop" < "$HOME/.local/bin/open-appimage" <<'EOF' #!/usr/bin/env bash f="${1:-}" [[ -n "$f" && -f "$f" ]] || exit 1 base="$(basename "$f")" name="${base%.[Aa]pp[Ii]mage}" mkdir -p "$HOME/Applications" "$HOME/.local/share/icons" "$HOME/Documents/AppFiles/appimages" dest="$HOME/Applications/$base" if [[ "$f" != "$dest" ]]; then mv "$f" "$dest" fi chmod +x "$dest" || true ln -sfn "$dest" "$HOME/Documents/AppFiles/appimages/$base" icon="$HOME/.local/share/icons/${name}.png" cat > "$HOME/.local/share/applications/human-linux-appimage-${name}.desktop" </dev/null 2>&1 || true found="" if [[ -e squashfs-root/.DirIcon ]]; then found="$(readlink -f squashfs-root/.DirIcon)" fi if [[ ! -s "$found" ]]; then found="$(find squashfs-root -type f \( -iname '*.png' -o -iname '*.svg' \) 2>/dev/null | head -n1)" fi if [[ -s "$found" ]]; then cp -f "$found" "$icon" sed -i "s|^Icon=.*|Icon=$icon|" "$HOME/.local/share/applications/human-linux-appimage-${name}.desktop" fi rm -rf squashfs-root ) update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true exec "$dest" EOF chmod +x "$HOME/.local/bin/open-appimage" cat > "$HOME/.local/share/applications/open-appimage.desktop" < "$HOME/.local/share/mime/packages/appimage.xml" <<'EOF' AppImage EOF update-mime-database "$HOME/.local/share/mime" 2>/dev/null || true xdg-mime default open-appimage.desktop application/vnd.appimage || true xdg-mime default open-appimage.desktop application/x-iso9660-appimage || true # An executable AppImage is launched by Files and skips this handler. # Leave Downloads copies non-executable so a click installs them. shopt -s nullglob for d in "$HOME/Downloads"/*.AppImage "$HOME/Downloads"/*.appimage; do chmod a-x "$d" 2>/dev/null || true done shopt -u nullglob if id="$(first_existing_desktop gnome-disk-image-writer.desktop org.gnome.DiskUtility.desktop)"; then mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications" cat > "$HOME/.local/bin/open-iso" <<'EOF' #!/usr/bin/env bash iso="${1:-}" [[ -n "$iso" && -f "$iso" ]] || exit 1 real="$(readlink -f "$iso")" # Files mounts the ISO on click. Disk Image Writer errors if it is mounted. while read -r loop back; do [[ "$back" == "$real" ]] || continue udisksctl unmount -b "$loop" >/dev/null 2>&1 || umount -l "$loop" >/dev/null 2>&1 || true udisksctl loop-delete -b "$loop" >/dev/null 2>&1 || losetup -d "$loop" >/dev/null 2>&1 || true done < <(losetup -lnO NAME,BACK-FILE 2>/dev/null || true) # A restore fails if the USB stick is still mounted. Unmount removable disks only. while read -r dev rm; do [[ "$rm" == "1" ]] || continue while read -r part mp; do [[ -n "$mp" ]] || continue udisksctl unmount -b "$part" >/dev/null 2>&1 || umount -l "$part" >/dev/null 2>&1 || true done < <(lsblk -pnr -o PATH,MOUNTPOINT "$dev") done < <(lsblk -dnr -o PATH,RM) if command -v gnome-disk-image-writer >/dev/null; then exec gnome-disk-image-writer "$iso" fi exec gnome-disks EOF chmod +x "$HOME/.local/bin/open-iso" cat > "$HOME/.local/share/applications/open-iso.desktop" </dev/null 2>&1 || return 0 gsettings list-keys "$schema" 2>/dev/null | grep -qx favorite-apps || return 0 python3 - "$schema" "$files" "$editor" <<'PY' import ast, subprocess, sys schema, files, editor = sys.argv[1:] wanted = [ "brave-browser.desktop", "google-chrome.desktop", editor, files, "org.gnome.Calculator.desktop", "org.gnome.Terminal.desktop", ] # Browsers and mail by real filename. Zorin may not use firefox.desktop. import pathlib def first_named(word): for folder in ( pathlib.Path("/usr/share/applications"), pathlib.Path("/var/lib/snapd/desktop/applications"), pathlib.Path.home()/".local/share/applications", ): if not folder.is_dir(): continue for f in sorted(folder.glob("*.desktop")): if word in f.name.lower(): return f.name return None browser = first_named("firefox") if browser: wanted.insert(2, browser) mail = first_named("thunderbird") if mail: wanted.insert(3, mail) wanted.insert(4, "org.keepassxc.KeePassXC.desktop") wanted.insert(5, "libreoffice-writer.desktop") raw = subprocess.check_output(["gsettings", "get", schema, "favorite-apps"], text=True) try: cur = ast.literal_eval(raw.strip()) except Exception: cur = [] if not isinstance(cur, list): cur = [] rest = [x for x in cur if x not in wanted] out = [] for x in wanted: if x in ("", "None"): continue p1 = pathlib.Path("/usr/share/applications") / x p2 = pathlib.Path.home() / ".local/share/applications" / x p3 = pathlib.Path("/var/lib/snapd/desktop/applications") / x if p1.is_file() or p2.is_file() or p3.is_file(): out.append(x) out += [x for x in rest if x not in out] repr_out = "[" + ", ".join("'" + x.replace("'", "") + "'" for x in out) + "]" subprocess.check_call(["gsettings", "set", schema, "favorite-apps", repr_out]) print("pinned", repr_out) PY # AppImages belong in Show Apps, not on the dock. python3 - "$schema" <<'PY' || true import ast, subprocess, sys schema = sys.argv[1] raw = subprocess.check_output(["gsettings", "get", schema, "favorite-apps"], text=True) try: cur = ast.literal_eval(raw.strip()) except Exception: cur = [] if isinstance(cur, list): out = [x for x in cur if "appimage" not in x.lower()] if out != cur: repr_out = "[" + ", ".join("'" + x.replace("'", "") + "'" for x in out) + "]" subprocess.check_call(["gsettings", "set", schema, "favorite-apps", repr_out]) print("unpinned appimages", repr_out) PY } case "$de" in *cosmic*) log "COSMIC dock and Files sidebar" fav="$HOME/.config/cosmic/com.system76.CosmicAppList/v1/favorites" mkdir -p "$(dirname "$fav")" python3 - "$fav" <<'PY' || true import json, os, sys from pathlib import Path path = sys.argv[1] candidates = [ "brave-browser", "com.brave.Browser", "google-chrome", "com.google.Chrome", "firefox", "thunderbird", "org.mozilla.Thunderbird", "org.keepassxc.KeePassXC", "libreoffice-writer", "com.system76.CosmicEdit", "org.gnome.TextEditor", "com.system76.CosmicFiles", "org.gnome.Nautilus", "org.gnome.Calculator", "com.system76.CosmicStore", "com.system76.CosmicTerm", "com.system76.CosmicSettings", ] def exists(appid): return Path(f"/usr/share/applications/{appid}.desktop").is_file() or Path.home().joinpath(f".local/share/applications/{appid}.desktop").is_file() wanted = [] seen = set() for appid in candidates: if appid in seen or not exists(appid): continue wanted.append(appid) seen.add(appid) cur = [] if os.path.exists(path): try: cur = json.load(open(path)) except Exception: cur = [] if not isinstance(cur, list): cur = [] rest = [x for x in cur if x not in wanted] json.dump(wanted + rest, open(path, "w"), indent=4) print("cosmic dock", wanted) PY files_fav="$HOME/.config/cosmic/com.system76.CosmicFiles/v1/favorites" mkdir -p "$(dirname "$files_fav")" if [[ ! -f "$files_fav" ]]; then cat > "$files_fav" </dev/null 2>&1 & ;; *mate*) log "MATE menu pins" if gsettings list-schemas | grep -qx 'com.linuxmint.mintmenu.plugins.applications'; then python3 - <<'PY' || true import ast, subprocess from pathlib import Path schema = "com.linuxmint.mintmenu.plugins.applications" key = "favorite-apps-list" wanted = [ "brave-browser.desktop", "google-chrome.desktop", "firefox.desktop", "thunderbird.desktop", "org.keepassxc.KeePassXC.desktop", "libreoffice-writer.desktop", ] for editor in ("xed.desktop", "pluma.desktop", "org.gnome.TextEditor.desktop"): if Path("/usr/share/applications", editor).is_file(): wanted.append(editor) break wanted += ["caja.desktop", "org.gnome.Calculator.desktop", "mate-terminal.desktop"] raw = subprocess.check_output(["gsettings", "get", schema, key], text=True).strip() try: cur = ast.literal_eval(raw) except Exception: cur = [] if not isinstance(cur, list): cur = [] rest = [x for x in cur if x not in wanted and x not in ("org.gnome.Nautilus.desktop", "nemo.desktop")] out = wanted + rest repr_out = "[" + ", ".join("'" + x.replace("'", "") + "'" for x in out) + "]" subprocess.check_call(["gsettings", "set", schema, key, repr_out]) print("mate pins", repr_out) PY fi log "MATE panel launchers" python3 - <<'PY' || true import ast, subprocess from pathlib import Path wanted = [ "brave-browser.desktop", "google-chrome.desktop", "firefox.desktop", "thunderbird.desktop", "org.keepassxc.KeePassXC.desktop", "libreoffice-writer.desktop", ] for editor in ("xed.desktop", "pluma.desktop", "org.gnome.TextEditor.desktop"): if Path("/usr/share/applications", editor).is_file(): wanted.append(editor) break wanted += ["caja.desktop", "org.gnome.Calculator.desktop", "mate-terminal.desktop"] def run(args): return subprocess.check_output(args, text=True).strip() try: ids = ast.literal_eval(run(["gsettings", "get", "org.mate.panel", "object-id-list"])) except Exception: print("no mate panel") raise SystemExit if not isinstance(ids, list): ids = [] existing = set() for oid in ids: loc = subprocess.run(["gsettings", "get", f"org.mate.panel.object:/org/mate/panel/objects/{oid}/", "launcher-location"], text=True, capture_output=True) if loc.returncode == 0: existing.add(loc.stdout.strip().strip("'")) pos = 20 added = [] for name in wanted: desktop = f"/usr/share/applications/{name}" if not Path(desktop).is_file() or desktop in existing or name in existing: continue oid = f"readylinux-{name.replace('.desktop','')}" path = f"org.mate.panel.object:/org/mate/panel/objects/{oid}/" subprocess.check_call(["gsettings", "set", path, "object-type", "launcher"]) subprocess.check_call(["gsettings", "set", path, "launcher-location", desktop]) subprocess.check_call(["gsettings", "set", path, "toplevel-id", "bottom"]) subprocess.check_call(["gsettings", "set", path, "position", str(pos)]) subprocess.check_call(["gsettings", "set", path, "locked", "true"]) ids.append(oid) added.append(name) pos += 20 if added: repr_ids = "[" + ", ".join("'" + x.replace("'", "") + "'" for x in ids) + "]" subprocess.check_call(["gsettings", "set", "org.mate.panel", "object-id-list", repr_ids]) print("mate panel", added) else: print("mate panel unchanged") PY # Mint's bottom bar is 24px. 36 is readable without being tall. python3 - <<'PY' || true import ast, subprocess raw = subprocess.check_output(["gsettings", "get", "org.mate.panel", "toplevel-id-list"], text=True).strip() try: ids = ast.literal_eval(raw) except Exception: ids = ["bottom"] for tid in ids: path = f"org.mate.panel.toplevel:/org/mate/panel/toplevels/{tid}/" subprocess.run(["gsettings", "set", path, "size", "40"], check=False) print("panel size", tid) PY ;; *xfce*) log "Xfce Whisker favorites and left-side pins" # Skip apps already on the bar. Reset the list, do not append. # An expanding separator is what right-justifies the tray and clock. if command -v xfconf-query >/dev/null; then names=(brave-browser.desktop google-chrome.desktop firefox.desktop \ thunderbird.desktop org.keepassxc.KeePassXC.desktop \ libreoffice-writer.desktop xed.desktop org.gnome.TextEditor.desktop kolourpaint.desktop org.kde.kolourpaint.desktop \ thunar.desktop org.gnome.Calculator.desktop xfce4-terminal.desktop) editor="$(ls /usr/share/applications | grep -iE '^(xed|org.gnome.TextEditor|mousepad)' | head -n1 || true)" [[ -n "$editor" ]] && names+=("$editor") args=(-c xfce4-panel -p /plugins/plugin-1/favorites -n) for n in "${names[@]}"; do [[ -f "/usr/share/applications/$n" ]] && args+=(-t string -s "$n") done xfconf-query "${args[@]}" || true ids="$(xfconf-query -c xfce4-panel -p /panels/panel-1/plugin-ids 2>/dev/null || true)" nums="$(echo "$ids" | grep -E '^[0-9]+$' | awk '!seen[$0]++' || true)" have="" for n in $nums; do item="$(xfconf-query -c xfce4-panel -p /plugins/plugin-$n/items 2>/dev/null | grep -E '\.desktop$' | head -n1 || true)" [[ -n "$item" ]] && have="$have $item" done next=40 new=() for name in "${names[@]}"; do src="/usr/share/applications/$name" [[ -f "$src" ]] || continue [[ "$have" == *" $name"* ]] && continue while echo "$nums" | grep -qx "$next"; do next=$((next + 1)); done mkdir -p "$HOME/.config/xfce4/panel/launcher-$next" cp -f "$src" "$HOME/.config/xfce4/panel/launcher-$next/$name" xfconf-query -c xfce4-panel -p "/plugins/plugin-$next" -n -t string -s launcher || true xfconf-query -c xfce4-panel -p "/plugins/plugin-$next/items" -r 2>/dev/null || true xfconf-query -c xfce4-panel -p "/plugins/plugin-$next/items" -n -t string -s "$name" || true new+=("$next") nums="$nums"$'\n'"$next" next=$((next + 1)) done left=() system=() sep="" seen_items=" " for n in $nums; do [[ " ${new[*]} " == *" $n "* ]] && continue kind="$(xfconf-query -c xfce4-panel -p /plugins/plugin-$n 2>/dev/null || true)" item="$(xfconf-query -c xfce4-panel -p /plugins/plugin-$n/items 2>/dev/null | grep -E '\.desktop$' | head -n1 || true)" if [[ -n "$item" && "$seen_items" == *" $item "* ]]; then continue fi [[ -n "$item" ]] && seen_items="$seen_items$item " if [[ "$item" == *xed* || "$item" == *TextEditor* || "$item" == *mousepad* || "$item" == *text-editor* ]]; then left+=("$n") continue fi case "$kind" in systray|notification-plugin|xapp-status-plugin|power-manager-plugin|pulseaudio|clock) xfconf-query -c xfce4-panel -p "/plugins/plugin-$n/items" -r 2>/dev/null || true system+=("$n") ;; separator) [[ -z "$sep" ]] && sep="$n" ;; *) left+=("$n") ;; esac done if [[ -z "$sep" ]]; then sep="$next" xfconf-query -c xfce4-panel -p "/plugins/plugin-$sep" -n -t string -s separator || true fi xfconf-query -c xfce4-panel -p "/plugins/plugin-$sep/expand" -n -t bool -s true || true xfconf-query -c xfce4-panel -p /panels/panel-1/plugin-ids -r || true args=(-c xfce4-panel -p /panels/panel-1/plugin-ids -n) for n in "${left[@]}" "${new[@]}" "$sep" "${system[@]}"; do [[ -n "$n" ]] && args+=(-t int -s "$n") done xfconf-query "${args[@]}" || true xfce4-panel -r || true xfconf-query -c xfce4-panel -p /panels/panel-1/size -n -t int -s 40 || true xfconf-query -c xfce4-panel -p /panels/panel-1/icon-size -n -t int -s 40 || true fi sudo apt-get install -y wmctrl xdotool || true if gsettings list-schemas | grep -qx 'org.gnome.TextEditor'; then gsettings set org.gnome.TextEditor use-system-font false || true gsettings set org.gnome.TextEditor custom-font 'Monospace 12' || true echo "text editor font $(gsettings get org.gnome.TextEditor custom-font 2>/dev/null || echo failed)" else echo "no text editor schema" fi # Xed remembers a maximized 1920x1003 window. 0 is not maximized. if gsettings list-schemas | grep -qx 'org.x.editor.state.window'; then gsettings set org.x.editor.state.window size "(1100, 700)" || true gsettings set org.x.editor.state.window state 0 || true fi if command -v xfconf-query >/dev/null; then xfconf-query -c thunar -p /last-window-width -n -t int -s 1100 2>/dev/null || true xfconf-query -c thunar -p /last-window-height -n -t int -s 700 2>/dev/null || true xfconf-query -c thunar -p /last-window-maximized -n -t bool -s false 2>/dev/null || true fi # Thunar does not remember position. The menu launcher moves it after open. if command -v wmctrl >/dev/null && command -v xdotool >/dev/null; then mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications" cat > "$HOME/.local/bin/thunar-place" <<'EOF' #!/usr/bin/env bash /usr/bin/thunar "$@" & sleep 0.6 hex="$(wmctrl -l | grep -i thunar | awk '{print $1; exit}')" [[ -n "${hex:-}" ]] || exit 0 dec=$((hex)) wmctrl -i -r "$hex" -b remove,maximized_vert,maximized_horz xdotool windowmove "$dec" 40 40 xdotool windowsize "$dec" 1100 700 EOF chmod +x "$HOME/.local/bin/thunar-place" cat > "$HOME/.local/share/applications/thunar.desktop" <' '' '' > "$xbel" fi for pair in "$HOME/Media|Media" "$HOME/Documents/AppFiles|AppFiles"; do uri="file://${pair%%|*}" label="${pair##*|}" grep -qF "$uri" "$xbel" || sed -i "s## $label\n#" "$xbel" done ;; *) pin_gnome org.gnome.shell org.gnome.Nautilus.desktop org.gnome.TextEditor.desktop || true if gsettings list-schemas | grep -q 'org.gnome.shell.extensions.dash-to-dock'; then gnome-extensions enable dash-to-dock@micxgx.gmail.com 2>/dev/null || true pos="$(gsettings get org.gnome.shell.extensions.dash-to-dock dock-position 2>/dev/null || echo '')" if [[ "$pos" == "'LEFT'" ]]; then gsettings set org.gnome.shell.extensions.dash-to-dock dock-position 'BOTTOM' || true gsettings set org.gnome.shell.extensions.dash-to-dock extend-height false || true fi size="$(gsettings get org.gnome.shell.extensions.dash-to-dock dash-max-icon-size 2>/dev/null || echo '')" gsettings set org.gnome.shell.extensions.dash-to-dock dash-max-icon-size 40 || true fi ;; esac if gsettings list-keys org.gnome.settings-daemon.plugins.power 2>/dev/null | grep -q lid-close-ac-action; then gsettings set org.gnome.settings-daemon.plugins.power lid-close-ac-action 'nothing' || true gsettings set org.gnome.settings-daemon.plugins.power lid-close-battery-action 'suspend' || true fi # Files remembers a maximized window. Open it at a normal size. if gsettings list-schemas | grep -qx 'org.gnome.nautilus.window-state'; then gsettings set org.gnome.nautilus.window-state maximized false || true gsettings set org.gnome.nautilus.window-state initial-size '(1100, 700)' || true fi if gsettings list-schemas | grep -qx 'org.gnome.TextEditor'; then gsettings set org.gnome.TextEditor style-scheme 'classic' || true gsettings set org.gnome.TextEditor use-system-font false || true gsettings set org.gnome.TextEditor custom-font 'Monospace 12' || true elif gsettings list-schemas | grep -qx 'org.gnome.gedit.preferences.editor'; then gsettings set org.gnome.gedit.preferences.editor scheme 'classic' || true gsettings set org.gnome.gedit.preferences.editor use-default-font false || true gsettings set org.gnome.gedit.preferences.editor editor-font 'Monospace 12' || true fi python3 - <<'PY' || true from pathlib import Path try: from gi.repository import GLib except Exception: raise SystemExit(0) path = Path.home() / ".local/share/org.gnome.TextEditor/session.gvariant" if not path.is_file(): raise SystemExit(0) data = GLib.Bytes.new(path.read_bytes()) variant = GLib.Variant.new_from_bytes(GLib.VariantType.new("a{sv}"), data, False) items = [] for i in range(variant.n_children()): entry = variant.get_child_value(i) key = entry.get_child_value(0).get_string() if key == "default-window-size": items.append((key, GLib.Variant("(uu)", (1100, 700)))) else: items.append((key, entry.get_child_value(1).get_variant())) new = GLib.Variant("a{sv}", items) path.write_bytes(bytes(new.get_data_as_bytes().get_data())) print("text editor window 1100x700") PY if gsettings list-schemas | grep -qx 'org.x.editor.preferences.editor'; then gsettings set org.x.editor.preferences.editor use-default-font false || true gsettings set org.x.editor.preferences.editor editor-font 'Monospace 12' || true fi # Terminal frame is 110x34 on the distros we support. # Newer Ubuntu uses Ptyxis. Zorin and Mint use GNOME Terminal. if gsettings list-schemas | grep -qx 'org.gnome.Ptyxis'; then gsettings set org.gnome.Ptyxis default-columns 110 || true gsettings set org.gnome.Ptyxis default-rows 34 || true gsettings set org.gnome.Ptyxis use-system-font false || true gsettings set org.gnome.Ptyxis font-name 'Monospace 10' || true fi if gsettings list-schemas | grep -qx 'org.gnome.Terminal.Legacy.Settings' && command -v gnome-terminal >/dev/null; then python3 - <<'PY' || true import subprocess def setv(path, key, val): subprocess.check_call(["gsettings", "set", path, key, val]) raw = subprocess.check_output(["dconf", "list", "/org/gnome/terminal/legacy/profiles:/"], text=True) uids = [line.strip("/:") for line in raw.splitlines() if line.strip()] default = "" if "default" in subprocess.check_output(["gsettings", "list-keys", "org.gnome.Terminal.Legacy.Settings"], text=True).split(): default = subprocess.check_output(["gsettings", "get", "org.gnome.Terminal.Legacy.Settings", "default"], text=True).strip().strip("'") if default and default not in uids: uids.append(default) if not uids: print("gnome-terminal has no profile yet; size applies after the first terminal is closed and opened") for uid in uids: path = f"org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:{uid}/" setv(path, "default-size-columns", "110") setv(path, "default-size-rows", "34") setv(path, "use-system-font", "false") setv(path, "font", "'Monospace 10'") setv(path, "use-theme-colors", "false") setv(path, "foreground-color", "'#FFFFFF'") setv(path, "background-color", "'#000000'") print("gnome-terminal next window 110x34", uid) PY mkdir -p "$HOME/.local/share/applications" cat > "$HOME/.local/share/applications/org.gnome.Terminal.desktop" <> "$termrc" for kv in "MiscDefaultGeometry=120x36" "FontName=Monospace 10" "ColorForeground=#ffffff" "ColorBackground=#000000"; do key="${kv%%=*}" sed -i "/^${key}=/d" "$termrc" printf '%s\n' "$kv" >> "$termrc" done fi # 125% on a laptop or a 1080p-class screen. A desktop at 1440p or 4K stays at 100%. primary_w="$(xrandr 2>/dev/null | awk '/ connected/{print $3; exit}' | cut -dx -f1)" if [[ -d /sys/class/power_supply ]] && compgen -G /sys/class/power_supply/BAT* >/dev/null; then SCALE="1.25" elif [[ -n "${primary_w:-}" && "$primary_w" -le 1920 ]]; then SCALE="1.25" else SCALE="1.0" fi log "Screen scale $SCALE" if gsettings writable org.gnome.desktop.interface text-scaling-factor >/dev/null 2>&1; then gsettings set org.gnome.desktop.interface text-scaling-factor "$SCALE" || true fi if gsettings writable org.cinnamon.desktop.interface text-scaling-factor >/dev/null 2>&1; then gsettings set org.cinnamon.desktop.interface text-scaling-factor "$SCALE" || true fi if gsettings writable org.cinnamon active-display-scale >/dev/null 2>&1; then gsettings set org.cinnamon active-display-scale "$SCALE" || true fi # MATE has no 125% display scale. 120 DPI is the same size change. if [[ "$SCALE" == "1.25" ]] && gsettings writable org.mate.font-rendering dpi >/dev/null 2>&1; then gsettings set org.mate.font-rendering dpi 120 || true fi # Xfce uses DPI, not the GNOME scale key. 120 is the 125% size. if [[ "$SCALE" == "1.25" ]] && command -v xfconf-query >/dev/null; then xfconf-query -c xsettings -p /Xft/DPI -n -t int -s 120 2>/dev/null || \ xfconf-query -c xsettings -p /Xft/DPI -t int -s 120 2>/dev/null || true fi if [[ "$SCALE" == "1.25" && -f "$HOME/.config/cinnamon-monitors.xml" ]]; then sed -i 's/scale="0.75"/scale="1.25"/g; s/0.75<\/scale>/1.25<\/scale>/g' \ "$HOME/.config/cinnamon-monitors.xml" || true fi if [[ -n "$WALLPAPER_URL" ]]; then mkdir -p "$HOME/.local/share/backgrounds" wall="$HOME/.local/share/backgrounds/human-linux-wallpaper.jpg" if gsettings list-schemas | grep -qx 'org.mate.background'; then current="$(gsettings get org.mate.background picture-filename 2>/dev/null || echo '')" if [[ "$current" == *"/usr/share/backgrounds/"* || "$current" == "''" ]]; then if curl -fsSL -o "$wall" "$WALLPAPER_URL"; then gsettings set org.mate.background picture-filename "$wall" || true gsettings set org.mate.background picture-options 'zoom' || true else warn "Wallpaper download failed" fi else log "Wallpaper already chosen. Skipped." fi else current="$(gsettings get org.gnome.desktop.background picture-uri 2>/dev/null || echo '')" dark="$(gsettings get org.gnome.desktop.background picture-uri-dark 2>/dev/null || echo '')" if [[ "$current" == *"human-linux-wallpaper.jpg"* && "$dark" == *"human-linux-wallpaper.jpg"* ]]; then log "Wallpaper already set." elif curl -fsSL -o "$wall" "$WALLPAPER_URL"; then gsettings set org.gnome.desktop.background picture-uri "file://$wall" || true gsettings set org.gnome.desktop.background picture-uri-dark "file://$wall" || true gsettings set org.gnome.desktop.background picture-options 'zoom' || true else warn "Wallpaper download failed" fi fi else log "Wallpaper URL blank. Skipped." fi # Xfce wallpaper is per monitor. The live laptop panel is monitoreDP-1. # last-image alone does not change the picture while cycling is on. if [[ -n "$WALLPAPER_URL" ]] && command -v xfconf-query >/dev/null && [[ "$de" == *xfce* ]]; then mkdir -p "$HOME/.local/share/backgrounds" wall="$HOME/.local/share/backgrounds/human-linux-wallpaper.jpg" if curl -fsSL -o "$wall" "$WALLPAPER_URL"; then for prop in \ /backdrop/screen0/monitoreDP-1/workspace0/last-image \ /backdrop/screen0/monitoreDP-1/workspace0/image-path \ /backdrop/screen0/monitor0/image-path \ /backdrop/screen0/monitor0/last-single-image do xfconf-query -c xfce4-desktop -p "$prop" -n -t string -s "$wall" 2>/dev/null || true done xfconf-query -c xfce4-desktop -p /backdrop/screen0/monitoreDP-1/workspace0/backdrop-cycle-enable -n -t bool -s false 2>/dev/null || true fi fi update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true # Click a .sh in Files: Run or Edit, instead of opening the text editor. log "Script launcher" cat > "$HOME/.local/bin/sh-launcher" <<'EOF' #!/usr/bin/env bash f="${1:-}" [[ -n "$f" && -f "$f" ]] || { zenity --error --text="No script given."; exit 1; } zenity --question --width=420 --title="Script" \ --ok-label="Run" --cancel-label="Edit" \ --text="File: $f Run starts this script. Edit opens it in the text editor." || { if command -v gnome-text-editor >/dev/null; then exec gnome-text-editor "$f" elif command -v gedit >/dev/null; then exec gedit "$f" elif command -v xed >/dev/null; then exec xed "$f" elif command -v pluma >/dev/null; then exec pluma "$f" elif command -v mousepad >/dev/null; then exec mousepad "$f" else zenity --error --text="No text editor found." fi exit 0 } chmod +x "$f" || true if command -v gnome-terminal >/dev/null; then gnome-terminal -- bash -c "bash \"$f\"; echo; read -r -p 'Press Enter to close.'" elif command -v kgx >/dev/null; then kgx -- bash -c "bash \"$f\"; echo; read -r -p 'Press Enter to close.'" elif command -v x-terminal-emulator >/dev/null; then x-terminal-emulator -e bash -c "bash \"$f\"; echo; read -r -p 'Press Enter to close.'" else zenity --error --text="No terminal program found." fi EOF chmod +x "$HOME/.local/bin/sh-launcher" cat > "$HOME/.local/share/applications/sh-launcher.desktop" </dev/null 2>&1; then gsettings set org.nemo.preferences executable-text-activation 'launch' || true fi # Clickable launchers. Files will not run a .sh; these .desktop files do. if [[ -f "$0" ]]; then kit="$HOME/.local/share/human-linux" mkdir -p "$kit" "$HOME/.local/share/applications" cp -f "$0" "$kit/install.sh" chmod +x "$kit/install.sh" if [[ -f "$(dirname "$0")/uninstall.sh" ]]; then cp -f "$(dirname "$0")/uninstall.sh" "$kit/uninstall.sh" chmod +x "$kit/uninstall.sh" fi cat > "$HOME/.local/share/applications/human-linux-install.desktop" < "$HOME/.local/share/applications/human-linux-uninstall.desktop" </dev/null || true gio set "$HOME/.local/share/applications/human-linux-uninstall.desktop" metadata::trusted true 2>/dev/null || true fi log "Done." echo "Plug a USB stick in. It should appear under ~/Media/Disks after Ubuntu mounts it." echo "Show Apps: ReadyLinux install, and ReadyLinux uninstall." echo "Log out and back in if Show Apps or the dock did not update." echo "On Zorin, dock icons can show but not click until that logout." echo "Lid-on-AC applies fully after the next reboot. logind was not restarted."