From f4787d9cab3bec4d2ed5dd1daf403ce08f8fb43f Mon Sep 17 00:00:00 2001 From: SkenSMasteR <75779174+SkenSMasteR@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:12:07 +0200 Subject: [PATCH] feat(install): add linux install script --- scripts/install/linux/install.sh | 707 +++++++++++++++++++++++++++++++ 1 file changed, 707 insertions(+) diff --git a/scripts/install/linux/install.sh b/scripts/install/linux/install.sh index e69de29..80e0121 100644 --- a/scripts/install/linux/install.sh +++ b/scripts/install/linux/install.sh @@ -0,0 +1,707 @@ +#!/usr/bin/env bash + +if [ -z "$BASH_VERSION" ]; then + exec bash "$0" "$@" +fi + +THEME_NAME="SpoTUI" +REPO_URL="https://github.com/SkenSMasteR/SpoTUI" +THEMES_DIR="$HOME/.config/spicetify/Themes" +THEME_PATH="$THEMES_DIR/$THEME_NAME" + +ESC=$'\033' +RESET="$ESC[0m" + +BLOCK_FULL=$'\xe2\x96\x88' +BLOCK_LOWER=$'\xe2\x96\x84' +BLOCK_UPPER=$'\xe2\x96\x80' +BLOCK_LEFT=$'\xe2\x96\x8c' + +get_rgb() { + local r=$1 g=$2 b=$3 + printf '%s[38;2;%d;%d;%dm' "$ESC" "$r" "$g" "$b" +} + +ORANGE_LIGHT=$(get_rgb 255 140 66) +ORANGE_DARK=$(get_rgb 224 123 57) +ORANGE_MID=$(get_rgb 240 131 61) +GREEN_ANSI=$(get_rgb 140 255 140) +SELECT_BG="${ESC}[48;2;255;140;66m${ESC}[38;2;0;0;0m" + +WHITE="$ESC[97m" +GRAY="$ESC[90m" +RED="$ESC[91m" +GREEN="$ESC[92m" +CYAN="$ESC[96m" + +PKG_REFRESH_ATTEMPTED=0 + +get_gradient_color() { + local index=$1 total=$2 + local r1=255 g1=140 b1=66 + local r2=224 g2=123 b2=57 + local scale=1000 + local t + if [ "$total" -le 1 ]; then + t=0 + else + t=$(( index * scale / (total - 1) )) + fi + local r g b + r=$(( r1 + (r2 - r1) * t / scale )) + g=$(( g1 + (g2 - g1) * t / scale )) + b=$(( b1 + (b2 - b1) * t / scale )) + get_rgb "$r" "$g" "$b" +} + +get_ascii_art_line() { + local out="$1" + out="${out//A/$BLOCK_FULL}" + out="${out//B/$BLOCK_LOWER}" + out="${out//C/$BLOCK_UPPER}" + out="${out//D/$BLOCK_LEFT}" + printf '%s' "$out" +} + +get_header_lines() { + local templates=( + " BAAAAAAAA BAAAAAAAB BAAAAAAAB AAA AAA AB BA " + " AAA AAA AAA AAA AAA AAA CAAAAAAAAAB AAA AAA AAA " + " AAA AC AAA AAA AAA AAA CAAACCAA AAA AAA AAAD " + " AAA AAA AAA AAA AAA AAA C AAA AAA AAAD " + "CAAAAAAAAAAA CAAAAAAAAAC AAA AAA AAA AAA AAA AAAD " + " AAA AAA AAA AAA AAA AAA AAA AAA " + " BA AAA AAA AAA AAA AAA AAA AAA AAA " + " BAAAAAAAAC BAAAAC CAAAAAAC BAAAAC AAAAAAAAC AC " + ) + HEADER_LINES=() + HEADER_LINES+=("") + local total=${#templates[@]} + local i line color + for ((i = 0; i < total; i++)); do + color=$(get_gradient_color "$i" "$total") + line=$(get_ascii_art_line "${templates[$i]}") + HEADER_LINES+=("${color}${line}${RESET}") + done + HEADER_LINES+=("") + HEADER_LINES+=("${ORANGE_MID} Spicetify Theme Manager${RESET}") + HEADER_LINES+=("${ORANGE_DARK} =============================================================${RESET}") + HEADER_LINES+=("") +} + +show_header() { + clear + get_header_lines + local line + for line in "${HEADER_LINES[@]}"; do + printf '%s\n' "$line" + done +} + +get_visible_length() { + local text="$1" + local stripped + stripped=$(printf '%s' "$text" | sed -E "s/${ESC}\[[0-9;]*m//g") + printf '%d' "${#stripped}" +} + +write_frame() { + local -n frame_lines=$1 + printf '%s[H' "$ESC" + local width + width=$(tput cols 2>/dev/null) + [ -z "$width" ] && width=80 + local line visible pad + for line in "${frame_lines[@]}"; do + visible=$(get_visible_length "$line") + pad=$((width - visible - 1)) + [ "$pad" -lt 0 ] && pad=0 + printf '%s%*s\n' "$line" "$pad" "" + done +} + +get_console_height() { + local height + height=$(tput lines 2>/dev/null) + [ -z "$height" ] && height=30 + printf '%d' "$height" +} + +refresh_path() { + local candidate + for candidate in "$HOME/.local/bin" "$HOME/.spicetify" "/usr/local/bin"; do + if [[ ":$PATH:" != *":$candidate:"* ]] && [ -d "$candidate" ]; then + PATH="$candidate:$PATH" + fi + done + export PATH +} + +detect_pkg_manager() { + if command -v apt-get >/dev/null 2>&1; then echo "apt" + elif command -v dnf >/dev/null 2>&1; then echo "dnf" + elif command -v pacman >/dev/null 2>&1; then echo "pacman" + elif command -v zypper >/dev/null 2>&1; then echo "zypper" + elif command -v apk >/dev/null 2>&1; then echo "apk" + else echo "" + fi +} + +refresh_pkg_manager() { + local mgr + mgr=$(detect_pkg_manager) + printf '%s Refreshing package lists...%s\n' "$ORANGE_MID" "$RESET" + case "$mgr" in + apt) sudo apt-get update -y ;; + dnf) sudo dnf check-update -y || true ;; + pacman) sudo pacman -Sy --noconfirm ;; + zypper) sudo zypper refresh ;; + apk) sudo apk update ;; + *) + printf ' No supported package manager found (looked for apt, dnf, pacman, zypper, apk).%s\n' "" \ + 2>/dev/null + printf '%s No supported package manager found. Install git manually.%s\n' "$RED" "$RESET" + ;; + esac +} + +install_dependency() { + local name="$1" + if [ "$name" = "git" ]; then + local mgr + mgr=$(detect_pkg_manager) + if [ -n "$mgr" ]; then + printf '%s Installing git via %s...%s\n' "$GRAY" "$mgr" "$RESET" + case "$mgr" in + apt) sudo apt-get install -y git ;; + dnf) sudo dnf install -y git ;; + pacman) sudo pacman -S --noconfirm git ;; + zypper) sudo zypper install -y git ;; + apk) sudo apk add git ;; + esac + local exit_code=$? + printf ' package manager exited with code %d\n' "$exit_code" + + if [ "$exit_code" -ne 0 ] && [ "$PKG_REFRESH_ATTEMPTED" -eq 0 ]; then + printf '%s Install failed. Refreshing package lists and retrying...%s\n' "$RED" "$RESET" + PKG_REFRESH_ATTEMPTED=1 + refresh_pkg_manager + + printf '%s Retrying git install...%s\n' "$GRAY" "$RESET" + case "$mgr" in + apt) sudo apt-get install -y git ;; + dnf) sudo dnf install -y git ;; + pacman) sudo pacman -S --noconfirm git ;; + zypper) sudo zypper install -y git ;; + apk) sudo apk add git ;; + esac + fi + + refresh_path + else + printf '%s No supported package manager was found. Install git manually for your distro.%s\n' "$RED" "$RESET" + fi + elif [ "$name" = "spicetify" ]; then + printf '%s Installing Spicetify...%s\n' "$GRAY" "$RESET" + curl -fsSL https://raw.githubusercontent.com/spicetify/cli/main/install.sh | sh + printf '%s Spicetify installer finished.%s\n' "$GRAY" "$RESET" + refresh_path + fi +} + +test_dependencies() { + local missing=() + command -v git >/dev/null 2>&1 || missing+=("git") + command -v spicetify >/dev/null 2>&1 || missing+=("spicetify") + + if [ ${#missing[@]} -eq 0 ]; then + return 0 + fi + + printf '%s Missing dependencies: %s%s\n' "$RED" "$(IFS=', '; echo "${missing[*]}")" "$RESET" + echo "" + read -rp " Press I to install them now, or any other key to cancel: " confirm + if [ "$confirm" != "I" ] && [ "$confirm" != "i" ]; then + return 1 + fi + + local dep + for dep in "${missing[@]}"; do + echo "" + printf '%s Installing %s...%s\n' "$ORANGE_MID" "$dep" "$RESET" + install_dependency "$dep" + done + + echo "" + printf '%s Refreshing environment PATH...%s\n' "$ORANGE_MID" "$RESET" + refresh_path + + local still_missing=() + command -v git >/dev/null 2>&1 || still_missing+=("git") + command -v spicetify >/dev/null 2>&1 || still_missing+=("spicetify") + + if [ ${#still_missing[@]} -gt 0 ]; then + echo "" + printf '%s Still missing: %s. You may need to restart your terminal.%s\n' "$RED" "$(IFS=', '; echo "${still_missing[*]}")" "$RESET" + return 1 + fi + + echo "" + printf '%s All dependencies installed successfully.%s\n' "$GREEN" "$RESET" + return 0 +} + +get_default_branch() { + local ref + ref=$(git -C "$THEME_PATH" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null) + if [ -n "$ref" ]; then + printf '%s' "${ref##*/}" + else + printf 'main' + fi +} + +test_is_detached() { + if git -C "$THEME_PATH" symbolic-ref -q HEAD >/dev/null 2>&1; then + return 1 + else + return 0 + fi +} + +get_theme_status_text="" +get_theme_status_color="" + +get_theme_status_detailed() { + if [ ! -d "$THEME_PATH" ]; then + get_theme_status_text="Not Installed" + get_theme_status_color="$RED" + return + fi + + if ! command -v git >/dev/null 2>&1; then + get_theme_status_text="Installed" + get_theme_status_color="$GREEN" + return + fi + + git -C "$THEME_PATH" fetch origin --quiet 2>/dev/null + local local_hash + local_hash=$(git -C "$THEME_PATH" rev-parse HEAD 2>/dev/null) + + if test_is_detached; then + local short_hash="unknown" + [ -n "$local_hash" ] && short_hash="${local_hash:0:7}" + get_theme_status_text="Installed (custom commit $short_hash)" + get_theme_status_color="$CYAN" + return + fi + + local branch remote_hash + branch=$(get_default_branch) + remote_hash=$(git -C "$THEME_PATH" rev-parse "origin/$branch" 2>/dev/null) + + if [ -z "$local_hash" ] || [ -z "$remote_hash" ]; then + get_theme_status_text="Installed" + get_theme_status_color="$GREEN" + return + fi + + if [ "$local_hash" = "$remote_hash" ]; then + get_theme_status_text="Installed (up to date)" + get_theme_status_color="$GREEN" + else + get_theme_status_text="Installed (outdated)" + get_theme_status_color="$RED" + fi +} + +install_theme() { + show_header + printf '%s Installing %s...%s\n' "$ORANGE_LIGHT" "$THEME_NAME" "$RESET" + echo "" + + if ! test_dependencies; then + pause_return + return + fi + + mkdir -p "$THEMES_DIR" + + if [ -d "$THEME_PATH" ]; then + printf '%s Theme already exists locally. Pulling latest changes...%s\n' "$ORANGE_MID" "$RESET" + git -C "$THEME_PATH" pull + else + (cd "$THEMES_DIR" && git clone "$REPO_URL" "$THEME_NAME") + fi + + if [ -d "$THEME_PATH" ]; then + echo "" + printf '%s Setting current theme to %s...%s\n' "$ORANGE_MID" "$THEME_NAME" "$RESET" + spicetify config current_theme "$THEME_NAME" + + printf '%s Applying Spicetify...%s\n' "$ORANGE_MID" "$RESET" + spicetify apply + + echo "" + printf '%s %s installed and applied successfully.%s\n' "$GREEN" "$THEME_NAME" "$RESET" + else + echo "" + printf '%s Installation failed. Check the errors above.%s\n' "$RED" "$RESET" + fi + + pause_return +} + +update_theme() { + show_header + printf '%s Updating %s...%s\n' "$ORANGE_LIGHT" "$THEME_NAME" "$RESET" + echo "" + + if [ ! -d "$THEME_PATH" ]; then + printf '%s %s is not installed. Use Install instead.%s\n' "$RED" "$THEME_NAME" "$RESET" + pause_return + return + fi + + if ! test_dependencies; then + pause_return + return + fi + + if test_is_detached; then + local branch + branch=$(get_default_branch) + printf '%s Currently on a custom commit. Returning to %s...%s\n' "$ORANGE_MID" "$branch" "$RESET" + git -C "$THEME_PATH" checkout "$branch" --quiet + fi + + git -C "$THEME_PATH" pull + + echo "" + printf '%s Re-applying Spicetify...%s\n' "$ORANGE_MID" "$RESET" + spicetify apply + + echo "" + printf '%s %s updated successfully.%s\n' "$GREEN" "$THEME_NAME" "$RESET" + pause_return +} + +uninstall_theme() { + show_header + printf '%s Uninstalling %s...%s\n' "$ORANGE_LIGHT" "$THEME_NAME" "$RESET" + echo "" + + if [ ! -d "$THEME_PATH" ]; then + printf '%s %s is not installed.%s\n' "$RED" "$THEME_NAME" "$RESET" + pause_return + return + fi + + printf '%s This will remove the theme folder and switch to Marketplace.%s\n' "$GRAY" "$RESET" + read -rp " Type Y to confirm: " confirm + if [ "$confirm" != "Y" ] && [ "$confirm" != "y" ]; then + printf '%s Cancelled.%s\n' "$GRAY" "$RESET" + pause_return + return + fi + + if command -v spicetify >/dev/null 2>&1; then + printf '%s Switching Spicetify theme...%s\n' "$ORANGE_MID" "$RESET" + spicetify config current_theme "SpoTUI-" + spicetify config current_theme marketplace + spicetify apply + fi + + rm -rf "$THEME_PATH" + + echo "" + printf '%s %s has been uninstalled.%s\n' "$GREEN" "$THEME_NAME" "$RESET" + pause_return +} + +read_key() { + local key rest + IFS= read -rsn1 key + if [ "$key" = "$ESC" ]; then + read -rsn2 -t 0.01 rest + case "$rest" in + "[A") echo "UP" ;; + "[B") echo "DOWN" ;; + *) echo "ESC" ;; + esac + elif [ -z "$key" ]; then + echo "ENTER" + else + echo "OTHER" + fi +} + +read_arrow_selection() { + local -n out_result=$1 + local -n _ras_items=$2 + local current_index=$3 + local -n _ras_title_lines=$4 + + local selected_index=0 + [ "$current_index" -ge 0 ] && selected_index=$current_index + + get_header_lines + local overhead=$(( ${#HEADER_LINES[@]} + ${#_ras_title_lines[@]} + 4 )) + + local page_size + page_size=$(( $(get_console_height) - overhead )) + [ "$page_size" -gt "${#_ras_items[@]}" ] && page_size=${#_ras_items[@]} + [ "$page_size" -lt 1 ] && page_size=1 + + clear + printf '%s[?25l' "$ESC" + + while true; do + local total_pages=$(( (${#_ras_items[@]} + page_size - 1) / page_size )) + [ "$total_pages" -lt 1 ] && total_pages=1 + local current_page=$(( selected_index / page_size )) + local page_start=$(( current_page * page_size )) + local page_end=$(( page_start + page_size - 1 )) + [ "$page_end" -gt $((${#_ras_items[@]} - 1)) ] && page_end=$((${#_ras_items[@]} - 1)) + + local frame=() + frame+=("${HEADER_LINES[@]}") + frame+=("${_ras_title_lines[@]}") + + local i prefix text + for ((i = page_start; i <= page_end; i++)); do + if [ "$i" -eq "$current_index" ]; then + prefix="> " + else + prefix=" " + fi + text="${prefix}${_ras_items[$i]}" + if [ "$i" -eq "$selected_index" ]; then + frame+=("${SELECT_BG}${text}${RESET}") + elif [ "$i" -eq "$current_index" ]; then + frame+=("${GREEN_ANSI}${text}${RESET}") + else + frame+=("$text") + fi + done + + local lines_used=$(( page_end - page_start + 1 )) + local p + for ((p = lines_used; p < page_size; p++)); do + frame+=("") + done + + frame+=("") + frame+=("${ORANGE_DARK} =============================================================${RESET}") + frame+=(" Up/Down to move, Enter to select, Esc to go back Page $((current_page + 1)) of $total_pages") + + write_frame frame + + local key + key=$(read_key) + + case "$key" in + UP) + if [ "$selected_index" -gt 0 ]; then + selected_index=$((selected_index - 1)) + else + selected_index=$((${#_ras_items[@]} - 1)) + fi + ;; + DOWN) + if [ "$selected_index" -lt $((${#_ras_items[@]} - 1)) ]; then + selected_index=$((selected_index + 1)) + else + selected_index=0 + fi + ;; + ENTER) + printf '%s[?25h' "$ESC" + out_result=$selected_index + return + ;; + ESC) + printf '%s[?25h' "$ESC" + out_result=-1 + return + ;; + esac + done +} + +get_commit_list() { + git -C "$THEME_PATH" fetch origin --quiet 2>/dev/null + COMMIT_FULL=() + COMMIT_SHORT=() + COMMIT_DATE=() + COMMIT_SUBJECT=() + local line full short date subject + while IFS='|' read -r full short date subject; do + [ -z "$full" ] && continue + COMMIT_FULL+=("$full") + COMMIT_SHORT+=("$short") + COMMIT_DATE+=("$date") + COMMIT_SUBJECT+=("$subject") + done < <(git -C "$THEME_PATH" log --all --pretty=format:"%H|%h|%ad|%s" --date=short) +} + +show_commit_history() { + if [ ! -d "$THEME_PATH" ]; then + show_header + printf '%s %s is not installed.%s\n' "$RED" "$THEME_NAME" "$RESET" + pause_return + return + fi + + if ! test_dependencies; then + pause_return + return + fi + + local viewing=1 + while [ "$viewing" -eq 1 ]; do + get_commit_list + if [ ${#COMMIT_FULL[@]} -eq 0 ]; then + show_header + printf '%s No commits found.%s\n' "$RED" "$RESET" + pause_return + return + fi + + local current_hash + current_hash=$(git -C "$THEME_PATH" rev-parse HEAD 2>/dev/null) + + local current_index=-1 + local items=() + local i + for ((i = 0; i < ${#COMMIT_FULL[@]}; i++)); do + items+=("${COMMIT_SHORT[$i]} ${COMMIT_DATE[$i]} ${COMMIT_SUBJECT[$i]}") + if [ "${COMMIT_FULL[$i]}" = "$current_hash" ]; then + current_index=$i + fi + done + local return_latest_index=${#items[@]} + items+=("Return to latest version") + local back_index=${#items[@]} + items+=("Back") + + local title_lines=("${ORANGE_LIGHT} Commit History${RESET}" "") + + local selection + read_arrow_selection selection items "$current_index" title_lines + + if [ "$selection" -eq -1 ] || [ "$selection" -eq "$back_index" ]; then + viewing=0 + elif [ "$selection" -eq "$return_latest_index" ]; then + update_theme + elif [ "$selection" -ge 0 ] && [ "$selection" -lt ${#COMMIT_FULL[@]} ]; then + checkout_commit "${COMMIT_FULL[$selection]}" "${COMMIT_SHORT[$selection]}" "${COMMIT_DATE[$selection]}" "${COMMIT_SUBJECT[$selection]}" + fi + done +} + +checkout_commit() { + local full="$1" short="$2" date="$3" subject="$4" + show_header + printf '%s Checking out commit %s...%s\n' "$ORANGE_LIGHT" "$short" "$RESET" + printf '%s %s %s%s\n' "$GRAY" "$date" "$subject" "$RESET" + echo "" + printf '%s This will switch the theme to this specific version.%s\n' "$GRAY" "$RESET" + read -rp " Type Y to confirm: " confirm + if [ "$confirm" != "Y" ] && [ "$confirm" != "y" ]; then + printf '%s Cancelled.%s\n' "$GRAY" "$RESET" + pause_return + return + fi + + git -C "$THEME_PATH" checkout "$full" --quiet + + if command -v spicetify >/dev/null 2>&1; then + echo "" + printf '%s Applying Spicetify...%s\n' "$ORANGE_MID" "$RESET" + spicetify apply + fi + + echo "" + printf '%s %s is now on commit %s.%s\n' "$GREEN" "$THEME_NAME" "$short" "$RESET" + pause_return +} + +check_for_updates() { + show_header + printf '%s Checking for updates...%s\n' "$ORANGE_LIGHT" "$RESET" + echo "" + + if [ ! -d "$THEME_PATH" ]; then + printf '%s %s is not installed.%s\n' "$RED" "$THEME_NAME" "$RESET" + pause_return + return + fi + + if ! test_dependencies; then + pause_return + return + fi + + get_theme_status_detailed + printf '%s Status: %s%s%s\n' "$WHITE" "$get_theme_status_color" "$get_theme_status_text" "$RESET" + + if [ "$get_theme_status_text" = "Installed (outdated)" ]; then + echo "" + printf '%s A newer version is available.%s\n' "$GRAY" "$RESET" + read -rp " Type Y to update now: " confirm + if [ "$confirm" = "Y" ] || [ "$confirm" = "y" ]; then + update_theme + return + fi + fi + + pause_return +} + +pause_return() { + echo "" + printf '%s Press any key to return to the menu...%s\n' "$GRAY" "$RESET" + IFS= read -rsn1 +} + +show_menu() { + show_header + get_theme_status_detailed + + printf '%s Status: %s%s%s\n' "$WHITE" "$get_theme_status_color" "$get_theme_status_text" "$RESET" + echo "" + printf '%s [1] Install %s%s\n' "$WHITE" "$THEME_NAME" "$RESET" + printf '%s [2] Update %s%s\n' "$WHITE" "$THEME_NAME" "$RESET" + printf '%s [3] Uninstall %s%s\n' "$WHITE" "$THEME_NAME" "$RESET" + printf '%s [4] Commit History / Downgrade%s\n' "$WHITE" "$RESET" + printf '%s [5] Check for Updates%s\n' "$WHITE" "$RESET" + printf '%s [6] Exit%s\n' "$WHITE" "$RESET" + echo "" + printf '%s =============================================================%s\n' "$ORANGE_DARK" "$RESET" + echo "" + read -rp " Select an option: " choice + MENU_CHOICE="$choice" +} + +running=1 +while [ "$running" -eq 1 ]; do + show_menu + choice="$MENU_CHOICE" + case "$choice" in + 1) install_theme ;; + 2) update_theme ;; + 3) uninstall_theme ;; + 4) show_commit_history ;; + 5) check_for_updates ;; + 6) running=0 ;; + *) + show_header + printf '%s Invalid option.%s\n' "$RED" "$RESET" + pause_return + ;; + esac +done + +clear \ No newline at end of file