#!/usr/bin/env bash # install-ibus-rime.sh — Install IBus + Rime and add it # to GNOME's Input Sources. Tuned for Ubuntu 26.04 (GNOME 50 / Wayland) but # works on any modern Ubuntu/Debian GNOME desktop. # # Hardened: distro-detect, runs as the desktop user (not root) for gsettings, # uses sudo only for apt steps, idempotent re-runs, ERR trap, --dry-run. set -euo pipefail IFS=$'\n\t' readonly SCRIPT_NAME="${0##*/}" DRY_RUN=0 SKIP_GSETTINGS=0 usage() { cat <&2; } die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR while (( $# )); do case "$1" in --dry-run) DRY_RUN=1 ;; --skip-gsettings) SKIP_GSETTINGS=1 ;; -h|--help) usage; exit 0 ;; *) die "Unknown argument: $1 (try --help)" ;; esac shift done # Resolve the desktop user. If invoked via sudo, gsettings must target SUDO_USER. if (( EUID == 0 )); then DESKTOP_USER="${SUDO_USER:-}" [[ -n "$DESKTOP_USER" && "$DESKTOP_USER" != "root" ]] \ || die "Run as a normal user (the script will call sudo itself for apt). gsettings can't run as root." else DESKTOP_USER="$USER" fi # Look up the user's UID and Home Directory for configs and DBus. USER_ENTRY="$(getent passwd "$DESKTOP_USER")" || die "User '$DESKTOP_USER' not found in passwd." USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")" USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")" [[ -r /etc/os-release ]] || die "/etc/os-release not found." # shellcheck disable=SC1091 . /etc/os-release case "${ID:-}:${ID_LIKE:-}" in *ubuntu*|*debian*) : ;; *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;; esac log "Detected: ${PRETTY_NAME:-unknown}, desktop user: $DESKTOP_USER" # Helper: invoke a command as $DESKTOP_USER with a working DBus address. run_as_user() { local cmd=("$@") if (( DRY_RUN )); then printf ' DRY-RUN (as %s): %s\n' "$DESKTOP_USER" "${cmd[*]}" return 0 fi if (( EUID == 0 )); then sudo -u "$DESKTOP_USER" \ DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ XDG_RUNTIME_DIR="/run/user/$USER_ID" \ "${cmd[@]}" else "${cmd[@]}" fi } # Helper: invoke a command with sudo when the caller is non-root. sudo_run() { if (( DRY_RUN )); then printf ' DRY-RUN (sudo): %s\n' "$*" return 0 fi if (( EUID == 0 )); then "$@"; else sudo "$@"; fi } export DEBIAN_FRONTEND=noninteractive log "Removing old ibus-libpinyin if present..." sudo_run apt-get remove --purge -y ibus-libpinyin || true log "Installing IBus Rime + Chinese language packs..." sudo_run apt-get update -qq sudo_run apt-get install -y \ ibus \ ibus-rime \ language-pack-zh-hans \ language-pack-gnome-zh-hans log "Configuring Rime to permanently default to Simplified Pinyin..." RIME_DIR="$USER_HOME/.config/ibus/rime" if (( DRY_RUN )); then printf ' DRY-RUN (as %s): Create %s/default.custom.yaml\n' "$DESKTOP_USER" "$RIME_DIR" else if (( EUID == 0 )); then sudo -u "$DESKTOP_USER" mkdir -p "$RIME_DIR" sudo -u "$DESKTOP_USER" tee "$RIME_DIR/default.custom.yaml" > /dev/null <<'EOF' patch: schema_list: - schema: luna_pinyin_simp EOF else mkdir -p "$RIME_DIR" tee "$RIME_DIR/default.custom.yaml" > /dev/null <<'EOF' patch: schema_list: - schema: luna_pinyin_simp EOF fi fi # Make sure ibus-daemon picks up the new engines and config for the user. if command -v ibus >/dev/null 2>&1; then log "Restarting ibus-daemon for $DESKTOP_USER..." # `ibus exit` will fail if no daemon is running — treat as non-fatal. run_as_user ibus exit >/dev/null 2>&1 || true # Start fresh in the background; -drx replaces a running daemon. if (( ! DRY_RUN )); then sudo -u "$DESKTOP_USER" \ DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ XDG_RUNTIME_DIR="/run/user/$USER_ID" \ sh -c 'nohup ibus-daemon -drx >/dev/null 2>&1 &' || \ warn "ibus-daemon restart returned non-zero (non-fatal)." sleep 1 fi fi if (( SKIP_GSETTINGS )); then log "Skipping GNOME input-sources update (--skip-gsettings)." log "Done. Add 'Chinese (Rime)' manually under Settings → Keyboard → Input Sources." exit 0 fi # Only manage gsettings if GNOME schemas are present. if ! run_as_user gsettings list-schemas 2>/dev/null | grep -q '^org.gnome.desktop.input-sources$'; then warn "GNOME schema org.gnome.desktop.input-sources not found; skipping gsettings update." log "Done. Add 'Chinese (Rime)' manually in your DE's input settings." exit 0 fi log "Adding 'Rime' to GNOME Input Sources (idempotent)..." CURRENT_SOURCES="$(run_as_user gsettings get org.gnome.desktop.input-sources sources 2>/dev/null || echo '[]')" # Strip the optional "@as " type annotation gvariant sometimes prepends. CLEAN_SOURCES="${CURRENT_SOURCES#@as }" if [[ "$CLEAN_SOURCES" == *"'ibus', 'rime'"* ]]; then log "Rime already present in input sources — nothing to do." else if [[ "$CLEAN_SOURCES" == *"'ibus', 'libpinyin'"* ]]; then # Dynamically replace libpinyin with rime NEW_SOURCES="${CLEAN_SOURCES//\'libpinyin\'/\'rime\'}" elif [[ -z "$CLEAN_SOURCES" || "$CLEAN_SOURCES" == "[]" || "$CLEAN_SOURCES" == "@as []" ]]; then NEW_SOURCES="[('xkb', 'us'), ('ibus', 'rime')]" else # Insert the rime tuple before the closing bracket of the existing list. NEW_SOURCES="${CLEAN_SOURCES%]*}, ('ibus', 'rime')]" fi run_as_user gsettings set org.gnome.desktop.input-sources sources "$NEW_SOURCES" log "Input sources updated to include: ('ibus', 'rime')" fi log "---" log "Done! Rime is installed and configured for Simplified Pinyin." log "Switch input methods with: Super + Space" log "If 'Chinese (Rime)' doesn't appear in the top bar right away, log out and back in."