clone of my dotfiles.ssp.sh
1#!/bin/bash
2
3# Safe dotfiles restore script
4# This script provides multiple restoration modes to safely restore dotfiles
5# without overwriting existing configurations
6
7set -e # Exit on any error
8
9# Color codes for output
10RED='\033[0;31m'
11GREEN='\033[0;32m'
12YELLOW='\033[1;33m'
13BLUE='\033[0;34m'
14NC='\033[0m' # No Color
15
16# Configuration
17DOTFILES_DIR="$HOME/dotfiles" # Adjust this path
18BACKUP_DIR="$HOME/.dotfiles_backup_$(date +%Y%m%d_%H%M%S)"
19DRY_RUN=false
20INTERACTIVE=true
21FORCE=false
22
23# Usage function
24usage() {
25 echo "Usage: $0 [OPTIONS]"
26 echo "Options:"
27 echo " -d, --dotfiles-dir DIR Dotfiles directory (default: $DOTFILES_DIR)"
28 echo " -b, --backup-dir DIR Backup directory (default: $BACKUP_DIR)"
29 echo " -n, --dry-run Show what would be done without making changes"
30 echo " -y, --yes Non-interactive mode (assume yes)"
31 echo " -f, --force Force overwrite existing files"
32 echo " -h, --help Show this help"
33 echo ""
34 echo "Restoration modes:"
35 echo " 1. Interactive mode (default): Ask before each file"
36 echo " 2. Backup mode: Backup existing files before restore"
37 echo " 3. Merge mode: Attempt to merge configurations"
38 echo " 4. Symlink mode: Create symlinks instead of copying"
39}
40
41# Parse command line arguments
42while [[ $# -gt 0 ]]; do
43 case $1 in
44 -d|--dotfiles-dir)
45 DOTFILES_DIR="$2"
46 shift 2
47 ;;
48 -b|--backup-dir)
49 BACKUP_DIR="$2"
50 shift 2
51 ;;
52 -n|--dry-run)
53 DRY_RUN=true
54 shift
55 ;;
56 -y|--yes)
57 INTERACTIVE=false
58 shift
59 ;;
60 -f|--force)
61 FORCE=true
62 shift
63 ;;
64 -h|--help)
65 usage
66 exit 0
67 ;;
68 *)
69 echo "Unknown option: $1"
70 usage
71 exit 1
72 ;;
73 esac
74done
75
76# Utility functions
77log_info() {
78 echo -e "${BLUE}[INFO]${NC} $1"
79}
80
81log_success() {
82 echo -e "${GREEN}[SUCCESS]${NC} $1"
83}
84
85log_warning() {
86 echo -e "${YELLOW}[WARNING]${NC} $1"
87}
88
89log_error() {
90 echo -e "${RED}[ERROR]${NC} $1"
91}
92
93ask_user() {
94 if [[ "$INTERACTIVE" == false ]]; then
95 return 0
96 fi
97
98 local prompt="$1"
99 local default="${2:-n}"
100
101 while true; do
102 read -p "$prompt [y/N]: " -n 1 -r
103 echo
104 if [[ $REPLY =~ ^[Yy]$ ]]; then
105 return 0
106 elif [[ $REPLY =~ ^[Nn]$ ]] || [[ -z $REPLY ]]; then
107 return 1
108 else
109 echo "Please answer y or n."
110 fi
111 done
112}
113
114# Create backup directory
115create_backup_dir() {
116 if [[ ! -d "$BACKUP_DIR" ]]; then
117 log_info "Creating backup directory: $BACKUP_DIR"
118 if [[ "$DRY_RUN" == false ]]; then
119 mkdir -p "$BACKUP_DIR"
120 fi
121 fi
122}
123
124# Backup existing file
125backup_file() {
126 local target="$1"
127 local backup_path="$BACKUP_DIR/$(dirname "$target")"
128
129 if [[ -e "$target" ]]; then
130 log_info "Backing up existing file: $target"
131 if [[ "$DRY_RUN" == false ]]; then
132 mkdir -p "$backup_path"
133 cp -r "$target" "$backup_path/"
134 fi
135 return 0
136 fi
137 return 1
138}
139
140# Safe copy function
141safe_copy() {
142 local source="$1"
143 local target="$2"
144 local action="$3" # copy, symlink, or merge
145
146 # Check if source exists
147 if [[ ! -e "$source" ]]; then
148 log_warning "Source file does not exist: $source"
149 return 1
150 fi
151
152 # Create target directory if it doesn't exist
153 local target_dir="$(dirname "$target")"
154 if [[ ! -d "$target_dir" ]]; then
155 log_info "Creating directory: $target_dir"
156 if [[ "$DRY_RUN" == false ]]; then
157 mkdir -p "$target_dir"
158 fi
159 fi
160
161 # Handle existing target
162 if [[ -e "$target" ]]; then
163 if [[ "$FORCE" == true ]]; then
164 log_info "Force overwriting: $target"
165 backup_file "$target"
166 elif ask_user "File exists: $target. Overwrite?"; then
167 backup_file "$target"
168 else
169 log_info "Skipping: $target"
170 return 0
171 fi
172 fi
173
174 # Perform the action
175 case "$action" in
176 copy)
177 log_info "Copying: $source -> $target"
178 if [[ "$DRY_RUN" == false ]]; then
179 cp -r "$source" "$target"
180 fi
181 ;;
182 symlink)
183 log_info "Symlinking: $source -> $target"
184 if [[ "$DRY_RUN" == false ]]; then
185 ln -sf "$source" "$target"
186 fi
187 ;;
188 merge)
189 log_info "Merging: $source -> $target"
190 # This is a placeholder - implement specific merge logic per file type
191 if [[ "$DRY_RUN" == false ]]; then
192 cp -r "$source" "$target"
193 fi
194 ;;
195 esac
196}
197
198# Platform detection
199detect_platform() {
200 if [[ "$OSTYPE" == "linux-gnu"* ]]; then
201 echo "linux"
202 elif [[ "$OSTYPE" == "darwin"* ]]; then
203 echo "macos"
204 else
205 echo "unknown"
206 fi
207}
208
209# Adjust paths for different platforms
210adjust_path() {
211 local original_path="$1"
212 local platform="$2"
213
214 case "$platform" in
215 linux)
216 # Convert macOS paths to Linux equivalents
217 echo "$original_path" | sed \
218 -e 's|~/Library/ApplicationSupport/Code/User/|~/.config/Code/User/|g' \
219 -e 's|~/Library/Application Support/lazygit/|~/.config/lazygit/|g'
220 ;;
221 macos)
222 # Convert Linux paths to macOS equivalents
223 echo "$original_path" | sed \
224 -e 's|~/.config/Code/User/|~/Library/ApplicationSupport/Code/User/|g' \
225 -e 's|~/.config/lazygit/|~/Library/Application Support/lazygit/|g'
226 ;;
227 *)
228 echo "$original_path"
229 ;;
230 esac
231}
232
233# Main restoration function
234restore_dotfiles() {
235 local platform=$(detect_platform)
236 local mode="$1"
237
238 log_info "Detected platform: $platform"
239 log_info "Restoration mode: $mode"
240
241 if [[ "$DRY_RUN" == true ]]; then
242 log_warning "DRY RUN MODE - No changes will be made"
243 fi
244
245 create_backup_dir
246
247 # Define file mappings (source -> target)
248 declare -A file_mappings=(
249 # VS Code
250 ["vscode/settings.json"]="$(adjust_path "~/Library/ApplicationSupport/Code/User/settings.json" "$platform")"
251 ["vscode/keybindings.json"]="$(adjust_path "~/Library/ApplicationSupport/Code/User/keybindings.json" "$platform")"
252
253 # Neovim
254 ["nvim/init.vim"]="~/.config/nvim/init.vim"
255 ["nvim-wp/init.vim"]="~/.config/nvim-wp/init.vim"
256
257 # Tmux
258 ["tmux/tmux.conf"]="~/.tmux.conf"
259 ["tmux/ide"]="~/.tmux/ide"
260
261 # Zsh
262 ["zsh/zshrc"]="~/.zshrc"
263 ["zsh/.secrets"]="~/.dotfiles/zsh/.secrets"
264 ["zsh/aliases.shrc"]="~/.dotfiles/zsh/aliases.shrc"
265
266 # Kitty
267 ["kitty/kitty.conf"]="~/.config/kitty/kitty.conf"
268 ["kitty/gruvbox-kitty.conf"]="~/.config/kitty/gruvbox-kitty.conf"
269
270 # Ghostty
271 ["ghostty/config"]="~/.config/ghostty/config"
272
273 # Yabai & SKHD (macOS only)
274 ["yabai/yabairc"]="~/.config/yabai/yabairc"
275 ["skhd/skhdrc"]="~/.config/skhd/skhdrc"
276
277 # Helix
278 ["helix/config.toml"]="~/.config/helix/config.toml"
279
280 # Git (be careful with this one)
281 # ["git/gitconfig"]="~/.gitconfig"
282
283 # Ranger
284 ["ranger/rc.conf"]="~/.config/ranger/rc.conf"
285
286 # Yazi
287 ["yazi/yazi.toml"]="~/.config/yazi/yazi.toml"
288
289 # Linting
290 ["linting/pylintrc"]="~/.pylintrc"
291 ["linting/flake8"]="~/.config/flake8"
292
293 # Karabiner (macOS only)
294 ["karabiner/karabiner.json"]="~/.config/karabiner/karabiner.json"
295
296 # Lazygit
297 ["lazygit/config.yml"]="$(adjust_path "~/Library/Application Support/lazygit/config.yml" "$platform")"
298 )
299
300 # Process each file
301 for source_file in "${!file_mappings[@]}"; do
302 local source_path="$DOTFILES_DIR/$source_file"
303 local target_path="${file_mappings[$source_file]}"
304
305 # Expand tilde
306 target_path="${target_path/#\~/$HOME}"
307
308 # Skip platform-specific files
309 if [[ "$platform" == "linux" ]] && [[ "$source_file" == *"yabai"* || "$source_file" == *"skhd"* || "$source_file" == *"karabiner"* ]]; then
310 log_info "Skipping macOS-specific file: $source_file"
311 continue
312 fi
313
314 case "$mode" in
315 copy)
316 safe_copy "$source_path" "$target_path" "copy"
317 ;;
318 symlink)
319 safe_copy "$source_path" "$target_path" "symlink"
320 ;;
321 merge)
322 safe_copy "$source_path" "$target_path" "merge"
323 ;;
324 esac
325 done
326
327 log_success "Dotfiles restoration completed!"
328 if [[ -d "$BACKUP_DIR" ]] && [[ "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]]; then
329 log_info "Backup of original files created at: $BACKUP_DIR"
330 fi
331}
332
333# Special handling for sensitive files
334handle_sensitive_files() {
335 local sensitive_files=(
336 ".ssh"
337 ".secrets"
338 "gitconfig"
339 )
340
341 for file in "${sensitive_files[@]}"; do
342 if ask_user "Restore sensitive file: $file?"; then
343 log_warning "Restoring sensitive file: $file"
344 # Add specific handling here
345 fi
346 done
347}
348
349# Main execution
350main() {
351 log_info "Starting dotfiles restoration"
352
353 # Check if dotfiles directory exists
354 if [[ ! -d "$DOTFILES_DIR" ]]; then
355 log_error "Dotfiles directory not found: $DOTFILES_DIR"
356 exit 1
357 fi
358
359 # Choose restoration mode
360 if [[ "$INTERACTIVE" == true ]]; then
361 echo "Choose restoration mode:"
362 echo "1. Copy files (default)"
363 echo "2. Create symlinks"
364 echo "3. Merge configurations"
365 read -p "Enter choice [1-3]: " -n 1 -r
366 echo
367
368 case "$REPLY" in
369 2) restore_dotfiles "symlink" ;;
370 3) restore_dotfiles "merge" ;;
371 *) restore_dotfiles "copy" ;;
372 esac
373 else
374 restore_dotfiles "copy"
375 fi
376
377 # Handle sensitive files separately
378 handle_sensitive_files
379}
380
381# Run main function
382main "$@"