#!/usr/bin/env bash set -euo pipefail # Simple theme switcher for WezTerm integration and other apps. # Usage: theme-switcher [dark|light|toggle] # --- Configuration & Initialization --- CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}" CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}" CACHE_FILE="$CACHE_DIR/current_theme" mkdir -p "$CACHE_DIR" 2>/dev/null || true # --- Helper Functions --- # Resolve symlinks to actual files resolve_symlink() { local file="$1" [[ ! -L "$file" ]] && echo "$file" && return # Use 'realpath' if available for reliability if command -v realpath >/dev/null 2>&1; then realpath "$file" # Fallback for macOS and others elif [[ "$OSTYPE" == "darwin"* ]]; then local target target=$(readlink "$file") [[ "$target" != /* ]] && target="$(dirname "$file")/$target" # Recurse if the link target is also a link if [[ -L "$target" ]]; then resolve_symlink "$target" else echo "$target" fi # General fallback for Linux that might not have 'readlink -f' else readlink -f "$file" 2>/dev/null || echo "$file" fi } # Detect system theme (macOS/Gnome) detect_theme() { if [[ "$OSTYPE" == "darwin"* ]]; then # Check for AppleInterfaceStyle: Dark or Light defaults read -g AppleInterfaceStyle 2>/dev/null | grep -qi "Dark" && echo "dark" || echo "light" elif command -v gsettings >/dev/null 2>&1; then # Check Gnome color-scheme gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | grep -qi 'dark' && echo "dark" || echo "light" else # Default to dark on unknown systems/environments echo "dark" fi } # Get current mode (cached or detected) get_mode() { if [[ -f "$CACHE_FILE" ]]; then local cached # Read file, trim whitespace cached=$(<"$CACHE_FILE") cached="${cached//[[:space:]]/}" # Validate content before returning [[ "$cached" =~ ^(dark|light)$ ]] && echo "$cached" && return fi detect_theme } # Update a config file using a regex pattern update_config() { local config_file="$1" local pattern_regex="$2" # The pattern to search for (e.g., 'key = ".*"') local dark_val="$3" # The replacement string for dark mode local light_val="$4" # The replacement string for light mode [[ ! -f "$config_file" ]] && return 0 local mode="${TARGET_MODE:-$(get_mode)}" local replacement="$dark_val" [[ "$mode" != "dark" ]] && replacement="$light_val" local target_file target_file=$(resolve_symlink "$config_file") # Check if the target file exists and is writable [[ ! -f "$target_file" || ! -w "$target_file" ]] && return 0 # Check if the pattern exists in the file before trying to replace # NOTE: Using 'grep -q' with the search pattern regex to verify existence if grep -qE "$pattern_regex" "$target_file" 2>/dev/null; then # Use a safe delimiter (e.g., '#') for sed to avoid conflicts with paths/values local sed_command="s|${pattern_regex}|${replacement}|g" if [[ "$OSTYPE" == "darwin"* ]]; then # macOS sed requires an empty string for the backup extension sed -i '' "$sed_command" "$target_file" 2>/dev/null else # Linux sed (GNU sed) sed -i "$sed_command" "$target_file" 2>/dev/null fi # Only echo success if sed was run and likely succeeded echo "✓ ${config_file##*/}" fi } # Update zoxide FZF colors in shell config update_zoxide() { command -v zoxide >/dev/null 2>&1 || return 0 local mode="${TARGET_MODE:-$(get_mode)}" local shell_configs=() # Detect all standard shell config files [[ -f "$HOME/.zshrc" ]] && shell_configs+=("$HOME/.zshrc") [[ -f "$HOME/.bashrc" ]] && shell_configs+=("$HOME/.bashrc") [[ -f "$HOME/.bash_profile" ]] && shell_configs+=("$HOME/.bash_profile") [[ ${#shell_configs[@]} -eq 0 ]] && return 0 # Define FZF opts based on mode (quotes are tricky, use a function for safety) local fzf_opts if [[ "$mode" == "dark" ]]; then # Corrected for easier reading/maintenance fzf_opts="--height 40% --layout=reverse --border --preview-window=down:3:wrap --color=fg:#f8f8f2,bg:#2a2f3b,hl:#f92672,fg+:#f8f8f2,bg+:#49483e,hl+:#f92672,info:#a6e22e,prompt:#66d9ef,pointer:#f92672,marker:#a6e22e,spinner:#a6e22e,header:#66d9ef" else fzf_opts="--height 40% --layout=reverse --border --preview-window=down:3:wrap --color=fg:#586e75,bg:#fdf6e3,hl:#268bd2,fg+:#073642,bg+:#eee8d5,hl+:#268bd2,info:#859900,prompt:#268bd2,pointer:#cb4b16,marker:#859900,spinner:#859900,header:#268bd2" fi # The replacement line to insert/update local export_line="export _ZO_FZF_OPTS=\"$fzf_opts\"" for shell_config in "${shell_configs[@]}"; do # 1. Update existing line (handles potential existing spacing/quotes) local pattern_regex="^export[[:space:]]+_ZO_FZF_OPTS=.*" if grep -qE "$pattern_regex" "$shell_config" 2>/dev/null; then if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|${pattern_regex}|${export_line}|" "$shell_config" else sed -i "s|${pattern_regex}|${export_line}|" "$shell_config" fi # 2. Add new line (prefer insertion before zoxide init for functionality) elif grep -q "zoxide init" "$shell_config" 2>/dev/null; then # Use sed to insert the line before the first 'zoxide init' if [[ "$OSTYPE" == "darwin"* ]]; then # macOS sed is tricky; this might not work perfectly on all versions sed -i '' "/zoxide init/i\\ ${export_line}" "$shell_config" else # GNU sed: /pattern/i insert_text sed -i "/zoxide init/i\\$export_line" "$shell_config" fi # 3. Fallback: Append to the end else echo "" >>"$shell_config" echo "# Zoxide FZF theme colors (managed by theme-switcher)" >>"$shell_config" echo "$export_line" >>"$shell_config" fi echo "✓ zoxide (${shell_config##*/})" done } # Find the btop themes path (more robust) get_btop_path() { local btop_path="" # 1. Standard XDG Data Home (most common for user installs) if [[ -d "${XDG_DATA_HOME:-$HOME/.local/share}/btop/themes" ]]; then btop_path="${XDG_DATA_HOME:-$HOME/.local/share}/btop/themes" # 2. Homebrew on macOS (Cellar path is complex/versioned) elif [[ "$OSTYPE" == "darwin"* ]]; then local cellar_path # Use 'find' starting from common Homebrew locations, looking for 'btop/themes' cellar_path=$(find /opt/homebrew/Cellar/ /usr/local/Cellar/ -type d -name "btop" -prune -exec find {} -type d -name themes \; 2>/dev/null | head -1) [[ -n "$cellar_path" ]] && btop_path="$cellar_path" # 3. Standard system installs (Debian/RPM/etc) elif [[ -d "/usr/share/btop/themes" ]]; then btop_path="/usr/share/btop/themes" elif [[ -d "/usr/local/share/btop/themes" ]]; then btop_path="/usr/local/share/btop/themes" fi echo "$btop_path" } # --- Main Logic --- main() { local mode="${TARGET_MODE:-$(get_mode)}" # Cache the mode echo "$mode" >"$CACHE_FILE" 2>/dev/null || true echo "Switching to $mode mode..." # Update configs in parallel for speed # 1. Starship { update_config \ "$CONFIG_DIR/starship.toml" \ 'palette = ".*"' \ 'palette = "monokai_pro"' \ 'palette = "solarized_light"' } & # 2. Btop { local btop_path btop_path=$(get_btop_path) if [[ -n "$btop_path" ]]; then # Ensure the theme is referenced only by its name in the replacement, not the full path, # as btop.conf should only contain the name unless the theme is custom/outside its path. # However, since you are referencing files outside the config dir, we keep the path structure. update_config \ "$CONFIG_DIR/btop/btop.conf" \ 'color_theme = ".*"' \ "color_theme = \"monokai.theme\"" \ "color_theme = \"solarized_light.theme\"" else echo "✗ btop (Could not determine Btop themes path)" fi } & # 3. AeroSpace (macOS only) { if [[ "$OSTYPE" == "darwin"* ]]; then # Pattern: matches the full exec-and-forget line local pattern_regex_aerospace="exec-and-forget borders active_color=0x[0-9a-fA-F]{8} inactive_color=0x[0-9a-fA-F]{8} width=[0-9.]+" # Replacement: The exact, correct command with colors for dark/light local dark_replacement="exec-and-forget borders active_color=0xfff5f5f5 inactive_color=0xff2a2f3b width=5.0" local light_replacement="exec-and-forget borders active_color=0xff268BD2 inactive_color=0xff93A1A1 width=5.0" update_config \ "$CONFIG_DIR/aerospace/aerospace.toml" \ "$pattern_regex_aerospace" \ "$dark_replacement" \ "$light_replacement" # Immediately apply the borders command via 'aerospace' binary (requires it to be in PATH) if command -v aerospace >/dev/null 2>&1; then if [[ "$mode" == "light" ]]; then aerospace "$light_replacement" 2>/dev/null || true else aerospace "$dark_replacement" 2>/dev/null || true fi echo "✓ AeroSpace (Borders applied)" else echo "✓ AeroSpace (Borders updated in config)" fi fi } & # 4. Zoxide/FZF { update_zoxide } & # Wait for all background jobs to finish wait echo "Done!" echo "" echo "Note: Restart your shell or run 'source ~/.zshrc' (or ~/.bashrc) to apply zoxide colors." } # --- Argument Parsing --- case "${1:-}" in "dark" | "light") export TARGET_MODE="$1" ;; "toggle") current=$(get_mode) export TARGET_MODE TARGET_MODE=$([[ "$current" == "dark" ]] && echo "light" || echo "dark") ;; *) # No args or invalid - auto-detect ;; esac # Execute main function main