weehong revised this gist 3 months ago. Go to revision
1 file changed, 67 insertions, 9 deletions
READMD.md
| @@ -1,18 +1,76 @@ | |||
| 1 | - | # Ubuntu Sans Nerd Font Setup | |
| 1 | + | # Ubuntu / Debian Setup Manager | |
| 2 | 2 | ||
| 3 | - | A lightweight script to automatically fetch and install the latest **Ubuntu Sans Nerd Font** for Ubuntu Desktop. | |
| 3 | + | A collection of hardened install scripts for a fresh Ubuntu / Debian desktop, driven by an interactive `menu.sh`. Each script auto-detects the distro, uses `signed-by` APT keyrings (no `apt-key`), is idempotent (safe to re-run), and supports `--help` and `--dry-run`. | |
| 4 | 4 | ||
| 5 | - | ## Quick Install | |
| 5 | + | ## Quick start — run the menu | |
| 6 | 6 | ||
| 7 | - | Run the following command in your terminal: | |
| 7 | + | One-liner — fetches `menu.sh` and runs it. You'll see a numeric picker and can select one option, several, or all: | |
| 8 | 8 | ||
| 9 | 9 | ```bash | |
| 10 | + | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)" | |
| 11 | + | ``` | |
| 12 | + | ||
| 13 | + | How to use the picker: | |
| 14 | + | ||
| 15 | + | - Enter one or more numbers separated by spaces, e.g. `1 6 11` | |
| 16 | + | - `0` runs every option in order | |
| 17 | + | - `-1` exits | |
| 18 | + | ||
| 19 | + | The menu caches `sudo` credentials up-front so multi-task runs don't keep re-prompting, and falls back to per-user execution for the three scripts that must not run as root (JetBrains Toolbox, IBus Pinyin, Ubuntu Sans Nerd Font). | |
| 20 | + | ||
| 21 | + | ## Available scripts | |
| 22 | + | ||
| 23 | + | | # | Script | Runs as | What it does | | |
| 24 | + | |---|---|---|---| | |
| 25 | + | | 1 | [`install-firefox.sh`](install-firefox.sh) | sudo | Removes the Firefox Snap and installs Firefox from Mozilla's official APT repo, with APT pinning so it stays on the Mozilla build. Verifies the Mozilla signing-key fingerprint. | | |
| 26 | + | | 2 | [`install-thunderbird.sh`](install-thunderbird.sh) | sudo | Ubuntu: removes the Snap, adds the Mozilla Team PPA, and pins it. Debian: installs from the standard repos. | | |
| 27 | + | | 3 | [`install-1password.sh`](install-1password.sh) | sudo | Configures the 1Password APT repo with `debsig` signature policy. Arch-aware (amd64 / arm64). | | |
| 28 | + | | 4 | [`install-espanso.sh`](install-espanso.sh) | sudo | Installs Espanso. Auto-detects Wayland vs X11 from `$XDG_SESSION_TYPE` and registers the systemd-user service as the invoking desktop user (not root). | | |
| 29 | + | | 5 | [`install-libreoffice.sh`](install-libreoffice.sh) | sudo | Detects the latest stable release on documentfoundation.org, downloads the matching `.deb` tarball, verifies the published MD5, and installs. Purges the distro's `libreoffice*` first to avoid library conflicts (opt-out with `--keep-distro-libreoffice`). | | |
| 30 | + | | 6 | [`install-vscode.sh`](install-vscode.sh) | sudo | Microsoft's official `code` APT repo, signed-by keyring. `--insiders` flag installs `code-insiders` instead. | | |
| 31 | + | | 7 | [`install-jetbrains-toolbox.sh`](install-jetbrains-toolbox.sh) | **user** | Per-user install into `~/.local/share/JetBrains/Toolbox`. SHA-256 verified against JetBrains' release feed. x86_64 + aarch64. Drops a `.desktop` launcher. | | |
| 32 | + | | 8 | [`install-bruno.sh`](install-bruno.sh) | sudo | Bruno API client from the official APT repo. Keyserver fetch is wrapped in a 5-attempt retry/backoff because `keyserver.ubuntu.com` is occasionally flaky. | | |
| 33 | + | | 9 | [`install-ipatool.sh`](install-ipatool.sh) | sudo | Installs the latest release of `majd/ipatool` from GitHub. SHA-256 verified against the release `checksums.txt`. Honors `$GITHUB_TOKEN` to avoid API rate limits. | | |
| 34 | + | | 10 | [`install-network_drive.sh`](install-network_drive.sh) | sudo | Discovers SMB shares on a Synology NAS and adds them to `/etc/fstab` under `/mnt/Synology` with `x-systemd.automount`. fstab block is managed via begin/end markers so re-runs replace rather than duplicate. Credentials file is `0600`. Best-effort GNOME Dock pin. | | |
| 35 | + | | 11 | [`install-ibus-pinyin.sh`](install-ibus-pinyin.sh) | **user** | Installs `ibus-libpinyin` and Simplified Chinese language packs, restarts the IBus daemon, and idempotently adds `('ibus', 'libpinyin')` to GNOME's input sources via `gsettings`. | | |
| 36 | + | | 12 | [`install_font.sh`](install_font.sh) | **user** | Installs the latest Ubuntu Sans Nerd Font to `~/.local/share/fonts/UbuntuSans` and refreshes the font cache. No sudo needed. | | |
| 37 | + | ||
| 38 | + | "Runs as **user**" entries must be invoked as your normal desktop user, not via `sudo`. The other entries elevate via `sudo` internally and the menu primes `sudo -v` up-front, so you'll only be prompted once. | |
| 39 | + | ||
| 40 | + | ## Running a single script directly | |
| 41 | + | ||
| 42 | + | If you'd rather skip the menu, each script can be run on its own. Use the right invocation pattern for that script's privilege mode: | |
| 43 | + | ||
| 44 | + | ```bash | |
| 45 | + | # sudo scripts (1, 2, 3, 4, 5, 6, 8, 9, 10) — pipe through sudo bash | |
| 46 | + | curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-firefox.sh | sudo bash | |
| 47 | + | ||
| 48 | + | # user scripts (7, 11, 12) — DO NOT use sudo; they install per-user | |
| 10 | 49 | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)" | |
| 11 | 50 | ``` | |
| 12 | 51 | ||
| 13 | - | ## Overview | |
| 52 | + | Every script accepts `--help` and `--dry-run` (the latter prints what would happen without executing). For example: | |
| 53 | + | ||
| 54 | + | ```bash | |
| 55 | + | curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-libreoffice.sh \ | |
| 56 | + | | bash -s -- --help | |
| 57 | + | ``` | |
| 58 | + | ||
| 59 | + | ## What "hardened" means here | |
| 60 | + | ||
| 61 | + | All scripts share the same robustness baseline: | |
| 62 | + | ||
| 63 | + | - `set -euo pipefail` + IFS hygiene + `ERR` trap reporting the failing line number | |
| 64 | + | - `/etc/os-release` distro auto-detection (no hardcoded codenames) | |
| 65 | + | - `dpkg --print-architecture` / `uname -m` for architecture (amd64 / arm64 / armhf / x86_64 / aarch64 as applicable to each upstream) | |
| 66 | + | - Idempotent — re-running a script does not duplicate APT sources, fstab entries, gsettings entries, or `.desktop` files | |
| 67 | + | - `signed-by` keyrings in `/etc/apt/keyrings` (no deprecated `apt-key add`) | |
| 68 | + | - Checksum verification where the upstream publishes one (JetBrains SHA-256, LibreOffice MD5, ipatool SHA-256) | |
| 69 | + | - Per-user installers refuse to run as root; system installers refuse to run as non-root | |
| 70 | + | ||
| 71 | + | ## Supported distros | |
| 72 | + | ||
| 73 | + | - Ubuntu (any modern release; some scripts target Ubuntu 25.10 specifically but work elsewhere) | |
| 74 | + | - Debian (most scripts; `install-thunderbird.sh` takes a different code path since Debian has no PPAs) | |
| 14 | 75 | ||
| 15 | - | * **Always Latest:** Dynamically pulls the newest release from the official Nerd Fonts GitHub. | |
| 16 | - | * **No Sudo Needed:** Installs safely to your local user directory (`~/.local/share/fonts/UbuntuSans`). | |
| 17 | - | * **Ready to Use:** Automatically updates the font cache (`fc-cache`) so fonts are available immediately. | |
| 18 | - | * **Dependencies:** Requires `curl` and `unzip`. | |
| 76 | + | Other distros are rejected up-front rather than failing later in unpredictable ways. | |
weehong revised this gist 3 months ago. Go to revision
1 file changed, 225 insertions
menu.sh(file created)
| @@ -0,0 +1,225 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # menu.sh — Ubuntu/Debian Setup Manager | |
| 3 | + | # | |
| 4 | + | # Interactive menu that fetches and runs the hardened install scripts in this | |
| 5 | + | # Opengist. Each script is downloaded to a temp file (not blindly piped to bash) | |
| 6 | + | # and executed with the appropriate privilege level for that script: | |
| 7 | + | # | |
| 8 | + | # - "sudo" mode for system-wide installers (apt, /etc, /usr/local/bin) | |
| 9 | + | # - "user" mode for per-user installers that must NOT run as root | |
| 10 | + | # (gsettings, ~/.local, JetBrains Toolbox, fonts) | |
| 11 | + | # | |
| 12 | + | # Usage: | |
| 13 | + | # bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)" | |
| 14 | + | # | |
| 15 | + | # Or save and run locally: | |
| 16 | + | # curl -fsSLO https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh | |
| 17 | + | # bash menu.sh | |
| 18 | + | ||
| 19 | + | # Note: NOT using `set -e` because we want the menu loop to survive a failed | |
| 20 | + | # sub-script. We do use -u and pipefail to catch real bugs in this file. | |
| 21 | + | set -uo pipefail | |
| 22 | + | IFS=$'\n\t' | |
| 23 | + | ||
| 24 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 25 | + | readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD' | |
| 26 | + | ||
| 27 | + | log() { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; } | |
| 28 | + | warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; } | |
| 29 | + | err() { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; } | |
| 30 | + | hr() { printf '%s\n' "------------------------------------------------------"; } | |
| 31 | + | ||
| 32 | + | # Refuse to run as root: per-user scripts (jetbrains, fonts, ibus) need a real | |
| 33 | + | # desktop user. Sub-scripts elevate via sudo on their own. | |
| 34 | + | if (( EUID == 0 )); then | |
| 35 | + | err "Don't run $SCRIPT_NAME as root. Run as your normal user — it will call sudo for installers that need it." | |
| 36 | + | exit 1 | |
| 37 | + | fi | |
| 38 | + | ||
| 39 | + | # Sanity-check tools we depend on. | |
| 40 | + | for tool in curl bash mktemp; do | |
| 41 | + | command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; } | |
| 42 | + | done | |
| 43 | + | ||
| 44 | + | # Distro check (warn-only; sub-scripts enforce strictly). | |
| 45 | + | if [[ -r /etc/os-release ]]; then | |
| 46 | + | # shellcheck disable=SC1091 | |
| 47 | + | . /etc/os-release | |
| 48 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 49 | + | *ubuntu*|*debian*) : ;; | |
| 50 | + | *) warn "Detected ${PRETTY_NAME:-unknown}. These installers target Debian/Ubuntu; some will refuse to run." ;; | |
| 51 | + | esac | |
| 52 | + | fi | |
| 53 | + | ||
| 54 | + | # Cache sudo credentials up-front so sub-scripts that elevate don't keep | |
| 55 | + | # re-prompting in the middle of a multi-task run. | |
| 56 | + | prime_sudo() { | |
| 57 | + | if ! sudo -n true 2>/dev/null; then | |
| 58 | + | log "Caching sudo credentials (you may be prompted)..." | |
| 59 | + | sudo -v || { err "sudo authentication failed."; return 1; } | |
| 60 | + | fi | |
| 61 | + | # Keep sudo timestamp refreshed in the background while the menu runs. | |
| 62 | + | ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) & | |
| 63 | + | SUDO_KEEPALIVE_PID=$! | |
| 64 | + | trap 'kill "${SUDO_KEEPALIVE_PID:-}" 2>/dev/null || true' EXIT | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | # run_script <filename> <mode> | |
| 68 | + | # mode: "sudo" (run as root via sudo) or "user" (run as current user) | |
| 69 | + | # Returns: 0 on success, non-zero on failure (does NOT exit the menu). | |
| 70 | + | run_script() { | |
| 71 | + | local name="$1" mode="$2" | |
| 72 | + | local url="$BASE_URL/$name" | |
| 73 | + | local tmp | |
| 74 | + | tmp="$(mktemp -t "${name%.sh}.XXXXXX.sh")" || { err "mktemp failed"; return 1; } | |
| 75 | + | ||
| 76 | + | log "Fetching $name..." | |
| 77 | + | if ! curl -fsSL --max-time 60 --retry 2 "$url" -o "$tmp"; then | |
| 78 | + | err "Download failed: $url" | |
| 79 | + | rm -f "$tmp" | |
| 80 | + | return 1 | |
| 81 | + | fi | |
| 82 | + | if [[ ! -s "$tmp" ]]; then | |
| 83 | + | err "Downloaded $name is empty." | |
| 84 | + | rm -f "$tmp" | |
| 85 | + | return 1 | |
| 86 | + | fi | |
| 87 | + | # Cheap sanity: confirm it looks like a shell script. | |
| 88 | + | if ! head -n1 "$tmp" | grep -qE '^#!.*sh'; then | |
| 89 | + | warn "$name doesn't start with a shebang; proceeding anyway." | |
| 90 | + | fi | |
| 91 | + | chmod +x "$tmp" | |
| 92 | + | ||
| 93 | + | local rc=0 | |
| 94 | + | if [[ "$mode" == "sudo" ]]; then | |
| 95 | + | sudo bash "$tmp" || rc=$? | |
| 96 | + | else | |
| 97 | + | bash "$tmp" || rc=$? | |
| 98 | + | fi | |
| 99 | + | rm -f "$tmp" | |
| 100 | + | if (( rc != 0 )); then | |
| 101 | + | err "$name exited with status $rc" | |
| 102 | + | fi | |
| 103 | + | return "$rc" | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | # Catalog: number | label | script-filename | mode | |
| 107 | + | # (Edit here to add/remove options — the menu loop is data-driven.) | |
| 108 | + | OPTIONS=( | |
| 109 | + | "1|Install Firefox (Mozilla APT)|install-firefox.sh|sudo" | |
| 110 | + | "2|Install Thunderbird|install-thunderbird.sh|sudo" | |
| 111 | + | "3|Install 1Password|install-1password.sh|sudo" | |
| 112 | + | "4|Install Espanso (text expander)|install-espanso.sh|sudo" | |
| 113 | + | "5|Install LibreOffice (latest stable .deb)|install-libreoffice.sh|sudo" | |
| 114 | + | "6|Install Visual Studio Code|install-vscode.sh|sudo" | |
| 115 | + | "7|Install JetBrains Toolbox (per-user)|install-jetbrains-toolbox.sh|user" | |
| 116 | + | "8|Install Bruno (API client)|install-bruno.sh|sudo" | |
| 117 | + | "9|Install IPATool|install-ipatool.sh|sudo" | |
| 118 | + | "10|Mount Synology Network Drive|install-network_drive.sh|sudo" | |
| 119 | + | "11|Install IBus Intelligent Pinyin (per-user)|install-ibus-pinyin.sh|user" | |
| 120 | + | "12|Install Ubuntu Sans Nerd Font (per-user)|install_font.sh|user" | |
| 121 | + | ) | |
| 122 | + | ||
| 123 | + | print_menu() { | |
| 124 | + | hr | |
| 125 | + | echo " Ubuntu / Debian Setup Manager" | |
| 126 | + | hr | |
| 127 | + | echo "--- [ Browsers & Mail ] ---" | |
| 128 | + | echo " 1) Install Firefox" | |
| 129 | + | echo " 2) Install Thunderbird" | |
| 130 | + | echo "--- [ Productivity & Security ] ---" | |
| 131 | + | echo " 3) Install 1Password" | |
| 132 | + | echo " 4) Install Espanso" | |
| 133 | + | echo " 5) Install LibreOffice" | |
| 134 | + | echo "--- [ Development Tools ] ---" | |
| 135 | + | echo " 6) Install Visual Studio Code" | |
| 136 | + | echo " 7) Install JetBrains Toolbox (runs as you, not root)" | |
| 137 | + | echo " 8) Install Bruno" | |
| 138 | + | echo " 9) Install IPATool" | |
| 139 | + | echo "--- [ System ] ---" | |
| 140 | + | echo " 10) Mount Synology Network Drive" | |
| 141 | + | echo " 11) Install IBus Intelligent Pinyin (runs as you, not root)" | |
| 142 | + | echo " 12) Install Ubuntu Sans Nerd Font (runs as you, not root)" | |
| 143 | + | hr | |
| 144 | + | echo " 0) Run ALL options (1-12)" | |
| 145 | + | echo " -1) Exit" | |
| 146 | + | hr | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | # Resolve a numeric choice to its catalog entry; print "name|mode" to stdout. | |
| 150 | + | resolve_choice() { | |
| 151 | + | local want="$1" entry num | |
| 152 | + | for entry in "${OPTIONS[@]}"; do | |
| 153 | + | num="${entry%%|*}" | |
| 154 | + | if [[ "$num" == "$want" ]]; then | |
| 155 | + | # Strip the leading "N|label|"; what remains is "filename|mode" | |
| 156 | + | printf '%s' "${entry#*|*|}" | |
| 157 | + | return 0 | |
| 158 | + | fi | |
| 159 | + | done | |
| 160 | + | return 1 | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12) | |
| 164 | + | ||
| 165 | + | prime_sudo || exit 1 | |
| 166 | + | ||
| 167 | + | while true; do | |
| 168 | + | print_menu | |
| 169 | + | read -rp "Enter choices separated by spaces (e.g., 1 6 11), 0 for ALL, -1 to exit: " choices </dev/tty || { | |
| 170 | + | echo; log "Input closed; exiting." | |
| 171 | + | break | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | # Trim whitespace | |
| 175 | + | choices="${choices##[[:space:]]}" | |
| 176 | + | choices="${choices%%[[:space:]]}" | |
| 177 | + | ||
| 178 | + | if [[ -z "$choices" ]]; then | |
| 179 | + | continue | |
| 180 | + | fi | |
| 181 | + | ||
| 182 | + | if [[ "$choices" == "-1" ]]; then | |
| 183 | + | log "Exiting." | |
| 184 | + | break | |
| 185 | + | fi | |
| 186 | + | ||
| 187 | + | # Expand "0" to all options | |
| 188 | + | if [[ "$choices" == "0" ]]; then | |
| 189 | + | choices="${ALL_NUMS[*]}" | |
| 190 | + | fi | |
| 191 | + | ||
| 192 | + | # Validate every token first; reject the whole batch if any token is bogus, | |
| 193 | + | # so the user sees the problem before any work starts. | |
| 194 | + | bad="" | |
| 195 | + | for c in $choices; do | |
| 196 | + | if ! [[ "$c" =~ ^-?[0-9]+$ ]] || ! resolve_choice "$c" >/dev/null; then | |
| 197 | + | bad+=" $c" | |
| 198 | + | fi | |
| 199 | + | done | |
| 200 | + | if [[ -n "$bad" ]]; then | |
| 201 | + | err "Invalid option(s):$bad" | |
| 202 | + | sleep 1 | |
| 203 | + | continue | |
| 204 | + | fi | |
| 205 | + | ||
| 206 | + | failures=() | |
| 207 | + | for c in $choices; do | |
| 208 | + | spec="$(resolve_choice "$c")" | |
| 209 | + | name="${spec%|*}" | |
| 210 | + | mode="${spec##*|}" | |
| 211 | + | hr | |
| 212 | + | log "[$c] Running $name ($mode)..." | |
| 213 | + | if ! run_script "$name" "$mode"; then | |
| 214 | + | failures+=("$c:$name") | |
| 215 | + | fi | |
| 216 | + | done | |
| 217 | + | ||
| 218 | + | hr | |
| 219 | + | if (( ${#failures[@]} == 0 )); then | |
| 220 | + | log "All selected tasks completed." | |
| 221 | + | else | |
| 222 | + | warn "Completed with ${#failures[@]} failure(s): ${failures[*]}" | |
| 223 | + | fi | |
| 224 | + | echo | |
| 225 | + | done | |
weehong revised this gist 3 months ago. Go to revision
1 file changed, 159 insertions
install-ibus-pinyin.sh(file created)
| @@ -0,0 +1,159 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-ibus-pinyin.sh — Install IBus + libpinyin and add Intelligent Pinyin | |
| 3 | + | # to GNOME's Input Sources. Tuned for Ubuntu 25.10 (GNOME 49 / Wayland) but | |
| 4 | + | # works on any modern Ubuntu/Debian GNOME desktop. | |
| 5 | + | # | |
| 6 | + | # Hardened: distro-detect, runs as the desktop user (not root) for gsettings, | |
| 7 | + | # uses sudo only for apt steps, idempotent re-runs, ERR trap, --dry-run. | |
| 8 | + | ||
| 9 | + | set -euo pipefail | |
| 10 | + | IFS=$'\n\t' | |
| 11 | + | ||
| 12 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 13 | + | DRY_RUN=0 | |
| 14 | + | SKIP_GSETTINGS=0 | |
| 15 | + | ||
| 16 | + | usage() { | |
| 17 | + | cat <<EOF | |
| 18 | + | Usage: $SCRIPT_NAME [--dry-run] [--skip-gsettings] [--help] | |
| 19 | + | ||
| 20 | + | Installs ibus-libpinyin (plus Simplified Chinese language packs) and registers | |
| 21 | + | 'Intelligent Pinyin' as a GNOME input source. Idempotent. | |
| 22 | + | ||
| 23 | + | Do NOT run with sudo: gsettings is per-user and must run as the desktop user. | |
| 24 | + | The script invokes sudo internally only for apt-get steps. | |
| 25 | + | ||
| 26 | + | Options: | |
| 27 | + | --skip-gsettings Install packages but don't touch GNOME input sources | |
| 28 | + | (useful on non-GNOME desktops; configure manually afterwards). | |
| 29 | + | --dry-run Print actions without executing. | |
| 30 | + | --help, -h Show this help. | |
| 31 | + | ||
| 32 | + | Switch input methods at runtime with: Super + Space | |
| 33 | + | EOF | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 37 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 38 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 39 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 40 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 41 | + | ||
| 42 | + | while (( $# )); do | |
| 43 | + | case "$1" in | |
| 44 | + | --dry-run) DRY_RUN=1 ;; | |
| 45 | + | --skip-gsettings) SKIP_GSETTINGS=1 ;; | |
| 46 | + | -h|--help) usage; exit 0 ;; | |
| 47 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 48 | + | esac | |
| 49 | + | shift | |
| 50 | + | done | |
| 51 | + | ||
| 52 | + | # Resolve the desktop user. If invoked via sudo, gsettings must target SUDO_USER. | |
| 53 | + | if (( EUID == 0 )); then | |
| 54 | + | DESKTOP_USER="${SUDO_USER:-}" | |
| 55 | + | [[ -n "$DESKTOP_USER" && "$DESKTOP_USER" != "root" ]] \ | |
| 56 | + | || die "Run as a normal user (the script will call sudo itself for apt). gsettings can't run as root." | |
| 57 | + | else | |
| 58 | + | DESKTOP_USER="$USER" | |
| 59 | + | fi | |
| 60 | + | ||
| 61 | + | # Look up the user's UID for the DBus session bus. | |
| 62 | + | USER_ENTRY="$(getent passwd "$DESKTOP_USER")" || die "User '$DESKTOP_USER' not found in passwd." | |
| 63 | + | USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")" | |
| 64 | + | ||
| 65 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 66 | + | # shellcheck disable=SC1091 | |
| 67 | + | . /etc/os-release | |
| 68 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 69 | + | *ubuntu*|*debian*) : ;; | |
| 70 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;; | |
| 71 | + | esac | |
| 72 | + | log "Detected: ${PRETTY_NAME:-unknown}, desktop user: $DESKTOP_USER" | |
| 73 | + | ||
| 74 | + | # Helper: invoke a command as $DESKTOP_USER with a working DBus address. | |
| 75 | + | run_as_user() { | |
| 76 | + | local cmd=("$@") | |
| 77 | + | if (( DRY_RUN )); then | |
| 78 | + | printf ' DRY-RUN (as %s): %s\n' "$DESKTOP_USER" "${cmd[*]}" | |
| 79 | + | return 0 | |
| 80 | + | fi | |
| 81 | + | if (( EUID == 0 )); then | |
| 82 | + | sudo -u "$DESKTOP_USER" \ | |
| 83 | + | DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ | |
| 84 | + | XDG_RUNTIME_DIR="/run/user/$USER_ID" \ | |
| 85 | + | "${cmd[@]}" | |
| 86 | + | else | |
| 87 | + | "${cmd[@]}" | |
| 88 | + | fi | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | # Helper: invoke a command with sudo when the caller is non-root. | |
| 92 | + | sudo_run() { | |
| 93 | + | if (( DRY_RUN )); then | |
| 94 | + | printf ' DRY-RUN (sudo): %s\n' "$*" | |
| 95 | + | return 0 | |
| 96 | + | fi | |
| 97 | + | if (( EUID == 0 )); then "$@"; else sudo "$@"; fi | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | export DEBIAN_FRONTEND=noninteractive | |
| 101 | + | ||
| 102 | + | log "Installing IBus Pinyin + Simplified Chinese language packs..." | |
| 103 | + | sudo_run apt-get update -qq | |
| 104 | + | sudo_run apt-get install -y \ | |
| 105 | + | ibus \ | |
| 106 | + | ibus-libpinyin \ | |
| 107 | + | language-pack-zh-hans \ | |
| 108 | + | language-pack-gnome-zh-hans | |
| 109 | + | ||
| 110 | + | # Make sure ibus-daemon picks up the new engines for the user. | |
| 111 | + | if command -v ibus >/dev/null 2>&1; then | |
| 112 | + | log "Restarting ibus-daemon for $DESKTOP_USER..." | |
| 113 | + | # `ibus exit` will fail if no daemon is running — treat as non-fatal. | |
| 114 | + | run_as_user ibus exit >/dev/null 2>&1 || true | |
| 115 | + | # Start fresh in the background; -drx replaces a running daemon. | |
| 116 | + | if (( ! DRY_RUN )); then | |
| 117 | + | sudo -u "$DESKTOP_USER" \ | |
| 118 | + | DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ | |
| 119 | + | XDG_RUNTIME_DIR="/run/user/$USER_ID" \ | |
| 120 | + | sh -c 'nohup ibus-daemon -drx >/dev/null 2>&1 &' || \ | |
| 121 | + | warn "ibus-daemon restart returned non-zero (non-fatal)." | |
| 122 | + | sleep 1 | |
| 123 | + | fi | |
| 124 | + | fi | |
| 125 | + | ||
| 126 | + | if (( SKIP_GSETTINGS )); then | |
| 127 | + | log "Skipping GNOME input-sources update (--skip-gsettings)." | |
| 128 | + | log "Done. Add 'Chinese (Intelligent Pinyin)' manually under Settings → Keyboard → Input Sources." | |
| 129 | + | exit 0 | |
| 130 | + | fi | |
| 131 | + | ||
| 132 | + | # Only manage gsettings if GNOME schemas are present. | |
| 133 | + | if ! run_as_user gsettings list-schemas 2>/dev/null | grep -q '^org.gnome.desktop.input-sources$'; then | |
| 134 | + | warn "GNOME schema org.gnome.desktop.input-sources not found; skipping gsettings update." | |
| 135 | + | log "Done. Add 'Chinese (Intelligent Pinyin)' manually in your DE's input settings." | |
| 136 | + | exit 0 | |
| 137 | + | fi | |
| 138 | + | ||
| 139 | + | log "Adding 'Intelligent Pinyin' to GNOME Input Sources (idempotent)..." | |
| 140 | + | ||
| 141 | + | CURRENT_SOURCES="$(run_as_user gsettings get org.gnome.desktop.input-sources sources 2>/dev/null || echo '[]')" | |
| 142 | + | # Strip the optional "@as " type annotation gvariant sometimes prepends. | |
| 143 | + | CLEAN_SOURCES="${CURRENT_SOURCES#@as }" | |
| 144 | + | ||
| 145 | + | if [[ "$CLEAN_SOURCES" == *"'ibus', 'libpinyin'"* ]]; then | |
| 146 | + | log "Intelligent Pinyin already present in input sources — nothing to do." | |
| 147 | + | else | |
| 148 | + | if [[ -z "$CLEAN_SOURCES" || "$CLEAN_SOURCES" == "[]" || "$CLEAN_SOURCES" == "@as []" ]]; then | |
| 149 | + | NEW_SOURCES="[('xkb', 'us'), ('ibus', 'libpinyin')]" | |
| 150 | + | else | |
| 151 | + | # Insert the libpinyin tuple before the closing bracket of the existing list. | |
| 152 | + | NEW_SOURCES="${CLEAN_SOURCES%]*}, ('ibus', 'libpinyin')]" | |
| 153 | + | fi | |
| 154 | + | run_as_user gsettings set org.gnome.desktop.input-sources sources "$NEW_SOURCES" | |
| 155 | + | log "Added: ('ibus', 'libpinyin')" | |
| 156 | + | fi | |
| 157 | + | ||
| 158 | + | log "Done. Switch input methods with: Super + Space" | |
| 159 | + | log "If 'Chinese (Intelligent Pinyin)' doesn't appear in the top bar, log out and back in." | |
weehong revised this gist 3 months ago. Go to revision
10 files changed, 1236 insertions
install-1password.sh(file created)
| @@ -0,0 +1,97 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-1password.sh — Install 1Password desktop from the official 1Password APT repo. | |
| 3 | + | # Hardened: arch-aware, signed-by keyring, debsig verification dir, idempotent, --dry-run. | |
| 4 | + | ||
| 5 | + | set -euo pipefail | |
| 6 | + | IFS=$'\n\t' | |
| 7 | + | ||
| 8 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 9 | + | DRY_RUN=0 | |
| 10 | + | ||
| 11 | + | usage() { | |
| 12 | + | cat <<EOF | |
| 13 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] | |
| 14 | + | ||
| 15 | + | Configures the official 1Password APT repository (with debsig policy and | |
| 16 | + | keyring) and installs the 1Password desktop application. Idempotent: safe to | |
| 17 | + | re-run. | |
| 18 | + | ||
| 19 | + | Options: | |
| 20 | + | --dry-run Print actions without executing. | |
| 21 | + | --help, -h Show this help. | |
| 22 | + | EOF | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 26 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 27 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 28 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 29 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 30 | + | ||
| 31 | + | while (( $# )); do | |
| 32 | + | case "$1" in | |
| 33 | + | --dry-run) DRY_RUN=1 ;; | |
| 34 | + | -h|--help) usage; exit 0 ;; | |
| 35 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 36 | + | esac | |
| 37 | + | shift | |
| 38 | + | done | |
| 39 | + | ||
| 40 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 41 | + | ||
| 42 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 43 | + | # shellcheck disable=SC1091 | |
| 44 | + | . /etc/os-release | |
| 45 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 46 | + | *ubuntu*|*debian*) : ;; | |
| 47 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; | |
| 48 | + | esac | |
| 49 | + | ||
| 50 | + | # 1Password publishes builds for amd64 and arm64. | |
| 51 | + | ARCH="$(dpkg --print-architecture)" | |
| 52 | + | case "$ARCH" in | |
| 53 | + | amd64|arm64) : ;; | |
| 54 | + | *) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;; | |
| 55 | + | esac | |
| 56 | + | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" | |
| 57 | + | ||
| 58 | + | export DEBIAN_FRONTEND=noninteractive | |
| 59 | + | ||
| 60 | + | KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg | |
| 61 | + | SOURCES=/etc/apt/sources.list.d/1password.list | |
| 62 | + | DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22 | |
| 63 | + | DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22 | |
| 64 | + | ||
| 65 | + | log "Installing prerequisites..." | |
| 66 | + | run "apt-get update -qq" | |
| 67 | + | run "apt-get install -y curl gpg ca-certificates" | |
| 68 | + | ||
| 69 | + | log "Configuring 1Password APT repository..." | |
| 70 | + | run "install -d -m 0755 /etc/apt/keyrings" | |
| 71 | + | if [[ ! -s "$KEYRING" ]]; then | |
| 72 | + | run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'" | |
| 73 | + | run "chmod 0644 '$KEYRING'" | |
| 74 | + | fi | |
| 75 | + | ||
| 76 | + | DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main" | |
| 77 | + | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then | |
| 78 | + | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" | |
| 79 | + | fi | |
| 80 | + | ||
| 81 | + | log "Installing debsig policy (verifies package signatures on install)..." | |
| 82 | + | run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'" | |
| 83 | + | if [[ ! -s "${DEBSIG_POLICY_DIR}/1password.pol" ]]; then | |
| 84 | + | run "curl -fsSL https://downloads.1password.com/linux/debian/debsig/1password.pol -o '${DEBSIG_POLICY_DIR}/1password.pol'" | |
| 85 | + | fi | |
| 86 | + | if [[ ! -s "${DEBSIG_KEYRING_DIR}/debsig.gpg" ]]; then | |
| 87 | + | run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '${DEBSIG_KEYRING_DIR}/debsig.gpg'" | |
| 88 | + | fi | |
| 89 | + | ||
| 90 | + | log "Installing 1Password..." | |
| 91 | + | run "apt-get update -qq" | |
| 92 | + | run "apt-get install -y 1password" | |
| 93 | + | ||
| 94 | + | if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then | |
| 95 | + | log "1Password binary available at: $(command -v 1password)" | |
| 96 | + | fi | |
| 97 | + | log "Done." | |
install-bruno.sh(file created)
| @@ -0,0 +1,117 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-bruno.sh — Install Bruno API client from the official APT repository. | |
| 3 | + | # Hardened: arch-aware, signed-by keyring with retries (keyserver flake), idempotent, | |
| 4 | + | # clean desktop entry, --dry-run. | |
| 5 | + | ||
| 6 | + | set -euo pipefail | |
| 7 | + | IFS=$'\n\t' | |
| 8 | + | ||
| 9 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 10 | + | DRY_RUN=0 | |
| 11 | + | ||
| 12 | + | usage() { | |
| 13 | + | cat <<EOF | |
| 14 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] | |
| 15 | + | ||
| 16 | + | Configures the official Bruno APT repository (signed-by keyring fetched from | |
| 17 | + | keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to | |
| 18 | + | re-run. | |
| 19 | + | ||
| 20 | + | Options: | |
| 21 | + | --dry-run Print actions without executing. | |
| 22 | + | --help, -h Show this help. | |
| 23 | + | EOF | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 27 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 28 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 29 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 30 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 31 | + | ||
| 32 | + | while (( $# )); do | |
| 33 | + | case "$1" in | |
| 34 | + | --dry-run) DRY_RUN=1 ;; | |
| 35 | + | -h|--help) usage; exit 0 ;; | |
| 36 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 37 | + | esac | |
| 38 | + | shift | |
| 39 | + | done | |
| 40 | + | ||
| 41 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 42 | + | ||
| 43 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 44 | + | # shellcheck disable=SC1091 | |
| 45 | + | . /etc/os-release | |
| 46 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 47 | + | *ubuntu*|*debian*) : ;; | |
| 48 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; | |
| 49 | + | esac | |
| 50 | + | ||
| 51 | + | ARCH="$(dpkg --print-architecture)" | |
| 52 | + | [[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)." | |
| 53 | + | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" | |
| 54 | + | ||
| 55 | + | export DEBIAN_FRONTEND=noninteractive | |
| 56 | + | ||
| 57 | + | KEYRING=/etc/apt/keyrings/bruno.gpg | |
| 58 | + | SOURCES=/etc/apt/sources.list.d/bruno.list | |
| 59 | + | KEY_ID="9FA6017ECABE0266" | |
| 60 | + | DESKTOP=/usr/share/applications/bruno.desktop | |
| 61 | + | ||
| 62 | + | log "Installing prerequisites..." | |
| 63 | + | run "apt-get update -qq" | |
| 64 | + | run "apt-get install -y curl gpg ca-certificates" | |
| 65 | + | ||
| 66 | + | log "Configuring Bruno APT repository..." | |
| 67 | + | run "install -d -m 0755 /etc/apt/keyrings" | |
| 68 | + | ||
| 69 | + | if [[ ! -s "$KEYRING" ]]; then | |
| 70 | + | # keyserver.ubuntu.com is occasionally flaky — retry a few times. | |
| 71 | + | ok=0 | |
| 72 | + | for attempt in 1 2 3 4 5; do | |
| 73 | + | if (( DRY_RUN )); then | |
| 74 | + | printf ' DRY-RUN: fetch key 0x%s (attempt %d)\n' "$KEY_ID" "$attempt" | |
| 75 | + | ok=1 | |
| 76 | + | break | |
| 77 | + | fi | |
| 78 | + | if curl -fsSL --max-time 30 "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${KEY_ID}" \ | |
| 79 | + | | gpg --dearmor -o "$KEYRING" 2>/dev/null && [[ -s "$KEYRING" ]]; then | |
| 80 | + | ok=1 | |
| 81 | + | break | |
| 82 | + | fi | |
| 83 | + | warn "Key fetch failed (attempt $attempt/5); retrying in $((attempt*2))s..." | |
| 84 | + | sleep "$((attempt*2))" | |
| 85 | + | done | |
| 86 | + | (( ok )) || die "Could not retrieve Bruno signing key after 5 attempts." | |
| 87 | + | run "chmod 0644 '$KEYRING'" | |
| 88 | + | fi | |
| 89 | + | ||
| 90 | + | DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable" | |
| 91 | + | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then | |
| 92 | + | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" | |
| 93 | + | fi | |
| 94 | + | ||
| 95 | + | log "Installing Bruno..." | |
| 96 | + | run "apt-get update -qq" | |
| 97 | + | run "apt-get install -y bruno" | |
| 98 | + | ||
| 99 | + | log "Writing desktop entry..." | |
| 100 | + | if (( DRY_RUN )); then | |
| 101 | + | printf ' DRY-RUN: write %s\n' "$DESKTOP" | |
| 102 | + | else | |
| 103 | + | cat >"$DESKTOP" <<'EOF' | |
| 104 | + | [Desktop Entry] | |
| 105 | + | Name=Bruno | |
| 106 | + | Comment=Open-source API Client | |
| 107 | + | Exec=bruno %U | |
| 108 | + | Terminal=false | |
| 109 | + | Type=Application | |
| 110 | + | Icon=bruno | |
| 111 | + | Categories=Development;Utility; | |
| 112 | + | StartupNotify=true | |
| 113 | + | EOF | |
| 114 | + | chmod 0644 "$DESKTOP" | |
| 115 | + | fi | |
| 116 | + | ||
| 117 | + | log "Done. Bruno installed." | |
install-espanso.sh(file created)
| @@ -0,0 +1,116 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-espanso.sh — Install Espanso (text expander) with Wayland or X11 build. | |
| 3 | + | # Auto-detects session type; falls back to X11 if Wayland is not active. | |
| 4 | + | # Hardened: session detection, registers service as the invoking user, idempotent. | |
| 5 | + | ||
| 6 | + | set -euo pipefail | |
| 7 | + | IFS=$'\n\t' | |
| 8 | + | ||
| 9 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 10 | + | DRY_RUN=0 | |
| 11 | + | FORCE_VARIANT="" # "wayland" | "x11" | |
| 12 | + | ||
| 13 | + | usage() { | |
| 14 | + | cat <<EOF | |
| 15 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help] | |
| 16 | + | ||
| 17 | + | Installs Espanso from the latest GitHub release. Auto-detects whether to use | |
| 18 | + | the Wayland or X11 build based on \$XDG_SESSION_TYPE of the invoking user | |
| 19 | + | (SUDO_USER). The systemd-user service is registered for that user, not root. | |
| 20 | + | ||
| 21 | + | Options: | |
| 22 | + | --wayland Force the Wayland build. | |
| 23 | + | --x11 Force the X11 build. | |
| 24 | + | --dry-run Print actions without executing. | |
| 25 | + | --help, -h Show this help. | |
| 26 | + | EOF | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 30 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 31 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 32 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 33 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 34 | + | ||
| 35 | + | while (( $# )); do | |
| 36 | + | case "$1" in | |
| 37 | + | --dry-run) DRY_RUN=1 ;; | |
| 38 | + | --wayland) FORCE_VARIANT=wayland ;; | |
| 39 | + | --x11) FORCE_VARIANT=x11 ;; | |
| 40 | + | -h|--help) usage; exit 0 ;; | |
| 41 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 42 | + | esac | |
| 43 | + | shift | |
| 44 | + | done | |
| 45 | + | ||
| 46 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 47 | + | ||
| 48 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 49 | + | # shellcheck disable=SC1091 | |
| 50 | + | . /etc/os-release | |
| 51 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 52 | + | *ubuntu*|*debian*) : ;; | |
| 53 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; | |
| 54 | + | esac | |
| 55 | + | ||
| 56 | + | ARCH="$(dpkg --print-architecture)" | |
| 57 | + | [[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)." | |
| 58 | + | ||
| 59 | + | ACTUAL_USER="${SUDO_USER:-${USER:-}}" | |
| 60 | + | [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user; cannot register the service as root." | |
| 61 | + | USER_ID="$(id -u "$ACTUAL_USER")" | |
| 62 | + | ||
| 63 | + | # Detect session type from the invoking user's environment, fallback to env, fallback to wayland | |
| 64 | + | if [[ -n "$FORCE_VARIANT" ]]; then | |
| 65 | + | VARIANT="$FORCE_VARIANT" | |
| 66 | + | else | |
| 67 | + | SESSION_TYPE="$(sudo -u "$ACTUAL_USER" -i printenv XDG_SESSION_TYPE 2>/dev/null || true)" | |
| 68 | + | [[ -z "$SESSION_TYPE" ]] && SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}" | |
| 69 | + | case "$SESSION_TYPE" in | |
| 70 | + | wayland) VARIANT=wayland ;; | |
| 71 | + | x11|tty) VARIANT=x11 ;; | |
| 72 | + | *) warn "Unknown XDG_SESSION_TYPE='$SESSION_TYPE'; defaulting to wayland."; VARIANT=wayland ;; | |
| 73 | + | esac | |
| 74 | + | fi | |
| 75 | + | log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT" | |
| 76 | + | ||
| 77 | + | export DEBIAN_FRONTEND=noninteractive | |
| 78 | + | TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)" | |
| 79 | + | trap 'rm -f "$TEMP_DEB"' EXIT | |
| 80 | + | ||
| 81 | + | DEB_NAME="espanso-debian-${VARIANT}-amd64.deb" | |
| 82 | + | URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}" | |
| 83 | + | ||
| 84 | + | log "Installing prerequisites..." | |
| 85 | + | run "apt-get update -qq" | |
| 86 | + | run "apt-get install -y wget libcap2-bin" | |
| 87 | + | ||
| 88 | + | log "Downloading ${DEB_NAME}..." | |
| 89 | + | run "wget -qO '$TEMP_DEB' '$URL'" | |
| 90 | + | ||
| 91 | + | log "Installing package..." | |
| 92 | + | run "apt-get install -y '$TEMP_DEB'" | |
| 93 | + | ||
| 94 | + | ESPANSO_BIN="$(command -v espanso || true)" | |
| 95 | + | [[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install." | |
| 96 | + | ||
| 97 | + | if [[ "$VARIANT" == "wayland" ]]; then | |
| 98 | + | log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..." | |
| 99 | + | run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'" | |
| 100 | + | fi | |
| 101 | + | ||
| 102 | + | log "Registering & starting espanso service for $ACTUAL_USER..." | |
| 103 | + | # Register may fail if already registered — treat that as success. | |
| 104 | + | if (( DRY_RUN )); then | |
| 105 | + | printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso service register || true\n' "$ACTUAL_USER" "$USER_ID" | |
| 106 | + | printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso start || true\n' "$ACTUAL_USER" "$USER_ID" | |
| 107 | + | else | |
| 108 | + | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso service register || true | |
| 109 | + | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso restart || \ | |
| 110 | + | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso start || true | |
| 111 | + | fi | |
| 112 | + | ||
| 113 | + | log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER." | |
| 114 | + | if [[ "$VARIANT" == "wayland" ]]; then | |
| 115 | + | log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml" | |
| 116 | + | fi | |
install-firefox.sh(file created)
| @@ -0,0 +1,121 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-firefox.sh — Install Firefox from Mozilla's official APT repository. | |
| 3 | + | # Removes the Snap transition package and pins Mozilla as the source of truth. | |
| 4 | + | # Hardened: distro-detect, idempotent, signed-by keyring, ERR trap, --dry-run. | |
| 5 | + | ||
| 6 | + | set -euo pipefail | |
| 7 | + | IFS=$'\n\t' | |
| 8 | + | ||
| 9 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 10 | + | DRY_RUN=0 | |
| 11 | + | ASSUME_YES=0 | |
| 12 | + | ||
| 13 | + | usage() { | |
| 14 | + | cat <<EOF | |
| 15 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help] | |
| 16 | + | ||
| 17 | + | Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the | |
| 18 | + | official Mozilla APT repository (signed-by keyring + APT pin), then installs | |
| 19 | + | Firefox so it auto-updates from Mozilla. | |
| 20 | + | ||
| 21 | + | Options: | |
| 22 | + | --dry-run Print the actions without executing them. | |
| 23 | + | --yes, -y Pass -y to apt for non-interactive install. | |
| 24 | + | --help, -h Show this help. | |
| 25 | + | EOF | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 29 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 30 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 31 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 32 | + | ||
| 33 | + | on_err() { local rc=$? line=$1; printf '\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n' "${SCRIPT_NAME%.sh}" "$line" "$rc" >&2; } | |
| 34 | + | trap 'on_err $LINENO' ERR | |
| 35 | + | ||
| 36 | + | while (( $# )); do | |
| 37 | + | case "$1" in | |
| 38 | + | --dry-run) DRY_RUN=1 ;; | |
| 39 | + | -y|--yes) ASSUME_YES=1 ;; | |
| 40 | + | -h|--help) usage; exit 0 ;; | |
| 41 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 42 | + | esac | |
| 43 | + | shift | |
| 44 | + | done | |
| 45 | + | ||
| 46 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 47 | + | ||
| 48 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found; cannot detect distro." | |
| 49 | + | # shellcheck disable=SC1091 | |
| 50 | + | . /etc/os-release | |
| 51 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 52 | + | *ubuntu*|*debian*) : ;; | |
| 53 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;; | |
| 54 | + | esac | |
| 55 | + | log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})" | |
| 56 | + | ||
| 57 | + | APT_YES=() | |
| 58 | + | (( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use | |
| 59 | + | export DEBIAN_FRONTEND=noninteractive | |
| 60 | + | ||
| 61 | + | KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc | |
| 62 | + | SOURCES=/etc/apt/sources.list.d/mozilla.list | |
| 63 | + | PIN=/etc/apt/preferences.d/mozilla | |
| 64 | + | ||
| 65 | + | log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..." | |
| 66 | + | if command -v snap >/dev/null 2>&1; then | |
| 67 | + | run "snap disable firefox >/dev/null 2>&1 || true" | |
| 68 | + | run "snap remove --purge firefox >/dev/null 2>&1 || true" | |
| 69 | + | fi | |
| 70 | + | run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true" | |
| 71 | + | run "rm -f /usr/bin/firefox" | |
| 72 | + | ||
| 73 | + | log "Installing prerequisites (wget, gpg, ca-certificates)..." | |
| 74 | + | run "apt-get update -qq" | |
| 75 | + | run "apt-get install ${APT_YES[*]} wget gpg ca-certificates" | |
| 76 | + | ||
| 77 | + | log "Configuring Mozilla APT repository..." | |
| 78 | + | run "install -d -m 0755 /etc/apt/keyrings" | |
| 79 | + | if [[ ! -s "$KEYRING" ]]; then | |
| 80 | + | run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg" | |
| 81 | + | run "chmod 0644 '$KEYRING'" | |
| 82 | + | else | |
| 83 | + | log "Keyring already present at $KEYRING (skipping download)." | |
| 84 | + | fi | |
| 85 | + | ||
| 86 | + | # Verify key fingerprint matches Mozilla's published fingerprint | |
| 87 | + | EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3" | |
| 88 | + | ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')" | |
| 89 | + | if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then | |
| 90 | + | warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually." | |
| 91 | + | else | |
| 92 | + | log "Mozilla key fingerprint verified." | |
| 93 | + | fi | |
| 94 | + | ||
| 95 | + | DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main' | |
| 96 | + | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then | |
| 97 | + | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" | |
| 98 | + | fi | |
| 99 | + | ||
| 100 | + | log "Pinning Mozilla repo to priority 1000..." | |
| 101 | + | if [[ ! -f "$PIN" ]] || ! grep -q 'origin packages.mozilla.org' "$PIN"; then | |
| 102 | + | if (( DRY_RUN )); then | |
| 103 | + | printf ' DRY-RUN: write %s\n' "$PIN" | |
| 104 | + | else | |
| 105 | + | cat >"$PIN" <<'EOF' | |
| 106 | + | Package: * | |
| 107 | + | Pin: origin packages.mozilla.org | |
| 108 | + | Pin-Priority: 1000 | |
| 109 | + | EOF | |
| 110 | + | fi | |
| 111 | + | fi | |
| 112 | + | ||
| 113 | + | log "Installing Firefox from Mozilla repo..." | |
| 114 | + | run "apt-get update -qq" | |
| 115 | + | run "apt-get install ${APT_YES[*]} firefox" | |
| 116 | + | ||
| 117 | + | if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then | |
| 118 | + | log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')" | |
| 119 | + | fi | |
| 120 | + | ||
| 121 | + | log "Done. Firefox installed from Mozilla APT and will auto-update." | |
install-ipatool.sh(file created)
| @@ -0,0 +1,114 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-ipatool.sh — Install ipatool from the latest GitHub release. | |
| 3 | + | # Hardened: arch detection (amd64/arm64), GitHub API token support, SHA-256 verification | |
| 4 | + | # from the release checksum file, atomic install to /usr/local/bin, --dry-run. | |
| 5 | + | ||
| 6 | + | set -euo pipefail | |
| 7 | + | IFS=$'\n\t' | |
| 8 | + | ||
| 9 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 10 | + | DRY_RUN=0 | |
| 11 | + | REPO="majd/ipatool" | |
| 12 | + | INSTALL_PATH="/usr/local/bin/ipatool" | |
| 13 | + | ||
| 14 | + | usage() { | |
| 15 | + | cat <<EOF | |
| 16 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] | |
| 17 | + | ||
| 18 | + | Resolves the latest release of majd/ipatool, downloads the tarball for your | |
| 19 | + | architecture, verifies its SHA-256 against the published checksums.txt, and | |
| 20 | + | installs the binary to /usr/local/bin/ipatool atomically. | |
| 21 | + | ||
| 22 | + | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the | |
| 23 | + | GitHub API request (avoids rate limits). | |
| 24 | + | ||
| 25 | + | Options: | |
| 26 | + | --dry-run Print actions without executing. | |
| 27 | + | --help, -h Show this help. | |
| 28 | + | EOF | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 32 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 33 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 34 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 35 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 36 | + | ||
| 37 | + | while (( $# )); do | |
| 38 | + | case "$1" in | |
| 39 | + | --dry-run) DRY_RUN=1 ;; | |
| 40 | + | -h|--help) usage; exit 0 ;; | |
| 41 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 42 | + | esac | |
| 43 | + | shift | |
| 44 | + | done | |
| 45 | + | ||
| 46 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 47 | + | ||
| 48 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 49 | + | # shellcheck disable=SC1091 | |
| 50 | + | . /etc/os-release | |
| 51 | + | log "Detected: ${PRETTY_NAME:-unknown}" | |
| 52 | + | ||
| 53 | + | ARCH_RAW="$(uname -m)" | |
| 54 | + | case "$ARCH_RAW" in | |
| 55 | + | x86_64) GO_ARCH=amd64 ;; | |
| 56 | + | aarch64) GO_ARCH=arm64 ;; | |
| 57 | + | armv7l|armv6l) GO_ARCH=arm ;; | |
| 58 | + | *) die "Unsupported architecture: $ARCH_RAW" ;; | |
| 59 | + | esac | |
| 60 | + | ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz" | |
| 61 | + | ||
| 62 | + | export DEBIAN_FRONTEND=noninteractive | |
| 63 | + | log "Installing prerequisites..." | |
| 64 | + | run "apt-get update -qq" | |
| 65 | + | run "apt-get install -y curl jq tar ca-certificates libsecret-1-0" | |
| 66 | + | ||
| 67 | + | GH_HDRS=(-H "Accept: application/vnd.github+json") | |
| 68 | + | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") | |
| 69 | + | ||
| 70 | + | log "Querying GitHub API for latest release of $REPO..." | |
| 71 | + | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" | |
| 72 | + | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" | |
| 73 | + | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." | |
| 74 | + | log "Latest release: $TAG" | |
| 75 | + | ||
| 76 | + | DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" | |
| 77 | + | CHECKSUM_URL="$(jq -r '.assets[] | select(.name | test("checksums?\\.txt$")) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" | |
| 78 | + | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No release asset matching '*${ASSET_SUFFIX}'." | |
| 79 | + | ||
| 80 | + | STAGE="$(mktemp -d -t ipatool.XXXXXX)" | |
| 81 | + | trap 'rm -rf "$STAGE"' EXIT | |
| 82 | + | TARBALL="$STAGE/ipatool.tar.gz" | |
| 83 | + | ||
| 84 | + | log "Downloading $(basename "$DOWNLOAD_URL")..." | |
| 85 | + | run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'" | |
| 86 | + | ||
| 87 | + | if [[ -n "$CHECKSUM_URL" ]]; then | |
| 88 | + | log "Verifying SHA-256..." | |
| 89 | + | EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk -v f="$(basename "$DOWNLOAD_URL")" '$2 ~ f || $2 == "*"f {print $1; exit}')" | |
| 90 | + | ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')" | |
| 91 | + | if [[ -n "$EXPECTED" && "$EXPECTED" != "$ACTUAL" ]]; then | |
| 92 | + | die "SHA-256 mismatch: expected=$EXPECTED actual=$ACTUAL" | |
| 93 | + | fi | |
| 94 | + | [[ -n "$EXPECTED" ]] && log "SHA-256 ok." || warn "Asset not listed in checksums.txt; skipping." | |
| 95 | + | else | |
| 96 | + | warn "No checksums.txt in release; skipping SHA-256 verification." | |
| 97 | + | fi | |
| 98 | + | ||
| 99 | + | log "Extracting..." | |
| 100 | + | run "tar -xzf '$TARBALL' -C '$STAGE'" | |
| 101 | + | BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)" | |
| 102 | + | if [[ -z "$BINARY_PATH" ]]; then | |
| 103 | + | # Some releases ship the binary without +x; relax the find | |
| 104 | + | BINARY_PATH="$(find "$STAGE" -type f -name ipatool -not -name '*.tar.gz' | head -n1 || true)" | |
| 105 | + | fi | |
| 106 | + | [[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive." | |
| 107 | + | ||
| 108 | + | log "Installing to $INSTALL_PATH..." | |
| 109 | + | run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'" | |
| 110 | + | ||
| 111 | + | if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then | |
| 112 | + | log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)" | |
| 113 | + | fi | |
| 114 | + | log "Done." | |
install-jetbrains-toolbox.sh(file created)
| @@ -0,0 +1,159 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-jetbrains-toolbox.sh — Install JetBrains Toolbox into the invoking user's home. | |
| 3 | + | # Hardened: dependency check w/ auto-install, SHA-256 verification against JetBrains' | |
| 4 | + | # release metadata, idempotent (replaces atomically), .desktop entry, --dry-run. | |
| 5 | + | ||
| 6 | + | set -euo pipefail | |
| 7 | + | IFS=$'\n\t' | |
| 8 | + | ||
| 9 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 10 | + | DRY_RUN=0 | |
| 11 | + | ASSUME_YES=0 | |
| 12 | + | ||
| 13 | + | usage() { | |
| 14 | + | cat <<EOF | |
| 15 | + | Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help] | |
| 16 | + | ||
| 17 | + | Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and | |
| 18 | + | creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is | |
| 19 | + | verified against the SHA-256 published in JetBrains' release feed. | |
| 20 | + | ||
| 21 | + | Do NOT run with sudo: Toolbox is a per-user install. If a missing system | |
| 22 | + | package (curl/jq/tar/libfuse2) needs installing, the script will call sudo | |
| 23 | + | just for that step. | |
| 24 | + | ||
| 25 | + | Options: | |
| 26 | + | --yes, -y Auto-confirm prompts for installing missing system packages. | |
| 27 | + | --dry-run Print actions without executing. | |
| 28 | + | --help, -h Show this help. | |
| 29 | + | EOF | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 33 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 34 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 35 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 36 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 37 | + | ||
| 38 | + | while (( $# )); do | |
| 39 | + | case "$1" in | |
| 40 | + | --dry-run) DRY_RUN=1 ;; | |
| 41 | + | -y|--yes) ASSUME_YES=1 ;; | |
| 42 | + | -h|--help) usage; exit 0 ;; | |
| 43 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 44 | + | esac | |
| 45 | + | shift | |
| 46 | + | done | |
| 47 | + | ||
| 48 | + | (( EUID != 0 )) || die "Do NOT run as root. Toolbox is a per-user install." | |
| 49 | + | ||
| 50 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 51 | + | # shellcheck disable=SC1091 | |
| 52 | + | . /etc/os-release | |
| 53 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 54 | + | *ubuntu*|*debian*) PKG_MGR=apt ;; | |
| 55 | + | *fedora*|*rhel*) PKG_MGR=dnf ;; | |
| 56 | + | *arch*) PKG_MGR=pacman ;; | |
| 57 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; | |
| 58 | + | esac | |
| 59 | + | log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)" | |
| 60 | + | ||
| 61 | + | confirm() { | |
| 62 | + | (( ASSUME_YES )) && return 0 | |
| 63 | + | read -rp "$1 [y/N] " ans | |
| 64 | + | [[ "$ans" =~ ^[Yy] ]] | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | ensure_pkg() { | |
| 68 | + | local pkg="$1" probe="$2" | |
| 69 | + | if eval "$probe" >/dev/null 2>&1; then return 0; fi | |
| 70 | + | if ! confirm "Package '$pkg' is missing. Install via sudo $PKG_MGR?"; then | |
| 71 | + | die "'$pkg' is required." | |
| 72 | + | fi | |
| 73 | + | case "$PKG_MGR" in | |
| 74 | + | apt) run "sudo apt-get update -qq && sudo apt-get install -y '$pkg'" ;; | |
| 75 | + | dnf) run "sudo dnf install -y '$pkg'" ;; | |
| 76 | + | pacman) run "sudo pacman -Sy --noconfirm '$pkg'" ;; | |
| 77 | + | esac | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | # Map deps: command-or-package-probe | |
| 81 | + | ensure_pkg curl "command -v curl" | |
| 82 | + | ensure_pkg jq "command -v jq" | |
| 83 | + | ensure_pkg tar "command -v tar" | |
| 84 | + | case "$PKG_MGR" in | |
| 85 | + | apt) ensure_pkg libfuse2 "dpkg -s libfuse2" ;; | |
| 86 | + | dnf) ensure_pkg fuse-libs "rpm -q fuse-libs" ;; | |
| 87 | + | pacman) ensure_pkg fuse2 "pacman -Q fuse2" ;; | |
| 88 | + | esac | |
| 89 | + | ||
| 90 | + | ARCH_RAW="$(uname -m)" | |
| 91 | + | case "$ARCH_RAW" in | |
| 92 | + | x86_64) TOOLBOX_ARCH_KEY=linux ;; | |
| 93 | + | aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;; | |
| 94 | + | *) die "Unsupported architecture: $ARCH_RAW" ;; | |
| 95 | + | esac | |
| 96 | + | ||
| 97 | + | log "Querying JetBrains release feed..." | |
| 98 | + | RELEASE_JSON="$(curl -fsSL 'https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release' \ | |
| 99 | + | -H 'Origin: https://www.jetbrains.com' \ | |
| 100 | + | -H 'Referer: https://www.jetbrains.com/toolbox/download/')" | |
| 101 | + | ||
| 102 | + | TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")" | |
| 103 | + | TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")" | |
| 104 | + | TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")" | |
| 105 | + | [[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed." | |
| 106 | + | log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)" | |
| 107 | + | ||
| 108 | + | INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox" | |
| 109 | + | STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)" | |
| 110 | + | trap 'rm -rf "$STAGE_DIR"' EXIT | |
| 111 | + | ||
| 112 | + | TARBALL="$STAGE_DIR/toolbox.tar.gz" | |
| 113 | + | log "Downloading..." | |
| 114 | + | run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'" | |
| 115 | + | ||
| 116 | + | if [[ -n "$TOOLBOX_SHA" ]]; then | |
| 117 | + | log "Verifying SHA-256..." | |
| 118 | + | EXPECTED_SHA="$(curl -fsSL "$TOOLBOX_SHA" | awk '{print $1}')" | |
| 119 | + | ACTUAL_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')" | |
| 120 | + | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then | |
| 121 | + | die "SHA-256 mismatch! expected=$EXPECTED_SHA actual=$ACTUAL_SHA" | |
| 122 | + | fi | |
| 123 | + | log "SHA-256 ok." | |
| 124 | + | else | |
| 125 | + | warn "No checksum URL in release feed; skipping verification." | |
| 126 | + | fi | |
| 127 | + | ||
| 128 | + | log "Extracting to $INSTALL_DIR..." | |
| 129 | + | run "mkdir -p '$INSTALL_DIR'" | |
| 130 | + | run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'" | |
| 131 | + | ||
| 132 | + | BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox" | |
| 133 | + | DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop" | |
| 134 | + | ICON_PATH="$INSTALL_DIR/bin/toolbox-tray-color.png" | |
| 135 | + | ||
| 136 | + | [[ -x "$BIN_PATH" || $DRY_RUN -eq 1 ]] || die "Toolbox binary missing after extract." | |
| 137 | + | ||
| 138 | + | # Optional ~/bin symlink | |
| 139 | + | if [[ -d "$HOME/bin" ]]; then | |
| 140 | + | run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'" | |
| 141 | + | fi | |
| 142 | + | ||
| 143 | + | # .desktop launcher | |
| 144 | + | APPS_DIR="$HOME/.local/share/applications" | |
| 145 | + | run "mkdir -p '$APPS_DIR'" | |
| 146 | + | DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop" | |
| 147 | + | if [[ -f "$DESKTOP_SRC" ]] || (( DRY_RUN )); then | |
| 148 | + | run "cp '$DESKTOP_SRC' '$DESKTOP_DST'" | |
| 149 | + | run "sed -i 's|^Exec=.*|Exec=$BIN_PATH %u|' '$DESKTOP_DST'" | |
| 150 | + | if [[ -f "$ICON_PATH" ]]; then | |
| 151 | + | run "sed -i 's|^Icon=.*|Icon=$ICON_PATH|' '$DESKTOP_DST'" | |
| 152 | + | fi | |
| 153 | + | run "chmod 0755 '$DESKTOP_DST'" | |
| 154 | + | log "Desktop entry installed: $DESKTOP_DST" | |
| 155 | + | else | |
| 156 | + | warn "No .desktop file in archive; skipping launcher." | |
| 157 | + | fi | |
| 158 | + | ||
| 159 | + | log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR" | |
install-libreoffice.sh(file created)
| @@ -0,0 +1,115 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-libreoffice.sh — Install the latest stable LibreOffice from documentfoundation.org. | |
| 3 | + | # Hardened: arch detection, version JSON when available with HTML fallback, MD5 verification, | |
| 4 | + | # uninstalls bundled distro libreoffice* first to avoid conflicts, --dry-run. | |
| 5 | + | ||
| 6 | + | set -euo pipefail | |
| 7 | + | IFS=$'\n\t' | |
| 8 | + | ||
| 9 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 10 | + | DRY_RUN=0 | |
| 11 | + | SKIP_REMOVE_DISTRO=0 | |
| 12 | + | ||
| 13 | + | usage() { | |
| 14 | + | cat <<EOF | |
| 15 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help] | |
| 16 | + | ||
| 17 | + | Detects the latest stable LibreOffice version on download.documentfoundation.org, | |
| 18 | + | downloads the matching DEB tarball for your architecture, verifies the MD5 sum | |
| 19 | + | published alongside it, and installs the .deb packages. | |
| 20 | + | ||
| 21 | + | Options: | |
| 22 | + | --keep-distro-libreoffice Don't purge any pre-installed distro libreoffice* | |
| 23 | + | (default: purge to avoid library conflicts). | |
| 24 | + | --dry-run Print actions without executing. | |
| 25 | + | --help, -h Show this help. | |
| 26 | + | EOF | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 30 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 31 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 32 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 33 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 34 | + | ||
| 35 | + | while (( $# )); do | |
| 36 | + | case "$1" in | |
| 37 | + | --dry-run) DRY_RUN=1 ;; | |
| 38 | + | --keep-distro-libreoffice) SKIP_REMOVE_DISTRO=1 ;; | |
| 39 | + | -h|--help) usage; exit 0 ;; | |
| 40 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 41 | + | esac | |
| 42 | + | shift | |
| 43 | + | done | |
| 44 | + | ||
| 45 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 46 | + | ||
| 47 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 48 | + | # shellcheck disable=SC1091 | |
| 49 | + | . /etc/os-release | |
| 50 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 51 | + | *ubuntu*|*debian*) : ;; | |
| 52 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;; | |
| 53 | + | esac | |
| 54 | + | ||
| 55 | + | ARCH_RAW="$(uname -m)" | |
| 56 | + | case "$ARCH_RAW" in | |
| 57 | + | x86_64) LO_ARCH=x86_64; LO_SUFFIX=Linux_x86-64 ;; | |
| 58 | + | aarch64) LO_ARCH=aarch64; LO_SUFFIX=Linux_aarch64 ;; | |
| 59 | + | *) die "Unsupported architecture: $ARCH_RAW (LibreOffice ships x86_64 and aarch64)." ;; | |
| 60 | + | esac | |
| 61 | + | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW" | |
| 62 | + | ||
| 63 | + | export DEBIAN_FRONTEND=noninteractive | |
| 64 | + | ||
| 65 | + | log "Installing prerequisites..." | |
| 66 | + | run "apt-get update -qq" | |
| 67 | + | run "apt-get install -y curl wget tar coreutils ca-certificates" | |
| 68 | + | ||
| 69 | + | log "Resolving latest stable LibreOffice version..." | |
| 70 | + | INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)" | |
| 71 | + | LATEST_VERSION="$(printf '%s\n' "$INDEX" \ | |
| 72 | + | | grep -oP '(?<=href=")[0-9]+\.[0-9]+\.[0-9]+(?=/")' \ | |
| 73 | + | | sort -V | tail -1 || true)" | |
| 74 | + | [[ -n "$LATEST_VERSION" ]] || die "Could not detect latest version from index page." | |
| 75 | + | log "Latest stable: $LATEST_VERSION" | |
| 76 | + | ||
| 77 | + | BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}" | |
| 78 | + | ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz" | |
| 79 | + | URL="${BASE}/${ARCHIVE_NAME}" | |
| 80 | + | SUM_URL="${URL}.md5" | |
| 81 | + | ||
| 82 | + | STAGE="$(mktemp -d -t lo.XXXXXX)" | |
| 83 | + | trap 'rm -rf "$STAGE"' EXIT | |
| 84 | + | ARCHIVE="$STAGE/$ARCHIVE_NAME" | |
| 85 | + | ||
| 86 | + | log "Downloading $ARCHIVE_NAME..." | |
| 87 | + | run "wget -q --show-progress -O '$ARCHIVE' '$URL'" | |
| 88 | + | ||
| 89 | + | log "Verifying MD5..." | |
| 90 | + | if EXPECTED_MD5="$(curl -fsSL "$SUM_URL" 2>/dev/null | awk '{print $1}')" && [[ -n "$EXPECTED_MD5" ]]; then | |
| 91 | + | ACTUAL_MD5="$(md5sum "$ARCHIVE" | awk '{print $1}')" | |
| 92 | + | [[ "$EXPECTED_MD5" == "$ACTUAL_MD5" ]] || die "MD5 mismatch: expected=$EXPECTED_MD5 actual=$ACTUAL_MD5" | |
| 93 | + | log "MD5 ok." | |
| 94 | + | else | |
| 95 | + | warn "No MD5 published for $SUM_URL; skipping checksum." | |
| 96 | + | fi | |
| 97 | + | ||
| 98 | + | log "Extracting..." | |
| 99 | + | run "tar -xzf '$ARCHIVE' -C '$STAGE'" | |
| 100 | + | DEBS_DIR="$(find "$STAGE" -maxdepth 3 -type d -name DEBS | head -n1 || true)" | |
| 101 | + | [[ -n "$DEBS_DIR" || $DRY_RUN -eq 1 ]] || die "DEBS/ directory not found in archive." | |
| 102 | + | ||
| 103 | + | if (( ! SKIP_REMOVE_DISTRO )); then | |
| 104 | + | if dpkg -l 'libreoffice*' 2>/dev/null | awk '/^ii/ {print $2}' | grep -q .; then | |
| 105 | + | log "Removing distro-supplied libreoffice* packages to avoid conflicts..." | |
| 106 | + | run "apt-get remove --purge -y 'libreoffice*'" | |
| 107 | + | fi | |
| 108 | + | fi | |
| 109 | + | ||
| 110 | + | log "Installing .deb packages..." | |
| 111 | + | run "dpkg -i -R '$DEBS_DIR'" | |
| 112 | + | log "Resolving any missing dependencies..." | |
| 113 | + | run "apt-get install -f -y" | |
| 114 | + | ||
| 115 | + | log "Done. LibreOffice $LATEST_VERSION installed." | |
install-network_drive.sh(file created)
| @@ -0,0 +1,207 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-network_drive.sh — Auto-mount Synology (SMB/CIFS) shares under /mnt/Synology. | |
| 3 | + | # Hardened: safe input handling, no eval-on-username, hostname validation, | |
| 4 | + | # fstab managed via begin/end markers (idempotent re-run), creds file 0600, | |
| 5 | + | # GNOME dock pinning is best-effort. | |
| 6 | + | ||
| 7 | + | set -euo pipefail | |
| 8 | + | IFS=$'\n\t' | |
| 9 | + | ||
| 10 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 11 | + | DRY_RUN=0 | |
| 12 | + | SERVER_ADDR="" | |
| 13 | + | SMB_USER="" | |
| 14 | + | SMB_PASS="" | |
| 15 | + | MASTER_DIR="/mnt/Synology" | |
| 16 | + | ||
| 17 | + | usage() { | |
| 18 | + | cat <<EOF | |
| 19 | + | Usage: sudo $SCRIPT_NAME [options] | |
| 20 | + | ||
| 21 | + | Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they | |
| 22 | + | auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort) | |
| 23 | + | pins it to the top of the GNOME Dock. | |
| 24 | + | ||
| 25 | + | Options: | |
| 26 | + | --server HOST_OR_IP Synology address (skips prompt). | |
| 27 | + | --user USERNAME SMB username (skips prompt). | |
| 28 | + | --password-stdin Read SMB password from stdin (skips prompt). | |
| 29 | + | (Otherwise the script prompts on the controlling tty.) | |
| 30 | + | --mount-root DIR Override parent directory (default: $MASTER_DIR). | |
| 31 | + | --dry-run Print actions without executing. | |
| 32 | + | --help, -h Show this help. | |
| 33 | + | EOF | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 37 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 38 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 39 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 40 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 41 | + | ||
| 42 | + | PASS_FROM_STDIN=0 | |
| 43 | + | while (( $# )); do | |
| 44 | + | case "$1" in | |
| 45 | + | --server) SERVER_ADDR="${2:?}"; shift ;; | |
| 46 | + | --user) SMB_USER="${2:?}"; shift ;; | |
| 47 | + | --password-stdin) PASS_FROM_STDIN=1 ;; | |
| 48 | + | --mount-root) MASTER_DIR="${2:?}"; shift ;; | |
| 49 | + | --dry-run) DRY_RUN=1 ;; | |
| 50 | + | -h|--help) usage; exit 0 ;; | |
| 51 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 52 | + | esac | |
| 53 | + | shift | |
| 54 | + | done | |
| 55 | + | ||
| 56 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 57 | + | ||
| 58 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 59 | + | # shellcheck disable=SC1091 | |
| 60 | + | . /etc/os-release | |
| 61 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 62 | + | *ubuntu*|*debian*) : ;; | |
| 63 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; | |
| 64 | + | esac | |
| 65 | + | ||
| 66 | + | ACTUAL_USER="${SUDO_USER:-${USER:-}}" | |
| 67 | + | [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user." | |
| 68 | + | # Use getent so we don't depend on eval/~expansion or shell glob safety. | |
| 69 | + | USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd." | |
| 70 | + | USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")" | |
| 71 | + | USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")" | |
| 72 | + | USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")" | |
| 73 | + | ||
| 74 | + | export DEBIAN_FRONTEND=noninteractive | |
| 75 | + | log "Installing cifs-utils, smbclient..." | |
| 76 | + | run "apt-get update -qq" | |
| 77 | + | run "apt-get install -y cifs-utils smbclient" | |
| 78 | + | ||
| 79 | + | # --- Collect creds --- | |
| 80 | + | [[ -n "$SERVER_ADDR" ]] || read -rp "Synology NAS address or IP: " SERVER_ADDR </dev/tty | |
| 81 | + | SERVER_ADDR="${SERVER_ADDR#smb://}" | |
| 82 | + | SERVER_ADDR="${SERVER_ADDR#//}" | |
| 83 | + | SERVER_ADDR="${SERVER_ADDR%/}" | |
| 84 | + | [[ "$SERVER_ADDR" =~ ^[A-Za-z0-9._-]+$ ]] || die "Invalid host/IP: '$SERVER_ADDR'" | |
| 85 | + | ||
| 86 | + | [[ -n "$SMB_USER" ]] || read -rp "Synology username: " SMB_USER </dev/tty | |
| 87 | + | [[ "$SMB_USER" =~ ^[A-Za-z0-9._@-]+$ ]] || die "Invalid SMB username." | |
| 88 | + | ||
| 89 | + | if (( PASS_FROM_STDIN )); then | |
| 90 | + | IFS= read -r SMB_PASS | |
| 91 | + | else | |
| 92 | + | read -rsp "Synology password: " SMB_PASS </dev/tty | |
| 93 | + | echo | |
| 94 | + | fi | |
| 95 | + | [[ -n "$SMB_PASS" ]] || die "Password is empty." | |
| 96 | + | ||
| 97 | + | # --- Credentials file --- | |
| 98 | + | CRED_FILE="$USER_HOME/.smbcredentials_synology" | |
| 99 | + | log "Writing credentials to $CRED_FILE (mode 0600)..." | |
| 100 | + | if (( DRY_RUN )); then | |
| 101 | + | printf ' DRY-RUN: write %s\n' "$CRED_FILE" | |
| 102 | + | else | |
| 103 | + | umask 077 | |
| 104 | + | { | |
| 105 | + | printf 'username=%s\n' "$SMB_USER" | |
| 106 | + | printf 'password=%s\n' "$SMB_PASS" | |
| 107 | + | } >"$CRED_FILE" | |
| 108 | + | chown "$USER_ID:$USER_GID" "$CRED_FILE" | |
| 109 | + | chmod 0600 "$CRED_FILE" | |
| 110 | + | fi | |
| 111 | + | ||
| 112 | + | # --- Query shares --- | |
| 113 | + | log "Discovering shares on //$SERVER_ADDR..." | |
| 114 | + | if (( DRY_RUN )); then | |
| 115 | + | SHARE_LIST=$'home\nphoto\nvideo' | |
| 116 | + | else | |
| 117 | + | SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \ | |
| 118 | + | | awk -F'|' '$1=="Disk" {print $2}' || true)" | |
| 119 | + | fi | |
| 120 | + | [[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network." | |
| 121 | + | ||
| 122 | + | # --- Parent mount dir --- | |
| 123 | + | run "mkdir -p '$MASTER_DIR'" | |
| 124 | + | run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'" | |
| 125 | + | ||
| 126 | + | # --- Backup fstab once --- | |
| 127 | + | if [[ ! -f /etc/fstab.bak.synology ]]; then | |
| 128 | + | run "cp /etc/fstab /etc/fstab.bak.synology" | |
| 129 | + | log "Backed up fstab -> /etc/fstab.bak.synology" | |
| 130 | + | fi | |
| 131 | + | ||
| 132 | + | MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})" | |
| 133 | + | MARK_END="# <<< synology-master <<<" | |
| 134 | + | # Remove any previous managed block (so re-run replaces, not appends). | |
| 135 | + | if grep -qF "$MARK_BEGIN" /etc/fstab; then | |
| 136 | + | if (( DRY_RUN )); then | |
| 137 | + | printf ' DRY-RUN: strip previous managed block from /etc/fstab\n' | |
| 138 | + | else | |
| 139 | + | sed -i "\|$MARK_BEGIN|,\|$MARK_END|d" /etc/fstab | |
| 140 | + | fi | |
| 141 | + | fi | |
| 142 | + | ||
| 143 | + | log "Writing managed block to /etc/fstab..." | |
| 144 | + | FSTAB_BLOCK="$MARK_BEGIN"$'\n' | |
| 145 | + | while IFS= read -r SHARE; do | |
| 146 | + | [[ -z "$SHARE" || "$SHARE" == "IPC\$" || "$SHARE" == "print\$" ]] && continue | |
| 147 | + | MOUNT_POINT="$MASTER_DIR/$SHARE" | |
| 148 | + | run "mkdir -p '$MOUNT_POINT'" | |
| 149 | + | run "chown '$USER_ID:$USER_GID' '$MOUNT_POINT'" | |
| 150 | + | FSTAB_BLOCK+="//${SERVER_ADDR}/${SHARE} ${MOUNT_POINT} cifs credentials=${CRED_FILE},uid=${USER_ID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.idle-timeout=60 0 0"$'\n' | |
| 151 | + | done <<<"$SHARE_LIST" | |
| 152 | + | FSTAB_BLOCK+="$MARK_END"$'\n' | |
| 153 | + | ||
| 154 | + | if (( DRY_RUN )); then | |
| 155 | + | printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK" | |
| 156 | + | else | |
| 157 | + | printf '%s' "$FSTAB_BLOCK" >>/etc/fstab | |
| 158 | + | fi | |
| 159 | + | ||
| 160 | + | log "Reloading systemd and mounting..." | |
| 161 | + | run "systemctl daemon-reload" | |
| 162 | + | run "mount -a -t cifs" | |
| 163 | + | ||
| 164 | + | # --- Desktop entry --- | |
| 165 | + | APPS_DIR="$USER_HOME/.local/share/applications" | |
| 166 | + | DESKTOP_FILENAME="synology-master.desktop" | |
| 167 | + | DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME" | |
| 168 | + | run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'" | |
| 169 | + | ||
| 170 | + | if (( DRY_RUN )); then | |
| 171 | + | printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE" | |
| 172 | + | else | |
| 173 | + | cat >"$DESKTOP_FILE" <<EOF | |
| 174 | + | [Desktop Entry] | |
| 175 | + | Name=Synology NAS | |
| 176 | + | Comment=Open Synology Master Directory | |
| 177 | + | Exec=xdg-open ${MASTER_DIR} | |
| 178 | + | Icon=folder-remote | |
| 179 | + | Terminal=false | |
| 180 | + | Type=Application | |
| 181 | + | Categories=Network;FileTools; | |
| 182 | + | EOF | |
| 183 | + | chown "$USER_ID:$USER_GID" "$DESKTOP_FILE" | |
| 184 | + | chmod 0755 "$DESKTOP_FILE" | |
| 185 | + | fi | |
| 186 | + | ||
| 187 | + | # --- Best-effort GNOME dock pin --- | |
| 188 | + | if (( ! DRY_RUN )) && sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings list-keys org.gnome.shell >/dev/null 2>&1; then | |
| 189 | + | log "Pinning to top of GNOME dock..." | |
| 190 | + | CURRENT_FAVS="$(sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings get org.gnome.shell favorite-apps 2>/dev/null || echo '[]')" | |
| 191 | + | if [[ "$CURRENT_FAVS" != *"$DESKTOP_FILENAME"* ]]; then | |
| 192 | + | CLEAN_FAVS="${CURRENT_FAVS#@as }" | |
| 193 | + | if [[ "$CLEAN_FAVS" == "[]" || -z "$CLEAN_FAVS" ]]; then | |
| 194 | + | NEW_FAVS="['$DESKTOP_FILENAME']" | |
| 195 | + | else | |
| 196 | + | NEW_FAVS="['$DESKTOP_FILENAME', ${CLEAN_FAVS:1}" | |
| 197 | + | fi | |
| 198 | + | sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ | |
| 199 | + | gsettings set org.gnome.shell favorite-apps "$NEW_FAVS" || warn "Dock pin failed (non-fatal)." | |
| 200 | + | else | |
| 201 | + | log "Already pinned." | |
| 202 | + | fi | |
| 203 | + | else | |
| 204 | + | log "GNOME not detected (or no active dbus session); skipping dock pin." | |
| 205 | + | fi | |
| 206 | + | ||
| 207 | + | log "Done. Shares mounted under $MASTER_DIR." | |
install-thunderbird.sh(file created)
| @@ -0,0 +1,102 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-thunderbird.sh — Install Thunderbird. | |
| 3 | + | # On Ubuntu: uses the Mozilla Team PPA (with APT pinning so Snap transition is bypassed). | |
| 4 | + | # On Debian: uses the regular Debian package (no PPA available). | |
| 5 | + | # Hardened: distro-detect, idempotent, --dry-run. | |
| 6 | + | ||
| 7 | + | set -euo pipefail | |
| 8 | + | IFS=$'\n\t' | |
| 9 | + | ||
| 10 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 11 | + | DRY_RUN=0 | |
| 12 | + | ||
| 13 | + | usage() { | |
| 14 | + | cat <<EOF | |
| 15 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] | |
| 16 | + | ||
| 17 | + | Installs Thunderbird. | |
| 18 | + | - Ubuntu: removes Snap version (if any), adds Mozilla Team PPA, pins it, | |
| 19 | + | and installs the .deb. This avoids the Ubuntu snap transition wrapper. | |
| 20 | + | - Debian: installs from the standard Debian repos. | |
| 21 | + | ||
| 22 | + | Options: | |
| 23 | + | --dry-run Print actions without executing. | |
| 24 | + | --help, -h Show this help. | |
| 25 | + | EOF | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 29 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 30 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 31 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 32 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 33 | + | ||
| 34 | + | while (( $# )); do | |
| 35 | + | case "$1" in | |
| 36 | + | --dry-run) DRY_RUN=1 ;; | |
| 37 | + | -h|--help) usage; exit 0 ;; | |
| 38 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 39 | + | esac | |
| 40 | + | shift | |
| 41 | + | done | |
| 42 | + | ||
| 43 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 44 | + | ||
| 45 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 46 | + | # shellcheck disable=SC1091 | |
| 47 | + | . /etc/os-release | |
| 48 | + | log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})" | |
| 49 | + | ||
| 50 | + | export DEBIAN_FRONTEND=noninteractive | |
| 51 | + | ||
| 52 | + | case "${ID:-}" in | |
| 53 | + | ubuntu) | |
| 54 | + | log "Removing Thunderbird Snap (if present)..." | |
| 55 | + | if command -v snap >/dev/null 2>&1; then | |
| 56 | + | run "snap remove --purge thunderbird >/dev/null 2>&1 || true" | |
| 57 | + | fi | |
| 58 | + | ||
| 59 | + | log "Installing prerequisites..." | |
| 60 | + | run "apt-get update -qq" | |
| 61 | + | run "apt-get install -y software-properties-common" | |
| 62 | + | ||
| 63 | + | log "Adding Mozilla Team PPA..." | |
| 64 | + | PPA_LIST=/etc/apt/sources.list.d/mozillateam-ubuntu-ppa-*.list | |
| 65 | + | # shellcheck disable=SC2086 | |
| 66 | + | if ! ls $PPA_LIST >/dev/null 2>&1; then | |
| 67 | + | run "add-apt-repository -y ppa:mozillateam/ppa" | |
| 68 | + | else | |
| 69 | + | log "Mozilla Team PPA already configured." | |
| 70 | + | fi | |
| 71 | + | ||
| 72 | + | PIN=/etc/apt/preferences.d/mozillateamppa | |
| 73 | + | log "Pinning Mozilla Team PPA so .deb wins over Snap transition wrapper..." | |
| 74 | + | if [[ ! -f "$PIN" ]] || ! grep -q 'release o=LP-PPA-mozillateam' "$PIN"; then | |
| 75 | + | if (( DRY_RUN )); then | |
| 76 | + | printf ' DRY-RUN: write %s\n' "$PIN" | |
| 77 | + | else | |
| 78 | + | cat >"$PIN" <<'EOF' | |
| 79 | + | Package: thunderbird* | |
| 80 | + | Pin: release o=LP-PPA-mozillateam | |
| 81 | + | Pin-Priority: 1001 | |
| 82 | + | EOF | |
| 83 | + | fi | |
| 84 | + | fi | |
| 85 | + | ||
| 86 | + | log "Installing thunderbird from PPA..." | |
| 87 | + | run "apt-get update -qq" | |
| 88 | + | run "apt-get install -y --allow-downgrades thunderbird" | |
| 89 | + | ;; | |
| 90 | + | ||
| 91 | + | debian) | |
| 92 | + | log "Installing thunderbird from Debian repos..." | |
| 93 | + | run "apt-get update -qq" | |
| 94 | + | run "apt-get install -y thunderbird" | |
| 95 | + | ;; | |
| 96 | + | ||
| 97 | + | *) | |
| 98 | + | die "Unsupported distro: ${PRETTY_NAME:-unknown}. Supported: ubuntu, debian." | |
| 99 | + | ;; | |
| 100 | + | esac | |
| 101 | + | ||
| 102 | + | log "Done." | |
install-vscode.sh(file created)
| @@ -0,0 +1,88 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # install-vscode.sh — Install Visual Studio Code from Microsoft's APT repository. | |
| 3 | + | # Hardened: arch-aware, signed-by keyring, idempotent, --dry-run. | |
| 4 | + | ||
| 5 | + | set -euo pipefail | |
| 6 | + | IFS=$'\n\t' | |
| 7 | + | ||
| 8 | + | readonly SCRIPT_NAME="${0##*/}" | |
| 9 | + | DRY_RUN=0 | |
| 10 | + | INSIDERS=0 | |
| 11 | + | ||
| 12 | + | usage() { | |
| 13 | + | cat <<EOF | |
| 14 | + | Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help] | |
| 15 | + | ||
| 16 | + | Configures the official Microsoft 'vscode' APT repository (signed-by keyring) | |
| 17 | + | and installs Visual Studio Code. Idempotent: safe to re-run. | |
| 18 | + | ||
| 19 | + | Options: | |
| 20 | + | --insiders Install the 'code-insiders' build instead of stable 'code'. | |
| 21 | + | --dry-run Print actions without executing. | |
| 22 | + | --help, -h Show this help. | |
| 23 | + | EOF | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } | |
| 27 | + | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } | |
| 28 | + | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } | |
| 29 | + | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } | |
| 30 | + | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR | |
| 31 | + | ||
| 32 | + | while (( $# )); do | |
| 33 | + | case "$1" in | |
| 34 | + | --dry-run) DRY_RUN=1 ;; | |
| 35 | + | --insiders) INSIDERS=1 ;; | |
| 36 | + | -h|--help) usage; exit 0 ;; | |
| 37 | + | *) die "Unknown argument: $1 (try --help)" ;; | |
| 38 | + | esac | |
| 39 | + | shift | |
| 40 | + | done | |
| 41 | + | ||
| 42 | + | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" | |
| 43 | + | ||
| 44 | + | [[ -r /etc/os-release ]] || die "/etc/os-release not found." | |
| 45 | + | # shellcheck disable=SC1091 | |
| 46 | + | . /etc/os-release | |
| 47 | + | case "${ID:-}:${ID_LIKE:-}" in | |
| 48 | + | *ubuntu*|*debian*) : ;; | |
| 49 | + | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; | |
| 50 | + | esac | |
| 51 | + | ||
| 52 | + | ARCH="$(dpkg --print-architecture)" | |
| 53 | + | case "$ARCH" in | |
| 54 | + | amd64|arm64|armhf) : ;; | |
| 55 | + | *) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;; | |
| 56 | + | esac | |
| 57 | + | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" | |
| 58 | + | ||
| 59 | + | export DEBIAN_FRONTEND=noninteractive | |
| 60 | + | ||
| 61 | + | KEYRING=/etc/apt/keyrings/packages.microsoft.gpg | |
| 62 | + | SOURCES=/etc/apt/sources.list.d/vscode.list | |
| 63 | + | PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code) | |
| 64 | + | ||
| 65 | + | log "Installing prerequisites..." | |
| 66 | + | run "apt-get update -qq" | |
| 67 | + | run "apt-get install -y wget gpg apt-transport-https ca-certificates" | |
| 68 | + | ||
| 69 | + | log "Configuring Microsoft vscode APT repository..." | |
| 70 | + | run "install -d -m 0755 /etc/apt/keyrings" | |
| 71 | + | if [[ ! -s "$KEYRING" ]]; then | |
| 72 | + | run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'" | |
| 73 | + | run "chmod 0644 '$KEYRING'" | |
| 74 | + | fi | |
| 75 | + | ||
| 76 | + | DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main" | |
| 77 | + | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then | |
| 78 | + | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" | |
| 79 | + | fi | |
| 80 | + | ||
| 81 | + | log "Installing $PKG..." | |
| 82 | + | run "apt-get update -qq" | |
| 83 | + | run "apt-get install -y '$PKG'" | |
| 84 | + | ||
| 85 | + | if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then | |
| 86 | + | log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")" | |
| 87 | + | fi | |
| 88 | + | log "Done." | |
weehong revised this gist 3 months ago. Go to revision
1 file changed, 10 insertions, 24 deletions
READMD.md
| @@ -1,32 +1,18 @@ | |||
| 1 | - | # Ubuntu Sans Nerd Font Installer | |
| 1 | + | # Ubuntu Sans Nerd Font Setup | |
| 2 | 2 | ||
| 3 | - | A quick, automated bash script designed specifically for Ubuntu Desktop setups. This script fetches, downloads, and installs the absolute latest release of the **Ubuntu Sans Nerd Font** directly from the official Nerd Fonts repository. | |
| 3 | + | A lightweight script to automatically fetch and install the latest **Ubuntu Sans Nerd Font** for Ubuntu Desktop. | |
| 4 | 4 | ||
| 5 | - | Perfect for developers looking to quickly configure a fresh Ubuntu installation with beautifully patched fonts for their terminal, IDE, or system UI. | |
| 5 | + | ## Quick Install | |
| 6 | 6 | ||
| 7 | - | ## 🚀 Quick Install | |
| 8 | - | ||
| 9 | - | To install the latest Ubuntu Sans Nerd Font, simply copy and paste the following one-liner into your terminal: | |
| 7 | + | Run the following command in your terminal: | |
| 10 | 8 | ||
| 11 | 9 | ```bash | |
| 12 | - | bash -c "$(curl -fsSL [https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh](https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh))" | |
| 10 | + | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)" | |
| 13 | 11 | ``` | |
| 14 | 12 | ||
| 15 | - | ## ⚙️ What It Does Under the Hood | |
| 16 | - | ||
| 17 | - | Running curl-to-bash scripts requires trust. Here is exactly what this script automates: | |
| 18 | - | ||
| 19 | - | * **Dynamic Versioning:** Queries the GitHub API to always fetch the latest stable release of `UbuntuSans.zip`. | |
| 20 | - | * **Safe Extraction:** Creates a temporary directory (`/tmp/...`) to safely download and extract the archive without cluttering your system. | |
| 21 | - | * **Targeted Installation:** Installs only the relevant `.ttf` and `.otf` font files into your user-level font directory: `~/.local/share/fonts/UbuntuSans`. | |
| 22 | - | * **Zero `sudo` Required:** Installs locally for the current user, avoiding the need for root privileges. | |
| 23 | - | * **Instant Activation:** Automatically runs `fc-cache` to rebuild the system's font cache, making the fonts instantly accessible to your applications. | |
| 24 | - | * **Automatic Cleanup:** Deletes all temporary download and extraction folders once finished. | |
| 25 | - | ||
| 26 | - | ## 📋 Prerequisites | |
| 27 | - | ||
| 28 | - | Your system will need a couple of basic utilities to run the script. Most Ubuntu Desktops come with these pre-installed: | |
| 29 | - | * `curl` (to fetch the script and the font zip) | |
| 30 | - | * `unzip` (to extract the font archive) | |
| 13 | + | ## Overview | |
| 31 | 14 | ||
| 32 | - | *Note: If `unzip` is missing from your system, the script will gracefully exit and remind you to run `sudo apt install unzip`.* | |
| 15 | + | * **Always Latest:** Dynamically pulls the newest release from the official Nerd Fonts GitHub. | |
| 16 | + | * **No Sudo Needed:** Installs safely to your local user directory (`~/.local/share/fonts/UbuntuSans`). | |
| 17 | + | * **Ready to Use:** Automatically updates the font cache (`fc-cache`) so fonts are available immediately. | |
| 18 | + | * **Dependencies:** Requires `curl` and `unzip`. | |
weehong revised this gist 3 months ago. Go to revision
2 files changed, 34 insertions, 4 deletions
READMD.md
| @@ -1,3 +1,32 @@ | |||
| 1 | + | # Ubuntu Sans Nerd Font Installer | |
| 2 | + | ||
| 3 | + | A quick, automated bash script designed specifically for Ubuntu Desktop setups. This script fetches, downloads, and installs the absolute latest release of the **Ubuntu Sans Nerd Font** directly from the official Nerd Fonts repository. | |
| 4 | + | ||
| 5 | + | Perfect for developers looking to quickly configure a fresh Ubuntu installation with beautifully patched fonts for their terminal, IDE, or system UI. | |
| 6 | + | ||
| 7 | + | ## 🚀 Quick Install | |
| 8 | + | ||
| 9 | + | To install the latest Ubuntu Sans Nerd Font, simply copy and paste the following one-liner into your terminal: | |
| 10 | + | ||
| 11 | + | ```bash | |
| 12 | + | bash -c "$(curl -fsSL [https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh](https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh))" | |
| 1 | 13 | ``` | |
| 2 | - | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)" | |
| 3 | - | ``` | |
| 14 | + | ||
| 15 | + | ## ⚙️ What It Does Under the Hood | |
| 16 | + | ||
| 17 | + | Running curl-to-bash scripts requires trust. Here is exactly what this script automates: | |
| 18 | + | ||
| 19 | + | * **Dynamic Versioning:** Queries the GitHub API to always fetch the latest stable release of `UbuntuSans.zip`. | |
| 20 | + | * **Safe Extraction:** Creates a temporary directory (`/tmp/...`) to safely download and extract the archive without cluttering your system. | |
| 21 | + | * **Targeted Installation:** Installs only the relevant `.ttf` and `.otf` font files into your user-level font directory: `~/.local/share/fonts/UbuntuSans`. | |
| 22 | + | * **Zero `sudo` Required:** Installs locally for the current user, avoiding the need for root privileges. | |
| 23 | + | * **Instant Activation:** Automatically runs `fc-cache` to rebuild the system's font cache, making the fonts instantly accessible to your applications. | |
| 24 | + | * **Automatic Cleanup:** Deletes all temporary download and extraction folders once finished. | |
| 25 | + | ||
| 26 | + | ## 📋 Prerequisites | |
| 27 | + | ||
| 28 | + | Your system will need a couple of basic utilities to run the script. Most Ubuntu Desktops come with these pre-installed: | |
| 29 | + | * `curl` (to fetch the script and the font zip) | |
| 30 | + | * `unzip` (to extract the font archive) | |
| 31 | + | ||
| 32 | + | *Note: If `unzip` is missing from your system, the script will gracefully exit and remind you to run `sudo apt install unzip`.* | |
install_font.sh
| @@ -5,7 +5,8 @@ set -e | |||
| 5 | 5 | ||
| 6 | 6 | FONT_NAME="UbuntuSans" | |
| 7 | 7 | FONT_ZIP="${FONT_NAME}.zip" | |
| 8 | - | FONT_DIR="$HOME/.local/share/fonts/NerdFonts/${FONT_NAME}" | |
| 8 | + | # Simplified directory: removed the "NerdFonts" subfolder | |
| 9 | + | FONT_DIR="$HOME/.local/share/fonts/${FONT_NAME}" | |
| 9 | 10 | TMP_DIR=$(mktemp -d) | |
| 10 | 11 | ||
| 11 | 12 | echo "Searching for the latest release of $FONT_NAME Nerd Font..." | |
| @@ -38,7 +39,7 @@ fi | |||
| 38 | 39 | unzip -q -o "$TMP_DIR/$FONT_ZIP" -d "$TMP_DIR/extracted" | |
| 39 | 40 | ||
| 40 | 41 | echo "Installing fonts to $FONT_DIR..." | |
| 41 | - | # Create the font directory if it doesn't exist | |
| 42 | + | # Create the simplified font directory if it doesn't exist | |
| 42 | 43 | mkdir -p "$FONT_DIR" | |
| 43 | 44 | ||
| 44 | 45 | # Move only the TrueType (.ttf) or OpenType (.otf) files to the fonts directory | |
weehong revised this gist 3 months ago. Go to revision
1 file changed, 1 insertion, 1 deletion
READMD.md
| @@ -1,3 +1,3 @@ | |||
| 1 | 1 | ``` | |
| 2 | - | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/af1c64c143a44ffbb0a1632dd6a32af1/raw/HEAD/menu.sh)" | |
| 2 | + | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)" | |
| 3 | 3 | ``` | |
weehong revised this gist 3 months ago. Go to revision
1 file changed, 3 insertions
READMD.md(file created)
| @@ -0,0 +1,3 @@ | |||
| 1 | + | ``` | |
| 2 | + | bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/af1c64c143a44ffbb0a1632dd6a32af1/raw/HEAD/menu.sh)" | |
| 3 | + | ``` | |
weehong revised this gist 3 months ago. Go to revision
1 file changed, 53 insertions
install_font.sh(file created)
| @@ -0,0 +1,53 @@ | |||
| 1 | + | #!/bin/bash | |
| 2 | + | ||
| 3 | + | # Exit immediately if a command exits with a non-zero status | |
| 4 | + | set -e | |
| 5 | + | ||
| 6 | + | FONT_NAME="UbuntuSans" | |
| 7 | + | FONT_ZIP="${FONT_NAME}.zip" | |
| 8 | + | FONT_DIR="$HOME/.local/share/fonts/NerdFonts/${FONT_NAME}" | |
| 9 | + | TMP_DIR=$(mktemp -d) | |
| 10 | + | ||
| 11 | + | echo "Searching for the latest release of $FONT_NAME Nerd Font..." | |
| 12 | + | ||
| 13 | + | # Use GitHub API to find the latest release download URL for UbuntuSans.zip | |
| 14 | + | DOWNLOAD_URL=$(curl -s https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \ | |
| 15 | + | | grep "browser_download_url.*${FONT_ZIP}" \ | |
| 16 | + | | cut -d '"' -f 4) | |
| 17 | + | ||
| 18 | + | if [ -z "$DOWNLOAD_URL" ]; then | |
| 19 | + | echo "Error: Could not find the download URL for $FONT_NAME. Check your internet connection or GitHub API limits." | |
| 20 | + | rm -rf "$TMP_DIR" | |
| 21 | + | exit 1 | |
| 22 | + | fi | |
| 23 | + | ||
| 24 | + | echo "Latest version found!" | |
| 25 | + | echo "Downloading from: $DOWNLOAD_URL" | |
| 26 | + | ||
| 27 | + | # Download the zip file to the temporary directory | |
| 28 | + | curl -L -q "$DOWNLOAD_URL" -o "$TMP_DIR/$FONT_ZIP" | |
| 29 | + | ||
| 30 | + | echo "Extracting fonts..." | |
| 31 | + | # Ensure unzip is installed (will fail gracefully if not) | |
| 32 | + | if ! command -v unzip &> /dev/null; then | |
| 33 | + | echo "Error: 'unzip' is not installed. Please install it using 'sudo apt install unzip' and try again." | |
| 34 | + | rm -rf "$TMP_DIR" | |
| 35 | + | exit 1 | |
| 36 | + | fi | |
| 37 | + | ||
| 38 | + | unzip -q -o "$TMP_DIR/$FONT_ZIP" -d "$TMP_DIR/extracted" | |
| 39 | + | ||
| 40 | + | echo "Installing fonts to $FONT_DIR..." | |
| 41 | + | # Create the font directory if it doesn't exist | |
| 42 | + | mkdir -p "$FONT_DIR" | |
| 43 | + | ||
| 44 | + | # Move only the TrueType (.ttf) or OpenType (.otf) files to the fonts directory | |
| 45 | + | find "$TMP_DIR/extracted" -name '*.[ot]tf' -type f -exec cp {} "$FONT_DIR/" \; | |
| 46 | + | ||
| 47 | + | echo "Updating the system font cache..." | |
| 48 | + | fc-cache -f "$FONT_DIR" | |
| 49 | + | ||
| 50 | + | echo "Cleaning up temporary files..." | |
| 51 | + | rm -rf "$TMP_DIR" | |
| 52 | + | ||
| 53 | + | echo "Done! $FONT_NAME Nerd Font has been successfully installed." | |