Last active 2 weeks ago

Hardened interactive Ubuntu/Debian setup scripts, including CCUsage multi-device sync for Claude Code, Codex CLI, and OpenCode.

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."
Newer Older