diff --git a/README.md b/README.md
index d369d73..dc34000 100644
--- a/README.md
+++ b/README.md
@@ -26,11 +26,42 @@ Install all packages:
yay -S $(grep -v '^\s*$' ~/scripts/packages.txt | tr '\n' ' ')
```
-## Linking Configs
+## Install / Uninstall
-Pick a window manager section below, then add the shared configs.
+`install.sh` symlinks configs, sources shell files, and backs up any existing files it would overwrite. `uninstall.sh` reverses the process, removing symlinks and restoring backups.
-### Hyprland
+```bash
+# Install everything (uses Hyprland as the WM)
+~/scripts/install.sh all
+
+# Install specific targets
+~/scripts/install.sh hyprland shell nvim tmux
+
+# Uninstall specific targets (restores from most recent backup)
+~/scripts/uninstall.sh nvim shell
+
+# Uninstall using a specific backup
+~/scripts/uninstall.sh --backup ~/scripts/backups/20260319_120000 all
+```
+
+Available targets: `hyprland`, `sway`, `i3`, `cursor`, `shell`, `alacritty`, `tmux`, `nvim`, `vim`, `all`.
+
+### tmux prerequisite
+
+Install the plugin manager before running `install.sh tmux`:
+
+```bash
+git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
+```
+
+### Manual linking
+
+If you prefer to link configs manually instead of using `install.sh`:
+
+
+Manual symlink commands
+
+#### Hyprland
```bash
ln -sf ~/scripts/hypr ~/.config/hypr
@@ -38,7 +69,7 @@ ln -sf ~/scripts/waybar ~/.config/waybar
ln -sf ~/scripts/wofi ~/.config/wofi
```
-### Sway
+#### Sway
```bash
ln -sf ~/scripts/sway ~/.config/sway
@@ -53,7 +84,7 @@ mv ~/scripts/waybar/config ~/scripts/waybar/config.hypr.json
mv ~/scripts/waybar/waybar_sway_config.json ~/scripts/waybar/config
```
-### i3
+#### i3
```bash
ln -sf ~/scripts/i3 ~/.config/i3
@@ -61,13 +92,13 @@ ln -sf ~/scripts/i3blocks ~/.config/i3blocks
ln -sf ~/scripts/i3status ~/.config/i3status
```
-### Cursor
+#### Cursor
```bash
ln -sf ~/scripts/hatsune-miku-windows-linux-cursors/miku-cursor-linux ~/.local/share/icons/"Miku Cursor"
```
-### Shell
+#### Shell
Add to `~/.bashrc` or `~/.zshrc`:
@@ -76,7 +107,7 @@ source ~/scripts/bashrc # or zshrc
export PATH="~/scripts/sh:$PATH"
```
-### Terminals
+#### Terminals
**Alacritty** - add to `~/.config/alacritty/alacritty.toml`:
@@ -86,13 +117,7 @@ import = ["~/scripts/alacritty.toml"]
**Kitty** - kitty.conf is used directly by the WM configs.
-### tmux
-
-Install the plugin manager first:
-
-```bash
-git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
-```
+#### tmux
Add to `~/.tmux.conf`:
@@ -100,7 +125,7 @@ Add to `~/.tmux.conf`:
source ~/scripts/tmux.conf
```
-### Vim
+#### Vim
Add to `~/.vimrc`:
@@ -108,6 +133,8 @@ Add to `~/.vimrc`:
source ~/scripts/vimrc
```
+
+
## Keybindings
All window managers and tmux use vim-style hjkl navigation. `$mod` is the Super key.
diff --git a/install.sh b/install.sh
index df9107c..79a3185 100755
--- a/install.sh
+++ b/install.sh
@@ -100,11 +100,11 @@ install_cursor() {
install_shell() {
echo "Installing shell configs..."
- if [ -f "$HOME/.zshrc" ] || [ "$SHELL" = *zsh* ]; then
+ if [ -f "$HOME/.zshrc" ] || [[ "$SHELL" == *zsh* ]]; then
append_if_missing "$HOME/.zshrc" "source ~/scripts/zshrc"
append_if_missing "$HOME/.zshrc" 'export PATH="$HOME/scripts/sh:$PATH"'
fi
- if [ -f "$HOME/.bashrc" ] || [ "$SHELL" = *bash* ]; then
+ if [ -f "$HOME/.bashrc" ] || [[ "$SHELL" == *bash* ]]; then
append_if_missing "$HOME/.bashrc" "source ~/scripts/bashrc"
append_if_missing "$HOME/.bashrc" 'export PATH="$HOME/scripts/sh:$PATH"'
fi
diff --git a/uninstall.sh b/uninstall.sh
new file mode 100755
index 0000000..b919e9b
--- /dev/null
+++ b/uninstall.sh
@@ -0,0 +1,216 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPTS_DIR="$HOME/scripts"
+CONFIG_DIR="$HOME/.config"
+BACKUP_DIR=""
+
+usage() {
+ echo "Usage: $(basename "$0") [OPTIONS] ..."
+ echo
+ echo "Targets:"
+ echo " hyprland Hyprland + waybar + wofi"
+ echo " sway Sway + waybar + wofi"
+ echo " i3 i3 + i3blocks + i3status"
+ echo " cursor Miku cursor theme"
+ echo " shell Remove sourced bashrc/zshrc and sh/ from PATH"
+ echo " alacritty Remove import from alacritty.toml"
+ echo " tmux Remove sourced tmux.conf"
+ echo " nvim Neovim (LazyVim) config"
+ echo " vim Remove sourced vimrc"
+ echo " all Everything (uses hyprland as WM)"
+ echo
+ echo "Options:"
+ echo " -h, --help Show this help"
+ echo " --backup Use specific backup directory for restoring"
+ echo " (default: most recent in $SCRIPTS_DIR/backups/)"
+}
+
+# Find the backup directory to restore from
+find_backup() {
+ if [ -n "$BACKUP_DIR" ]; then
+ if [ ! -d "$BACKUP_DIR" ]; then
+ echo "Error: backup directory not found: $BACKUP_DIR"
+ exit 1
+ fi
+ return
+ fi
+ local backups_root="$SCRIPTS_DIR/backups"
+ if [ -d "$backups_root" ]; then
+ local latest
+ latest=$(ls -1d "$backups_root"/*/ 2>/dev/null | sort | tail -n1 || true)
+ if [ -n "$latest" ]; then
+ BACKUP_DIR="${latest%/}"
+ echo "Using backup: $BACKUP_DIR"
+ fi
+ fi
+ if [ -z "$BACKUP_DIR" ]; then
+ echo "No backups found, will only remove without restoring"
+ fi
+}
+
+# Remove a symlink if it points into $SCRIPTS_DIR, then restore from backup if available
+restore() {
+ local dest="$1"
+ local name
+ name=$(basename "$dest")
+
+ if [ -L "$dest" ]; then
+ local target
+ target=$(readlink "$dest")
+ if [[ "$target" == "$SCRIPTS_DIR"* ]]; then
+ rm "$dest"
+ echo " Removed symlink $dest"
+ else
+ echo " Skipping $dest (symlink does not point into $SCRIPTS_DIR)"
+ return
+ fi
+ elif [ -e "$dest" ]; then
+ echo " Skipping $dest (not a symlink, won't remove)"
+ return
+ else
+ echo " $dest does not exist, nothing to remove"
+ fi
+
+ # Restore from backup if available
+ if [ -n "$BACKUP_DIR" ] && [ -e "$BACKUP_DIR/$name" ]; then
+ cp -a "$BACKUP_DIR/$name" "$dest"
+ echo " Restored $dest from backup"
+ fi
+}
+
+# Remove exact matching lines from a file
+remove_line() {
+ local file="$1" line="$2"
+ if [ ! -f "$file" ]; then
+ echo " $file does not exist, nothing to remove"
+ return
+ fi
+ if grep -qF "$line" "$file"; then
+ local tmp
+ tmp=$(mktemp)
+ grep -vF "$line" "$file" > "$tmp" || true
+ mv "$tmp" "$file"
+ echo " Removed from $file: $line"
+ # Delete file if only whitespace remains
+ if [ ! -s "$file" ] || ! grep -q '[^[:space:]]' "$file"; then
+ rm "$file"
+ echo " Deleted empty $file"
+ fi
+ else
+ echo " Line not found in $file: $line"
+ fi
+}
+
+uninstall_hyprland() {
+ echo "Uninstalling Hyprland configs..."
+ restore "$CONFIG_DIR/hypr"
+ restore "$CONFIG_DIR/waybar"
+ restore "$CONFIG_DIR/wofi"
+}
+
+uninstall_sway() {
+ echo "Uninstalling Sway configs..."
+ restore "$CONFIG_DIR/sway"
+ restore "$CONFIG_DIR/waybar"
+ restore "$CONFIG_DIR/wofi"
+}
+
+uninstall_i3() {
+ echo "Uninstalling i3 configs..."
+ restore "$CONFIG_DIR/i3"
+ restore "$CONFIG_DIR/i3blocks"
+ restore "$CONFIG_DIR/i3status"
+}
+
+uninstall_cursor() {
+ echo "Uninstalling cursor theme..."
+ restore "$HOME/.local/share/icons/Miku Cursor"
+}
+
+uninstall_shell() {
+ echo "Uninstalling shell configs..."
+ if [ -f "$HOME/.zshrc" ]; then
+ remove_line "$HOME/.zshrc" "source ~/scripts/zshrc"
+ remove_line "$HOME/.zshrc" 'export PATH="$HOME/scripts/sh:$PATH"'
+ fi
+ if [ -f "$HOME/.bashrc" ]; then
+ remove_line "$HOME/.bashrc" "source ~/scripts/bashrc"
+ remove_line "$HOME/.bashrc" 'export PATH="$HOME/scripts/sh:$PATH"'
+ fi
+}
+
+uninstall_alacritty() {
+ echo "Uninstalling Alacritty config..."
+ local conf="$CONFIG_DIR/alacritty/alacritty.toml"
+ remove_line "$conf" 'import = ["~/scripts/alacritty.toml"]'
+}
+
+uninstall_tmux() {
+ echo "Uninstalling tmux config..."
+ remove_line "$HOME/.tmux.conf" "source ~/scripts/tmux.conf"
+}
+
+uninstall_nvim() {
+ echo "Uninstalling Neovim config..."
+ restore "$CONFIG_DIR/nvim"
+}
+
+uninstall_vim() {
+ echo "Uninstalling vim config..."
+ remove_line "$HOME/.vimrc" "source ~/scripts/vimrc"
+}
+
+if [ $# -eq 0 ]; then
+ usage
+ exit 1
+fi
+
+targets=()
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -h | --help)
+ usage
+ exit 0
+ ;;
+ --backup)
+ if [ $# -lt 2 ]; then
+ echo "Error: --backup requires a directory argument"
+ exit 1
+ fi
+ BACKUP_DIR="$2"
+ shift 2
+ ;;
+ all)
+ targets+=(hyprland cursor shell alacritty tmux nvim vim)
+ shift
+ ;;
+ *)
+ targets+=("$1")
+ shift
+ ;;
+ esac
+done
+
+find_backup
+
+for target in "${targets[@]}"; do
+ case "$target" in
+ hyprland) uninstall_hyprland ;;
+ sway) uninstall_sway ;;
+ i3) uninstall_i3 ;;
+ cursor) uninstall_cursor ;;
+ shell) uninstall_shell ;;
+ alacritty) uninstall_alacritty ;;
+ tmux) uninstall_tmux ;;
+ nvim) uninstall_nvim ;;
+ vim) uninstall_vim ;;
+ *)
+ echo "Unknown target: $target"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+echo "Done!"