Ostatnio aktywny 2 weeks ago

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

Rewizja 269a0a0ec9007695a00d6a4c733503549ad5189c

READMD.md Surowy

Ubuntu Sans Nerd Font Setup

A lightweight script to automatically fetch and install the latest Ubuntu Sans Nerd Font for Ubuntu Desktop.

Quick Install

Run the following command in your terminal:

bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)"

Overview

  • Always Latest: Dynamically pulls the newest release from the official Nerd Fonts GitHub.
  • No Sudo Needed: Installs safely to your local user directory (~/.local/share/fonts/UbuntuSans).
  • Ready to Use: Automatically updates the font cache (fc-cache) so fonts are available immediately.
  • Dependencies: Requires curl and unzip.
install-1password.sh Surowy
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
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10
11usage() {
12 cat <<EOF
13Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
14
15Configures the official 1Password APT repository (with debsig policy and
16keyring) and installs the 1Password desktop application. Idempotent: safe to
17re-run.
18
19Options:
20 --dry-run Print actions without executing.
21 --help, -h Show this help.
22EOF
23}
24
25log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
26warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
27die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
28run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
29trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
30
31while (( $# )); 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
38done
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
45case "${ID:-}:${ID_LIKE:-}" in
46 *ubuntu*|*debian*) : ;;
47 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
48esac
49
50# 1Password publishes builds for amd64 and arm64.
51ARCH="$(dpkg --print-architecture)"
52case "$ARCH" in
53 amd64|arm64) : ;;
54 *) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;;
55esac
56log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
57
58export DEBIAN_FRONTEND=noninteractive
59
60KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg
61SOURCES=/etc/apt/sources.list.d/1password.list
62DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22
63DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y curl gpg ca-certificates"
68
69log "Configuring 1Password APT repository..."
70run "install -d -m 0755 /etc/apt/keyrings"
71if [[ ! -s "$KEYRING" ]]; then
72 run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'"
73 run "chmod 0644 '$KEYRING'"
74fi
75
76DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main"
77if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
78 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
79fi
80
81log "Installing debsig policy (verifies package signatures on install)..."
82run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'"
83if [[ ! -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'"
85fi
86if [[ ! -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'"
88fi
89
90log "Installing 1Password..."
91run "apt-get update -qq"
92run "apt-get install -y 1password"
93
94if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then
95 log "1Password binary available at: $(command -v 1password)"
96fi
97log "Done."
98
install-bruno.sh Surowy
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
15
16Configures the official Bruno APT repository (signed-by keyring fetched from
17keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to
18re-run.
19
20Options:
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
28die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
29run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
30trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
31
32while (( $# )); 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
39done
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
46case "${ID:-}:${ID_LIKE:-}" in
47 *ubuntu*|*debian*) : ;;
48 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
49esac
50
51ARCH="$(dpkg --print-architecture)"
52[[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)."
53log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
54
55export DEBIAN_FRONTEND=noninteractive
56
57KEYRING=/etc/apt/keyrings/bruno.gpg
58SOURCES=/etc/apt/sources.list.d/bruno.list
59KEY_ID="9FA6017ECABE0266"
60DESKTOP=/usr/share/applications/bruno.desktop
61
62log "Installing prerequisites..."
63run "apt-get update -qq"
64run "apt-get install -y curl gpg ca-certificates"
65
66log "Configuring Bruno APT repository..."
67run "install -d -m 0755 /etc/apt/keyrings"
68
69if [[ ! -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'"
88fi
89
90DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable"
91if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
92 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
93fi
94
95log "Installing Bruno..."
96run "apt-get update -qq"
97run "apt-get install -y bruno"
98
99log "Writing desktop entry..."
100if (( DRY_RUN )); then
101 printf ' DRY-RUN: write %s\n' "$DESKTOP"
102else
103 cat >"$DESKTOP" <<'EOF'
104[Desktop Entry]
105Name=Bruno
106Comment=Open-source API Client
107Exec=bruno %U
108Terminal=false
109Type=Application
110Icon=bruno
111Categories=Development;Utility;
112StartupNotify=true
113EOF
114 chmod 0644 "$DESKTOP"
115fi
116
117log "Done. Bruno installed."
118
install-espanso.sh Surowy
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11FORCE_VARIANT="" # "wayland" | "x11"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help]
16
17Installs Espanso from the latest GitHub release. Auto-detects whether to use
18the 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
21Options:
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.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
31die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
32run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
33trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
34
35while (( $# )); 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
44done
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
51case "${ID:-}:${ID_LIKE:-}" in
52 *ubuntu*|*debian*) : ;;
53 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
54esac
55
56ARCH="$(dpkg --print-architecture)"
57[[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)."
58
59ACTUAL_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."
61USER_ID="$(id -u "$ACTUAL_USER")"
62
63# Detect session type from the invoking user's environment, fallback to env, fallback to wayland
64if [[ -n "$FORCE_VARIANT" ]]; then
65 VARIANT="$FORCE_VARIANT"
66else
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
74fi
75log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT"
76
77export DEBIAN_FRONTEND=noninteractive
78TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)"
79trap 'rm -f "$TEMP_DEB"' EXIT
80
81DEB_NAME="espanso-debian-${VARIANT}-amd64.deb"
82URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}"
83
84log "Installing prerequisites..."
85run "apt-get update -qq"
86run "apt-get install -y wget libcap2-bin"
87
88log "Downloading ${DEB_NAME}..."
89run "wget -qO '$TEMP_DEB' '$URL'"
90
91log "Installing package..."
92run "apt-get install -y '$TEMP_DEB'"
93
94ESPANSO_BIN="$(command -v espanso || true)"
95[[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install."
96
97if [[ "$VARIANT" == "wayland" ]]; then
98 log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..."
99 run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'"
100fi
101
102log "Registering & starting espanso service for $ACTUAL_USER..."
103# Register may fail if already registered — treat that as success.
104if (( 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"
107else
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
111fi
112
113log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER."
114if [[ "$VARIANT" == "wayland" ]]; then
115 log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml"
116fi
117
install-firefox.sh Surowy
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11ASSUME_YES=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help]
16
17Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the
18official Mozilla APT repository (signed-by keyring + APT pin), then installs
19Firefox so it auto-updates from Mozilla.
20
21Options:
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.
25EOF
26}
27
28log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
29warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
32
33on_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; }
34trap 'on_err $LINENO' ERR
35
36while (( $# )); 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
44done
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
51case "${ID:-}:${ID_LIKE:-}" in
52 *ubuntu*|*debian*) : ;;
53 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
54esac
55log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})"
56
57APT_YES=()
58(( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use
59export DEBIAN_FRONTEND=noninteractive
60
61KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc
62SOURCES=/etc/apt/sources.list.d/mozilla.list
63PIN=/etc/apt/preferences.d/mozilla
64
65log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..."
66if 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"
69fi
70run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true"
71run "rm -f /usr/bin/firefox"
72
73log "Installing prerequisites (wget, gpg, ca-certificates)..."
74run "apt-get update -qq"
75run "apt-get install ${APT_YES[*]} wget gpg ca-certificates"
76
77log "Configuring Mozilla APT repository..."
78run "install -d -m 0755 /etc/apt/keyrings"
79if [[ ! -s "$KEYRING" ]]; then
80 run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg"
81 run "chmod 0644 '$KEYRING'"
82else
83 log "Keyring already present at $KEYRING (skipping download)."
84fi
85
86# Verify key fingerprint matches Mozilla's published fingerprint
87EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3"
88ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')"
89if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then
90 warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually."
91else
92 log "Mozilla key fingerprint verified."
93fi
94
95DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main'
96if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
97 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
98fi
99
100log "Pinning Mozilla repo to priority 1000..."
101if [[ ! -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'
106Package: *
107Pin: origin packages.mozilla.org
108Pin-Priority: 1000
109EOF
110 fi
111fi
112
113log "Installing Firefox from Mozilla repo..."
114run "apt-get update -qq"
115run "apt-get install ${APT_YES[*]} firefox"
116
117if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then
118 log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')"
119fi
120
121log "Done. Firefox installed from Mozilla APT and will auto-update."
122
install-ibus-pinyin.sh Surowy
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
9set -euo pipefail
10IFS=$'\n\t'
11
12readonly SCRIPT_NAME="${0##*/}"
13DRY_RUN=0
14SKIP_GSETTINGS=0
15
16usage() {
17 cat <<EOF
18Usage: $SCRIPT_NAME [--dry-run] [--skip-gsettings] [--help]
19
20Installs ibus-libpinyin (plus Simplified Chinese language packs) and registers
21'Intelligent Pinyin' as a GNOME input source. Idempotent.
22
23Do NOT run with sudo: gsettings is per-user and must run as the desktop user.
24The script invokes sudo internally only for apt-get steps.
25
26Options:
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
32Switch input methods at runtime with: Super + Space
33EOF
34}
35
36log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
37warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
38die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
39run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
40trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
41
42while (( $# )); 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
50done
51
52# Resolve the desktop user. If invoked via sudo, gsettings must target SUDO_USER.
53if (( 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."
57else
58 DESKTOP_USER="$USER"
59fi
60
61# Look up the user's UID for the DBus session bus.
62USER_ENTRY="$(getent passwd "$DESKTOP_USER")" || die "User '$DESKTOP_USER' not found in passwd."
63USER_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
68case "${ID:-}:${ID_LIKE:-}" in
69 *ubuntu*|*debian*) : ;;
70 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
71esac
72log "Detected: ${PRETTY_NAME:-unknown}, desktop user: $DESKTOP_USER"
73
74# Helper: invoke a command as $DESKTOP_USER with a working DBus address.
75run_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.
92sudo_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
100export DEBIAN_FRONTEND=noninteractive
101
102log "Installing IBus Pinyin + Simplified Chinese language packs..."
103sudo_run apt-get update -qq
104sudo_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.
111if 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
124fi
125
126if (( 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
130fi
131
132# Only manage gsettings if GNOME schemas are present.
133if ! 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
137fi
138
139log "Adding 'Intelligent Pinyin' to GNOME Input Sources (idempotent)..."
140
141CURRENT_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.
143CLEAN_SOURCES="${CURRENT_SOURCES#@as }"
144
145if [[ "$CLEAN_SOURCES" == *"'ibus', 'libpinyin'"* ]]; then
146 log "Intelligent Pinyin already present in input sources — nothing to do."
147else
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')"
156fi
157
158log "Done. Switch input methods with: Super + Space"
159log "If 'Chinese (Intelligent Pinyin)' doesn't appear in the top bar, log out and back in."
160
install-ipatool.sh Surowy
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="majd/ipatool"
12INSTALL_PATH="/usr/local/bin/ipatool"
13
14usage() {
15 cat <<EOF
16Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
17
18Resolves the latest release of majd/ipatool, downloads the tarball for your
19architecture, verifies its SHA-256 against the published checksums.txt, and
20installs the binary to /usr/local/bin/ipatool atomically.
21
22If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
23GitHub API request (avoids rate limits).
24
25Options:
26 --dry-run Print actions without executing.
27 --help, -h Show this help.
28EOF
29}
30
31log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
32warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
33die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
34run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
35trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
36
37while (( $# )); 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
44done
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
51log "Detected: ${PRETTY_NAME:-unknown}"
52
53ARCH_RAW="$(uname -m)"
54case "$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" ;;
59esac
60ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz"
61
62export DEBIAN_FRONTEND=noninteractive
63log "Installing prerequisites..."
64run "apt-get update -qq"
65run "apt-get install -y curl jq tar ca-certificates libsecret-1-0"
66
67GH_HDRS=(-H "Accept: application/vnd.github+json")
68[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
69
70log "Querying GitHub API for latest release of $REPO..."
71RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
72TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
73[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
74log "Latest release: $TAG"
75
76DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
77CHECKSUM_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
80STAGE="$(mktemp -d -t ipatool.XXXXXX)"
81trap 'rm -rf "$STAGE"' EXIT
82TARBALL="$STAGE/ipatool.tar.gz"
83
84log "Downloading $(basename "$DOWNLOAD_URL")..."
85run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'"
86
87if [[ -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."
95else
96 warn "No checksums.txt in release; skipping SHA-256 verification."
97fi
98
99log "Extracting..."
100run "tar -xzf '$TARBALL' -C '$STAGE'"
101BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)"
102if [[ -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)"
105fi
106[[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive."
107
108log "Installing to $INSTALL_PATH..."
109run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'"
110
111if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then
112 log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)"
113fi
114log "Done."
115
install-jetbrains-toolbox.sh Surowy
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11ASSUME_YES=0
12
13usage() {
14 cat <<EOF
15Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help]
16
17Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and
18creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is
19verified against the SHA-256 published in JetBrains' release feed.
20
21Do NOT run with sudo: Toolbox is a per-user install. If a missing system
22package (curl/jq/tar/libfuse2) needs installing, the script will call sudo
23just for that step.
24
25Options:
26 --yes, -y Auto-confirm prompts for installing missing system packages.
27 --dry-run Print actions without executing.
28 --help, -h Show this help.
29EOF
30}
31
32log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
33warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
34die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
35run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
36trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
37
38while (( $# )); 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
46done
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
53case "${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}." ;;
58esac
59log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)"
60
61confirm() {
62 (( ASSUME_YES )) && return 0
63 read -rp "$1 [y/N] " ans
64 [[ "$ans" =~ ^[Yy] ]]
65}
66
67ensure_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
81ensure_pkg curl "command -v curl"
82ensure_pkg jq "command -v jq"
83ensure_pkg tar "command -v tar"
84case "$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" ;;
88esac
89
90ARCH_RAW="$(uname -m)"
91case "$ARCH_RAW" in
92 x86_64) TOOLBOX_ARCH_KEY=linux ;;
93 aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;;
94 *) die "Unsupported architecture: $ARCH_RAW" ;;
95esac
96
97log "Querying JetBrains release feed..."
98RELEASE_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
102TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")"
103TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")"
104TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")"
105[[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed."
106log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)"
107
108INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox"
109STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)"
110trap 'rm -rf "$STAGE_DIR"' EXIT
111
112TARBALL="$STAGE_DIR/toolbox.tar.gz"
113log "Downloading..."
114run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'"
115
116if [[ -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."
124else
125 warn "No checksum URL in release feed; skipping verification."
126fi
127
128log "Extracting to $INSTALL_DIR..."
129run "mkdir -p '$INSTALL_DIR'"
130run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'"
131
132BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox"
133DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop"
134ICON_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
139if [[ -d "$HOME/bin" ]]; then
140 run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'"
141fi
142
143# .desktop launcher
144APPS_DIR="$HOME/.local/share/applications"
145run "mkdir -p '$APPS_DIR'"
146DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop"
147if [[ -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"
155else
156 warn "No .desktop file in archive; skipping launcher."
157fi
158
159log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR"
160
install-libreoffice.sh Surowy
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11SKIP_REMOVE_DISTRO=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help]
16
17Detects the latest stable LibreOffice version on download.documentfoundation.org,
18downloads the matching DEB tarball for your architecture, verifies the MD5 sum
19published alongside it, and installs the .deb packages.
20
21Options:
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.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
31die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
32run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
33trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
34
35while (( $# )); 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
43done
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
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;;
53esac
54
55ARCH_RAW="$(uname -m)"
56case "$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)." ;;
60esac
61log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW"
62
63export DEBIAN_FRONTEND=noninteractive
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y curl wget tar coreutils ca-certificates"
68
69log "Resolving latest stable LibreOffice version..."
70INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)"
71LATEST_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."
75log "Latest stable: $LATEST_VERSION"
76
77BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}"
78ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz"
79URL="${BASE}/${ARCHIVE_NAME}"
80SUM_URL="${URL}.md5"
81
82STAGE="$(mktemp -d -t lo.XXXXXX)"
83trap 'rm -rf "$STAGE"' EXIT
84ARCHIVE="$STAGE/$ARCHIVE_NAME"
85
86log "Downloading $ARCHIVE_NAME..."
87run "wget -q --show-progress -O '$ARCHIVE' '$URL'"
88
89log "Verifying MD5..."
90if 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."
94else
95 warn "No MD5 published for $SUM_URL; skipping checksum."
96fi
97
98log "Extracting..."
99run "tar -xzf '$ARCHIVE' -C '$STAGE'"
100DEBS_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
103if (( ! 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
108fi
109
110log "Installing .deb packages..."
111run "dpkg -i -R '$DEBS_DIR'"
112log "Resolving any missing dependencies..."
113run "apt-get install -f -y"
114
115log "Done. LibreOffice $LATEST_VERSION installed."
116
install-network_drive.sh Surowy
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
7set -euo pipefail
8IFS=$'\n\t'
9
10readonly SCRIPT_NAME="${0##*/}"
11DRY_RUN=0
12SERVER_ADDR=""
13SMB_USER=""
14SMB_PASS=""
15MASTER_DIR="/mnt/Synology"
16
17usage() {
18 cat <<EOF
19Usage: sudo $SCRIPT_NAME [options]
20
21Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they
22auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort)
23pins it to the top of the GNOME Dock.
24
25Options:
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.
33EOF
34}
35
36log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
37warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
38die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
39run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
40trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
41
42PASS_FROM_STDIN=0
43while (( $# )); 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
54done
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
61case "${ID:-}:${ID_LIKE:-}" in
62 *ubuntu*|*debian*) : ;;
63 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
64esac
65
66ACTUAL_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.
69USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd."
70USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")"
71USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")"
72USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")"
73
74export DEBIAN_FRONTEND=noninteractive
75log "Installing cifs-utils, smbclient..."
76run "apt-get update -qq"
77run "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
81SERVER_ADDR="${SERVER_ADDR#smb://}"
82SERVER_ADDR="${SERVER_ADDR#//}"
83SERVER_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
89if (( PASS_FROM_STDIN )); then
90 IFS= read -r SMB_PASS
91else
92 read -rsp "Synology password: " SMB_PASS </dev/tty
93 echo
94fi
95[[ -n "$SMB_PASS" ]] || die "Password is empty."
96
97# --- Credentials file ---
98CRED_FILE="$USER_HOME/.smbcredentials_synology"
99log "Writing credentials to $CRED_FILE (mode 0600)..."
100if (( DRY_RUN )); then
101 printf ' DRY-RUN: write %s\n' "$CRED_FILE"
102else
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"
110fi
111
112# --- Query shares ---
113log "Discovering shares on //$SERVER_ADDR..."
114if (( DRY_RUN )); then
115 SHARE_LIST=$'home\nphoto\nvideo'
116else
117 SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \
118 | awk -F'|' '$1=="Disk" {print $2}' || true)"
119fi
120[[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network."
121
122# --- Parent mount dir ---
123run "mkdir -p '$MASTER_DIR'"
124run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'"
125
126# --- Backup fstab once ---
127if [[ ! -f /etc/fstab.bak.synology ]]; then
128 run "cp /etc/fstab /etc/fstab.bak.synology"
129 log "Backed up fstab -> /etc/fstab.bak.synology"
130fi
131
132MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})"
133MARK_END="# <<< synology-master <<<"
134# Remove any previous managed block (so re-run replaces, not appends).
135if 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
141fi
142
143log "Writing managed block to /etc/fstab..."
144FSTAB_BLOCK="$MARK_BEGIN"$'\n'
145while 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'
151done <<<"$SHARE_LIST"
152FSTAB_BLOCK+="$MARK_END"$'\n'
153
154if (( DRY_RUN )); then
155 printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK"
156else
157 printf '%s' "$FSTAB_BLOCK" >>/etc/fstab
158fi
159
160log "Reloading systemd and mounting..."
161run "systemctl daemon-reload"
162run "mount -a -t cifs"
163
164# --- Desktop entry ---
165APPS_DIR="$USER_HOME/.local/share/applications"
166DESKTOP_FILENAME="synology-master.desktop"
167DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME"
168run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'"
169
170if (( DRY_RUN )); then
171 printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE"
172else
173 cat >"$DESKTOP_FILE" <<EOF
174[Desktop Entry]
175Name=Synology NAS
176Comment=Open Synology Master Directory
177Exec=xdg-open ${MASTER_DIR}
178Icon=folder-remote
179Terminal=false
180Type=Application
181Categories=Network;FileTools;
182EOF
183 chown "$USER_ID:$USER_GID" "$DESKTOP_FILE"
184 chmod 0755 "$DESKTOP_FILE"
185fi
186
187# --- Best-effort GNOME dock pin ---
188if (( ! 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
203else
204 log "GNOME not detected (or no active dbus session); skipping dock pin."
205fi
206
207log "Done. Shares mounted under $MASTER_DIR."
208
install-thunderbird.sh Surowy
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
7set -euo pipefail
8IFS=$'\n\t'
9
10readonly SCRIPT_NAME="${0##*/}"
11DRY_RUN=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Installs 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
22Options:
23 --dry-run Print actions without executing.
24 --help, -h Show this help.
25EOF
26}
27
28log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
29warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
32trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
33
34while (( $# )); 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
41done
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
48log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})"
49
50export DEBIAN_FRONTEND=noninteractive
51
52case "${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'
79Package: thunderbird*
80Pin: release o=LP-PPA-mozillateam
81Pin-Priority: 1001
82EOF
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 ;;
100esac
101
102log "Done."
103
install-vscode.sh Surowy
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
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10INSIDERS=0
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help]
15
16Configures the official Microsoft 'vscode' APT repository (signed-by keyring)
17and installs Visual Studio Code. Idempotent: safe to re-run.
18
19Options:
20 --insiders Install the 'code-insiders' build instead of stable 'code'.
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
28die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
29run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
30trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
31
32while (( $# )); 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
40done
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
47case "${ID:-}:${ID_LIKE:-}" in
48 *ubuntu*|*debian*) : ;;
49 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
50esac
51
52ARCH="$(dpkg --print-architecture)"
53case "$ARCH" in
54 amd64|arm64|armhf) : ;;
55 *) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;;
56esac
57log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
58
59export DEBIAN_FRONTEND=noninteractive
60
61KEYRING=/etc/apt/keyrings/packages.microsoft.gpg
62SOURCES=/etc/apt/sources.list.d/vscode.list
63PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code)
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y wget gpg apt-transport-https ca-certificates"
68
69log "Configuring Microsoft vscode APT repository..."
70run "install -d -m 0755 /etc/apt/keyrings"
71if [[ ! -s "$KEYRING" ]]; then
72 run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'"
73 run "chmod 0644 '$KEYRING'"
74fi
75
76DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main"
77if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
78 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
79fi
80
81log "Installing $PKG..."
82run "apt-get update -qq"
83run "apt-get install -y '$PKG'"
84
85if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then
86 log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")"
87fi
88log "Done."
89
install_font.sh Surowy
1#!/bin/bash
2
3# Exit immediately if a command exits with a non-zero status
4set -e
5
6FONT_NAME="UbuntuSans"
7FONT_ZIP="${FONT_NAME}.zip"
8# Simplified directory: removed the "NerdFonts" subfolder
9FONT_DIR="$HOME/.local/share/fonts/${FONT_NAME}"
10TMP_DIR=$(mktemp -d)
11
12echo "Searching for the latest release of $FONT_NAME Nerd Font..."
13
14# Use GitHub API to find the latest release download URL for UbuntuSans.zip
15DOWNLOAD_URL=$(curl -s https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \
16 | grep "browser_download_url.*${FONT_ZIP}" \
17 | cut -d '"' -f 4)
18
19if [ -z "$DOWNLOAD_URL" ]; then
20 echo "Error: Could not find the download URL for $FONT_NAME. Check your internet connection or GitHub API limits."
21 rm -rf "$TMP_DIR"
22 exit 1
23fi
24
25echo "Latest version found!"
26echo "Downloading from: $DOWNLOAD_URL"
27
28# Download the zip file to the temporary directory
29curl -L -q "$DOWNLOAD_URL" -o "$TMP_DIR/$FONT_ZIP"
30
31echo "Extracting fonts..."
32# Ensure unzip is installed (will fail gracefully if not)
33if ! command -v unzip &> /dev/null; then
34 echo "Error: 'unzip' is not installed. Please install it using 'sudo apt install unzip' and try again."
35 rm -rf "$TMP_DIR"
36 exit 1
37fi
38
39unzip -q -o "$TMP_DIR/$FONT_ZIP" -d "$TMP_DIR/extracted"
40
41echo "Installing fonts to $FONT_DIR..."
42# Create the simplified font directory if it doesn't exist
43mkdir -p "$FONT_DIR"
44
45# Move only the TrueType (.ttf) or OpenType (.otf) files to the fonts directory
46find "$TMP_DIR/extracted" -name '*.[ot]tf' -type f -exec cp {} "$FONT_DIR/" \;
47
48echo "Updating the system font cache..."
49fc-cache -f "$FONT_DIR"
50
51echo "Cleaning up temporary files..."
52rm -rf "$TMP_DIR"
53
54echo "Done! $FONT_NAME Nerd Font has been successfully installed."
menu.sh Surowy
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.
21set -uo pipefail
22IFS=$'\n\t'
23
24readonly SCRIPT_NAME="${0##*/}"
25readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD'
26
27log() { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; }
28warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; }
29err() { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; }
30hr() { 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.
34if (( 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
37fi
38
39# Sanity-check tools we depend on.
40for tool in curl bash mktemp; do
41 command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; }
42done
43
44# Distro check (warn-only; sub-scripts enforce strictly).
45if [[ -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
52fi
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.
56prime_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).
70run_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.)
108OPTIONS=(
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
123print_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.
150resolve_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
163ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12)
164
165prime_sudo || exit 1
166
167while 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
225done
226