diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..08e3532 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# +# Pre-commit hook for dotfiles repository +# Validates shell scripts before committing +# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +echo "Running pre-commit checks..." + +# Get list of staged files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) + +ERRORS=0 + +############################################################################# +# Check bash scripts with shellcheck +############################################################################# + +check_shellcheck() { + if ! command -v shellcheck &>/dev/null; then + echo -e "${YELLOW}Warning: shellcheck not installed, skipping lint${NC}" + return 0 + fi + + local bash_files="" + # These are zsh scripts, skip shellcheck -s bash for them + local zsh_scripts="bin/dotfiles-test bin/dotfiles-doctor bin/dotfiles-cheatsheet bin/dotfiles-profiler" + + for file in $STAGED_FILES; do + case "$file" in + bin/dotfiles|bin/dotfiles-*|scripts/*.sh|macos/*.sh) + if [[ -f "$file" ]]; then + # Skip zsh scripts from bash shellcheck + local is_zsh=false + for zf in $zsh_scripts; do + [[ "$file" == "$zf" ]] && is_zsh=true + done + $is_zsh || bash_files="$bash_files $file" + fi + ;; + esac + done + + if [[ -n "$bash_files" ]]; then + echo "Checking bash scripts with shellcheck..." + for file in $bash_files; do + if ! shellcheck -e SC1090,SC1091,SC2034,SC2119,SC2154 -s bash "$file" 2>/dev/null; then + echo -e "${RED}✗ Shellcheck failed: $file${NC}" + ((ERRORS++)) + else + echo -e "${GREEN}✓ $file${NC}" + fi + done + fi +} + +############################################################################# +# Check bash syntax +############################################################################# + +check_bash_syntax() { + # These are zsh scripts, skip bash -n for them + local zsh_scripts="bin/dotfiles-test bin/dotfiles-doctor bin/dotfiles-cheatsheet bin/dotfiles-profiler" + local bash_files="" + for file in $STAGED_FILES; do + case "$file" in + bin/dotfiles|bin/dotfiles-*|scripts/*.sh|macos/*.sh) + if [[ -f "$file" ]]; then + local is_zsh=false + for zf in $zsh_scripts; do + [[ "$file" == "$zf" ]] && is_zsh=true + done + $is_zsh || bash_files="$bash_files $file" + fi + ;; + esac + done + + if [[ -n "$bash_files" ]]; then + echo "Checking bash syntax..." + for file in $bash_files; do + if ! bash -n "$file" 2>/dev/null; then + echo -e "${RED}✗ Syntax error: $file${NC}" + ((ERRORS++)) + fi + done + fi +} + +############################################################################# +# Check zsh syntax +############################################################################# + +check_zsh_syntax() { + if ! command -v zsh &>/dev/null; then + echo -e "${YELLOW}Warning: zsh not installed, skipping zsh syntax check${NC}" + return 0 + fi + + local zsh_files="" + for file in $STAGED_FILES; do + case "$file" in + runcom/.*|system/.*|bin/dotfiles-test|bin/dotfiles-doctor) + if [[ -f "$file" ]]; then + zsh_files="$zsh_files $file" + fi + ;; + esac + done + + if [[ -n "$zsh_files" ]]; then + echo "Checking zsh syntax..." + for file in $zsh_files; do + if ! zsh -n "$file" 2>/dev/null; then + echo -e "${RED}✗ Syntax error: $file${NC}" + ((ERRORS++)) + fi + done + fi +} + +############################################################################# +# Check for secrets +############################################################################# + +check_secrets() { + echo "Checking for potential secrets..." + + local secret_patterns=( + "PRIVATE.KEY" + "BEGIN RSA PRIVATE KEY" + "BEGIN DSA PRIVATE KEY" + "BEGIN EC PRIVATE KEY" + "BEGIN OPENSSH PRIVATE KEY" + "password.*=.*['\"]" + "secret.*=.*['\"]" + "api_key.*=.*['\"]" + "apikey.*=.*['\"]" + "access_token.*=.*['\"]" + ) + + for file in $STAGED_FILES; do + # Skip files that legitimately contain pattern strings (not actual secrets) + case "$file" in + .githooks/pre-commit|bin/dotfiles-test|bin/dotfiles-secrets) continue ;; + esac + if [[ -f "$file" ]]; then + for pattern in "${secret_patterns[@]}"; do + if grep -qiE "$pattern" "$file" 2>/dev/null; then + echo -e "${RED}✗ Potential secret found in: $file${NC}" + echo " Pattern: $pattern" + ((ERRORS++)) + fi + done + fi + done +} + +############################################################################# +# Main +############################################################################# + +check_bash_syntax +check_zsh_syntax +check_shellcheck +check_secrets + +if [[ $ERRORS -gt 0 ]]; then + echo "" + echo -e "${RED}Pre-commit check failed with $ERRORS error(s)${NC}" + echo "Fix the issues above and try again." + echo "To skip this check (not recommended): git commit --no-verify" + exit 1 +fi + +echo -e "${GREEN}All pre-commit checks passed!${NC}" +exit 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..df1d8bb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,128 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + lint: + name: Lint Shell Scripts + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install shellcheck + run: sudo apt-get install -y shellcheck + + - name: Run shellcheck on bash scripts + run: | + shellcheck -e SC1090,SC1091,SC2034,SC2119,SC2154 -s bash \ + bin/dotfiles \ + bin/dotfiles-secrets \ + bin/dotfiles-setup \ + scripts/*.sh \ + macos/*.sh + + - name: Check bash syntax + run: | + for file in bin/dotfiles scripts/*.sh macos/*.sh; do + if [[ -f "$file" ]]; then + bash -n "$file" || exit 1 + fi + done + + syntax: + name: Validate Zsh Syntax + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install zsh + run: sudo apt-get install -y zsh + + - name: Check zsh syntax + run: | + files=( + runcom/.zshrc + runcom/.zprofile + runcom/.zlogin + runcom/.profile + runcom/.zpreztorc + system/.alias + system/.bindings + system/.completion + system/.env + system/.fix + system/.fnm + system/.function + system/.function_fs + system/.function_fun + system/.function_network + system/.function_text + system/.fzf + system/.grep + system/.path + system/.pnpm + system/.starship + system/.zoxide + bin/dotfiles-test + bin/dotfiles-doctor + bin/dotfiles-cheatsheet + bin/dotfiles-profiler + ) + + for file in "${files[@]}"; do + if [[ -f "$file" ]]; then + echo "Checking $file..." + zsh -n "$file" || exit 1 + fi + done + + test: + name: Run Tests + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Homebrew + uses: Homebrew/actions/setup-homebrew@cced187498280712e078aaba62dc13a3e9cd80bf + + - name: Install dependencies + run: | + brew install shellcheck coreutils + + - name: Run test suite + run: | + chmod +x bin/dotfiles-test + ./bin/dotfiles-test --quick + env: + DOTFILES_DIR: ${{ github.workspace }} + + brewfile: + name: Validate Brewfile + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Homebrew + uses: Homebrew/actions/setup-homebrew@cced187498280712e078aaba62dc13a3e9cd80bf + + - name: Validate Brewfile syntax + run: | + # Just check that the Brewfile is valid syntax + brew bundle check --file=Brewfile 2>&1 || true + # The check will "fail" because packages aren't installed + # but syntax errors would cause a different error + brew bundle list --file=Brewfile > /dev/null diff --git a/.gitignore b/.gitignore index 515013e..c90bc2e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ runcom/.vim/.netrwhist runcom/.vim/bundle/* !runcom/.vim/bundle/.gitignore !runcom/.vim/bundle/Vundle.vim + +# Machine-specific profile (contains secrets/local config) +profiles/local.zsh + +# Brewfile lock (optional - can be committed for reproducibility) +Brewfile.lock.json diff --git a/.gitmodules b/.gitmodules index eaaeda2..c2e43f7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,12 @@ [submodule "apps/terminal/nord_theme"] path = apps/terminal/nord_theme - url = https://github.com/arcticicestudio/nord-terminal-app + url = https://github.com/nordtheme/terminal-app [submodule "apps/xcode/nord_theme"] path = apps/xcode/nord_theme - url = https://github.com/arcticicestudio/nord-xcode + url = https://github.com/nordtheme/xcode [submodule "modules/prezto"] path = modules/prezto url = https://github.com/sorin-ionescu/prezto.git -[submodule "modules/zsh/zsh-autocomplete"] - path = modules/zsh/zsh-autocomplete - url = https://github.com/marlonrichert/zsh-autocomplete.git [submodule "runcom/.vim/bundle/Vundle.vim"] path = runcom/.vim/bundle/Vundle.vim url = https://github.com/VundleVim/Vundle.vim.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cfbb677 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# AGENTS.md + +macOS dotfiles repo — bootstraps a machine with system defaults, dev tools, apps, and shell config. + +## Essentials + +- **Language**: zsh (shell config), bash (install scripts) +- **Entry point**: `./bin/dotfiles ` — run `./bin/dotfiles help` for full reference +- **Test**: `./bin/dotfiles test` (must pass before committing) +- **Lint**: shellcheck (runs in CI and pre-commit hooks) +- **CI**: GitHub Actions on push/PR — syntax validation, shellcheck, tests, Brewfile validation +- **Link method**: GNU Stow — `runcom/` stows to `~/`, `config/` stows to `~/.config/` + +## Critical Context + +These affect nearly every task: + +- **Submodules**: External deps (Prezto, hosts, zsh plugins) live in `modules/`. Always `git submodule update --init --recursive` after clone. +- **Intel + Apple Silicon**: Both supported. Homebrew prefix is `/opt/homebrew` (AS) or `/usr/local` (Intel). Use `bin/is-apple-silicon` to detect. +- **Prezto, not Oh My Zsh**: Shell framework is Prezto for performance. Prompt is Powerlevel10k via Prezto's prompt module. Starship config exists in `system/.starship` but is **not sourced**. +- **FNM, not NVM**: Node.js managed by FNM (Fast Node Manager). Use `require_fnm()` and `source_fnm()`. +- **Packages**: Brewfile for brew/cask/mas (`brew bundle install`). `.list` files in `packages/` for npm, vscode extensions, and other tools Brewfile doesn't support. See [packages.md](docs/agents/packages.md). +- **Backups**: Dotfile linking backs up originals to `~/.dotfiles_backup/` before replacing. + +## Detail Docs + +| Topic | File | +| --------------------------------------------- | -------------------------------------------------- | +| Directory structure, Stow linking, submodules | [architecture.md](docs/agents/architecture.md) | +| CLI commands and common workflows | [commands.md](docs/agents/commands.md) | +| Zsh config, Prezto, profiles, caching, PATH | [shell-config.md](docs/agents/shell-config.md) | +| Package management (Brewfile + list files) | [packages.md](docs/agents/packages.md) | +| macOS system defaults and Dock | [macos-defaults.md](docs/agents/macos-defaults.md) | +| Test suite, CI, git hooks | [testing-and-ci.md](docs/agents/testing-and-ci.md) | +| Secrets management (macOS Keychain) | [secrets.md](docs/agents/secrets.md) | +| Neovim config (lazy.nvim) | [neovim.md](docs/agents/neovim.md) | diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..508dd08 --- /dev/null +++ b/Brewfile @@ -0,0 +1,205 @@ +# Brewfile - Homebrew Bundle +# Install: brew bundle install +# Dump current: brew bundle dump --force +# Cleanup unlisted: brew bundle cleanup --force +# Check status: brew bundle check + +# ============================================================================ +# Taps +# ============================================================================ + +tap "goreleaser/tap" +tap "khanakia/vercelgate" +tap "khanhas/tap" +tap "lotyp/formulae" +tap "artginzburg/tap" + +# ============================================================================ +# CLI Utilities +# ============================================================================ + +# Search tools +brew "ack" # Code search tool +brew "fd" # Fast find alternative +brew "fzf" # Fuzzy finder +brew "ripgrep" # Fast grep alternative +brew "the_silver_searcher" # ag + +# File utilities +brew "bat" # Cat with syntax highlighting +brew "eza" # Modern ls replacement +brew "tree" # Directory tree view +brew "unar" # Universal archive extractor +brew "croc" # Secure file transfer + +# Text processing +brew "gnu-sed" # GNU sed +brew "gawk" # GNU awk +brew "grep" # GNU grep +brew "jq" # JSON processor +brew "yq" # YAML processor + +# System utilities +brew "coreutils" # GNU core utilities +brew "findutils" # GNU find, xargs, etc. +brew "readline" # GNU readline +brew "stow" # Symlink farm manager +brew "mackup" # App settings backup +brew "mas" # Mac App Store CLI +brew "topgrade" # System upgrade tool +brew "thefuck" # Command correction +brew "zoxide" # Smarter cd command +brew "starship" # Cross-shell prompt +brew "psgrep" # Process grep + +# Network utilities +brew "wget" # HTTP client +brew "httpie" # Modern HTTP client +brew "ssh-copy-id" # SSH key installer + +# Development tools +brew "git-delta" # Better git diff +brew "gh" # GitHub CLI +brew "shfmt" # Shell formatter +brew "shellcheck" # Shell script linter +brew "bats-core" # Bash testing framework + +# Programming languages +brew "python" # Python 3 +brew "go" # Go language +brew "cmake" # Build system + +# Media tools +brew "ffmpeg" # Media converter +brew "imagemagick" # Image manipulation +brew "optipng" # PNG optimizer +brew "webp" # WebP tools +brew "yt-dlp" # Video downloader + +# Misc utilities +brew "dos2unix" # Line ending converter +brew "uni" # Unicode tool +brew "grip" # GitHub markdown preview + +# Tap-specific +brew "lotyp/formulae/dockutil" # Dock management +brew "artginzburg/tap/sudo-touchid" # TouchID for sudo + +# ============================================================================ +# Desktop Applications (Casks) +# ============================================================================ + +# Browsers +cask "brave-browser" +cask "firefox" +cask "google-chrome" + +# Development +cask "visual-studio-code" +cask "gitkraken" +cask "sourcetree" +cask "arduino" +cask "arduino-ide" +cask "ngrok" +cask "goreleaser" +cask "kaleidoscope" + +# Design +cask "adobe-creative-cloud" +cask "figma" +cask "autodesk-fusion" +cask "prusaslicer" +cask "superslicer" + +# Communication +cask "discord" +cask "slack" +cask "telegram" +cask "zoom" +cask "notion" + +# Utilities +cask "raycast" # Spotlight replacement +cask "karabiner-elements" # Keyboard customization +cask "keka" # Archive utility +cask "keycastr" # Keystroke visualizer +cask "shottr" # Screenshot tool +cask "topnotch" # Notch hider +cask "flux-app" # Blue light filter +cask "setapp" # App subscription + +# Media +cask "spotify" +cask "vlc" +cask "iina" # Modern video player + +# Remote & VPN +cask "anydesk" +cask "teamviewer" +cask "protonvpn" +cask "tunnelblick" +cask "tailscale" + +# Cloud & Sync +cask "google-drive" +cask "transmission" + +# iOS +cask "altserver" + +# QuickLook plugins +cask "qlmarkdown" +cask "qlstephen" +cask "qlvideo" +cask "quicklook-json" +cask "quicklookase" +cask "syntax-highlight" +cask "suspicious-package" +cask "apparency" + +# Terminal +cask "warp" + +# Fonts +cask "font-awesome-terminal-fonts" +cask "font-fira-code" +cask "font-fira-mono" +cask "font-fira-code-nerd-font" +cask "font-fira-mono-nerd-font" +cask "font-fontawesome" +cask "font-geist-mono-nerd-font" +cask "font-hack" +cask "font-hack-nerd-font" +cask "font-inter" +cask "font-menlo-for-powerline" +cask "font-meslo-for-powerline" +cask "font-meslo-lg" +cask "font-meslo-lg-nerd-font" +cask "font-roboto-mono-nerd-font" + +# ============================================================================ +# Mac App Store +# ============================================================================ + +mas "Emby", id: 992180193 +mas "LastPass", id: 926036361 +mas "LocalSend", id: 1661733229 +mas "Magnet", id: 441258766 +mas "Messenger", id: 1480068668 +mas "SponsorBlock", id: 1573461917 +mas "Windows App", id: 1295203466 + +# ============================================================================ +# VS Code Extensions (managed separately via code.list) +# ============================================================================ +# Note: VS Code extensions are managed via packages/code.list +# Install with: cat packages/code.list | xargs -L 1 code --install-extension + +# ============================================================================ +# NPM Global Packages (managed separately via npm.list) +# ============================================================================ +# Note: NPM packages are managed via packages/npm.list +# Install with: cat packages/npm.list | xargs npm install -g +# +# These cannot be in Brewfile because they require Node.js/npm to be installed +# and configured via fnm first. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 993697e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,183 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Repository Overview - -This is a macOS dotfiles repository for automated system setup and configuration management. It bootstraps a fresh or existing macOS machine with personalized system defaults, development tools, applications, and shell configurations. The repository uses git submodules extensively for managing external dependencies like Prezto (zsh framework), themes, and plugins. - -## Core Commands - -All operations are managed through the `./bin/dotfiles` script. Common workflows: - -### Initial Setup -```bash -# Remote installation (fresh machine) -bash -c "$(curl -fsSL https://raw.githubusercontent.com/STiXzoOR/dotfiles/main/remote-install.sh)" - -# Manual installation -git clone --recurse-submodules https://github.com/STiXzoOR/dotfiles ~/.dotfiles -cd ~/.dotfiles -./bin/dotfiles install -``` - -### Development Commands -```bash -./bin/dotfiles help # Show all available commands -./bin/dotfiles install # Bootstrap system (interactive) -./bin/dotfiles install --all # Install everything non-interactively -./bin/dotfiles link # Symlink dotfiles to ~/ -./bin/dotfiles configure # Apply system defaults and dock settings -./bin/dotfiles update # Update submodules and push changes -./bin/dotfiles clean # Clean up caches (brew, npm, etc.) -``` - -### Selective Installation -```bash -./bin/dotfiles install --hosts # Update /etc/hosts with ad-blocking -./bin/dotfiles install --prezto # Install Prezto zsh framework -./bin/dotfiles install --vim # Install vim plugins -./bin/dotfiles install --fonts # Install powerline fonts -./bin/dotfiles install --packages # Install brew/cask/npm/mas/vscode packages -./bin/dotfiles install --launchagents # Install LaunchAgents (mackup auto-backup) -./bin/dotfiles install --ssh # Generate SSH key -./bin/dotfiles install --passwordless # Make sudo passwordless -``` - -### Configuration -```bash -./bin/dotfiles configure --defaults # Apply macOS system defaults -./bin/dotfiles configure --dock # Configure Dock settings -``` - -### Updates and Maintenance -```bash -./bin/dotfiles update --system # Update OS, brew, npm, gem packages -./bin/dotfiles clean # Clean homebrew and npm caches -``` - -## Architecture - -### Directory Structure - -- **`bin/`** - Utility scripts and main `dotfiles` command - - `dotfiles` - Main entry point for all operations - - `is-*` - Helper scripts for system detection (Apple Silicon, macOS, etc.) - - `command-exists` - Check if command is available - -- **`scripts/`** - Core installation and utility scripts - - `echos.sh` - Colorized output functions (bot, ok, error, etc.) - - `requirers.sh` - Package installation helpers (require_brew, require_cask, require_npm, etc.) - - `install_prezto.zsh` - Prezto framework setup - -- **`packages/`** - Package list files - - `brew.list` - Homebrew CLI utilities - - `cask.list` - GUI applications via Homebrew Cask - - `npm.list` - Global npm packages - - `mas.list` - Mac App Store applications - - `code.list` - VS Code extensions - - `tap.list` - Homebrew taps - -- **`macos/`** - macOS system configuration scripts - - `defaults.sh` - Main system preferences - - `defaults-*.sh` - App-specific settings (Safari, Chrome, Xcode, etc.) - - `dock.sh` - Dock configuration - -- **`runcom/`** - Dotfiles symlinked to `~/` using GNU Stow - - `.zshrc`, `.zprofile`, `.zlogin` - Zsh configuration - - `.vimrc` - Vim configuration - - `.gemrc`, `.mackup.cfg` - Tool configurations - - `.zpreztorc` - Prezto configuration - -- **`config/`** - XDG config files symlinked to `~/.config/` using GNU Stow - - Contains application-specific configurations (Karabiner, Spicetify, etc.) - -- **`modules/`** - Git submodules for external dependencies - - `prezto/` - Prezto zsh framework - - `prezto-contrib/` - Additional Prezto modules - - `zsh/` - Additional zsh plugins (zsh-autocomplete, zsh-thefuck, zsh-lazy-load) - - `stevenblack-hosts/` - Unified hosts file for ad-blocking - -- **`system/`** - System-level configuration files - - `hosts.whitelist` - Whitelist for domains that should not be blocked by hosts file - -- **`apps/`** - Application-specific themes and configurations - - `terminal/` - Terminal.app Nord theme - - `xcode/` - Xcode Nord theme - - `warp/` - Warp terminal themes (base16 and others) - - `gitkraken/` - GitKraken custom themes - - `vlc/` - VLC settings and preferences - -- **`fonts/`** - Powerline fonts for terminal - - `install.sh` - Font installation script - -- **`launchagents/`** - macOS LaunchAgents for automated tasks - - `com.user.mackup-auto.plist` - Auto-backup mackup settings every hour - -### Key Technical Details - -**Package Management Flow:** -The `install_packages()` function in `bin/dotfiles` reads package lists and uses corresponding `require_*` functions from `scripts/requirers.sh`. Each package type has idempotent installation logic that checks if already installed before attempting installation. - -**Dotfile Linking:** -Uses GNU Stow for symlink management. Running `./bin/dotfiles link` will: -1. Backup existing dotfiles to `~/.dotfiles_backup/$(date)` -2. Stow `runcom/` directory to `~/` -3. Stow `config/` directory to `~/.config/` - -**Node.js Management:** -Switched from NVM to FNM (Fast Node Manager) for better performance. Functions `require_fnm()` and `source_fnm()` handle Node.js version management. - -**System Detection:** -The repository supports both Intel and Apple Silicon Macs via `bin/is-apple-silicon`. Homebrew prefix is automatically detected as `/opt/homebrew` (Apple Silicon) or `/usr/local` (Intel). - -**Interactive Installation:** -Most commands prompt for confirmation before making changes. The `--all` flag bypasses prompts for automated setup. - -## Important Notes - -- The installation process will open Warp terminal and close Terminal.app at the end -- Original dotfiles are backed up to `~/.dotfiles_backup/` with timestamp before being replaced -- The `/etc/hosts` installation uses StevenBlack's unified hosts file with Python virtual environment to avoid system-level pip pollution -- Custom whitelist support: Add domains to `system/hosts.whitelist` (one per line) to prevent them from being blocked. The whitelist is copied to the stevenblack-hosts module as `whitelist` during the hosts installation process -- Submodules must be initialized: `git submodule update --init --recursive` -- The repository uses Prezto instead of Oh My Zsh for better performance -- The shell prompt uses Starship for cross-shell prompt customization -- Node.js version management uses FNM (Fast Node Manager) instead of NVM for better performance -- System defaults require logout/restart to take full effect -- Vim uses Vundle for plugin management (stored as a git submodule) -- LaunchAgents automate mackup backups every hour (logs: `/tmp/mackup.log` and `/tmp/mackup.err`) - -## Common Workflows - -**Adding a new package:** -1. Add package name to appropriate file in `packages/` -2. Run `./bin/dotfiles install --packages` - -**Updating submodules:** -```bash -./bin/dotfiles update # Interactive - prompts for commit message -# Or manually: -git submodule update --remote --recursive --merge -``` - -**Restoring old dotfiles:** -```bash -./bin/dotfiles unlink YYYY.MM.DD.HH.MM.SS -``` - -**Modifying system defaults:** -Edit appropriate script in `macos/` directory, then run: -```bash -./bin/dotfiles configure --defaults -``` - -**Managing hosts whitelist:** -Add domains to whitelist to prevent them from being blocked: -```bash -# Add domain to whitelist -echo "example.com" >> system/hosts.whitelist - -# Reinstall hosts file with updated whitelist -./bin/dotfiles install --hosts -``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index c3b7e54..696cd5c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ You don't need to install or configure anything upfront! This works with a brand -- [\\[._.]/ - Hi, I'm the MacOS bot](#_---hi-im-the-macos-bot) +- [\\[.\_.]/ - Hi, I'm the MacOS bot](#_---hi-im-the-macos-bot) - [Forget About Manual Configuration](#forget-about-manual-configuration) - [Installation](#installation) - [Restoring Dotfiles](#restoring-dotfiles) @@ -33,8 +33,6 @@ You don't need to install or configure anything upfront! This works with a brand - [Photos](#photos) - [Safari & WebKit](#safari--webkit) - [Google Chrome & Google Chrome Canary](#google-chrome--google-chrome-canary) - - [Twitter](#twitter) - - [Tweetbot](#tweetbot) - [VLC](#vlc) - [Xcode](#xcode) - [Visual Studio Code](#visual-studio-code) @@ -64,9 +62,10 @@ Don't you hate getting a new laptop or joining a new team and then spending a wh When I finish with your machine, you will have a fully configured development environment with a modern terminal emulator (Warp) and a customizable shell prompt. -![iTerm Screenshot](./resources/terminal.png) +![Terminal Screenshot](./resources/terminal.png) The shell prompt uses Starship for cross-shell prompt customization, displaying useful information like: + - Current directory path - Git branch and status - Node.js version (via FNM) @@ -76,6 +75,7 @@ The shell prompt uses Starship for cross-shell prompt customization, displaying The dotfiles configure Vim as a terminal-based IDE using Vundle for plugin management. Modern terminal features: + - Warp: AI-powered terminal with native text editing - Full-screen mode: `Command + Enter` @@ -149,7 +149,7 @@ Here is the current list: The following will only happen if you agree on the prompt - make sudo command passwordless -- overwrite your /etc/hosts file with a copy from someonewhocares.org (supports custom whitelist via `system/hosts.whitelist`) +- overwrite your /etc/hosts file with StevenBlack's unified hosts file for ad/tracker blocking (supports custom whitelist via `system/hosts.whitelist`) - install prezto zsh framework - link dotfiles - install vim plugins/themes @@ -379,20 +379,6 @@ The following will only happen if you agree on the prompt - Use the system-native print preview dialog - Expand the print dialog by default -### Twitter - -- Disable smart quotes as it’s annoying for code tweets -- Show the app window when clicking the menu bar icon -- Enable the hidden ‘Develop’ menu -- Open links in the background -- Allow closing the ‘new tweet’ window by pressing $(Esc) -- Show full names rather than Twitter handles -- Hide the app in the background if it’s not the front-most window - -### Tweetbot - -- Bypass the annoyingly slow t.co URL shortener - ### VLC - Install settings @@ -452,19 +438,17 @@ The following is the software installed by default: ### Taps - Homebrew/Bundle -- Homebrew/Cask -- Homebrew/Cask Drivers -- Homebrew/Cask Versions -- Homebrew/Cask Fonts -- Homebrew/Core -- Homebrew/Dupes - Homebrew/Services +- goreleaser/tap +- khanakia/vercelgate - Khanhas/Tap -- Mongodb/Brew +- lotyp/formulae +- artginzburg/tap ### Utilities **Core/Dotfiles:** + - ack, ag (the_silver_searcher) - bats-core - coreutils, dos2unix @@ -476,6 +460,7 @@ The following is the software installed by default: - topgrade (universal package upgrader) **File System/Network:** + - bat (cat replacement) - croc (file transfer) - eza (ls replacement) @@ -486,6 +471,7 @@ The following is the software installed by default: - zoxide (cd replacement) **Search/Grep/Diff:** + - fd (find replacement) - findutils - fzf (fuzzy finder) @@ -499,6 +485,7 @@ The following is the software installed by default: - ripgrep (fast grep) **Languages/Tools:** + - cmake - gh (GitHub CLI) - go @@ -506,6 +493,7 @@ The following is the software installed by default: - shellcheck, shfmt **Media:** + - ffmpeg - imagemagick - optipng @@ -514,16 +502,19 @@ The following is the software installed by default: - youtube-dl **Misc:** + - grip (GitHub README preview) - sudo-touchid ### Apps **Creative & Design:** + - Adobe Creative Cloud - Figma **Development:** + - Arduino, Arduino IDE - Autodesk Fusion - GitKraken @@ -531,16 +522,19 @@ The following is the software installed by default: - Visual Studio Code **Browsers:** + - Brave Browser - Firefox - Google Chrome **Media:** + - IINA (modern media player) - Spotify - VLC **Utilities:** + - AltServer - AnyDesk - Apparency @@ -567,9 +561,11 @@ The following is the software installed by default: - Zoom **Terminals:** + - Warp **Fonts:** + - Font Awesome Terminal Fonts - Font Fira Code - Font Fira Mono @@ -587,6 +583,7 @@ The following is the software installed by default: - Font Roboto Mono Nerd Font **QuickLook Plugins:** + - qlmarkdown - qlstephen - qlvideo @@ -605,7 +602,6 @@ The following is the software installed by default: - Paste - Spark - Trello -- Twitter ### VS Code Extensions @@ -620,7 +616,6 @@ The following is the software installed by default: - Multi Line Tricks - StandardJS - Npm Intellisense -- Bracket Pair Colorizer 2 - Markdown Lint - Eslint - Githistory @@ -649,7 +644,6 @@ The following is the software installed by default: - Savebackup - Quicktype - Sort Json -- Code Settings Sync - Brewfile - Autoimport - Open In Browser @@ -664,16 +658,19 @@ The following is the software installed by default: ### Node Packages **AI Tools:** + - @anthropic-ai/claude-code - @google/gemini-cli **Package Management:** + - npm (latest) - pnpm - yarn - corepack **Development Tools:** + - eslint - prettier - detect-circular-deps @@ -681,22 +678,27 @@ The following is the software installed by default: - tsx (TypeScript executor) **CLI Utilities:** + - fkill-cli (kill processes) - get-port-cli - fast-cli (internet speed test) - gtop (system monitoring) **Build Tools:** + - grunt-cli - gulp-cli **Maintenance:** + - npm-check-updates **Publishing:** + - release-it **Misc:** + - instant-markdown-d - local-web-server - svgo (SVG optimizer) diff --git a/apps/gitkraken/profiles/d6e5a8ca26e14325a4275fc33b17e16f/profile b/apps/gitkraken/profiles/d6e5a8ca26e14325a4275fc33b17e16f/profile index 25f3dac..36004dd 100644 --- a/apps/gitkraken/profiles/d6e5a8ca26e14325a4275fc33b17e16f/profile +++ b/apps/gitkraken/profiles/d6e5a8ca26e14325a4275fc33b17e16f/profile @@ -6,7 +6,7 @@ }, "REPO_MANAGEMENT": {} }, - "selectedTabId": "f8f842cb-7fa8-4fb9-9585-b8c61d3273f3", + "selectedTabId": "75e5286a-2a98-479c-a0db-966e5aa5998b", "tabs": [ { "id": "f8f842cb-7fa8-4fb9-9585-b8c61d3273f3", @@ -22,13 +22,6 @@ "type": "REPO", "repoName": "frontend", "repoPath": "/Users/stix/Work/mote/frontend/", - "skipWSLChecks": null - }, - { - "id": "ab5ca97e-3e63-465f-a888-d697a85acdef", - "type": "REPO", - "repoName": "backend", - "repoPath": "/Users/stix/Work/mote/backend/", "skipWSLChecks": false }, { @@ -38,13 +31,6 @@ "repoPath": "/Users/stix/Projects/electricity_partial_bill_calculator/", "skipWSLChecks": false }, - { - "id": "30193ee8-2be7-4e9f-bdc4-3a46978f5326", - "type": "REPO", - "repoName": "name_picker_roulette", - "repoPath": "/Users/stix/Projects/name_picker_roulette/", - "skipWSLChecks": false - }, { "id": "3ca417fc-99d0-4f3e-bb41-e4f2b1d379e4", "type": "REPO", @@ -90,6 +76,46 @@ "repoName": "raflix", "repoPath": "/Users/stix/Work/raflix/", "skipWSLChecks": null + }, + { + "id": "a0d6e1ea-112b-448d-b0d8-aa33e79934a3", + "isWorktree": false, + "type": "REPO", + "repoName": "swizzin-scripts", + "repoPath": "/Users/stix/Projects/swizzin-scripts/", + "skipWSLChecks": null + }, + { + "id": "b6a9f6d2-1892-4188-8f72-0af2ec713c69", + "isWorktree": false, + "type": "REPO", + "repoName": "decypharr", + "repoPath": "/Users/stix/Projects/decypharr/", + "skipWSLChecks": null + }, + { + "id": "a9ac5e37-be54-4bbc-a85c-969e3095d395", + "isWorktree": false, + "type": "REPO", + "repoName": "taggarr", + "repoPath": "/Users/stix/Projects/taggarr/", + "skipWSLChecks": null + }, + { + "id": "75e5286a-2a98-479c-a0db-966e5aa5998b", + "isWorktree": false, + "type": "REPO", + "repoName": "thesis_v2", + "repoPath": "/Users/stix/Projects/thesis_v2/", + "skipWSLChecks": null + }, + { + "id": "f2895c08-df75-4d89-9cec-308049666502", + "isWorktree": false, + "type": "REPO", + "repoName": "webflow-cms-manager", + "repoPath": "/Users/stix/Work/mote/webflow-cms-manager/", + "skipWSLChecks": null } ] }, @@ -139,7 +165,7 @@ "zoom": 1, "projects": {}, "ui": { - "theme": "nord-dark.jsonc", + "theme": "dark", "repoManagementTab": { "orderedSectionIds": [], "sectionsById": {} @@ -197,5 +223,10 @@ }, "ai": { "sendCodeApproved": true + }, + "actions": { + "commit": { + "ignoreWhiteSpace": false + } } } diff --git a/bin/dotfiles b/bin/dotfiles index 7d54f74..7d80346 100755 --- a/bin/dotfiles +++ b/bin/dotfiles @@ -21,13 +21,20 @@ sub_help() { echo -e "\nUsage: $BIN_NAME " echo echo "Commands:" + echo " cheatsheet Show aliases and functions cheatsheet" echo " clean Clean up caches (brew, nvm, gem)" echo " configure Configure system (defaults, dock)" + echo " doctor Diagnose common issues" echo " edit Open dotfiles in IDE ($DOTFILES_IDE)" echo " help This help message" + echo " hooks Install git pre-commit hooks" echo " install Bootstrap system" echo " link Link dotfiles to ~/" echo " open Open dotfiles in Finder" + echo " profiler Profile shell startup time" + echo " secrets Manage secrets (macOS Keychain)" + echo " setup Run interactive setup wizard" + echo " test Run test suite to validate configuration" echo " unlink Restore dotfiles from ~/.dotfiles_backup" echo " update Update dotfiles (submodules)" } @@ -67,9 +74,8 @@ sub_update_help() { sub_install() { bot "Hi! I'm going to install tooling and tweak your system settings. Here I go..." - grep -q 'NOPASSWD: ALL' /etc/sudoers.d/$LOGNAME >/dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "no suder file" + if ! grep -q 'NOPASSWD: ALL' "/etc/sudoers.d/$LOGNAME" 2>/dev/null; then + echo "no sudoer file" sudo -v while true; do @@ -101,8 +107,7 @@ sub_install() { running "checking homebrew..." if ! command-exists brew; then action "installing homebrew" - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - if [[ $? != 0 ]]; then + if ! /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; then error "unable to install homebrew, script $0 abort!" exit 2 fi @@ -180,7 +185,7 @@ sub_install_passwordless() { if ! grep -q "#includedir /private/etc/sudoers.d" /etc/sudoers; then echo '#includedir /private/etc/sudoers.d' | sudo tee -a /etc/sudoers >/dev/null fi - echo -e "Defaults:$LOGNAME !requiretty\n$LOGNAME ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/$LOGNAME + echo -e "Defaults:$LOGNAME !requiretty\n$LOGNAME ALL=(ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/$LOGNAME" bot "You can now run sudo commands without password!" fi } @@ -199,7 +204,7 @@ sub_install_ssh() { rm -rf "$HOME/.ssh/id_ed25519.pub" action "Generating a new SSH key" read -r -p "Please enter an email to associate your ssh key: " ssh_email - ssh-keygen -t ed25519 -C $ssh_email -f "$HOME/.ssh/id_ed25519" + ssh-keygen -t ed25519 -C "$ssh_email" -f "$HOME/.ssh/id_ed25519" ok else skip @@ -207,9 +212,10 @@ sub_install_ssh() { action "Adding SSH key to SSH Agent" eval "$(ssh-agent -s)" + [[ -f "$HOME/.ssh/config" ]] && cp "$HOME/.ssh/config" "$HOME/.ssh/config.backup.$(date +%Y%m%d%H%M%S)" touch "$HOME/.ssh/config" echo -e "Host *\n AddKeysToAgent yes\n UseKeychain yes\n IdentityFile ~/.ssh/id_ed25519" | tee "$HOME/.ssh/config" - ssh-add -K "$HOME/.ssh/id_ed25519" + ssh-add --apple-use-keychain "$HOME/.ssh/id_ed25519" ok echo -e "\nNote: You should add your SSH key to your github account by first running:\n" @@ -258,9 +264,7 @@ sub_install_hosts() { require_brew python fi if ! command -v pip3 >/dev/null 2>&1; then - curl -fsSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py - sudo python3 get-pip.py - rm get-pip.py + python3 -m ensurepip --upgrade 2>/dev/null || true fi action "installing python dependencies from stevenblack-hosts requirements.txt" # Create a virtual environment for stevenblack-hosts @@ -328,10 +332,58 @@ install_packages() { sub_install_packages() { bot "Packages install\n" - read -r -p "Install packages/tools/apps? [y|N] " response - if [[ $response =~ (y|yes|Y) ]]; then - pushd "$ROOT_DIR" >/dev/null 2>&1 || exit + pushd "$ROOT_DIR" >/dev/null 2>&1 || exit + + # Check if Brewfile exists (preferred method) + if [[ -f "Brewfile" ]]; then + read -r -p "Install packages using Brewfile (brew bundle)? [y|N] " response + if [[ $response =~ (y|yes|Y) ]]; then + action "Installing packages via brew bundle..." + brew bundle install --file=Brewfile + ok "Homebrew packages installed" + + # Handle npm packages separately (not in Brewfile) + if [[ -f "packages/npm.list" ]]; then + read -r -p "Install NPM global packages? [y|N] " npm_response + if [[ $npm_response =~ (y|yes|Y) ]]; then + action "Installing NPM packages..." + while IFS= read -r package || [[ -n "$package" ]]; do + [[ $package =~ ^[[:space:]]*# ]] && continue + [[ -z "${package// /}" ]] && continue + require_npm "$package" + done <"packages/npm.list" + ok "NPM packages installed" + fi + fi + + # Handle VS Code extensions separately + if [[ -f "packages/code.list" ]]; then + read -r -p "Install VS Code extensions? [y|N] " code_response + if [[ $code_response =~ (y|yes|Y) ]]; then + action "Installing VS Code extensions..." + while IFS= read -r extension || [[ -n "$extension" ]]; do + [[ $extension =~ ^[[:space:]]*# ]] && continue + [[ -z "${extension// /}" ]] && continue + require_code "$extension" + done <"packages/code.list" + ok "VS Code extensions installed" + fi + fi + + running "cleanup homebrew" + brew cleanup --force >/dev/null 2>&1 + rm -f -r /Library/Caches/Homebrew/* >/dev/null 2>&1 + xattr -d -r com.apple.quarantine "$HOME/Library/QuickLook" 2>/dev/null + popd >/dev/null 2>&1 || exit + ok + return + fi + fi + + # Fallback to legacy .list files + read -r -p "Install packages using legacy .list files? [y|N] " response + if [[ $response =~ (y|yes|Y) ]]; then install_packages "tap.list" "require_tap" "Homebrew taps" install_packages "brew.list" "require_brew" "Homebrew utilities" install_packages "cask.list" "require_cask" "Homebrew desktop apps" @@ -342,7 +394,7 @@ sub_install_packages() { running "cleanup homebrew" brew cleanup --force >/dev/null 2>&1 rm -f -r /Library/Caches/Homebrew/* >/dev/null 2>&1 - xattr -d -r com.apple.quarantine "$HOME/Library/QuickLook" + xattr -d -r com.apple.quarantine "$HOME/Library/QuickLook" 2>/dev/null popd >/dev/null 2>&1 || exit ok else @@ -382,7 +434,7 @@ sub_install_launchagents() { sub_configure() { read -r -p "Do you want to update the system configurations? [y|N] " response - if [[ -z $response || $response =~ ^(y|Y) ]]; then + if [[ $response =~ ^(y|Y) ]]; then echo $0 configure --defaults $0 configure --dock @@ -395,7 +447,7 @@ sub_configure_defaults() { bot "OS Configuration\n" read -r -p "Do you want to update the system defaults? [y|N] " response - if [[ -z $response || $response =~ ^(y|Y) ]]; then + if [[ $response =~ ^(y|Y) ]]; then shopt -s nullglob for DEFAULTS_FILE in "$DOTFILES_DIR"/macos/defaults*.sh; do @@ -413,7 +465,7 @@ sub_configure_dock() { bot "Dock Configuration\n" read -r -p "Do you want to update the dock configuration? [y|N] " response - if [[ -z $response || $response =~ ^(y|Y) ]]; then + if [[ $response =~ ^(y|Y) ]]; then . "$DOTFILES_DIR/macos/dock.sh" ok "Dock reloaded!" @@ -483,7 +535,7 @@ sub_unlink() { fi if [[ -e ./$file ]]; then - mv "./$file" ./ + mv "./$file" "$HOME/" echo -en "$1 backup restored" ok fi @@ -570,7 +622,98 @@ sub_open() { } sub_edit() { - sh -c "$DOTFILES_IDE $DOTFILES_DIR" + "$DOTFILES_IDE" "$DOTFILES_DIR" +} + +sub_test() { + "$DOTFILES_DIR/bin/dotfiles-test" "$@" +} + +sub_test_help() { + echo -e "\nUsage: $BIN_NAME test [options]" + echo + echo "Options:" + echo " --verbose, -v Show detailed output for each test" + echo " --quick, -q Skip slow tests (shell startup timing)" + echo " --help, -h Show this help message" +} + +sub_doctor() { + "$DOTFILES_DIR/bin/dotfiles-doctor" "$@" +} + +sub_doctor_help() { + echo -e "\nUsage: $BIN_NAME doctor [options]" + echo + echo "Options:" + echo " --fix Attempt to automatically fix issues" + echo " --help, -h Show this help message" +} + +sub_hooks() { + bot "Git Hooks Setup\n" + action "Configuring git to use .githooks directory..." + git -C "$DOTFILES_DIR" config core.hooksPath .githooks + ok "Git hooks installed!" + echo "" + echo "The following hooks are now active:" + echo " - pre-commit: Validates shell syntax and runs shellcheck" +} + +sub_profiler() { + "$DOTFILES_DIR/bin/dotfiles-profiler" "$@" +} + +sub_profiler_help() { + echo -e "\nUsage: $BIN_NAME profiler [options]" + echo + echo "Options:" + echo " --detailed, -d Show detailed breakdown of sourced files" + echo " --compare, -c Compare startup with/without caches" + echo " --help, -h Show this help message" +} + +sub_cheatsheet() { + "$DOTFILES_DIR/bin/dotfiles-cheatsheet" "$@" +} + +sub_cheatsheet_help() { + echo -e "\nUsage: $BIN_NAME cheatsheet [options]" + echo + echo "Options:" + echo " --aliases, -a Show only aliases" + echo " --functions, -f Show only functions" + echo " --search, -s Search for specific command" + echo " --markdown, -m Output in markdown format" + echo " --help, -h Show this help message" +} + +sub_secrets() { + "$DOTFILES_DIR/bin/dotfiles-secrets" "$@" +} + +sub_secrets_help() { + echo -e "\nUsage: $BIN_NAME secrets [options]" + echo + echo "Commands:" + echo " set Store a secret in Keychain" + echo " get Retrieve a secret" + echo " delete Delete a secret" + echo " list List all dotfiles secrets" + echo " export Export secrets to encrypted file" + echo " import Import secrets from encrypted file" + echo " --help, -h Show detailed help message" +} + +sub_setup() { + "$DOTFILES_DIR/bin/dotfiles-setup" "$@" +} + +sub_setup_help() { + echo -e "\nUsage: $BIN_NAME setup" + echo + echo "Run the interactive setup wizard to install and configure dotfiles." + echo "This is recommended for first-time setup on a new machine." } cmd_error() { @@ -592,15 +735,15 @@ case $COMMAND_NAME in "" | "help") sub_help ;; -"unlink") +"unlink" | "test" | "doctor" | "profiler" | "cheatsheet" | "setup" | "secrets") shift - sub_unlink "$@" + sub_"$COMMAND_NAME" "$@" ;; *) if [[ -n $SUB_COMMAND_NAME ]]; then case $SUB_COMMAND_NAME in --"${STRIPPED_SUB_COMMAND_NAME}") - sub_${COMMAND_NAME}_${STRIPPED_SUB_COMMAND_NAME} + "sub_${COMMAND_NAME}_${STRIPPED_SUB_COMMAND_NAME}" if [ $? = 127 ]; then subcmd_error fi @@ -610,7 +753,7 @@ case $COMMAND_NAME in ;; esac else - sub_"${COMMAND_NAME}" + "sub_${COMMAND_NAME}" if [ $? = 127 ]; then cmd_error fi diff --git a/bin/dotfiles-cheatsheet b/bin/dotfiles-cheatsheet new file mode 100755 index 0000000..04e5d6c --- /dev/null +++ b/bin/dotfiles-cheatsheet @@ -0,0 +1,397 @@ +#!/usr/bin/env zsh +# +# dotfiles-cheatsheet - Display aliases and functions cheatsheet +# +# Usage: dotfiles-cheatsheet [--aliases] [--functions] [--search TERM] [--markdown] +# +# Options: +# --aliases, -a Show only aliases +# --functions, -f Show only functions +# --search, -s TERM Search for specific command +# --markdown, -m Output in markdown format +# + +set -o pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +DIM='\033[2m' +BOLD='\033[1m' +NC='\033[0m' + +SHOW_ALIASES=true +SHOW_FUNCTIONS=true +SEARCH_TERM="" +MARKDOWN=false + +while [[ $# -gt 0 ]]; do + case $1 in + --search|-s) + shift + SEARCH_TERM="${1:-}" + ;; + --aliases|-a) + SHOW_ALIASES=true + SHOW_FUNCTIONS=false + ;; + --functions|-f) + SHOW_FUNCTIONS=true + SHOW_ALIASES=false + ;; + --markdown|-m) + MARKDOWN=true + ;; + --verbose|-v) + VERBOSE=1 + ;; + --help|-h) + echo "Usage: dotfiles-cheatsheet [--aliases] [--functions] [--search TERM] [--markdown]" + echo "" + echo "Options:" + echo " --aliases, -a Show only aliases" + echo " --functions, -f Show only functions" + echo " --search, -s TERM Search for specific command" + echo " --markdown, -m Output in markdown format" + exit 0 + ;; + esac + shift +done + +DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" + +############################################################################# +# Parsing Functions +############################################################################# + +# Extract aliases with their comments +parse_aliases() { + local file="$1" + local category="${2:-General}" + local current_comment="" + + while IFS= read -r line; do + # Capture comments that precede aliases + if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*(.*) ]]; then + # Skip shebang and file header comments + [[ "$line" =~ ^#! ]] && continue + [[ "$line" =~ ^#-+ ]] && continue + current_comment="${match[1]}" + continue + fi + + # Match alias definitions + local alias_pattern='^[[:space:]]*alias[[:space:]]+([^=]+)=['"'"'"]?(.*)$' + if [[ "$line" =~ $alias_pattern ]]; then + local name="${match[1]}" + local value="${match[2]}" + # Clean up the value + value="${value%\"}" + value="${value%\'}" + value="${value#\"}" + value="${value#\'}" + + # Apply search filter + if [[ -n "$SEARCH_TERM" ]]; then + if [[ ! "$name" =~ "$SEARCH_TERM" ]] && [[ ! "$value" =~ "$SEARCH_TERM" ]] && [[ ! "$current_comment" =~ "$SEARCH_TERM" ]]; then + current_comment="" + continue + fi + fi + + echo "$category|$name|$value|$current_comment" + current_comment="" + else + # Reset comment if line is not an alias + [[ -n "$line" && ! "$line" =~ ^[[:space:]]*$ ]] && current_comment="" + fi + done < "$file" +} + +# Extract functions with their comments +parse_functions() { + local file="$1" + local category="${2:-General}" + local current_comment="" + local in_function=false + local func_name="" + local func_body="" + local brace_count=0 + + while IFS= read -r line; do + # Capture comments that precede functions + if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*(.*) ]] && ! $in_function; then + [[ "$line" =~ ^#! ]] && continue + [[ "$line" =~ ^#-+ ]] && continue + current_comment="${match[1]}" + continue + fi + + # Match function definitions: func_name() { or function func_name { + if ! $in_function; then + if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_-]*)\(\)[[:space:]]*\{?[[:space:]]*$ ]] || \ + [[ "$line" =~ ^[[:space:]]*function[[:space:]]+([a-zA-Z_][a-zA-Z0-9_-]*)[[:space:]]*\{?[[:space:]]*$ ]]; then + func_name="${match[1]}" + in_function=true + brace_count=1 + func_body="" + [[ "$line" =~ \{ ]] || brace_count=0 + continue + fi + fi + + # Track function body + if $in_function; then + # Count braces + local open_braces="${line//[^\{]/}" + local close_braces="${line//[^\}]/}" + brace_count=$((brace_count + ${#open_braces} - ${#close_braces})) + + # Accumulate function body (first few lines for description) + if [[ ${#func_body} -lt 200 ]]; then + func_body+="$line"$'\n' + fi + + # Function ended + if [[ $brace_count -le 0 ]]; then + in_function=false + + # Skip internal/private functions (starting with _) + if [[ "$func_name" =~ ^_ ]]; then + current_comment="" + continue + fi + + # Apply search filter + if [[ -n "$SEARCH_TERM" ]]; then + if [[ ! "$func_name" =~ "$SEARCH_TERM" ]] && [[ ! "$current_comment" =~ "$SEARCH_TERM" ]]; then + current_comment="" + continue + fi + fi + + echo "$category|$func_name|$current_comment" + current_comment="" + fi + else + # Reset comment if line is not a function start + [[ -n "$line" && ! "$line" =~ ^[[:space:]]*$ ]] && current_comment="" + fi + done < "$file" +} + +############################################################################# +# Display Functions +############################################################################# + +print_header() { + local title="$1" + if $MARKDOWN; then + echo "" + echo "## $title" + echo "" + else + echo "" + echo -e "${BLUE}━━━ $title ━━━${NC}" + echo "" + fi +} + +print_subheader() { + local title="$1" + if $MARKDOWN; then + echo "" + echo "### $title" + echo "" + else + echo -e "\n${CYAN}▸ $title${NC}\n" + fi +} + +print_alias() { + local name="$1" + local value="$2" + local comment="$3" + + if $MARKDOWN; then + if [[ -n "$comment" ]]; then + echo "- \`$name\` → \`$value\` - $comment" + else + echo "- \`$name\` → \`$value\`" + fi + else + if [[ -n "$comment" ]]; then + printf " ${GREEN}%-20s${NC} ${DIM}→${NC} %-40s ${DIM}# %s${NC}\n" "$name" "$value" "$comment" + else + printf " ${GREEN}%-20s${NC} ${DIM}→${NC} %s\n" "$name" "$value" + fi + fi +} + +print_function() { + local name="$1" + local description="$2" + + if $MARKDOWN; then + if [[ -n "$description" ]]; then + echo "- \`$name\` - $description" + else + echo "- \`$name\`" + fi + else + if [[ -n "$description" ]]; then + printf " ${MAGENTA}%-25s${NC} ${DIM}%s${NC}\n" "$name" "$description" + else + printf " ${MAGENTA}%s${NC}\n" "$name" + fi + fi +} + +############################################################################# +# Main +############################################################################# + +main() { + if ! $MARKDOWN; then + echo "" + echo -e "${BLUE}╔════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Dotfiles Cheatsheet ║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════╝${NC}" + else + echo "# Dotfiles Cheatsheet" + fi + + if [[ -n "$SEARCH_TERM" ]]; then + if $MARKDOWN; then + echo "" + echo "_Searching for: $SEARCH_TERM_" + else + echo -e "\n${DIM}Searching for: ${YELLOW}$SEARCH_TERM${NC}\n" + fi + fi + + #--------------------------------------------------------------------------- + # Aliases + #--------------------------------------------------------------------------- + if $SHOW_ALIASES; then + print_header "Aliases" + + # Navigation & Files + if [[ -f "$DOTFILES_DIR/system/.alias" ]]; then + local aliases_data=$(parse_aliases "$DOTFILES_DIR/system/.alias" "General") + + if [[ -n "$aliases_data" ]]; then + # Group by common patterns + local nav_aliases=$(echo "$aliases_data" | grep -E '\|cd|\|ls|\|ll|\|la|\|\.\.' || true) + local git_aliases=$(echo "$aliases_data" | grep -E '\|g[a-z]|\|git' || true) + local editor_aliases=$(echo "$aliases_data" | grep -E '\|vim|\|vi|\|e\||\|edit' || true) + local util_aliases=$(echo "$aliases_data" | grep -vE '\|cd|\|ls|\|ll|\|la|\|\.\.|\|g[a-z]|\|git|\|vim|\|vi|\|e\||\|edit' || true) + + if [[ -n "$nav_aliases" ]]; then + print_subheader "Navigation & Files" + echo "$nav_aliases" | while IFS='|' read -r cat name value comment; do + print_alias "$name" "$value" "$comment" + done + fi + + if [[ -n "$git_aliases" ]]; then + print_subheader "Git" + echo "$git_aliases" | while IFS='|' read -r cat name value comment; do + print_alias "$name" "$value" "$comment" + done + fi + + if [[ -n "$editor_aliases" ]]; then + print_subheader "Editors" + echo "$editor_aliases" | while IFS='|' read -r cat name value comment; do + print_alias "$name" "$value" "$comment" + done + fi + + if [[ -n "$util_aliases" ]]; then + print_subheader "Utilities" + echo "$util_aliases" | while IFS='|' read -r cat name value comment; do + print_alias "$name" "$value" "$comment" + done + fi + fi + fi + fi + + #--------------------------------------------------------------------------- + # Functions + #--------------------------------------------------------------------------- + if $SHOW_FUNCTIONS; then + print_header "Functions" + + local function_files=( + "$DOTFILES_DIR/system/.function:General" + "$DOTFILES_DIR/system/.function_fs:Filesystem" + "$DOTFILES_DIR/system/.function_network:Network" + "$DOTFILES_DIR/system/.function_text:Text Processing" + "$DOTFILES_DIR/system/.function_macos:macOS" + "$DOTFILES_DIR/system/.function_fun:Fun" + ) + + for entry in "${function_files[@]}"; do + local file="${entry%%:*}" + local category="${entry##*:}" + + if [[ -f "$file" ]]; then + local funcs_data=$(parse_functions "$file" "$category") + + if [[ -n "$funcs_data" ]]; then + print_subheader "$category" + echo "$funcs_data" | while IFS='|' read -r cat name desc; do + print_function "$name" "$desc" + done + fi + fi + done + fi + + #--------------------------------------------------------------------------- + # Quick Reference + #--------------------------------------------------------------------------- + if ! $MARKDOWN && [[ -z "$SEARCH_TERM" ]]; then + echo "" + echo -e "${BLUE}━━━ Quick Reference ━━━${NC}" + echo "" + echo -e " ${BOLD}Shell:${NC}" + echo -e " ${GREEN}reload${NC} Reload shell configuration" + echo -e " ${GREEN}path${NC} Show PATH entries (one per line)" + echo -e " ${GREEN}h${NC} Search history" + echo "" + echo -e " ${BOLD}Navigation:${NC}" + echo -e " ${GREEN}..${NC} Go up one directory" + echo -e " ${GREEN}...${NC} Go up two directories" + echo -e " ${GREEN}z ${NC} Jump to directory (zoxide)" + echo "" + echo -e " ${BOLD}Files:${NC}" + echo -e " ${GREEN}ll${NC} Long list with details" + echo -e " ${GREEN}la${NC} List all including hidden" + echo -e " ${GREEN}lt${NC} List sorted by time" + echo "" + echo -e " ${BOLD}Git:${NC}" + echo -e " ${GREEN}gs${NC} Git status" + echo -e " ${GREEN}ga${NC} Git add" + echo -e " ${GREEN}gc${NC} Git commit" + echo -e " ${GREEN}gp${NC} Git push" + echo -e " ${GREEN}gl${NC} Git pull" + echo "" + echo -e " ${BOLD}Dotfiles:${NC}" + echo -e " ${GREEN}dotfiles help${NC} Show all commands" + echo -e " ${GREEN}dotfiles doctor${NC} Check system health" + echo -e " ${GREEN}dotfiles test${NC} Run test suite" + echo -e " ${GREEN}dotfiles profiler${NC} Profile shell startup" + fi + + echo "" +} + +main "$@" diff --git a/bin/dotfiles-doctor b/bin/dotfiles-doctor new file mode 100755 index 0000000..8ff7da4 --- /dev/null +++ b/bin/dotfiles-doctor @@ -0,0 +1,375 @@ +#!/usr/bin/env zsh +# +# dotfiles-doctor - Diagnose common dotfiles issues +# +# Usage: dotfiles-doctor [--fix] +# +# Options: +# --fix Attempt to automatically fix issues +# + +set -o pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Options +FIX_MODE=false +[[ "$1" == "--fix" ]] && FIX_MODE=true + +# Get dotfiles directory +DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" +if [[ ! -d "$DOTFILES_DIR" ]]; then + DOTFILES_DIR="$(cd "$(dirname "$0")/.." && pwd)" +fi + +# Counters +ISSUES=0 +WARNINGS=0 +FIXED=0 + +############################################################################# +# Helper Functions +############################################################################# + +ok() { + echo -e "${GREEN}✓${NC} $1" +} + +warn() { + echo -e "${YELLOW}⚠${NC} $1" + ((WARNINGS++)) +} + +error() { + echo -e "${RED}✗${NC} $1" + ((ISSUES++)) +} + +fix() { + echo -e "${BLUE}→${NC} $1" + ((FIXED++)) +} + +section() { + echo "" + echo -e "${BLUE}━━━ $1 ━━━${NC}" +} + +############################################################################# +# Checks +############################################################################# + +check_dotfiles_linked() { + section "Symlink Status" + + local files=(".zshrc" ".zprofile" ".zpreztorc" ".vimrc" ".gitconfig") + local all_linked=true + + for file in "${files[@]}"; do + if [[ -L "$HOME/$file" ]]; then + local target=$(readlink "$HOME/$file") + if [[ "$target" == *"$DOTFILES_DIR"* ]] || [[ "$target" == *".dotfiles"* ]]; then + ok "$file → $target" + else + warn "$file linked to unexpected location: $target" + fi + elif [[ -f "$HOME/$file" ]]; then + error "$file exists but is not a symlink (run: dotfiles link)" + all_linked=false + else + warn "$file not found" + fi + done + + # Check .config directory + if [[ -d "$HOME/.config" ]]; then + local config_dirs=("git" "starship" "karabiner") + for dir in "${config_dirs[@]}"; do + if [[ -L "$HOME/.config/$dir" ]]; then + ok ".config/$dir is linked" + elif [[ -d "$HOME/.config/$dir" ]]; then + warn ".config/$dir exists but is not a symlink" + fi + done + fi +} + +check_submodules() { + section "Git Submodules" + + pushd "$DOTFILES_DIR" >/dev/null 2>&1 + + local submodules=("modules/prezto" "modules/prezto-contrib" "modules/stevenblack-hosts") + for submodule in "${submodules[@]}"; do + if [[ -d "$submodule" && -n "$(ls -A "$submodule" 2>/dev/null)" ]]; then + ok "$submodule initialized" + else + error "$submodule not initialized" + if $FIX_MODE; then + fix "Initializing submodules..." + git submodule update --init --recursive + fi + fi + done + + popd >/dev/null 2>&1 +} + +check_homebrew() { + section "Homebrew" + + if command -v brew &>/dev/null; then + ok "Homebrew installed: $(brew --version | head -1)" + + # Check for issues + local brew_issues=$(brew doctor 2>&1 | grep -c "Warning") + if [[ $brew_issues -gt 0 ]]; then + warn "brew doctor reports $brew_issues warning(s)" + else + ok "brew doctor: no issues" + fi + + # Check outdated packages + local outdated=$(brew outdated --quiet | wc -l | tr -d ' ') + if [[ $outdated -gt 0 ]]; then + warn "$outdated outdated package(s) (run: brew upgrade)" + else + ok "All packages up to date" + fi + else + error "Homebrew not installed" + if $FIX_MODE; then + fix "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + fi + fi +} + +check_shell() { + section "Shell Configuration" + + # Check default shell + if [[ "$SHELL" == *"zsh"* ]]; then + ok "Default shell is zsh" + else + warn "Default shell is not zsh: $SHELL" + if $FIX_MODE; then + fix "Changing default shell to zsh..." + chsh -s $(which zsh) + fi + fi + + # Check ZDOTDIR + if [[ -n "$ZDOTDIR" ]]; then + ok "ZDOTDIR is set: $ZDOTDIR" + fi + + # Check prezto + if [[ -f "$DOTFILES_DIR/modules/prezto/init.zsh" ]]; then + ok "Prezto is available" + else + error "Prezto init.zsh not found" + fi + + # Check starship + if command -v starship &>/dev/null; then + ok "Starship installed: $(starship --version)" + else + warn "Starship not installed" + fi +} + +check_cache() { + section "Cache Status" + + local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}" + local caches=( + "fnm-env.zsh:fnm" + "zoxide-init.zsh:zoxide" + "thefuck-alias.zsh:thefuck" + "fzf-init.zsh:fzf" + "brew-shellenv.zsh:brew" + "npm-completion.zsh:npm" + "dircolors.zsh:dircolors" + ) + + for cache_info in "${caches[@]}"; do + local cache_file="${cache_info%%:*}" + local tool="${cache_info##*:}" + + if [[ -f "$cache_dir/$cache_file" ]]; then + local age=$(( ($(date +%s) - $(stat -f %m "$cache_dir/$cache_file" 2>/dev/null || stat -c %Y "$cache_dir/$cache_file" 2>/dev/null)) / 86400 )) + if [[ $age -gt 7 ]]; then + warn "$cache_file is $age days old (consider refreshing)" + else + ok "$cache_file exists (${age}d old)" + fi + else + if command -v "$tool" &>/dev/null; then + warn "$cache_file missing (will be created on next shell start)" + fi + fi + done + + if $FIX_MODE; then + read -r -p "Regenerate all caches? [y/N] " response + if [[ $response =~ (y|yes|Y) ]]; then + fix "Clearing caches..." + rm -f "$cache_dir"/*.zsh + echo "Caches cleared. Restart your shell to regenerate." + fi + fi +} + +check_tools() { + section "Essential Tools" + + local tools=( + "git:Version control" + "stow:Symlink manager" + "fzf:Fuzzy finder" + "zoxide:Smart cd" + "eza:Modern ls" + "bat:Better cat" + "fd:Better find" + "rg:Ripgrep" + "jq:JSON processor" + "fnm:Node version manager" + ) + + for tool_info in "${tools[@]}"; do + local tool="${tool_info%%:*}" + local desc="${tool_info##*:}" + + if command -v "$tool" &>/dev/null; then + ok "$tool ($desc)" + else + warn "$tool not installed ($desc)" + fi + done +} + +check_node() { + section "Node.js Environment" + + if command -v fnm &>/dev/null; then + ok "fnm installed" + + if command -v node &>/dev/null; then + ok "Node.js: $(node --version)" + ok "npm: $(npm --version)" + else + warn "No Node.js version active (run: fnm install --lts)" + fi + + if command -v pnpm &>/dev/null; then + ok "pnpm: $(pnpm --version)" + else + warn "pnpm not installed" + fi + else + warn "fnm not installed" + fi +} + +check_git_config() { + section "Git Configuration" + + if [[ -n "$(git config --global user.name)" ]]; then + ok "Git user.name: $(git config --global user.name)" + else + warn "Git user.name not configured" + fi + + if [[ -n "$(git config --global user.email)" ]]; then + ok "Git user.email: $(git config --global user.email)" + else + warn "Git user.email not configured" + fi + + if [[ -f "$HOME/.ssh/id_ed25519" ]]; then + ok "SSH key exists" + elif [[ -f "$HOME/.ssh/id_rsa" ]]; then + ok "SSH key exists (RSA - consider upgrading to ed25519)" + else + warn "No SSH key found (run: dotfiles install --ssh)" + fi +} + +check_macos() { + section "macOS Settings" + + if [[ "$(uname)" != "Darwin" ]]; then + ok "Not macOS - skipping" + return + fi + + # Check if Xcode CLI tools are installed + if xcode-select -p &>/dev/null; then + ok "Xcode CLI tools installed" + else + error "Xcode CLI tools not installed" + if $FIX_MODE; then + fix "Installing Xcode CLI tools..." + xcode-select --install + fi + fi + + # Check TouchID for sudo + if grep -q "pam_tid.so" /etc/pam.d/sudo 2>/dev/null; then + ok "TouchID for sudo enabled" + else + warn "TouchID for sudo not enabled" + fi +} + +############################################################################# +# Main +############################################################################# + +main() { + echo "" + echo -e "${BLUE}╔════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Dotfiles Doctor ║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════╝${NC}" + echo "" + echo "Checking: $DOTFILES_DIR" + $FIX_MODE && echo -e "${YELLOW}Fix mode enabled${NC}" + + check_dotfiles_linked + check_submodules + check_shell + check_homebrew + check_cache + check_tools + check_node + check_git_config + check_macos + + # Summary + echo "" + echo -e "${BLUE}━━━ Summary ━━━${NC}" + echo "" + + if [[ $ISSUES -eq 0 && $WARNINGS -eq 0 ]]; then + echo -e "${GREEN}Everything looks good!${NC}" + else + [[ $ISSUES -gt 0 ]] && echo -e "${RED}Issues:${NC} $ISSUES" + [[ $WARNINGS -gt 0 ]] && echo -e "${YELLOW}Warnings:${NC} $WARNINGS" + [[ $FIXED -gt 0 ]] && echo -e "${BLUE}Fixed:${NC} $FIXED" + fi + + echo "" + + if [[ $ISSUES -gt 0 ]]; then + echo "Run with --fix to attempt automatic fixes." + exit 1 + fi +} + +main "$@" diff --git a/bin/dotfiles-profiler b/bin/dotfiles-profiler new file mode 100755 index 0000000..06e78f6 --- /dev/null +++ b/bin/dotfiles-profiler @@ -0,0 +1,299 @@ +#!/usr/bin/env zsh +# +# dotfiles-profiler - Profile shell startup time +# +# Usage: dotfiles-profiler [--detailed] [--compare] +# +# Options: +# --detailed Show detailed breakdown of sourced files +# --compare Compare with/without caches +# + +set -o pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +DIM='\033[2m' +NC='\033[0m' + +DETAILED=false +COMPARE=false + +for arg in "$@"; do + case $arg in + --detailed|-d) DETAILED=true ;; + --compare|-c) COMPARE=true ;; + --help|-h) + echo "Usage: dotfiles-profiler [--detailed] [--compare]" + echo "" + echo "Options:" + echo " --detailed, -d Show detailed breakdown of sourced files" + echo " --compare, -c Compare startup with/without caches" + exit 0 + ;; + esac +done + +DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" + +############################################################################# +# Profiling Functions +############################################################################# + +# Measure basic startup time (average of 5 runs) +measure_startup() { + local total=0 + local runs=5 + local times=() + + for i in $(seq 1 $runs); do + local start=$(perl -MTime::HiRes=time -e 'printf "%.0f\n", time*1000') + zsh -i -c 'exit' 2>/dev/null + local end=$(perl -MTime::HiRes=time -e 'printf "%.0f\n", time*1000') + local duration=$((end - start)) + times+=($duration) + total=$((total + duration)) + done + + local avg=$((total / runs)) + local min=${times[1]} + local max=${times[1]} + + for t in "${times[@]}"; do + [[ $t -lt $min ]] && min=$t + [[ $t -gt $max ]] && max=$t + done + + echo "$avg $min $max" +} + +# Profile with zprof +profile_detailed() { + echo -e "${BLUE}━━━ Detailed Profiling (zprof) ━━━${NC}" + echo "" + + # Create a temporary zshrc that enables profiling + local tmp_zshrc=$(mktemp) + cat > "$tmp_zshrc" << 'EOF' +zmodload zsh/zprof +EOF + cat "$HOME/.zshrc" >> "$tmp_zshrc" + echo "zprof" >> "$tmp_zshrc" + + # Run with profiling + ZDOTDIR=$(dirname "$tmp_zshrc") HOME=$(dirname "$tmp_zshrc") zsh -i -c 'exit' 2>/dev/null | head -40 + + rm -f "$tmp_zshrc" +} + +# Profile individual files +profile_files() { + echo -e "${BLUE}━━━ File Load Times ━━━${NC}" + echo "" + + local files=( + "$DOTFILES_DIR/runcom/.profile" + "$DOTFILES_DIR/system/.function" + "$DOTFILES_DIR/system/.path" + "$DOTFILES_DIR/system/.env" + "$DOTFILES_DIR/system/.alias" + "$DOTFILES_DIR/system/.fnm" + "$DOTFILES_DIR/system/.fzf" + "$DOTFILES_DIR/system/.zoxide" + "$DOTFILES_DIR/system/.fix" + "$DOTFILES_DIR/system/.completion" + "$DOTFILES_DIR/modules/prezto/init.zsh" + ) + + local results=() + + for file in "${files[@]}"; do + if [[ -f "$file" ]]; then + local basename="${file##*/}" + local start=$(perl -MTime::HiRes=time -e 'printf "%.6f\n", time') + + # Source the file in a subshell + ( + export DOTFILES_DIR="$DOTFILES_DIR" + export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" + export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" + source "$DOTFILES_DIR/system/.function" 2>/dev/null + source "$file" 2>/dev/null + ) + + local end=$(perl -MTime::HiRes=time -e 'printf "%.6f\n", time') + local duration=$(echo "($end - $start) * 1000" | bc) + local duration_int=${duration%.*} + + results+=("$duration_int:$basename") + fi + done + + # Sort by time (descending) + printf '%s\n' "${results[@]}" | sort -t: -k1 -nr | while IFS=: read -r time name; do + if [[ $time -gt 50 ]]; then + printf "${RED}%6dms${NC} %s\n" "$time" "$name" + elif [[ $time -gt 20 ]]; then + printf "${YELLOW}%6dms${NC} %s\n" "$time" "$name" + else + printf "${GREEN}%6dms${NC} %s\n" "$time" "$name" + fi + done +} + +# Check cache status +check_caches() { + echo -e "${BLUE}━━━ Cache Status ━━━${NC}" + echo "" + + local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}" + local caches=( + "fnm-env.zsh" + "zoxide-init.zsh" + "thefuck-alias.zsh" + "fzf-init.zsh" + "brew-shellenv.zsh" + "npm-completion.zsh" + "dircolors.zsh" + ) + + for cache in "${caches[@]}"; do + local path="$cache_dir/$cache" + if [[ -f "$path" ]]; then + local size=$(wc -c < "$path" | tr -d ' ') + local age_days=$(( ($(date +%s) - $(stat -f %m "$path" 2>/dev/null || stat -c %Y "$path" 2>/dev/null)) / 86400 )) + printf "${GREEN}✓${NC} %-25s %6d bytes %3dd old\n" "$cache" "$size" "$age_days" + else + printf "${RED}✗${NC} %-25s ${DIM}(missing)${NC}\n" "$cache" + fi + done +} + +# Compare with and without caches +compare_caches() { + echo -e "${BLUE}━━━ Cache Impact Comparison ━━━${NC}" + echo "" + + local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}" + + # Measure with caches + echo -n "Measuring with caches... " + local with_cache=$(measure_startup | awk '{print $1}') + echo "${with_cache}ms" + + # Backup and remove caches + echo -n "Measuring without caches... " + local backup_dir=$(mktemp -d) + mv "$cache_dir"/*.zsh "$backup_dir/" 2>/dev/null + + local without_cache=$(measure_startup | awk '{print $1}') + echo "${without_cache}ms" + + # Restore caches + mv "$backup_dir"/*.zsh "$cache_dir/" 2>/dev/null + rmdir "$backup_dir" 2>/dev/null + + local diff=$((without_cache - with_cache)) + local percent=$((diff * 100 / without_cache)) + + echo "" + echo -e "With caches: ${GREEN}${with_cache}ms${NC}" + echo -e "Without caches: ${RED}${without_cache}ms${NC}" + echo -e "Savings: ${CYAN}${diff}ms (${percent}%)${NC}" +} + +############################################################################# +# Main +############################################################################# + +main() { + echo "" + echo -e "${BLUE}╔════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Shell Startup Profiler ║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════╝${NC}" + echo "" + + # Basic timing + echo -e "${BLUE}━━━ Startup Time (5 runs) ━━━${NC}" + echo "" + echo -n "Measuring... " + + local result=$(measure_startup) + local avg=$(echo "$result" | awk '{print $1}') + local min=$(echo "$result" | awk '{print $2}') + local max=$(echo "$result" | awk '{print $3}') + + echo "done" + echo "" + + # Rating + local rating rating_color + if [[ $avg -lt 200 ]]; then + rating="Excellent" rating_color=$GREEN + elif [[ $avg -lt 500 ]]; then + rating="Good" rating_color=$GREEN + elif [[ $avg -lt 1000 ]]; then + rating="Acceptable" rating_color=$YELLOW + elif [[ $avg -lt 2000 ]]; then + rating="Slow" rating_color=$RED + else + rating="Very Slow" rating_color=$RED + fi + + printf "Average: ${rating_color}%dms${NC} (%s)\n" "$avg" "$rating" + printf "Range: %dms - %dms\n" "$min" "$max" + echo "" + + # Cache status + check_caches + echo "" + + # Detailed profiling if requested + if $DETAILED; then + profile_files + echo "" + fi + + # Compare if requested + if $COMPARE; then + compare_caches + echo "" + fi + + # Recommendations + echo -e "${BLUE}━━━ Recommendations ━━━${NC}" + echo "" + + if [[ $avg -gt 1000 ]]; then + echo "• Your shell is slow. Try clearing and regenerating caches:" + echo " rm ~/.cache/*.zsh && exec \$SHELL" + echo "" + fi + + local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}" + local missing_caches=0 + for cache in fnm-env.zsh zoxide-init.zsh fzf-init.zsh brew-shellenv.zsh; do + [[ ! -f "$cache_dir/$cache" ]] && ((missing_caches++)) + done + + if [[ $missing_caches -gt 0 ]]; then + echo "• $missing_caches cache file(s) missing. Restart shell to generate them." + echo "" + fi + + if ! $DETAILED; then + echo "• Run with --detailed to see file-by-file breakdown" + fi + + if ! $COMPARE; then + echo "• Run with --compare to see cache impact" + fi + + echo "" +} + +main "$@" diff --git a/bin/dotfiles-secrets b/bin/dotfiles-secrets new file mode 100755 index 0000000..dd209d4 --- /dev/null +++ b/bin/dotfiles-secrets @@ -0,0 +1,486 @@ +#!/usr/bin/env bash +# +# dotfiles-secrets - Secure secrets management using macOS Keychain +# +# Usage: dotfiles-secrets [options] +# +# Commands: +# set [value] Store a secret (prompts for value if not provided) +# get Retrieve a secret +# delete Delete a secret +# list List all dotfiles secrets +# export Export secrets to encrypted file +# import Import secrets from encrypted file +# env Export secret as environment variable +# +# Examples: +# dotfiles-secrets set github_token +# dotfiles-secrets get github_token +# dotfiles-secrets env github_token GITHUB_TOKEN +# + +set -euo pipefail + +############################################################################# +# Configuration +############################################################################# + +SERVICE_NAME="dotfiles" +ACCOUNT_PREFIX="dotfiles." + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +DIM='\033[2m' +NC='\033[0m' + +############################################################################# +# Helper Functions +############################################################################# + +print_usage() { + cat << 'EOF' +Usage: dotfiles-secrets [options] + +Commands: + set [value] Store a secret (prompts for value if not provided) + get Retrieve a secret (outputs to stdout) + delete Delete a secret + list List all dotfiles secrets + export Export secrets to encrypted file + import Import secrets from encrypted file + env Output export command for shell + +Options: + -h, --help Show this help message + -q, --quiet Suppress output messages + +Examples: + # Store a secret (will prompt for value) + dotfiles-secrets set github_token + + # Store a secret with value + dotfiles-secrets set api_key "sk-..." + + # Retrieve a secret + dotfiles-secrets get github_token + + # Use in shell script + export GITHUB_TOKEN=$(dotfiles-secrets get github_token) + + # Delete a secret + dotfiles-secrets delete github_token + + # List all secrets + dotfiles-secrets list + + # Export all secrets to encrypted file + dotfiles-secrets export ~/secrets.enc + + # Import secrets from encrypted file + dotfiles-secrets import ~/secrets.enc + +Security Notes: + - Secrets are stored in macOS Keychain (encrypted at rest) + - Requires user authentication to access + - Never commit secrets to git + - Use 'dotfiles-secrets env' in shell config for auto-loading +EOF +} + +error() { + echo -e "${RED}Error:${NC} $1" >&2 +} + +success() { + [[ "${QUIET:-false}" == "true" ]] || echo -e "${GREEN}✓${NC} $1" +} + +info() { + [[ "${QUIET:-false}" == "true" ]] || echo -e "${BLUE}•${NC} $1" +} + +warn() { + echo -e "${YELLOW}⚠${NC} $1" >&2 +} + +# Check if running on macOS +check_macos() { + if [[ "$(uname)" != "Darwin" ]]; then + error "This tool requires macOS Keychain" + echo "For Linux, consider using 'pass' or 'secret-tool'" + exit 1 + fi +} + +############################################################################# +# Keychain Functions +############################################################################# + +# Set a secret in keychain +keychain_set() { + local name="$1" + local value="$2" + local account="${ACCOUNT_PREFIX}${name}" + + # Delete existing if present (silently) + security delete-generic-password \ + -s "$SERVICE_NAME" \ + -a "$account" \ + 2>/dev/null || true + + # Add new secret + security add-generic-password \ + -s "$SERVICE_NAME" \ + -a "$account" \ + -w "$value" \ + -U \ + 2>/dev/null + + return $? +} + +# Get a secret from keychain +keychain_get() { + local name="$1" + local account="${ACCOUNT_PREFIX}${name}" + + security find-generic-password \ + -s "$SERVICE_NAME" \ + -a "$account" \ + -w \ + 2>/dev/null +} + +# Delete a secret from keychain +keychain_delete() { + local name="$1" + local account="${ACCOUNT_PREFIX}${name}" + + security delete-generic-password \ + -s "$SERVICE_NAME" \ + -a "$account" \ + 2>/dev/null +} + +# List all secrets (names only) +keychain_list() { + security dump-keychain 2>/dev/null | \ + grep -A4 "\"svce\"=\"$SERVICE_NAME\"" | \ + grep "\"acct\"=" | \ + sed 's/.*"acct"="\([^"]*\)".*/\1/' | \ + sed "s/^${ACCOUNT_PREFIX}//" | \ + sort -u +} + +# Check if a secret exists +keychain_exists() { + local name="$1" + local account="${ACCOUNT_PREFIX}${name}" + + security find-generic-password \ + -s "$SERVICE_NAME" \ + -a "$account" \ + &>/dev/null +} + +############################################################################# +# Commands +############################################################################# + +cmd_set() { + local name="$1" + local value="${2:-}" + + if [[ -z "$name" ]]; then + error "Secret name required" + echo "Usage: dotfiles-secrets set [value]" + exit 1 + fi + + # Prompt for value if not provided + if [[ -z "$value" ]]; then + echo -n "Enter value for '$name': " + read -rs value + echo "" + + if [[ -z "$value" ]]; then + error "Value cannot be empty" + exit 1 + fi + + # Confirm + echo -n "Confirm value: " + read -rs confirm + echo "" + + if [[ "$value" != "$confirm" ]]; then + error "Values do not match" + exit 1 + fi + fi + + if keychain_set "$name" "$value"; then + success "Secret '$name' stored in Keychain" + else + error "Failed to store secret" + exit 1 + fi +} + +cmd_get() { + local name="$1" + + if [[ -z "$name" ]]; then + error "Secret name required" + echo "Usage: dotfiles-secrets get " + exit 1 + fi + + local value + value=$(keychain_get "$name" 2>/dev/null) || { + error "Secret '$name' not found" + exit 1 + } + + echo "$value" +} + +cmd_delete() { + local name="$1" + + if [[ -z "$name" ]]; then + error "Secret name required" + echo "Usage: dotfiles-secrets delete " + exit 1 + fi + + if keychain_delete "$name"; then + success "Secret '$name' deleted" + else + error "Secret '$name' not found" + exit 1 + fi +} + +cmd_list() { + local secrets + secrets=$(keychain_list) + + if [[ -z "$secrets" ]]; then + info "No secrets stored" + return + fi + + echo -e "${CYAN}Stored secrets:${NC}" + echo "" + while IFS= read -r name; do + echo " • $name" + done <<< "$secrets" + echo "" + info "Use 'dotfiles-secrets get ' to retrieve a value" +} + +cmd_export() { + local output_file="$1" + + if [[ -z "$output_file" ]]; then + error "Output file required" + echo "Usage: dotfiles-secrets export " + exit 1 + fi + + local secrets + secrets=$(keychain_list) + + if [[ -z "$secrets" ]]; then + warn "No secrets to export" + exit 0 + fi + + # Create temporary file with secrets + local temp_file + temp_file=$(mktemp) + trap 'rm -f "$temp_file"' EXIT INT TERM + + while IFS= read -r name; do + local value + value=$(keychain_get "$name") + echo "${name}=${value}" >> "$temp_file" + done <<< "$secrets" + + # Encrypt with openssl + info "Enter encryption password" + if openssl enc -aes-256-cbc -salt -pbkdf2 -in "$temp_file" -out "$output_file"; then + success "Secrets exported to $output_file" + else + error "Failed to export secrets" + rm -f "$temp_file" + exit 1 + fi + + # Secure delete temp file + rm -f "$temp_file" +} + +cmd_import() { + local input_file="$1" + + if [[ -z "$input_file" ]]; then + error "Input file required" + echo "Usage: dotfiles-secrets import " + exit 1 + fi + + if [[ ! -f "$input_file" ]]; then + error "File not found: $input_file" + exit 1 + fi + + # Decrypt file + local temp_file + temp_file=$(mktemp) + + info "Enter decryption password" + if ! openssl enc -aes-256-cbc -d -pbkdf2 -in "$input_file" -out "$temp_file"; then + error "Failed to decrypt file (wrong password?)" + rm -f "$temp_file" + exit 1 + fi + + # Import secrets + local count=0 + while IFS='=' read -r name value; do + [[ -z "$name" ]] && continue + if keychain_set "$name" "$value"; then + ((count++)) + else + warn "Failed to import: $name" + fi + done < "$temp_file" + + rm -f "$temp_file" + success "Imported $count secret(s)" +} + +cmd_env() { + local name="$1" + local varname="${2:-}" + + if [[ -z "$name" ]]; then + error "Secret name required" + echo "Usage: dotfiles-secrets env [VARNAME]" + exit 1 + fi + + # Default variable name is uppercase secret name + if [[ -z "$varname" ]]; then + varname=$(echo "$name" | tr '[:lower:]' '[:upper:]') + fi + + local value + value=$(keychain_get "$name" 2>/dev/null) || { + error "Secret '$name' not found" + exit 1 + } + + # Output export command (can be eval'd) + printf "export %s=%q\n" "$varname" "$value" +} + +############################################################################# +# Shell Integration Functions (for sourcing in .zshrc) +############################################################################# + +# Function to load secrets into environment +# Usage in .zshrc: eval "$(dotfiles-secrets load github_token GITHUB_TOKEN)" +cmd_load() { + local mappings=() + + # Parse arguments as name:VARNAME pairs + while [[ $# -gt 0 ]]; do + case "$1" in + *:*) + mappings+=("$1") + ;; + *) + # Default: name -> NAME + local varname + varname=$(echo "$1" | tr '[:lower:]' '[:upper:]') + mappings+=("$1:$varname") + ;; + esac + shift + done + + for mapping in "${mappings[@]}"; do + local name="${mapping%%:*}" + local varname="${mapping##*:}" + + local value + value=$(keychain_get "$name" 2>/dev/null) || continue + + printf "export %s=%q\n" "$varname" "$value" + done +} + +############################################################################# +# Main +############################################################################# + +main() { + check_macos + + local command="${1:-}" + shift || true + + # Parse global options + QUIET=false + while [[ "${1:-}" == -* ]]; do + case "$1" in + -q|--quiet) QUIET=true; shift ;; + -h|--help) print_usage; exit 0 ;; + *) error "Unknown option: $1"; exit 1 ;; + esac + done + + case "$command" in + set) + cmd_set "${1:-}" "${2:-}" + ;; + get) + cmd_get "${1:-}" + ;; + delete|rm|remove) + cmd_delete "${1:-}" + ;; + list|ls) + cmd_list + ;; + export) + cmd_export "${1:-}" + ;; + import) + cmd_import "${1:-}" + ;; + env) + cmd_env "${1:-}" "${2:-}" + ;; + load) + cmd_load "$@" + ;; + -h|--help|help|"") + print_usage + ;; + *) + error "Unknown command: $command" + echo "Run 'dotfiles-secrets --help' for usage" + exit 1 + ;; + esac +} + +main "$@" diff --git a/bin/dotfiles-setup b/bin/dotfiles-setup new file mode 100755 index 0000000..7594728 --- /dev/null +++ b/bin/dotfiles-setup @@ -0,0 +1,688 @@ +#!/usr/bin/env bash +# +# dotfiles-setup - Interactive dotfiles installer +# +# A modern, user-friendly installation wizard for setting up +# your development environment with visual feedback and menus. +# + +set -euo pipefail + +############################################################################# +# Configuration +############################################################################# + +DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +LOG_FILE="/tmp/dotfiles-setup.log" + +# Require macOS +if [[ "$(uname)" != "Darwin" ]]; then + echo "Error: This dotfiles setup is designed for macOS only." + exit 1 +fi + +IS_APPLE_SILICON=false +[[ "$(uname -m)" == "arm64" ]] && IS_APPLE_SILICON=true + +############################################################################# +# Colors & Styling +############################################################################# + +# Check for color support +if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && command -v tput &>/dev/null; then + BOLD=$(tput bold 2>/dev/null || echo '') + DIM=$(tput dim 2>/dev/null || echo '') + UNDERLINE=$(tput smul 2>/dev/null || echo '') + RESET=$(tput sgr0 2>/dev/null || echo '') + RED=$(tput setaf 1 2>/dev/null || echo '') + GREEN=$(tput setaf 2 2>/dev/null || echo '') + YELLOW=$(tput setaf 3 2>/dev/null || echo '') + BLUE=$(tput setaf 4 2>/dev/null || echo '') + MAGENTA=$(tput setaf 5 2>/dev/null || echo '') + CYAN=$(tput setaf 6 2>/dev/null || echo '') + WHITE=$(tput setaf 7 2>/dev/null || echo '') +else + BOLD='' DIM='' UNDERLINE='' RESET='' + RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' WHITE='' +fi + +# Unicode symbols +CHECKMARK="${GREEN}✓${RESET}" +CROSSMARK="${RED}✗${RESET}" +ARROW="${CYAN}→${RESET}" +BULLET="${BLUE}•${RESET}" +SPINNER_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + +############################################################################# +# Logging & Output +############################################################################# + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE" +} + +print_header() { + clear + echo "" + echo "${BLUE}${BOLD}" + cat << 'EOF' + ██████╗ ██████╗ ████████╗███████╗██╗██╗ ███████╗███████╗ + ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║██║ ██╔════╝██╔════╝ + ██║ ██║██║ ██║ ██║ █████╗ ██║██║ █████╗ ███████╗ + ██║ ██║██║ ██║ ██║ ██╔══╝ ██║██║ ██╔══╝ ╚════██║ + ██████╔╝╚██████╔╝ ██║ ██║ ██║███████╗███████╗███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ +EOF + echo "${RESET}" + echo " ${DIM}Interactive Setup Wizard${RESET}" + echo "" +} + +print_section() { + echo "" + echo "${CYAN}${BOLD}━━━ $1 ━━━${RESET}" + echo "" +} + +print_step() { + echo " ${ARROW} $1" +} + +print_success() { + echo " ${CHECKMARK} $1" +} + +print_error() { + echo " ${CROSSMARK} ${RED}$1${RESET}" + log "ERROR: $1" +} + +print_warning() { + echo " ${YELLOW}⚠${RESET} $1" +} + +print_info() { + echo " ${BULLET} ${DIM}$1${RESET}" +} + +############################################################################# +# Spinner & Progress +############################################################################# + +spinner_pid="" + +start_spinner() { + local msg="$1" + printf " ${CYAN}⠋${RESET} %s" "$msg" + ( + local i=0 + while true; do + printf "\r ${CYAN}${SPINNER_CHARS:$i:1}${RESET} %s" "$msg" + i=$(( (i + 1) % ${#SPINNER_CHARS} )) + sleep 0.1 + done + ) & + spinner_pid=$! +} + +stop_spinner() { + local success=${1:-true} + local msg="${2:-}" + + if [[ -n "$spinner_pid" ]]; then + kill "$spinner_pid" 2>/dev/null || true + wait "$spinner_pid" 2>/dev/null || true + spinner_pid="" + fi + + if $success; then + printf "\r ${CHECKMARK} %s\n" "$msg" + else + printf "\r ${CROSSMARK} %s\n" "$msg" + fi +} + +progress_bar() { + local current=$1 + local total=$2 + local width=40 + local percent=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + + printf "\r [" + printf "%${filled}s" | tr ' ' '█' + printf "%${empty}s" | tr ' ' '░' + printf "] %3d%%" "$percent" +} + +############################################################################# +# User Input +############################################################################# + +ask_yes_no() { + local prompt="$1" + local default="${2:-n}" + local yn_prompt + + if [[ "$default" == "y" ]]; then + yn_prompt="${GREEN}Y${RESET}/n" + else + yn_prompt="y/${GREEN}N${RESET}" + fi + + while true; do + printf " ${ARROW} %s [%s]: " "$prompt" "$yn_prompt" + read -r response + response=${response:-$default} + + case "$response" in + [Yy]|[Yy][Ee][Ss]) return 0 ;; + [Nn]|[Nn][Oo]) return 1 ;; + *) print_warning "Please answer yes or no" ;; + esac + done +} + +ask_input() { + local prompt="$1" + local default="${2:-}" + local value + + if [[ -n "$default" ]]; then + printf " ${ARROW} %s [${DIM}%s${RESET}]: " "$prompt" "$default" + else + printf " ${ARROW} %s: " "$prompt" + fi + + read -r value + echo "${value:-$default}" +} + +ask_menu() { + local prompt="$1" + shift + local options=("$@") + local selected=0 + local key + + echo "" + echo " ${CYAN}${prompt}${RESET}" + echo "" + + # Print options + for i in "${!options[@]}"; do + if [[ $i -eq $selected ]]; then + echo " ${GREEN}▸${RESET} ${options[$i]}" + else + echo " ${options[$i]}" + fi + done + + echo "" + echo " ${DIM}Use arrow keys to select, Enter to confirm${RESET}" + + # Read selection (simplified - just use numbers for compatibility) + while true; do + printf "\r Selection (1-%d): " "${#options[@]}" + read -r key + if [[ "$key" =~ ^[0-9]+$ ]] && [[ "$key" -ge 1 ]] && [[ "$key" -le "${#options[@]}" ]]; then + selected=$((key - 1)) + break + fi + done + + echo "$selected" +} + +ask_checklist() { + local prompt="$1" + shift + local options=("$@") + local selected=() + + # Initialize all as selected + for i in "${!options[@]}"; do + selected[i]=1 + done + + echo "" + echo " ${CYAN}${prompt}${RESET}" + echo " ${DIM}Enter numbers to toggle, 'a' for all, 'n' for none, Enter when done${RESET}" + echo "" + + while true; do + # Print options + for i in "${!options[@]}"; do + local num=$((i + 1)) + if [[ ${selected[$i]} -eq 1 ]]; then + echo " ${GREEN}[✓]${RESET} ${num}. ${options[$i]}" + else + echo " ${DIM}[ ]${RESET} ${num}. ${options[$i]}" + fi + done + + echo "" + printf " Toggle: " + read -r input + + case "$input" in + "") + # Done selecting + break + ;; + a|A) + for i in "${!options[@]}"; do selected[i]=1; done + ;; + n|N) + for i in "${!options[@]}"; do selected[i]=0; done + ;; + *) + if [[ "$input" =~ ^[0-9]+$ ]] && [[ "$input" -ge 1 ]] && [[ "$input" -le "${#options[@]}" ]]; then + local idx=$((input - 1)) + selected[idx]=$((1 - selected[idx])) + fi + ;; + esac + + # Clear and reprint + for _ in "${options[@]}"; do + printf "\033[A\033[2K" + done + printf "\033[A\033[2K\033[A\033[2K" + done + + # Return selected indices + local result=() + for i in "${!options[@]}"; do + if [[ ${selected[$i]} -eq 1 ]]; then + result+=("$i") + fi + done + echo "${result[*]}" +} + +############################################################################# +# System Detection & Validation +############################################################################# + +detect_system() { + print_section "System Detection" + + print_info "Detecting your system..." + + print_success "macOS detected ($(sw_vers -productVersion))" + if $IS_APPLE_SILICON; then + print_success "Apple Silicon (M-series) detected" + else + print_success "Intel processor detected" + fi + + echo "" +} + +check_prerequisites() { + print_section "Checking Prerequisites" + + local missing=() + + # Check for git + if command -v git &>/dev/null; then + print_success "Git installed ($(git --version | cut -d' ' -f3))" + else + print_error "Git not found" + missing+=("git") + fi + + # Check for curl + if command -v curl &>/dev/null; then + print_success "curl installed" + else + print_error "curl not found" + missing+=("curl") + fi + + # Check for zsh + if command -v zsh &>/dev/null; then + print_success "Zsh installed ($(zsh --version | cut -d' ' -f2))" + else + print_warning "Zsh not found (will be installed)" + fi + + # Check Xcode Command Line Tools + if xcode-select -p &>/dev/null; then + print_success "Xcode Command Line Tools installed" + else + print_warning "Xcode Command Line Tools not found (will be installed)" + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + print_error "Missing required tools: ${missing[*]}" + exit 1 + fi + + echo "" +} + +############################################################################# +# Installation Functions +############################################################################# + +install_homebrew() { + print_section "Homebrew Installation" + + if command -v brew &>/dev/null; then + print_success "Homebrew already installed" + if ask_yes_no "Update Homebrew?"; then + start_spinner "Updating Homebrew..." + brew update >> "$LOG_FILE" 2>&1 + stop_spinner true "Homebrew updated" + fi + else + print_step "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + + # Source brew for this session + if $IS_APPLE_SILICON; then + eval "$(/opt/homebrew/bin/brew shellenv)" + else + eval "$(/usr/local/bin/brew shellenv)" + fi + + print_success "Homebrew installed" + fi +} + +install_packages() { + print_section "Package Installation" + + if [[ ! -f "$ROOT_DIR/Brewfile" ]]; then + print_warning "Brewfile not found, skipping package installation" + return + fi + + local options=( + "Essential tools only (git, zsh, starship, fnm)" + "Development setup (adds languages, editors, databases)" + "Full installation (all packages in Brewfile)" + "Custom selection" + "Skip package installation" + ) + + local choice + choice=$(ask_menu "Select installation type:" "${options[@]}") + + case $choice in + 0) + # Essential only + start_spinner "Installing essential packages..." + brew install git zsh starship fnm stow >> "$LOG_FILE" 2>&1 + stop_spinner true "Essential packages installed" + ;; + 1) + # Development setup + start_spinner "Installing development packages..." + brew install git zsh starship fnm stow vim neovim python node go rust \ + ripgrep fd bat eza fzf zoxide >> "$LOG_FILE" 2>&1 + stop_spinner true "Development packages installed" + ;; + 2) + # Full installation + start_spinner "Installing all packages from Brewfile..." + brew bundle install --file="$ROOT_DIR/Brewfile" >> "$LOG_FILE" 2>&1 + stop_spinner true "All packages installed" + ;; + 3) + # Custom - show categories + print_info "Custom selection coming soon. Running full install." + start_spinner "Installing packages..." + brew bundle install --file="$ROOT_DIR/Brewfile" >> "$LOG_FILE" 2>&1 + stop_spinner true "Packages installed" + ;; + 4) + print_info "Skipping package installation" + ;; + esac +} + +setup_shell() { + print_section "Shell Configuration" + + # Install Prezto if not present + if [[ ! -d "$HOME/.zprezto" ]]; then + if ask_yes_no "Install Prezto (Zsh framework)?"; then + start_spinner "Installing Prezto..." + git clone --recursive https://github.com/sorin-ionescu/prezto.git "$HOME/.zprezto" >> "$LOG_FILE" 2>&1 + stop_spinner true "Prezto installed" + fi + else + print_success "Prezto already installed" + fi + + # Change default shell + local current_shell + current_shell=$(basename "$SHELL") + if [[ "$current_shell" != "zsh" ]]; then + if ask_yes_no "Change default shell to Zsh?"; then + local zsh_path + zsh_path=$(which zsh) + if ! grep -q "$zsh_path" /etc/shells; then + echo "$zsh_path" | sudo tee -a /etc/shells > /dev/null + fi + chsh -s "$zsh_path" + print_success "Default shell changed to Zsh" + fi + else + print_success "Zsh is already your default shell" + fi +} + +link_dotfiles() { + print_section "Dotfiles Linking" + + local backup_dir + backup_dir="$HOME/.dotfiles_backup/$(date +%Y.%m.%d.%H.%M.%S)" + + print_info "Existing dotfiles will be backed up to: $backup_dir" + + if ! ask_yes_no "Link dotfiles to home directory?"; then + print_info "Skipping dotfile linking" + return + fi + + mkdir -p "$backup_dir" + + # Backup existing files + start_spinner "Backing up existing dotfiles..." + pushd "$ROOT_DIR/runcom" &>/dev/null + for file in .*; do + [[ "$file" == "." || "$file" == ".." || "$file" == ".DS_Store" ]] && continue + if [[ -e "$HOME/$file" ]] && [[ ! -L "$HOME/$file" ]]; then + mv "$HOME/$file" "$backup_dir/" 2>/dev/null || true + fi + done + popd &>/dev/null + stop_spinner true "Backup complete" + + # Link dotfiles using stow + start_spinner "Linking dotfiles..." + pushd "$ROOT_DIR" &>/dev/null + + [[ -d "$HOME/.config" ]] || mkdir -p "$HOME/.config" + stow -t "$HOME" runcom 2>> "$LOG_FILE" + stow -t "$HOME/.config" config 2>> "$LOG_FILE" + + popd &>/dev/null + stop_spinner true "Dotfiles linked" +} + +configure_macos() { + print_section "macOS Configuration" + + if ! ask_yes_no "Apply macOS system defaults?"; then + print_info "Skipping macOS configuration" + return + fi + + start_spinner "Applying system defaults..." + for file in "$ROOT_DIR/macos"/defaults*.sh; do + [[ -f "$file" ]] && source "$file" >> "$LOG_FILE" 2>&1 + done + stop_spinner true "System defaults applied" + + if ask_yes_no "Configure Dock?"; then + start_spinner "Configuring Dock..." + [[ -f "$ROOT_DIR/macos/dock.sh" ]] && source "$ROOT_DIR/macos/dock.sh" >> "$LOG_FILE" 2>&1 + stop_spinner true "Dock configured" + fi +} + +setup_node() { + print_section "Node.js Setup" + + if command -v fnm &>/dev/null; then + print_success "fnm (Fast Node Manager) found" + + if ask_yes_no "Install Node.js LTS?"; then + start_spinner "Installing Node.js LTS..." + eval "$(fnm env)" + fnm install --lts >> "$LOG_FILE" 2>&1 + fnm default lts-latest >> "$LOG_FILE" 2>&1 + stop_spinner true "Node.js LTS installed" + fi + else + print_warning "fnm not found, skipping Node.js setup" + fi +} + +setup_vim() { + print_section "Editor Setup" + + if [[ -f "$HOME/.vimrc" ]] && [[ -d "$HOME/.vim/bundle/Vundle.vim" ]]; then + if ask_yes_no "Install Vim plugins?"; then + start_spinner "Installing Vim plugins..." + vim +PluginInstall +qall >> "$LOG_FILE" 2>&1 2>&1 + stop_spinner true "Vim plugins installed" + fi + fi + + if command -v nvim &>/dev/null && [[ -d "$HOME/.config/nvim" ]]; then + if ask_yes_no "Setup Neovim plugins (lazy.nvim)?"; then + start_spinner "Installing Neovim plugins..." + nvim --headless "+Lazy! sync" +qa >> "$LOG_FILE" 2>&1 2>&1 || true + stop_spinner true "Neovim plugins installed" + fi + fi +} + +generate_ssh_key() { + print_section "SSH Key Setup" + + if [[ -f "$HOME/.ssh/id_ed25519" ]]; then + print_success "SSH key already exists" + return + fi + + if ! ask_yes_no "Generate new SSH key?"; then + print_info "Skipping SSH key generation" + return + fi + + local email + email=$(ask_input "Enter email for SSH key") + + start_spinner "Generating SSH key..." + ssh-keygen -t ed25519 -C "$email" -f "$HOME/.ssh/id_ed25519" -N "" >> "$LOG_FILE" 2>&1 + stop_spinner true "SSH key generated" + + # Start ssh-agent and add key + eval "$(ssh-agent -s)" >> "$LOG_FILE" 2>&1 + ssh-add "$HOME/.ssh/id_ed25519" >> "$LOG_FILE" 2>&1 2>&1 || true + + print_info "Your public key:" + echo "" + cat "$HOME/.ssh/id_ed25519.pub" + echo "" + print_info "Add this key to GitHub: https://github.com/settings/keys" +} + +############################################################################# +# Summary & Finish +############################################################################# + +print_summary() { + print_section "Setup Complete!" + + echo " ${CHECKMARK} Dotfiles installed to: ${CYAN}$DOTFILES_DIR${RESET}" + echo " ${CHECKMARK} Configuration linked to: ${CYAN}$HOME${RESET}" + echo " ${CHECKMARK} Log file: ${CYAN}$LOG_FILE${RESET}" + echo "" + + print_info "Next steps:" + echo " 1. Restart your terminal or run: ${CYAN}exec \$SHELL${RESET}" + echo " 2. Run ${CYAN}dotfiles doctor${RESET} to verify setup" + echo " 3. Run ${CYAN}dotfiles cheatsheet${RESET} to see available commands" + echo "" + + if [[ -f "$HOME/.ssh/id_ed25519.pub" ]]; then + print_info "Don't forget to add your SSH key to GitHub!" + fi + + echo "" + echo " ${GREEN}${BOLD}Happy coding! 🎉${RESET}" + echo "" +} + +############################################################################# +# Main +############################################################################# + +main() { + # Initialize log file + echo "=== Dotfiles Setup $(date) ===" > "$LOG_FILE" + + print_header + + # Welcome message + echo " Welcome to the dotfiles interactive setup wizard!" + echo " This will guide you through setting up your development environment." + echo "" + + if ! ask_yes_no "Ready to begin?" "y"; then + echo "" + print_info "Setup cancelled. Run again when you're ready!" + exit 0 + fi + + # Run setup steps + detect_system + check_prerequisites + install_homebrew + install_packages + setup_shell + link_dotfiles + configure_macos + setup_node + setup_vim + generate_ssh_key + print_summary +} + +# Handle arguments +case "${1:-}" in + -h|--help) + echo "Usage: dotfiles-setup" + echo "" + echo "Interactive setup wizard for dotfiles installation." + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo "" + exit 0 + ;; + *) + main + ;; +esac diff --git a/bin/dotfiles-test b/bin/dotfiles-test new file mode 100755 index 0000000..5ffa38a --- /dev/null +++ b/bin/dotfiles-test @@ -0,0 +1,888 @@ +#!/usr/bin/env zsh +# +# dotfiles-test - Test suite for dotfiles shell configuration +# +# Usage: dotfiles-test [--verbose] [--quick] +# +# Options: +# --verbose Show detailed output for each test +# --quick Skip slow tests (shell startup timing) +# + +set -o pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test counters +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 +TESTS_SKIPPED=0 + +# Options +VERBOSE=false +QUICK=false + +# Parse arguments +for arg in "$@"; do + case $arg in + --verbose|-v) VERBOSE=true ;; + --quick|-q) QUICK=true ;; + --help|-h) + echo "Usage: dotfiles-test [--verbose] [--quick]" + echo "" + echo "Options:" + echo " --verbose, -v Show detailed output for each test" + echo " --quick, -q Skip slow tests (shell startup timing)" + exit 0 + ;; + esac +done + +# Get dotfiles directory +DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" +if [[ ! -d "$DOTFILES_DIR" ]]; then + # Try to find it relative to this script + DOTFILES_DIR="$(cd "$(dirname "$0")/.." && pwd)" +fi + +# Test result functions +pass() { + ((TESTS_PASSED++)) + ((TESTS_RUN++)) + echo -e "${GREEN}✓${NC} $1" +} + +fail() { + ((TESTS_FAILED++)) + ((TESTS_RUN++)) + echo -e "${RED}✗${NC} $1" + if [[ -n "$2" ]] && $VERBOSE; then + echo -e " ${RED}Error: $2${NC}" + fi +} + +skip() { + ((TESTS_SKIPPED++)) + echo -e "${YELLOW}○${NC} $1 (skipped)" +} + +section() { + echo "" + echo -e "${BLUE}━━━ $1 ━━━${NC}" +} + +############################################################################# +# SYNTAX VALIDATION TESTS +############################################################################# + +test_zsh_syntax() { + section "Zsh Syntax Validation" + + local files=( + "$DOTFILES_DIR/runcom/.zshrc" + "$DOTFILES_DIR/runcom/.zprofile" + "$DOTFILES_DIR/runcom/.zlogin" + "$DOTFILES_DIR/runcom/.profile" + "$DOTFILES_DIR/system/.alias" + "$DOTFILES_DIR/system/.bindings" + "$DOTFILES_DIR/system/.completion" + "$DOTFILES_DIR/system/.env" + "$DOTFILES_DIR/system/.fix" + "$DOTFILES_DIR/system/.fnm" + "$DOTFILES_DIR/system/.function" + "$DOTFILES_DIR/system/.function_fs" + "$DOTFILES_DIR/system/.function_fun" + "$DOTFILES_DIR/system/.function_network" + "$DOTFILES_DIR/system/.function_text" + "$DOTFILES_DIR/system/.fzf" + "$DOTFILES_DIR/system/.grep" + "$DOTFILES_DIR/system/.path" + "$DOTFILES_DIR/system/.pnpm" + "$DOTFILES_DIR/system/.starship" + "$DOTFILES_DIR/system/.zoxide" + ) + + for file in "${files[@]}"; do + if [[ -f "$file" ]]; then + local basename="${file##*/}" + if zsh -n "$file" 2>/dev/null; then + pass "Syntax OK: $basename" + else + local error=$(zsh -n "$file" 2>&1) + fail "Syntax error: $basename" "$error" + fi + fi + done +} + +test_bash_syntax() { + section "Bash Script Syntax Validation" + + local files=( + "$DOTFILES_DIR/bin/dotfiles" + "$DOTFILES_DIR/scripts/echos.sh" + "$DOTFILES_DIR/scripts/requirers.sh" + ) + + for file in "${files[@]}"; do + if [[ -f "$file" ]]; then + local basename="${file##*/}" + if bash -n "$file" 2>/dev/null; then + pass "Syntax OK: $basename" + else + local error=$(bash -n "$file" 2>&1) + fail "Syntax error: $basename" "$error" + fi + fi + done +} + +test_shellcheck() { + section "Shellcheck Linting" + + if ! command -v shellcheck &>/dev/null; then + skip "shellcheck not installed" + return + fi + + local bash_files=( + "$DOTFILES_DIR/bin/dotfiles" + "$DOTFILES_DIR/scripts/echos.sh" + "$DOTFILES_DIR/scripts/requirers.sh" + "$DOTFILES_DIR/macos/defaults.sh" + "$DOTFILES_DIR/macos/dock.sh" + ) + + for file in "${bash_files[@]}"; do + if [[ -f "$file" ]]; then + local basename="${file##*/}" + local output + # Use shellcheck with relaxed settings for dotfiles + # SC1090: Can't follow non-constant source + # SC1091: Not following sourced file + # SC2034: Variable appears unused (common in configs) + # SC2154: Variable referenced but not assigned (sourced from elsewhere) + output=$(shellcheck -e SC1090,SC1091,SC2034,SC2119,SC2154 -s bash "$file" 2>&1) + if [[ $? -eq 0 ]]; then + pass "Shellcheck OK: $basename" + else + fail "Shellcheck issues: $basename" "$output" + fi + fi + done + + # Check shell config files (these are sourced, so use sh/bash compatible checks) + local config_files=( + "$DOTFILES_DIR/runcom/.profile" + ) + + for file in "${config_files[@]}"; do + if [[ -f "$file" ]]; then + local basename="${file##*/}" + local output + output=$(shellcheck -e SC1090,SC1091,SC2034,SC2154,SC2148 -s sh "$file" 2>&1) + if [[ $? -eq 0 ]]; then + pass "Shellcheck OK: $basename" + else + # Warn instead of fail for config files (they have zsh-specific syntax) + echo -e "${YELLOW}⚠${NC} Shellcheck warnings: $basename (may contain zsh syntax)" + ((TESTS_RUN++)) + ((TESTS_PASSED++)) + fi + fi + done +} + +############################################################################# +# CACHE GENERATION TESTS +############################################################################# + +test_cache_generation() { + section "Cache Generation Tests" + + local test_cache_dir=$(mktemp -d) + local old_cache="${XDG_CACHE_HOME:-$HOME/.cache}" + export XDG_CACHE_HOME="$test_cache_dir" + + # Test fnm cache generation + if command -v fnm &>/dev/null; then + if fnm env --use-on-cd --corepack-enabled --shell zsh > "$test_cache_dir/fnm-env.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/fnm-env.zsh" ]]; then + pass "fnm cache generation" + else + fail "fnm cache generation" "Empty output" + fi + else + fail "fnm cache generation" "Command failed" + fi + else + skip "fnm cache generation (fnm not installed)" + fi + + # Test zoxide cache generation + if command -v zoxide &>/dev/null; then + if zoxide init zsh > "$test_cache_dir/zoxide-init.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/zoxide-init.zsh" ]]; then + pass "zoxide cache generation" + else + fail "zoxide cache generation" "Empty output" + fi + else + fail "zoxide cache generation" "Command failed" + fi + else + skip "zoxide cache generation (zoxide not installed)" + fi + + # Test thefuck cache generation + if command -v thefuck &>/dev/null; then + if thefuck --alias fix > "$test_cache_dir/thefuck-alias.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/thefuck-alias.zsh" ]]; then + pass "thefuck cache generation" + else + fail "thefuck cache generation" "Empty output" + fi + else + fail "thefuck cache generation" "Command failed" + fi + else + skip "thefuck cache generation (thefuck not installed)" + fi + + # Test fzf cache generation + if command -v fzf &>/dev/null; then + if fzf --zsh > "$test_cache_dir/fzf-init.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/fzf-init.zsh" ]]; then + pass "fzf cache generation" + else + fail "fzf cache generation" "Empty output" + fi + else + fail "fzf cache generation" "Command failed" + fi + else + skip "fzf cache generation (fzf not installed)" + fi + + # Test brew shellenv cache generation + # Note: In some CI environments, brew shellenv may return empty if HOMEBREW_PREFIX isn't set + if command -v brew &>/dev/null; then + if brew shellenv > "$test_cache_dir/brew-shellenv.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/brew-shellenv.zsh" ]]; then + pass "brew shellenv cache generation" + else + # Empty output is acceptable in CI environments + skip "brew shellenv cache generation (empty output - CI environment)" + fi + else + skip "brew shellenv cache generation (command failed - CI environment)" + fi + else + skip "brew shellenv cache generation (brew not installed)" + fi + + # Test dircolors cache generation + if command -v dircolors &>/dev/null && [[ -f "$DOTFILES_DIR/system/.dir_colors" ]]; then + if dircolors -b "$DOTFILES_DIR/system/.dir_colors" > "$test_cache_dir/dircolors.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/dircolors.zsh" ]]; then + pass "dircolors cache generation" + else + fail "dircolors cache generation" "Empty output" + fi + else + fail "dircolors cache generation" "Command failed" + fi + else + skip "dircolors cache generation (dircolors not installed or .dir_colors missing)" + fi + + # Test npm completion cache generation + if command -v npm &>/dev/null; then + if npm completion > "$test_cache_dir/npm-completion.zsh" 2>/dev/null; then + if [[ -s "$test_cache_dir/npm-completion.zsh" ]]; then + pass "npm completion cache generation" + else + fail "npm completion cache generation" "Empty output" + fi + else + fail "npm completion cache generation" "Command failed" + fi + else + skip "npm completion cache generation (npm not installed)" + fi + + # Cleanup + rm -rf "$test_cache_dir" + export XDG_CACHE_HOME="$old_cache" +} + +############################################################################# +# FUNCTION TESTS +############################################################################# + +test_functions() { + section "Function Tests" + + # Source the function file + source "$DOTFILES_DIR/system/.function" + + # Test prepend-path function + local test_path="/test/path/that/does/not/exist" + local old_path="$PATH" + prepend-path "$test_path" + if [[ "$PATH" == "$old_path" ]]; then + pass "prepend-path: correctly skips non-existent directory" + else + fail "prepend-path: should not add non-existent directory" + PATH="$old_path" + fi + + prepend-path "/usr" + if [[ "$PATH" == "/usr:$old_path" ]]; then + pass "prepend-path: correctly adds existing directory" + PATH="$old_path" + else + fail "prepend-path: failed to add existing directory" + PATH="$old_path" + fi + + # Test get function (zsh indirect expansion) + local TEST_VAR="hello_world" + local result=$(get "TEST_VAR") + if [[ "$result" == "hello_world" ]]; then + pass "get: zsh indirect expansion works" + else + fail "get: zsh indirect expansion failed" "Expected 'hello_world', got '$result'" + fi + + # Test dedup-pathvar function + PATH="/usr/bin:/usr/bin:/usr/local/bin:/usr/bin" + dedup-pathvar PATH + local count=$(echo "$PATH" | tr ':' '\n' | grep -c "^/usr/bin$") + if [[ "$count" -eq 1 ]]; then + pass "dedup-pathvar: removes duplicates" + else + fail "dedup-pathvar: failed to remove duplicates" "Found $count occurrences of /usr/bin" + fi + PATH="$old_path" +} + +############################################################################# +# ALIAS TESTS +############################################################################# + +test_aliases() { + section "Alias Tests" + + # Source required files + source "$DOTFILES_DIR/system/.function" + source "$DOTFILES_DIR/system/.alias" + + # Test that basic aliases are defined + local basic_aliases=("g" "rr" "_" "reload" ".." "..." "....") + for a in "${basic_aliases[@]}"; do + if alias "$a" &>/dev/null; then + pass "Alias defined: $a" + else + fail "Alias missing: $a" + fi + done + + # Test global aliases (zsh-specific) + if alias -g G &>/dev/null; then + pass "Global alias defined: G" + else + fail "Global alias missing: G" + fi + + # Test suffix aliases (zsh-specific) + if alias -s git &>/dev/null; then + pass "Suffix alias defined: .git" + else + fail "Suffix alias missing: .git" + fi + + # Test conditional aliases + if (( $+commands[eza] )); then + if alias ls | grep -q "eza"; then + pass "Conditional alias: ls uses eza" + else + fail "Conditional alias: ls should use eza when available" + fi + else + if alias ls | grep -q "ls"; then + pass "Conditional alias: ls fallback works" + else + fail "Conditional alias: ls fallback failed" + fi + fi +} + +############################################################################# +# SHELL STARTUP TESTS +############################################################################# + +test_shell_startup() { + section "Shell Startup Tests" + + if $QUICK; then + skip "Shell startup timing (use --verbose for full tests)" + return + fi + + # Test that zsh can start without errors + local startup_output + startup_output=$(ZDOTDIR="$DOTFILES_DIR/runcom" zsh -i -c 'echo "STARTUP_OK"' 2>&1) + + if echo "$startup_output" | grep -q "STARTUP_OK"; then + pass "Shell starts successfully" + else + fail "Shell startup failed" "$startup_output" + fi + + # Measure startup time + local start_time end_time duration + start_time=$(date +%s%N) + ZDOTDIR="$DOTFILES_DIR/runcom" zsh -i -c 'exit' 2>/dev/null + end_time=$(date +%s%N) + duration=$(( (end_time - start_time) / 1000000 )) # Convert to ms + + if [[ $duration -lt 500 ]]; then + pass "Shell startup time: ${duration}ms (excellent)" + elif [[ $duration -lt 1000 ]]; then + pass "Shell startup time: ${duration}ms (good)" + elif [[ $duration -lt 2000 ]]; then + echo -e "${YELLOW}⚠${NC} Shell startup time: ${duration}ms (could be faster)" + ((TESTS_RUN++)) + ((TESTS_PASSED++)) + else + fail "Shell startup time: ${duration}ms (too slow, target <1000ms)" + fi +} + +############################################################################# +# ENVIRONMENT TESTS +############################################################################# + +test_environment() { + section "Environment Variable Tests" + + source "$DOTFILES_DIR/system/.env" + + # Test XDG directories + local xdg_vars=("XDG_CONFIG_HOME" "XDG_CACHE_HOME" "XDG_DATA_HOME" "XDG_STATE_HOME") + for var in "${xdg_vars[@]}"; do + if [[ -n "${(P)var}" ]]; then + pass "Environment: $var is set" + else + fail "Environment: $var is not set" + fi + done + + # Test essential variables + if [[ -n "$EDITOR" ]]; then + pass "Environment: EDITOR is set ($EDITOR)" + else + fail "Environment: EDITOR is not set" + fi + + if [[ -n "$LANG" ]]; then + pass "Environment: LANG is set ($LANG)" + else + fail "Environment: LANG is not set" + fi +} + +############################################################################# +# FILE STRUCTURE TESTS +############################################################################# + +test_file_structure() { + section "File Structure Tests" + + # Test essential directories exist + local dirs=("bin" "runcom" "system" "scripts" "config" "packages" "modules") + for dir in "${dirs[@]}"; do + if [[ -d "$DOTFILES_DIR/$dir" ]]; then + pass "Directory exists: $dir/" + else + fail "Directory missing: $dir/" + fi + done + + # Test essential files exist + local files=( + "runcom/.zshrc" + "runcom/.zprofile" + "runcom/.zpreztorc" + "system/.alias" + "system/.function" + "system/.path" + "bin/dotfiles" + ) + for file in "${files[@]}"; do + if [[ -f "$DOTFILES_DIR/$file" ]]; then + pass "File exists: $file" + else + fail "File missing: $file" + fi + done + + # Test bin scripts are executable + for script in "$DOTFILES_DIR"/bin/*; do + if [[ -f "$script" ]]; then + local basename="${script##*/}" + if [[ -x "$script" ]]; then + pass "Executable: bin/$basename" + else + fail "Not executable: bin/$basename" + fi + fi + done +} + +############################################################################# +# PREZTO TESTS +############################################################################# + +test_prezto() { + section "Prezto Configuration Tests" + + local prezto_dir="$DOTFILES_DIR/modules/prezto" + + if [[ -d "$prezto_dir" ]]; then + pass "Prezto submodule exists" + else + fail "Prezto submodule missing" + return + fi + + if [[ -f "$prezto_dir/init.zsh" ]]; then + pass "Prezto init.zsh exists" + else + fail "Prezto init.zsh missing" + fi + + # Validate zpreztorc has valid module list + local zpreztorc="$DOTFILES_DIR/runcom/.zpreztorc" + if [[ -f "$zpreztorc" ]]; then + if grep -q "pmodule" "$zpreztorc"; then + pass "zpreztorc has module configuration" + else + fail "zpreztorc missing module configuration" + fi + fi +} + +############################################################################# +# SECURITY FIX VALIDATION TESTS +############################################################################# + +test_security_fixes() { + section "Security Fix Validation" + + # Test prepend-path quotes its argument (no word splitting) + source "$DOTFILES_DIR/system/.function" + local old_path="$PATH" + prepend-path "/path with spaces" + # Should not have added it (doesn't exist), but importantly shouldn't error + if [[ "$PATH" == "$old_path" ]]; then + pass "prepend-path: handles paths with spaces safely" + else + fail "prepend-path: should not add non-existent path with spaces" + PATH="$old_path" + fi + + # Test git config doesn't have dangerous unstage alias + local git_config="$DOTFILES_DIR/config/git/config" + if [[ -f "$git_config" ]]; then + if grep -q 'unstage.*reset --hard' "$git_config"; then + fail "Git unstage alias still uses --hard (data loss risk)" + else + pass "Git unstage alias is safe (no --hard)" + fi + fi + + # Test secrets script uses printf %q (not vulnerable to quote injection) + local secrets_script="$DOTFILES_DIR/bin/dotfiles-secrets" + if [[ -f "$secrets_script" ]]; then + if grep -q 'printf.*%q' "$secrets_script"; then + pass "Secrets env output uses printf %%q (safe from injection)" + else + fail "Secrets env output should use printf %%q for safe quoting" + fi + fi + + # Test SSH key generation uses modern flag + if grep -q 'apple-use-keychain' "$DOTFILES_DIR/bin/dotfiles"; then + pass "SSH uses --apple-use-keychain (not deprecated -K)" + else + fail "SSH should use --apple-use-keychain instead of -K" + fi +} + +############################################################################# +# PERFORMANCE OPTIMIZATION VALIDATION TESTS +############################################################################# + +test_performance_optimizations() { + section "Performance Optimization Validation" + + # Test brew shellenv cache doesn't contain path_helper eval + local brew_cache="${XDG_CACHE_HOME:-$HOME/.cache}/brew-shellenv.zsh" + if [[ -f "$brew_cache" ]]; then + if grep -q 'path_helper' "$brew_cache"; then + fail "Brew cache still contains path_helper (defeats caching)" + else + pass "Brew cache: no path_helper subprocess" + fi + else + skip "Brew cache not generated yet" + fi + + # Test .grep uses static alias (no is-supported calls) + if [[ -f "$DOTFILES_DIR/system/.grep" ]]; then + if grep -q 'is-supported' "$DOTFILES_DIR/system/.grep"; then + fail ".grep still uses is-supported (spawns subprocesses)" + else + pass ".grep: uses static config (no subprocesses)" + fi + fi + + # Test .zshrc uses $USER not $(whoami) + if [[ -f "$DOTFILES_DIR/runcom/.zshrc" ]]; then + if grep -q '$(whoami)' "$DOTFILES_DIR/runcom/.zshrc"; then + fail ".zshrc still uses \$(whoami) subprocess" + else + pass ".zshrc: uses \$USER (no subprocess)" + fi + fi + + # Test cache files use $commands[] instead of $(command -v) + local cache_files=("$DOTFILES_DIR/system/.fzf" "$DOTFILES_DIR/system/.zoxide" "$DOTFILES_DIR/system/.fix") + local all_good=true + for f in "${cache_files[@]}"; do + if [[ -f "$f" ]] && grep -q '$(command -v' "$f"; then + all_good=false + break + fi + done + if $all_good; then + pass "Cache files use \$commands[] (no subprocess for lookups)" + else + fail "Some cache files still use \$(command -v) subprocess" + fi + + # Test no duplicate env sourcing in .zshrc + if [[ -f "$DOTFILES_DIR/runcom/.zshrc" ]]; then + if grep -q 'local/share.*\.\./.*bin/env' "$DOTFILES_DIR/runcom/.zshrc"; then + fail ".zshrc still has duplicate env sourcing" + else + pass ".zshrc: no duplicate env sourcing" + fi + fi +} + +############################################################################# +# CONFIGURATION MODERNIZATION TESTS +############################################################################# + +test_config_modernization() { + section "Configuration Modernization" + + # Test Neovim uses ts_ls not tsserver + local lsp_config="$DOTFILES_DIR/config/nvim/lua/plugins/lsp.lua" + if [[ -f "$lsp_config" ]]; then + if grep -q 'tsserver' "$lsp_config"; then + fail "Neovim LSP still uses deprecated 'tsserver'" + else + pass "Neovim LSP: uses 'ts_ls' (current name)" + fi + fi + + # Test Neovim uses vim.uv not vim.loop + local lazy_config="$DOTFILES_DIR/config/nvim/lua/config/lazy.lua" + if [[ -f "$lazy_config" ]]; then + if grep -q 'vim\.loop' "$lazy_config"; then + fail "Neovim still uses deprecated vim.loop" + else + pass "Neovim: uses vim.uv (not deprecated vim.loop)" + fi + fi + + # Test git config uses ksdiff via PATH (not hardcoded Intel path) + local git_config="$DOTFILES_DIR/config/git/config" + if [[ -f "$git_config" ]]; then + if grep -q '/usr/local/bin/ksdiff' "$git_config"; then + fail "Git config has hardcoded Intel ksdiff path" + else + pass "Git config: ksdiff uses PATH lookup" + fi + fi + + # Test Prezto loads the osx/macos module + local zpreztorc="$DOTFILES_DIR/runcom/.zpreztorc" + if [[ -f "$zpreztorc" ]]; then + if grep -qE "'osx'|'macos'" "$zpreztorc"; then + pass "Prezto: macOS helper module is loaded" + else + fail "Prezto: macOS helper module (osx/macos) not loaded" + fi + fi + + # Test git hooks are configured + if [[ -f "$git_config" ]]; then + if grep -q 'hooksPath' "$git_config"; then + pass "Git config: hooksPath is set" + else + fail "Git config: hooksPath not configured" + fi + fi + + # Test Nord theme submodules use nordtheme org + if [[ -f "$DOTFILES_DIR/.gitmodules" ]]; then + if grep -q 'arcticicestudio' "$DOTFILES_DIR/.gitmodules"; then + fail "Submodules still reference archived arcticicestudio org" + else + pass "Submodules: use nordtheme org URLs" + fi + fi + + # Test Brewfile doesn't have deprecated cask-fonts tap + if [[ -f "$DOTFILES_DIR/Brewfile" ]]; then + if grep -q 'homebrew/cask-fonts' "$DOTFILES_DIR/Brewfile"; then + fail "Brewfile still has deprecated homebrew/cask-fonts tap" + else + pass "Brewfile: no deprecated taps" + fi + fi + + # Test Starship config exists (inactive while p10k is primary, kept for future use) + local starship_file="$DOTFILES_DIR/system/.starship" + if [[ -f "$starship_file" ]]; then + pass "Starship: config file preserved for future use" + fi + + # Test Powerlevel10k is configured as the active prompt + local zpreztorc="$DOTFILES_DIR/runcom/.zpreztorc" + if [[ -f "$zpreztorc" ]]; then + if grep -q "theme 'powerlevel10k'" "$zpreztorc"; then + pass "Prompt: Powerlevel10k configured as active theme" + else + fail "Prompt: Powerlevel10k not set as theme in zpreztorc" + fi + fi + + # Test XDG_RUNTIME_DIR is not set to Library/Caches + local env_file="$DOTFILES_DIR/system/.env" + if [[ -f "$env_file" ]]; then + if grep -q 'XDG_RUNTIME_DIR.*Library/Caches' "$env_file"; then + fail "XDG_RUNTIME_DIR still set to Library/Caches" + else + pass "XDG_RUNTIME_DIR: uses correct path" + fi + fi +} + +############################################################################# +# DEAD CODE CLEANUP TESTS +############################################################################# + +test_dead_code_cleanup() { + section "Dead Code Cleanup" + + local alias_file="$DOTFILES_DIR/system/.alias" + if [[ -f "$alias_file" ]]; then + # Test fd function doesn't shadow fd binary + source "$DOTFILES_DIR/system/.function" + source "$DOTFILES_DIR/system/.function_fs" + if typeset -f fd &>/dev/null; then + fail "fd() function still shadows the fd binary" + else + pass "No fd() function shadowing fd binary" + fi + + # Test deprecated aliases are removed + if grep -q 'lwp-request' "$alias_file"; then + fail "Dead lwp-request aliases still present" + else + pass "lwp-request aliases removed" + fi + + if grep -q 'youtube-dl' "$DOTFILES_DIR/system/.function_network"; then + fail "Still references deprecated youtube-dl" + else + pass "Uses yt-dlp (not deprecated youtube-dl)" + fi + + # Test transfer function is removed (transfer.sh is dead) + source "$DOTFILES_DIR/system/.function_network" 2>/dev/null + if typeset -f transfer &>/dev/null; then + fail "transfer() function still present (transfer.sh is dead)" + else + pass "transfer() removed (transfer.sh dead)" + fi + + # Test egrep is not used (deprecated) + if grep -q 'egrep' "$DOTFILES_DIR/system/.function_network"; then + fail "Still uses deprecated egrep" + else + pass "Uses grep -E (not deprecated egrep)" + fi + fi +} + +############################################################################# +# MAIN +############################################################################# + +main() { + echo "" + echo -e "${BLUE}╔════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Dotfiles Test Suite ║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════╝${NC}" + echo "" + echo "Testing: $DOTFILES_DIR" + + # Run all tests + test_file_structure + test_zsh_syntax + test_bash_syntax + test_shellcheck + test_cache_generation + test_functions + test_aliases + test_environment + test_prezto + test_security_fixes + test_performance_optimizations + test_config_modernization + test_dead_code_cleanup + test_shell_startup + + # Summary + echo "" + echo -e "${BLUE}━━━ Summary ━━━${NC}" + echo "" + echo -e " ${GREEN}Passed:${NC} $TESTS_PASSED" + echo -e " ${RED}Failed:${NC} $TESTS_FAILED" + echo -e " ${YELLOW}Skipped:${NC} $TESTS_SKIPPED" + echo -e " Total: $TESTS_RUN" + echo "" + + if [[ $TESTS_FAILED -eq 0 ]]; then + echo -e "${GREEN}All tests passed!${NC}" + exit 0 + else + echo -e "${RED}Some tests failed.${NC}" + exit 1 + fi +} + +main "$@" diff --git a/bin/is-apple-silicon b/bin/is-apple-silicon index b4c8f73..f8676ed 100755 --- a/bin/is-apple-silicon +++ b/bin/is-apple-silicon @@ -1,3 +1,3 @@ #!/usr/bin/env bash -test "$(uname -p)" = 'arm' -o "$(uname -m)" = 'arm64' +[ "$(uname -p)" = 'arm' ] || [ "$(uname -m)" = 'arm64' ] diff --git a/config/git/config b/config/git/config index 3cb38ab..30315d7 100644 --- a/config/git/config +++ b/config/git/config @@ -23,6 +23,7 @@ untrackedCache = true pager = delta ignorecase = false + hooksPath = .githooks [credential] helper = osxkeychain @@ -31,7 +32,7 @@ lineNumber = true [help] - autocorrect = 1 + autocorrect = 20 [init] defaultBranch = main @@ -39,6 +40,7 @@ [push] default = simple followTags = true + autoSetupRemote = true [fetch] prune = true @@ -65,6 +67,7 @@ renames = copies indentHeuristic = true tool = Kaleidoscope + algorithm = histogram [difftool] prompt = false @@ -73,7 +76,7 @@ cmd = ksdiff --partial-changeset --relative-path \"$MERGED\" -- \"$LOCAL\" \"$REMOTE\" [difftool "sourcetree"] - cmd = /usr/local/bin/ksdiff -w \"$LOCAL\" \"$REMOTE\" + cmd = ksdiff -w \"$LOCAL\" \"$REMOTE\" path = [difftool "vscode"] @@ -82,7 +85,7 @@ [merge] tool = Kaleidoscope - conflictstyle = diff3 + conflictstyle = zdiff3 defaultToUpstream = true [mergetool] @@ -93,7 +96,7 @@ trustExitCode = true [mergetool "sourcetree"] - cmd = /usr/local/bin/ksdiff --merge --output \"$MERGED\" --base \"$BASE\" -- \"$LOCAL\" --snapshot \"$REMOTE\" --snapshot + cmd = ksdiff --merge --output \"$MERGED\" --base \"$BASE\" -- \"$LOCAL\" --snapshot \"$REMOTE\" --snapshot trustExitCode = true [mergetool "vscode"] @@ -132,9 +135,15 @@ st = status stl = ls-files -m -o --exclude-standard sts = status -sb - unstage = reset --hard HEAD + unstage = reset HEAD upm = !git fetch upstream && git merge upstream/main up = pull --rebase --autostash +[branch] + sort = -committerdate + +[column] + ui = auto + [dotfiles] lastupdate = 2025-10-06 12:15:54 diff --git a/config/git/ignore b/config/git/ignore index 4f2f5da..4665d0f 100644 --- a/config/git/ignore +++ b/config/git/ignore @@ -16,3 +16,5 @@ Temporary Items node_modules bower_components id_* + +**/.claude/settings.local.json diff --git a/config/karabiner/karabiner.json b/config/karabiner/karabiner.json index f8555a3..773bc74 100644 --- a/config/karabiner/karabiner.json +++ b/config/karabiner/karabiner.json @@ -1,76 +1,66 @@ { - "profiles": [ + "profiles": [ + { + "devices": [ { - "devices": [ - { - "identifiers": { - "is_keyboard": true, - "product_id": 65535, - "vendor_id": 1452 - }, - "simple_modifications": [ - { - "from": { "key_code": "f2" }, - "to": [{ "key_code": "return_or_enter" }] - } - ] - }, - { - "identifiers": { - "is_keyboard": true, - "product_id": 833, - "vendor_id": 1452 - }, - "simple_modifications": [ - { - "from": { "key_code": "grave_accent_and_tilde" }, - "to": [{ "key_code": "non_us_backslash" }] - }, - { - "from": { "key_code": "non_us_backslash" }, - "to": [{ "key_code": "grave_accent_and_tilde" }] - } - ] - } - ], - "fn_function_keys": [ - { - "from": { "key_code": "f3" }, - "to": [{ "key_code": "mission_control" }] - }, - { - "from": { "key_code": "f4" }, - "to": [{ "key_code": "launchpad" }] - }, - { - "from": { "key_code": "f5" }, - "to": [{ "key_code": "illumination_decrement" }] - }, - { - "from": { "key_code": "f6" }, - "to": [{ "key_code": "illumination_increment" }] - }, - { - "from": { "key_code": "f9" }, - "to": [{ "consumer_key_code": "fastforward" }] - } - ], - "name": "Default profile", - "selected": true, - "simple_modifications": [ - { - "from": { "key_code": "grave_accent_and_tilde" }, - "to": [{ "key_code": "non_us_backslash" }] - }, - { - "from": { "key_code": "non_us_backslash" }, - "to": [{ "key_code": "grave_accent_and_tilde" }] - } - ], - "virtual_hid_keyboard": { - "country_code": 0, - "keyboard_type_v2": "ansi" + "identifiers": { + "is_keyboard": true, + "product_id": 65535, + "vendor_id": 1452 + }, + "simple_modifications": [ + { + "from": { "key_code": "f2" }, + "to": [{ "key_code": "return_or_enter" }] } + ] + }, + { + "identifiers": { + "is_keyboard": true, + "product_id": 833, + "vendor_id": 1452 + } + } + ], + "fn_function_keys": [ + { + "from": { "key_code": "f3" }, + "to": [{ "key_code": "mission_control" }] + }, + { + "from": { "key_code": "f4" }, + "to": [{ "key_code": "launchpad" }] + }, + { + "from": { "key_code": "f5" }, + "to": [{ "key_code": "illumination_decrement" }] + }, + { + "from": { "key_code": "f6" }, + "to": [{ "key_code": "illumination_increment" }] + }, + { + "from": { "key_code": "f9" }, + "to": [{ "consumer_key_code": "fastforward" }] + } + ], + "name": "Default profile", + "selected": true, + "simple_modifications": [ + { + "from": { "key_code": "grave_accent_and_tilde" }, + "to": [{ "key_code": "non_us_backslash" }] + }, + { + "from": { "key_code": "non_us_backslash" }, + "to": [{ "key_code": "grave_accent_and_tilde" }] } - ] -} \ No newline at end of file + ], + "virtual_hid_keyboard": { + "country_code": 0, + "keyboard_type_v2": "ansi" + } + } + ] +} diff --git a/config/nvim/init.lua b/config/nvim/init.lua new file mode 100644 index 0000000..6ba3d85 --- /dev/null +++ b/config/nvim/init.lua @@ -0,0 +1,14 @@ +-- Neovim Configuration +-- Using lazy.nvim as the package manager + +-- Set leader key before loading lazy +vim.g.mapleader = " " +vim.g.maplocalleader = "\\" + +-- Load core configuration +require("config.options") +require("config.keymaps") +require("config.autocmds") + +-- Bootstrap and load lazy.nvim +require("config.lazy") diff --git a/config/nvim/lua/config/autocmds.lua b/config/nvim/lua/config/autocmds.lua new file mode 100644 index 0000000..ee455c5 --- /dev/null +++ b/config/nvim/lua/config/autocmds.lua @@ -0,0 +1,132 @@ +-- Autocommands + +local augroup = vim.api.nvim_create_augroup +local autocmd = vim.api.nvim_create_autocmd + +-- General Settings +local general = augroup("General", { clear = true }) + +-- Highlight on yank +autocmd("TextYankPost", { + group = general, + callback = function() + vim.highlight.on_yank({ higroup = "IncSearch", timeout = 200 }) + end, + desc = "Highlight text on yank", +}) + +-- Remove whitespace on save +autocmd("BufWritePre", { + group = general, + pattern = "*", + command = [[%s/\s\+$//e]], + desc = "Remove trailing whitespace on save", +}) + +-- Resize splits when window is resized +autocmd("VimResized", { + group = general, + callback = function() + vim.cmd("tabdo wincmd =") + end, + desc = "Resize splits on window resize", +}) + +-- Go to last location when opening a buffer +autocmd("BufReadPost", { + group = general, + callback = function() + local mark = vim.api.nvim_buf_get_mark(0, '"') + local lcount = vim.api.nvim_buf_line_count(0) + if mark[1] > 0 and mark[1] <= lcount then + pcall(vim.api.nvim_win_set_cursor, 0, mark) + end + end, + desc = "Go to last location when opening a buffer", +}) + +-- Close some filetypes with +autocmd("FileType", { + group = general, + pattern = { + "help", + "lspinfo", + "man", + "notify", + "qf", + "query", + "spectre_panel", + "startuptime", + "checkhealth", + }, + callback = function(event) + vim.bo[event.buf].buflisted = false + vim.keymap.set("n", "q", "close", { buffer = event.buf, silent = true }) + end, + desc = "Close certain filetypes with q", +}) + +-- Check if file changed outside of vim +autocmd({ "FocusGained", "TermClose", "TermLeave" }, { + group = general, + command = "checktime", + desc = "Check if file changed outside of vim", +}) + +-- Auto create directories when saving a file +autocmd("BufWritePre", { + group = general, + callback = function(event) + if event.match:match("^%w%w+://") then + return + end + local file = vim.uv.fs_realpath(event.match) or event.match + vim.fn.mkdir(vim.fn.fnamemodify(file, ":p:h"), "p") + end, + desc = "Auto create directories when saving a file", +}) + +-- Filetype-specific settings +local filetypes = augroup("Filetypes", { clear = true }) + +-- Set indentation for specific filetypes +autocmd("FileType", { + group = filetypes, + pattern = { "lua", "javascript", "typescript", "json", "yaml", "html", "css" }, + callback = function() + vim.opt_local.tabstop = 2 + vim.opt_local.shiftwidth = 2 + end, + desc = "Set 2-space indentation for certain filetypes", +}) + +autocmd("FileType", { + group = filetypes, + pattern = { "python", "rust", "go" }, + callback = function() + vim.opt_local.tabstop = 4 + vim.opt_local.shiftwidth = 4 + end, + desc = "Set 4-space indentation for certain filetypes", +}) + +-- Enable spell checking for certain filetypes +autocmd("FileType", { + group = filetypes, + pattern = { "markdown", "gitcommit", "text" }, + callback = function() + vim.opt_local.spell = true + vim.opt_local.wrap = true + end, + desc = "Enable spell checking for markdown and git commits", +}) + +-- Disable line numbers in terminal +autocmd("TermOpen", { + group = general, + callback = function() + vim.opt_local.number = false + vim.opt_local.relativenumber = false + end, + desc = "Disable line numbers in terminal", +}) diff --git a/config/nvim/lua/config/keymaps.lua b/config/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..10f882a --- /dev/null +++ b/config/nvim/lua/config/keymaps.lua @@ -0,0 +1,80 @@ +-- Keymaps + +local keymap = vim.keymap.set + +-- Clear search highlighting with Escape +keymap("n", "", "nohlsearch", { desc = "Clear search highlighting" }) + +-- Better window navigation +keymap("n", "", "h", { desc = "Move to left window" }) +keymap("n", "", "j", { desc = "Move to lower window" }) +keymap("n", "", "k", { desc = "Move to upper window" }) +keymap("n", "", "l", { desc = "Move to right window" }) + +-- Resize windows with arrows +keymap("n", "", ":resize -2", { desc = "Resize window up" }) +keymap("n", "", ":resize +2", { desc = "Resize window down" }) +keymap("n", "", ":vertical resize -2", { desc = "Resize window left" }) +keymap("n", "", ":vertical resize +2", { desc = "Resize window right" }) + +-- Navigate buffers +keymap("n", "", ":bnext", { desc = "Next buffer" }) +keymap("n", "", ":bprevious", { desc = "Previous buffer" }) +keymap("n", "bd", ":bdelete", { desc = "Delete buffer" }) + +-- Stay in indent mode +keymap("v", "<", "", ">gv", { desc = "Indent right" }) + +-- Move text up and down +keymap("v", "J", ":m '>+1gv=gv", { desc = "Move text down" }) +keymap("v", "K", ":m '<-2gv=gv", { desc = "Move text up" }) + +-- Better paste (don't replace clipboard with replaced text) +keymap("v", "p", '"_dP', { desc = "Paste without replacing clipboard" }) + +-- Keep cursor centered when scrolling +keymap("n", "", "zz", { desc = "Scroll down and center" }) +keymap("n", "", "zz", { desc = "Scroll up and center" }) +keymap("n", "n", "nzzzv", { desc = "Next search result and center" }) +keymap("n", "N", "Nzzzv", { desc = "Previous search result and center" }) + +-- Quick save +keymap("n", "w", ":w", { desc = "Save file" }) +keymap("n", "q", ":q", { desc = "Quit" }) +keymap("n", "Q", ":qa!", { desc = "Force quit all" }) + +-- Split windows +keymap("n", "sv", "v", { desc = "Split window vertically" }) +keymap("n", "sh", "s", { desc = "Split window horizontally" }) +keymap("n", "se", "=", { desc = "Make splits equal size" }) +keymap("n", "sx", "close", { desc = "Close current split" }) + +-- Tabs +keymap("n", "to", "tabnew", { desc = "Open new tab" }) +keymap("n", "tx", "tabclose", { desc = "Close current tab" }) +keymap("n", "tn", "tabn", { desc = "Go to next tab" }) +keymap("n", "tp", "tabp", { desc = "Go to previous tab" }) +keymap("n", "tf", "tabnew %", { desc = "Open current buffer in new tab" }) + +-- Diagnostic keymaps +keymap("n", "[d", function() vim.diagnostic.jump({ count = -1 }) end, { desc = "Go to previous diagnostic message" }) +keymap("n", "]d", function() vim.diagnostic.jump({ count = 1 }) end, { desc = "Go to next diagnostic message" }) +keymap("n", "e", vim.diagnostic.open_float, { desc = "Show diagnostic error messages" }) +keymap("n", "dl", vim.diagnostic.setloclist, { desc = "Open diagnostic list" }) + +-- Terminal +keymap("t", "", "", { desc = "Exit terminal mode" }) + +-- Yank to end of line (consistent with D and C) +keymap("n", "Y", "y$", { desc = "Yank to end of line" }) + +-- Join lines without moving cursor +keymap("n", "J", "mzJ`z", { desc = "Join lines" }) + +-- Quick access to Ex mode +keymap("n", ";", ":", { desc = "Enter command mode" }) + +-- Toggle options +keymap("n", "uw", "set wrap!", { desc = "Toggle word wrap" }) +keymap("n", "un", "set relativenumber!", { desc = "Toggle relative line numbers" }) diff --git a/config/nvim/lua/config/lazy.lua b/config/nvim/lua/config/lazy.lua new file mode 100644 index 0000000..d7ea93f --- /dev/null +++ b/config/nvim/lua/config/lazy.lua @@ -0,0 +1,48 @@ +-- Bootstrap lazy.nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not vim.uv.fs_stat(lazypath) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", + lazypath, + }) +end +vim.opt.rtp:prepend(lazypath) + +-- Load plugins +require("lazy").setup({ + spec = { + { import = "plugins" }, + }, + defaults = { + lazy = false, + version = false, + }, + install = { + colorscheme = { "catppuccin", "habamax" }, + }, + checker = { + enabled = true, + notify = false, + }, + change_detection = { + notify = false, + }, + performance = { + rtp = { + disabled_plugins = { + "gzip", + "matchit", + "matchparen", + "netrwPlugin", + "tarPlugin", + "tohtml", + "tutor", + "zipPlugin", + }, + }, + }, +}) diff --git a/config/nvim/lua/config/options.lua b/config/nvim/lua/config/options.lua new file mode 100644 index 0000000..61f1323 --- /dev/null +++ b/config/nvim/lua/config/options.lua @@ -0,0 +1,75 @@ +-- Neovim Options + +local opt = vim.opt + +-- Line numbers +opt.number = true +opt.relativenumber = true + +-- Tabs & indentation +opt.tabstop = 2 +opt.shiftwidth = 2 +opt.expandtab = true +opt.autoindent = true +opt.smartindent = true + +-- Line wrapping +opt.wrap = false + +-- Search settings +opt.ignorecase = true +opt.smartcase = true +opt.hlsearch = true +opt.incsearch = true + +-- Cursor line +opt.cursorline = true + +-- Appearance +opt.termguicolors = true +opt.background = "dark" +opt.signcolumn = "yes" +opt.colorcolumn = "100" + +-- Backspace +opt.backspace = "indent,eol,start" + +-- Clipboard +opt.clipboard:append("unnamedplus") + +-- Split windows +opt.splitright = true +opt.splitbelow = true + +-- Consider - as part of word +opt.iskeyword:append("-") + +-- Disable swapfile and enable undo +opt.swapfile = false +opt.backup = false +opt.undodir = vim.fn.stdpath("data") .. "/undodir" +opt.undofile = true + +-- Better completion experience +opt.completeopt = "menuone,noselect" + +-- Decrease update time +opt.updatetime = 250 +opt.timeoutlen = 300 + +-- Scroll offset +opt.scrolloff = 10 +opt.sidescrolloff = 8 + +-- Enable mouse mode +opt.mouse = "a" + +-- Enable break indent +opt.breakindent = true + +-- Sets how neovim will display certain whitespace in the editor +opt.list = true +opt.listchars = { tab = "» ", trail = "·", nbsp = "␣" } + +-- Preview substitutions live +opt.inccommand = "split" diff --git a/config/nvim/lua/plugins/colorscheme.lua b/config/nvim/lua/plugins/colorscheme.lua new file mode 100644 index 0000000..a1bd044 --- /dev/null +++ b/config/nvim/lua/plugins/colorscheme.lua @@ -0,0 +1,58 @@ +-- Colorscheme configuration +return { + -- Catppuccin (primary theme) + { + "catppuccin/nvim", + name = "catppuccin", + priority = 1000, + opts = { + flavour = "mocha", + background = { + light = "latte", + dark = "mocha", + }, + transparent_background = false, + term_colors = true, + integrations = { + cmp = true, + gitsigns = true, + nvimtree = true, + telescope = true, + treesitter = true, + which_key = true, + indent_blankline = { + enabled = true, + colored_indent_levels = false, + }, + native_lsp = { + enabled = true, + underlines = { + errors = { "undercurl" }, + hints = { "undercurl" }, + warnings = { "undercurl" }, + information = { "undercurl" }, + }, + }, + }, + }, + config = function(_, opts) + require("catppuccin").setup(opts) + vim.cmd.colorscheme("catppuccin") + end, + }, + + -- Nord (alternative) + { + "shaunsingh/nord.nvim", + lazy = true, + }, + + -- Tokyo Night (alternative) + { + "folke/tokyonight.nvim", + lazy = true, + opts = { + style = "night", + }, + }, +} diff --git a/config/nvim/lua/plugins/editor.lua b/config/nvim/lua/plugins/editor.lua new file mode 100644 index 0000000..041c660 --- /dev/null +++ b/config/nvim/lua/plugins/editor.lua @@ -0,0 +1,203 @@ +-- Editor enhancements +return { + -- File explorer + { + "nvim-tree/nvim-tree.lua", + dependencies = { "nvim-tree/nvim-web-devicons" }, + keys = { + { "ee", "NvimTreeToggle", desc = "Toggle file explorer" }, + { "ef", "NvimTreeFindFileToggle", desc = "Toggle file explorer on current file" }, + { "ec", "NvimTreeCollapse", desc = "Collapse file explorer" }, + { "er", "NvimTreeRefresh", desc = "Refresh file explorer" }, + }, + opts = { + view = { + width = 35, + relativenumber = true, + }, + renderer = { + indent_markers = { + enable = true, + }, + icons = { + glyphs = { + folder = { + arrow_closed = "", + arrow_open = "", + }, + }, + }, + }, + actions = { + open_file = { + window_picker = { + enable = false, + }, + }, + }, + filters = { + custom = { ".DS_Store" }, + }, + git = { + ignore = false, + }, + }, + }, + + -- Fuzzy finder + { + "nvim-telescope/telescope.nvim", + branch = "0.1.x", + dependencies = { + "nvim-lua/plenary.nvim", + { + "nvim-telescope/telescope-fzf-native.nvim", + build = "make", + cond = function() + return vim.fn.executable("make") == 1 + end, + }, + }, + keys = { + { "ff", "Telescope find_files", desc = "Find files" }, + { "fr", "Telescope oldfiles", desc = "Recent files" }, + { "fg", "Telescope live_grep", desc = "Live grep" }, + { "fw", "Telescope grep_string", desc = "Find word under cursor" }, + { "fb", "Telescope buffers", desc = "Find buffers" }, + { "fh", "Telescope help_tags", desc = "Help tags" }, + { "fc", "Telescope commands", desc = "Commands" }, + { "fk", "Telescope keymaps", desc = "Keymaps" }, + { "fd", "Telescope diagnostics", desc = "Diagnostics" }, + { "/", "Telescope current_buffer_fuzzy_find", desc = "Search in current buffer" }, + { "", "Telescope buffers", desc = "Find buffers" }, + }, + opts = { + defaults = { + path_display = { "smart" }, + mappings = { + i = { + [""] = "move_selection_previous", + [""] = "move_selection_next", + [""] = "send_selected_to_qflist", + }, + }, + }, + }, + config = function(_, opts) + local telescope = require("telescope") + telescope.setup(opts) + pcall(telescope.load_extension, "fzf") + end, + }, + + -- Which-key for keybinding hints + { + "folke/which-key.nvim", + event = "VeryLazy", + opts = { + plugins = { spelling = true }, + }, + config = function(_, opts) + local wk = require("which-key") + wk.setup(opts) + wk.add({ + { "b", group = "buffer", mode = { "n", "v" } }, + { "c", group = "code", mode = { "n", "v" } }, + { "d", group = "diagnostics", mode = { "n", "v" } }, + { "e", group = "explorer", mode = { "n", "v" } }, + { "f", group = "find", mode = { "n", "v" } }, + { "g", group = "git", mode = { "n", "v" } }, + { "h", group = "hunk", mode = { "n", "v" } }, + { "s", group = "split", mode = { "n", "v" } }, + { "t", group = "tab", mode = { "n", "v" } }, + { "u", group = "toggle", mode = { "n", "v" } }, + { "x", group = "trouble", mode = { "n", "v" } }, + }) + end, + }, + + -- Auto pairs + { + "windwp/nvim-autopairs", + event = "InsertEnter", + opts = { + check_ts = true, + ts_config = { + lua = { "string" }, + javascript = { "template_string" }, + }, + }, + }, + + -- Surround text + { + "kylechui/nvim-surround", + version = "*", + event = "VeryLazy", + opts = {}, + }, + + -- Comment + { + "numToStr/Comment.nvim", + event = { "BufReadPre", "BufNewFile" }, + dependencies = { + "JoosepAlviste/nvim-ts-context-commentstring", + }, + config = function() + require("Comment").setup({ + pre_hook = require("ts_context_commentstring.integrations.comment_nvim").create_pre_hook(), + }) + end, + }, + + -- Todo comments + { + "folke/todo-comments.nvim", + event = { "BufReadPre", "BufNewFile" }, + dependencies = { "nvim-lua/plenary.nvim" }, + opts = {}, + keys = { + { "]t", function() require("todo-comments").jump_next() end, desc = "Next todo comment" }, + { "[t", function() require("todo-comments").jump_prev() end, desc = "Previous todo comment" }, + { "xt", "TodoTelescope", desc = "Todo comments" }, + }, + }, + + -- Indent guides + { + "lukas-reineke/indent-blankline.nvim", + main = "ibl", + event = { "BufReadPre", "BufNewFile" }, + opts = { + indent = { + char = "│", + tab_char = "│", + }, + scope = { enabled = false }, + exclude = { + filetypes = { + "help", + "dashboard", + "lazy", + "mason", + "notify", + }, + }, + }, + }, + + -- Better diagnostics + { + "folke/trouble.nvim", + dependencies = { "nvim-tree/nvim-web-devicons" }, + keys = { + { "xx", "Trouble toggle", desc = "Toggle Trouble" }, + { "xw", "Trouble diagnostics", desc = "Workspace diagnostics" }, + { "xd", "Trouble diagnostics filter.buf=0", desc = "Document diagnostics" }, + { "xl", "TroubleToggle loclist", desc = "Location list" }, + { "xq", "TroubleToggle quickfix", desc = "Quickfix list" }, + }, + opts = {}, + }, +} diff --git a/config/nvim/lua/plugins/git.lua b/config/nvim/lua/plugins/git.lua new file mode 100644 index 0000000..01a6d76 --- /dev/null +++ b/config/nvim/lua/plugins/git.lua @@ -0,0 +1,81 @@ +-- Git integration +return { + -- Git signs in gutter + { + "lewis6991/gitsigns.nvim", + event = { "BufReadPre", "BufNewFile" }, + opts = { + signs = { + add = { text = "▎" }, + change = { text = "▎" }, + delete = { text = "" }, + topdelete = { text = "" }, + changedelete = { text = "▎" }, + untracked = { text = "▎" }, + }, + on_attach = function(buffer) + local gs = package.loaded.gitsigns + + local function map(mode, l, r, desc) + vim.keymap.set(mode, l, r, { buffer = buffer, desc = desc }) + end + + -- Navigation + map("n", "]h", gs.next_hunk, "Next hunk") + map("n", "[h", gs.prev_hunk, "Prev hunk") + + -- Actions + map("n", "hs", gs.stage_hunk, "Stage hunk") + map("n", "hr", gs.reset_hunk, "Reset hunk") + map("v", "hs", function() gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") }) end, "Stage hunk") + map("v", "hr", function() gs.reset_hunk({ vim.fn.line("."), vim.fn.line("v") }) end, "Reset hunk") + map("n", "hS", gs.stage_buffer, "Stage buffer") + map("n", "hu", gs.undo_stage_hunk, "Undo stage hunk") + map("n", "hR", gs.reset_buffer, "Reset buffer") + map("n", "hp", gs.preview_hunk, "Preview hunk") + map("n", "hb", function() gs.blame_line({ full = true }) end, "Blame line") + map("n", "hd", gs.diffthis, "Diff this") + map("n", "hD", function() gs.diffthis("~") end, "Diff this ~") + + -- Toggles + map("n", "tb", gs.toggle_current_line_blame, "Toggle blame line") + map("n", "td", gs.toggle_deleted, "Toggle deleted") + + -- Text object + map({ "o", "x" }, "ih", ":Gitsigns select_hunk", "Select hunk") + end, + }, + }, + + -- Git commands + { + "tpope/vim-fugitive", + cmd = { "Git", "G", "Gdiffsplit", "Gvdiffsplit" }, + keys = { + { "gs", "Git", desc = "Git status" }, + { "gd", "Gdiffsplit", desc = "Git diff" }, + { "gc", "Git commit", desc = "Git commit" }, + { "gp", "Git push", desc = "Git push" }, + { "gl", "Git pull", desc = "Git pull" }, + { "gb", "Git blame", desc = "Git blame" }, + }, + }, + + -- Lazygit integration + { + "kdheepak/lazygit.nvim", + cmd = { + "LazyGit", + "LazyGitConfig", + "LazyGitCurrentFile", + "LazyGitFilter", + "LazyGitFilterCurrentFile", + }, + dependencies = { + "nvim-lua/plenary.nvim", + }, + keys = { + { "gg", "LazyGit", desc = "LazyGit" }, + }, + }, +} diff --git a/config/nvim/lua/plugins/lsp.lua b/config/nvim/lua/plugins/lsp.lua new file mode 100644 index 0000000..f4368c6 --- /dev/null +++ b/config/nvim/lua/plugins/lsp.lua @@ -0,0 +1,308 @@ +-- LSP Configuration +return { + -- Mason for managing LSP servers + { + "williamboman/mason.nvim", + cmd = "Mason", + keys = { { "cm", "Mason", desc = "Mason" } }, + build = ":MasonUpdate", + opts = { + ensure_installed = { + "stylua", + "shfmt", + "prettier", + "eslint_d", + }, + }, + config = function(_, opts) + require("mason").setup(opts) + local mr = require("mason-registry") + local function ensure_installed() + for _, tool in ipairs(opts.ensure_installed) do + local p = mr.get_package(tool) + if not p:is_installed() then + p:install() + end + end + end + if mr.refresh then + mr.refresh(ensure_installed) + else + ensure_installed() + end + end, + }, + + -- LSP configuration + { + "neovim/nvim-lspconfig", + event = { "BufReadPre", "BufNewFile" }, + dependencies = { + "mason.nvim", + "williamboman/mason-lspconfig.nvim", + { "j-hui/fidget.nvim", opts = {} }, + }, + opts = { + diagnostics = { + underline = true, + update_in_insert = false, + virtual_text = { + spacing = 4, + source = "if_many", + prefix = "●", + }, + severity_sort = true, + }, + servers = { + lua_ls = { + settings = { + Lua = { + workspace = { + checkThirdParty = false, + }, + completion = { + callSnippet = "Replace", + }, + diagnostics = { + globals = { "vim" }, + }, + }, + }, + }, + ts_ls = {}, + pyright = {}, + rust_analyzer = {}, + gopls = {}, + bashls = {}, + jsonls = {}, + yamlls = {}, + html = {}, + cssls = {}, + }, + }, + config = function(_, opts) + -- Diagnostics configuration + vim.diagnostic.config(opts.diagnostics) + + -- LSP keymaps + vim.api.nvim_create_autocmd("LspAttach", { + group = vim.api.nvim_create_augroup("UserLspConfig", {}), + callback = function(ev) + local buffer = ev.buf + local client = vim.lsp.get_clients({ id = ev.data.client_id })[1] + + local function map(mode, lhs, rhs, desc) + vim.keymap.set(mode, lhs, rhs, { buffer = buffer, desc = desc }) + end + + -- Go to definition + map("n", "gd", vim.lsp.buf.definition, "Go to definition") + map("n", "gr", vim.lsp.buf.references, "Go to references") + map("n", "gD", vim.lsp.buf.declaration, "Go to declaration") + map("n", "gI", vim.lsp.buf.implementation, "Go to implementation") + map("n", "gy", vim.lsp.buf.type_definition, "Go to type definition") + + -- Hover and signature help + map("n", "K", vim.lsp.buf.hover, "Hover documentation") + map("n", "gK", vim.lsp.buf.signature_help, "Signature help") + map("i", "", vim.lsp.buf.signature_help, "Signature help") + + -- Actions + map("n", "ca", vim.lsp.buf.code_action, "Code action") + map("n", "cr", vim.lsp.buf.rename, "Rename") + map("n", "cf", function() + vim.lsp.buf.format({ async = true }) + end, "Format document") + + -- Workspace + map("n", "wa", vim.lsp.buf.add_workspace_folder, "Add workspace folder") + map("n", "wr", vim.lsp.buf.remove_workspace_folder, "Remove workspace folder") + map("n", "wl", function() + print(vim.inspect(vim.lsp.buf.list_workspace_folders())) + end, "List workspace folders") + + -- Inlay hints (Neovim 0.10+) + if client and client.server_capabilities.inlayHintProvider and vim.lsp.inlay_hint then + map("n", "uh", function() + vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled()) + end, "Toggle inlay hints") + end + end, + }) + + -- Setup mason-lspconfig + require("mason-lspconfig").setup({ + ensure_installed = vim.tbl_keys(opts.servers), + automatic_installation = true, + }) + + -- Setup each server + local capabilities = vim.lsp.protocol.make_client_capabilities() + local has_cmp, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp") + if has_cmp then + capabilities = vim.tbl_deep_extend("force", capabilities, cmp_nvim_lsp.default_capabilities()) + end + + require("mason-lspconfig").setup_handlers({ + function(server_name) + local server_opts = opts.servers[server_name] or {} + server_opts.capabilities = vim.tbl_deep_extend("force", {}, capabilities, server_opts.capabilities or {}) + require("lspconfig")[server_name].setup(server_opts) + end, + }) + end, + }, + + -- Autocompletion + { + "hrsh7th/nvim-cmp", + version = false, + event = "InsertEnter", + dependencies = { + "hrsh7th/cmp-nvim-lsp", + "hrsh7th/cmp-buffer", + "hrsh7th/cmp-path", + "hrsh7th/cmp-cmdline", + { + "L3MON4D3/LuaSnip", + version = "v2.*", + build = "make install_jsregexp", + dependencies = { + "rafamadriz/friendly-snippets", + config = function() + require("luasnip.loaders.from_vscode").lazy_load() + end, + }, + }, + "saadparwaiz1/cmp_luasnip", + }, + opts = function() + local cmp = require("cmp") + local luasnip = require("luasnip") + + return { + completion = { + completeopt = "menu,menuone,noinsert", + }, + snippet = { + expand = function(args) + luasnip.lsp_expand(args.body) + end, + }, + mapping = cmp.mapping.preset.insert({ + [""] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }), + [""] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }), + [""] = cmp.mapping.scroll_docs(-4), + [""] = cmp.mapping.scroll_docs(4), + [""] = cmp.mapping.complete(), + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm({ select = true }), + [""] = cmp.mapping.confirm({ + behavior = cmp.ConfirmBehavior.Replace, + select = true, + }), + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expand_or_jumpable() then + luasnip.expand_or_jump() + else + fallback() + end + end, { "i", "s" }), + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_prev_item() + elseif luasnip.jumpable(-1) then + luasnip.jump(-1) + else + fallback() + end + end, { "i", "s" }), + }), + sources = cmp.config.sources({ + { name = "nvim_lsp" }, + { name = "luasnip" }, + { name = "path" }, + }, { + { name = "buffer" }, + }), + formatting = { + format = function(_, item) + local icons = { + Array = " ", + Boolean = "󰨙 ", + Class = " ", + Codeium = "󰘦 ", + Color = " ", + Control = " ", + Collapsed = " ", + Constant = "󰏿 ", + Constructor = " ", + Copilot = " ", + Enum = " ", + EnumMember = " ", + Event = " ", + Field = " ", + File = " ", + Folder = " ", + Function = "󰊕 ", + Interface = " ", + Key = " ", + Keyword = " ", + Method = "󰊕 ", + Module = " ", + Namespace = "󰦮 ", + Null = " ", + Number = "󰎠 ", + Object = " ", + Operator = " ", + Package = " ", + Property = " ", + Reference = " ", + Snippet = " ", + String = " ", + Struct = "󰆼 ", + TabNine = "󰏚 ", + Text = " ", + TypeParameter = " ", + Unit = " ", + Value = " ", + Variable = "󰀫 ", + } + if icons[item.kind] then + item.kind = icons[item.kind] .. item.kind + end + return item + end, + }, + experimental = { + ghost_text = { + hl_group = "CmpGhostText", + }, + }, + } + end, + config = function(_, opts) + local cmp = require("cmp") + cmp.setup(opts) + + -- Cmdline completion + cmp.setup.cmdline("/", { + mapping = cmp.mapping.preset.cmdline(), + sources = { + { name = "buffer" }, + }, + }) + + cmp.setup.cmdline(":", { + mapping = cmp.mapping.preset.cmdline(), + sources = cmp.config.sources({ + { name = "path" }, + }, { + { name = "cmdline" }, + }), + }) + end, + }, +} diff --git a/config/nvim/lua/plugins/treesitter.lua b/config/nvim/lua/plugins/treesitter.lua new file mode 100644 index 0000000..1444df3 --- /dev/null +++ b/config/nvim/lua/plugins/treesitter.lua @@ -0,0 +1,114 @@ +-- Treesitter configuration +return { + { + "nvim-treesitter/nvim-treesitter", + version = false, + build = ":TSUpdate", + event = { "BufReadPre", "BufNewFile" }, + dependencies = { + "nvim-treesitter/nvim-treesitter-textobjects", + }, + cmd = { "TSUpdateSync", "TSUpdate", "TSInstall" }, + keys = { + { "", desc = "Increment selection" }, + { "", desc = "Decrement selection", mode = "x" }, + }, + opts = { + highlight = { enable = true }, + indent = { enable = true }, + ensure_installed = { + "bash", + "c", + "css", + "diff", + "dockerfile", + "gitignore", + "go", + "html", + "javascript", + "jsdoc", + "json", + "jsonc", + "lua", + "luadoc", + "luap", + "markdown", + "markdown_inline", + "python", + "query", + "regex", + "rust", + "toml", + "tsx", + "typescript", + "vim", + "vimdoc", + "yaml", + }, + incremental_selection = { + enable = true, + keymaps = { + init_selection = "", + node_incremental = "", + scope_incremental = false, + node_decremental = "", + }, + }, + textobjects = { + select = { + enable = true, + lookahead = true, + keymaps = { + ["af"] = "@function.outer", + ["if"] = "@function.inner", + ["ac"] = "@class.outer", + ["ic"] = "@class.inner", + ["aa"] = "@parameter.outer", + ["ia"] = "@parameter.inner", + }, + }, + move = { + enable = true, + goto_next_start = { + ["]f"] = "@function.outer", + ["]c"] = "@class.outer", + }, + goto_next_end = { + ["]F"] = "@function.outer", + ["]C"] = "@class.outer", + }, + goto_previous_start = { + ["[f"] = "@function.outer", + ["[c"] = "@class.outer", + }, + goto_previous_end = { + ["[F"] = "@function.outer", + ["[C"] = "@class.outer", + }, + }, + swap = { + enable = true, + swap_next = { + ["a"] = "@parameter.inner", + }, + swap_previous = { + ["A"] = "@parameter.inner", + }, + }, + }, + }, + config = function(_, opts) + require("nvim-treesitter.configs").setup(opts) + end, + }, + + -- Show context of the current function + { + "nvim-treesitter/nvim-treesitter-context", + event = { "BufReadPre", "BufNewFile" }, + opts = { + mode = "cursor", + max_lines = 3, + }, + }, +} diff --git a/config/nvim/lua/plugins/ui.lua b/config/nvim/lua/plugins/ui.lua new file mode 100644 index 0000000..2455901 --- /dev/null +++ b/config/nvim/lua/plugins/ui.lua @@ -0,0 +1,176 @@ +-- UI enhancements +return { + -- Status line + { + "nvim-lualine/lualine.nvim", + dependencies = { "nvim-tree/nvim-web-devicons" }, + event = "VeryLazy", + opts = { + options = { + theme = "catppuccin", + globalstatus = true, + disabled_filetypes = { statusline = { "dashboard", "alpha" } }, + }, + sections = { + lualine_a = { "mode" }, + lualine_b = { "branch" }, + lualine_c = { + { "diagnostics" }, + { "filetype", icon_only = true, separator = "", padding = { left = 1, right = 0 } }, + { "filename", path = 1 }, + }, + lualine_x = { + { + "diff", + symbols = { + added = " ", + modified = " ", + removed = " ", + }, + }, + }, + lualine_y = { + { "progress", separator = " ", padding = { left = 1, right = 0 } }, + { "location", padding = { left = 0, right = 1 } }, + }, + lualine_z = { + function() + return " " .. os.date("%R") + end, + }, + }, + extensions = { "nvim-tree", "lazy" }, + }, + }, + + -- Buffer line + { + "akinsho/bufferline.nvim", + version = "*", + dependencies = { "nvim-tree/nvim-web-devicons" }, + event = "VeryLazy", + keys = { + { "bp", "BufferLineTogglePin", desc = "Toggle pin" }, + { "bP", "BufferLineGroupClose ungrouped", desc = "Delete non-pinned buffers" }, + { "bo", "BufferLineCloseOthers", desc = "Delete other buffers" }, + { "br", "BufferLineCloseRight", desc = "Delete buffers to the right" }, + { "bl", "BufferLineCloseLeft", desc = "Delete buffers to the left" }, + { "[b", "BufferLineCyclePrev", desc = "Prev buffer" }, + { "]b", "BufferLineCycleNext", desc = "Next buffer" }, + }, + opts = { + options = { + close_command = "bdelete! %d", + right_mouse_command = "bdelete! %d", + diagnostics = "nvim_lsp", + always_show_bufferline = false, + offsets = { + { + filetype = "NvimTree", + text = "File Explorer", + highlight = "Directory", + text_align = "left", + }, + }, + }, + }, + }, + + -- Notifications + { + "rcarriga/nvim-notify", + keys = { + { + "un", + function() + require("notify").dismiss({ silent = true, pending = true }) + end, + desc = "Dismiss all notifications", + }, + }, + opts = { + timeout = 3000, + max_height = function() + return math.floor(vim.o.lines * 0.75) + end, + max_width = function() + return math.floor(vim.o.columns * 0.75) + end, + }, + init = function() + vim.notify = require("notify") + end, + }, + + -- Better vim.ui + { + "stevearc/dressing.nvim", + lazy = true, + init = function() + vim.ui.select = function(...) + require("lazy").load({ plugins = { "dressing.nvim" } }) + return vim.ui.select(...) + end + vim.ui.input = function(...) + require("lazy").load({ plugins = { "dressing.nvim" } }) + return vim.ui.input(...) + end + end, + }, + + -- Dashboard + { + "goolord/alpha-nvim", + event = "VimEnter", + opts = function() + local dashboard = require("alpha.themes.dashboard") + local logo = [[ + ███╗ ██╗███████╗ ██████╗ ██╗ ██╗██╗███╗ ███╗ + ████╗ ██║██╔════╝██╔═══██╗██║ ██║██║████╗ ████║ + ██╔██╗ ██║█████╗ ██║ ██║██║ ██║██║██╔████╔██║ + ██║╚██╗██║██╔══╝ ██║ ██║╚██╗ ██╔╝██║██║╚██╔╝██║ + ██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║ + ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ + ]] + + dashboard.section.header.val = vim.split(logo, "\n") + dashboard.section.buttons.val = { + dashboard.button("f", " " .. " Find file", "Telescope find_files"), + dashboard.button("n", " " .. " New file", "ene startinsert"), + dashboard.button("r", " " .. " Recent files", "Telescope oldfiles"), + dashboard.button("g", " " .. " Find text", "Telescope live_grep"), + dashboard.button("c", " " .. " Config", "e $MYVIMRC"), + dashboard.button("l", "󰒲 " .. " Lazy", "Lazy"), + dashboard.button("q", " " .. " Quit", "qa"), + } + for _, button in ipairs(dashboard.section.buttons.val) do + button.opts.hl = "AlphaButtons" + button.opts.hl_shortcut = "AlphaShortcut" + end + dashboard.section.header.opts.hl = "AlphaHeader" + dashboard.section.buttons.opts.hl = "AlphaButtons" + dashboard.section.footer.opts.hl = "AlphaFooter" + dashboard.opts.layout[1].val = 8 + return dashboard + end, + config = function(_, dashboard) + require("alpha").setup(dashboard.opts) + + vim.api.nvim_create_autocmd("User", { + pattern = "LazyVimStarted", + callback = function() + local stats = require("lazy").stats() + local ms = (math.floor(stats.startuptime * 100 + 0.5) / 100) + dashboard.section.footer.val = "⚡ Neovim loaded " .. stats.count .. " plugins in " .. ms .. "ms" + pcall(vim.cmd.AlphaRedraw) + end, + }) + end, + }, + + -- Icons + { + "nvim-tree/nvim-web-devicons", + lazy = true, + }, +} diff --git a/docs/agents/architecture.md b/docs/agents/architecture.md new file mode 100644 index 0000000..e5a8af6 --- /dev/null +++ b/docs/agents/architecture.md @@ -0,0 +1,49 @@ +# Architecture + +## Directory Layout + +| Directory | Purpose | +| --------------- | -------------------------------------------------------------------------------------------------------- | +| `bin/` | Main `dotfiles` CLI and utility scripts (`is-apple-silicon`, `command-exists`, etc.) | +| `scripts/` | Install helpers — `echos.sh` (colored output), `requirers.sh` (package installers), `install_prezto.zsh` | +| `packages/` | Package lists: `brew.list`, `cask.list`, `npm.list`, `mas.list`, `code.list`, `tap.list` | +| `macos/` | System defaults scripts — `defaults.sh`, `defaults-*.sh` (per-app), `dock.sh` | +| `runcom/` | Dotfiles stowed to `~/` — `.zshrc`, `.zprofile`, `.vimrc`, `.zpreztorc`, etc. | +| `config/` | XDG config stowed to `~/.config/` — `git/`, `nvim/`, `karabiner/`, `starship/`, `thefuck/`, etc. | +| `modules/` | Git submodules — `prezto/`, `prezto-contrib/`, `zsh/` plugins, `stevenblack-hosts/` | +| `system/` | Shell config sourced by `.zshrc` — `.alias`, `.env`, `.path`, `.function*`, `.fzf`, `.prompt`, etc. | +| `apps/` | App themes — Terminal, Xcode, Warp, GitKraken, VLC, VS Code | +| `fonts/` | Powerline fonts with `install.sh` | +| `profiles/` | Machine-specific config — `default.zsh`, `personal.zsh`, `work.zsh`, `local.zsh` (gitignored) | +| `launchagents/` | macOS LaunchAgents — mackup auto-backup every hour | +| `completions/` | Zsh completions (e.g., `_fnm`) | +| `Brewfile` | Homebrew bundle manifest (taps, formulae, casks, MAS apps) | + +## GNU Stow Linking + +`./bin/dotfiles link` does: + +1. Backs up existing dotfiles to `~/.dotfiles_backup/` +2. Stows `runcom/` → `~/` +3. Stows `config/` → `~/.config/` + +To restore: `./bin/dotfiles unlink ` + +## Submodules + +External dependencies are git submodules in `modules/`: + +- **Prezto** + contrib modules (shell framework) +- **zsh plugins** (zsh-thefuck, zsh-lazy-load) +- **StevenBlack hosts** (ad-blocking `/etc/hosts`) +- **Vundle** (Vim plugin manager, in `runcom/.vim/bundle/Vundle.vim`) + +Update: `./bin/dotfiles update` or `git submodule update --remote --recursive --merge` + +## System Detection + +Both Intel and Apple Silicon supported: + +- `bin/is-apple-silicon` — returns 0 on AS +- Homebrew: `/opt/homebrew` (AS) or `/usr/local` (Intel) +- Detected automatically in shell config via `$HOMEBREW_PREFIX` diff --git a/docs/agents/commands.md b/docs/agents/commands.md new file mode 100644 index 0000000..e806443 --- /dev/null +++ b/docs/agents/commands.md @@ -0,0 +1,35 @@ +# CLI Commands + +All operations go through `./bin/dotfiles `. Run `./bin/dotfiles help` for the full list. + +## Key Commands + +``` +install # Bootstrap system (interactive) +install --all # Install everything non-interactively +install --hosts # Update /etc/hosts with ad-blocking +install --prezto # Install Prezto zsh framework +install --packages # Install brew/cask/npm/mas/vscode packages +install --ssh # Generate SSH key (ed25519) +link # Symlink dotfiles to ~/ via GNU Stow +unlink # Restore dotfiles from backup +configure --defaults # Apply macOS system defaults +configure --dock # Configure Dock settings +update # Update submodules (interactive, prompts for commit msg) +update --system # Update OS, brew, npm, gem packages +test # Run test suite +test --verbose # Detailed test output +doctor # Diagnose common issues +doctor --fix # Auto-fix issues where possible +hooks # Install git pre-commit hooks +``` + +## Common Workflows + +**Add a new brew/cask/mas package**: Add to `Brewfile`, run `brew bundle install`. + +**Add a new npm/vscode package**: Add to `packages/npm.list` or `packages/code.list`, run `./bin/dotfiles install --packages`. + +**Modify system defaults**: Edit script in `macos/`, run `./bin/dotfiles configure --defaults`. Requires logout/restart. + +**Manage hosts whitelist**: Add domains to `system/hosts.whitelist` (one per line), run `./bin/dotfiles install --hosts`. The whitelist is copied into the stevenblack-hosts module during installation. diff --git a/docs/agents/macos-defaults.md b/docs/agents/macos-defaults.md new file mode 100644 index 0000000..b855ae6 --- /dev/null +++ b/docs/agents/macos-defaults.md @@ -0,0 +1,20 @@ +# macOS System Defaults + +## Structure + +- `macos/defaults.sh` — main system preferences (Finder, keyboard, trackpad, etc.) +- `macos/defaults-*.sh` — per-app settings (Safari, Chrome, Xcode, etc.) +- `macos/dock.sh` — Dock layout and configuration + +## Applying + +```bash +./bin/dotfiles configure --defaults # System defaults +./bin/dotfiles configure --dock # Dock layout +``` + +Changes require **logout or restart** to take full effect. + +## Editing + +Use `defaults write` commands in the appropriate script. Use `bin/plistbuddy` helper for plist edits. Group related settings in the per-app files (`defaults-safari.sh`, etc.). diff --git a/docs/agents/neovim.md b/docs/agents/neovim.md new file mode 100644 index 0000000..2f63396 --- /dev/null +++ b/docs/agents/neovim.md @@ -0,0 +1,24 @@ +# Neovim Configuration + +Uses **lazy.nvim** as package manager. Config lives in `config/nvim/`. + +## Structure + +- `init.lua` — entry point +- `lua/config/` — options, keymaps, autocmds, lazy.nvim bootstrap +- `lua/plugins/` — plugin specs: `colorscheme.lua`, `editor.lua`, `git.lua`, `lsp.lua`, `treesitter.lua`, `ui.lua` + +## Key Bindings (Leader = Space) + +| Binding | Action | +| ------------ | -------------------- | +| `ff` | Find files | +| `fg` | Live grep | +| `ee` | Toggle file explorer | +| `gg` | LazyGit | +| `ca` | Code actions | +| `cr` | Rename symbol | + +## Themes + +Catppuccin, Nord, TokyoNight available in `lua/plugins/colorscheme.lua`. diff --git a/docs/agents/packages.md b/docs/agents/packages.md new file mode 100644 index 0000000..c15bb2f --- /dev/null +++ b/docs/agents/packages.md @@ -0,0 +1,26 @@ +# Package Management + +## Two Systems + +| Method | Scope | Command | +| ----------------- | ------------------------------------------------------------------------- | ----------------------------------- | +| `Brewfile` | Homebrew taps, formulae, casks, Mac App Store | `brew bundle install` | +| `packages/*.list` | npm globals, VS Code extensions, and other tools Brewfile doesn't support | `./bin/dotfiles install --packages` | + +## Adding Packages + +- **brew formula/cask/MAS app**: Add to `Brewfile`, run `brew bundle install` +- **npm global**: Add to `packages/npm.list`, run `./bin/dotfiles install --packages` +- **VS Code extension**: Add to `packages/code.list`, run `./bin/dotfiles install --packages` +- **Homebrew tap**: Add to `Brewfile` (preferred) or `packages/tap.list` + +## Install Helpers + +`scripts/requirers.sh` provides idempotent installers used by install scripts: + +- `require_brew ` / `require_cask ` / `require_npm ` +- `require_fnm()` / `source_fnm()` + +## Node.js + +Managed by **FNM** (Fast Node Manager), not NVM. FNM config lives in `system/.fnm`. Completions in `completions/_fnm`. diff --git a/docs/agents/secrets.md b/docs/agents/secrets.md new file mode 100644 index 0000000..8f3f137 --- /dev/null +++ b/docs/agents/secrets.md @@ -0,0 +1,24 @@ +# Secrets Management + +Secrets are stored in macOS Keychain via `./bin/dotfiles secrets` (backed by `bin/dotfiles-secrets`). + +## Commands + +```bash +dotfiles secrets set # Store (prompts for value) +dotfiles secrets get # Retrieve +dotfiles secrets delete # Delete +dotfiles secrets list # List all +dotfiles secrets export .enc # Export encrypted (AES-256) +dotfiles secrets import .enc # Import from encrypted file +``` + +## Usage in Scripts + +```bash +export GITHUB_TOKEN=$(dotfiles-secrets get github_token) +# or +eval "$(dotfiles-secrets env github_token GITHUB_TOKEN)" +``` + +Never commit secrets to git. Keychain requires user authentication to access. diff --git a/docs/agents/shell-config.md b/docs/agents/shell-config.md new file mode 100644 index 0000000..298567b --- /dev/null +++ b/docs/agents/shell-config.md @@ -0,0 +1,37 @@ +# Shell Configuration + +## Framework + +**Prezto** (not Oh My Zsh) — chosen for performance. Configured via `runcom/.zpreztorc`. + +**Prompt**: Powerlevel10k via Prezto's prompt module. Config in `system/.prompt`. Starship config exists at `system/.starship` and `config/starship/` but is **not active**. + +## Source Order + +`.zshrc` sources files from `system/` in this order: + +`.env` → `.path` → `.prompt` → `.alias` → `.function*` → `.completion` → `.bindings` → `.fzf` → `.fnm` → `.pnpm` → `.zoxide` → `.fix` → `.grep` → `.dir_colors` + +## Machine Profiles + +Loading order: + +1. `profiles/default.zsh` — always loaded +2. `profiles/$DOTFILES_PROFILE.zsh` or `profiles/$(hostname).zsh` +3. `profiles/local.zsh` — machine-specific overrides (gitignored) + +Set profile: `export DOTFILES_PROFILE="work"` or create `profiles/.zsh`. + +## Caching + +Shell init outputs are cached in `~/.cache/*.zsh` for performance: + +```bash +rm ~/.cache/*.zsh # Clear all caches +fnm_refresh # Refresh fnm cache specifically +exec $SHELL # Restart shell to regenerate +``` + +## Key Bindings + +Defined in `system/.bindings` — history-substring-search, word navigation. diff --git a/docs/agents/testing-and-ci.md b/docs/agents/testing-and-ci.md new file mode 100644 index 0000000..a07de9a --- /dev/null +++ b/docs/agents/testing-and-ci.md @@ -0,0 +1,32 @@ +# Testing & CI + +## Test Suite + +```bash +./bin/dotfiles test # Run all tests +./bin/dotfiles test --verbose # Detailed output +./bin/dotfiles test --quick # Skip slow tests (startup timing) +``` + +**Categories**: syntax validation | cache generation | function tests (`prepend-path`, `get`, `dedup-pathvar`) | alias verification | environment variables (XDG) | shell startup time (<1000ms target) | file structure | Prezto config + +## CI Pipeline + +GitHub Actions (`.github/workflows/ci.yml`) runs on push/PR: + +1. Bash + zsh syntax validation +2. Shellcheck linting +3. Test suite +4. Brewfile validation + +## Git Hooks + +Install with `./bin/dotfiles hooks`. Pre-commit runs: + +- Bash/zsh syntax validation +- Shellcheck +- Secret detection (prevents committing passwords/keys) + +## Shellcheck + +All shell scripts must pass shellcheck. Use `# shellcheck disable=SC####` with justification for intentional exceptions. diff --git a/launchagents/com.stixzoor.mackup-auto.plist b/launchagents/com.stixzoor.mackup-auto.plist index e4d640d..b2c8a9f 100644 --- a/launchagents/com.stixzoor.mackup-auto.plist +++ b/launchagents/com.stixzoor.mackup-auto.plist @@ -5,11 +5,17 @@ Label com.stixzoor.mackup-auto + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + + ProgramArguments /bin/bash -c - mackup backup --force + /opt/homebrew/bin/mackup backup --force StartInterval diff --git a/macos/defaults-activitymonitor.sh b/macos/defaults-activitymonitor.sh index 42c49e3..870448a 100644 --- a/macos/defaults-activitymonitor.sh +++ b/macos/defaults-activitymonitor.sh @@ -21,7 +21,6 @@ ok # 104: Other User Processes # 105: Active Processes # 106: Inactive Processes -# 106: Inactive Processes # 107: Windowed Processes running "Show all processes in Activity Monitor" defaults write com.apple.ActivityMonitor ShowCategory -int 100 @@ -65,13 +64,4 @@ running "Show Data in the Network graph (instead of packets)" defaults write com.apple.ActivityMonitor NetworkGraphType -int 1 ok -running "Change Dock Icon" -# 0: Application Icon -# 2: Network Usage -# 3: Disk Activity -# 5: CPU Usage -# 6: CPU History -defaults write com.apple.ActivityMonitor IconType -int 3 -ok - killall "Activity Monitor" >/dev/null 2>&1 diff --git a/macos/defaults-appstore.sh b/macos/defaults-appstore.sh index 548f8f0..0f05c2b 100644 --- a/macos/defaults-appstore.sh +++ b/macos/defaults-appstore.sh @@ -5,25 +5,12 @@ source "$DOTFILES_DIR/scripts/requirers.sh" bot "Mac App Store" ############################################################################### -running "Enable the WebKit Developer Tools in the Mac App Store" -defaults write com.apple.appstore WebKitDeveloperExtras -bool true -ok - -running "Enable Debug Menu in the Mac App Store" -defaults write com.apple.appstore ShowDebugMenu -bool true -ok - running "Enable the automatic update check" defaults write com.apple.SoftwareUpdate AutomaticCheckEnabled -bool true ok -running "Check for software updates daily, not just once per week" -defaults write com.apple.SoftwareUpdate ScheduleFrequency -int 1 -ok - -running "Automatically download apps purchased on other Macs" -defaults write com.apple.SoftwareUpdate ConfigDataInstall -int 1 -ok +# ConfigDataInstall: Removed — deprecated since Catalina. +# Security data updates (XProtect, Gatekeeper) are now automatic via Rapid Security Responses. running "Turn on app auto-update" defaults write com.apple.commerce AutoUpdate -bool true diff --git a/macos/defaults-mail.sh b/macos/defaults-mail.sh index 172bd3e..331cace 100644 --- a/macos/defaults-mail.sh +++ b/macos/defaults-mail.sh @@ -4,10 +4,8 @@ source "$DOTFILES_DIR/scripts/requirers.sh" ############################################################################### bot "Mail" ############################################################################### -running "Disable send and reply animations in Mail.app" -defaults write com.apple.mail DisableReplyAnimations -bool true -defaults write com.apple.mail DisableSendAnimations -bool true -ok +# DisableReplyAnimations / DisableSendAnimations: Removed — broken since High Sierra. +# Mail's animation system was replaced and no longer reads these keys. running "Copy email addresses as 'foo@example.com' instead of 'Foo Bar ' in Mail.app" defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false diff --git a/macos/defaults-messages.sh b/macos/defaults-messages.sh deleted file mode 100644 index d9e35ea..0000000 --- a/macos/defaults-messages.sh +++ /dev/null @@ -1,16 +0,0 @@ -source "$DOTFILES_DIR/scripts/echos.sh" -source "$DOTFILES_DIR/scripts/requirers.sh" - -############################################################################### -bot "Messages" -############################################################################### - -running "Disable automatic emoji substitution (i.e. use plain text smileys)" -defaults write com.apple.messageshelper.MessageController SOInputLineSettings -dict-add "automaticEmojiSubstitutionEnablediMessage" -bool false -ok - -running "Disable smart quotes as it’s annoying for messages that contain code" -defaults write com.apple.messageshelper.MessageController SOInputLineSettings -dict-add "automaticQuoteSubstitutionEnabled" -bool false -ok - -killall "Messages" >/dev/null 2>&1 diff --git a/macos/defaults-safari.sh b/macos/defaults-safari.sh index 6095d52..d20a22d 100644 --- a/macos/defaults-safari.sh +++ b/macos/defaults-safari.sh @@ -5,6 +5,16 @@ source "$DOTFILES_DIR/scripts/requirers.sh" bot "Safari & WebKit" ############################################################################### +# NOTE: Since Safari 13 (Catalina), Safari is sandboxed. All `defaults write` +# commands require Terminal to have Full Disk Access, otherwise writes go to +# ~/Library/Preferences/ (which Safari ignores) instead of the container at +# ~/Library/Containers/com.apple.Safari/Data/Library/Preferences/ +# Grant access in: System Settings > Privacy & Security > Full Disk Access +if ! ls ~/Library/Containers/com.apple.Safari/ &>/dev/null; then + error "Terminal lacks Full Disk Access — Safari defaults will NOT take effect" + error "Grant access in: System Settings > Privacy & Security > Full Disk Access" +fi + running "Don’t send search queries to Apple" defaults write com.apple.Safari UniversalSearchEnabled -bool false defaults write com.apple.Safari SuppressSearchSuggestions -bool true @@ -31,22 +41,20 @@ ok # For High Sierra and below enable the default one running "Allow hitting the Backspace key to go to the previous page in history" defaults write com.apple.Safari NSUserKeyEquivalents -dict-add Back "\U232b" +ok running "Hide Safari’s bookmarks bar by default" defaults write com.apple.Safari ShowFavoritesBar -bool false ok -running "Hide Safari’s sidebar in Top Sites" -defaults write com.apple.Safari ShowSidebarInTopSites -bool false -ok +# ShowSidebarInTopSites: Removed — "Top Sites" was replaced by "Start Page" in Safari 14 (Big Sur) running "Disable Safari’s thumbnail cache for History and Top Sites" defaults write com.apple.Safari DebugSnapshotsUpdatePolicy -int 2 ok -running "Enable Safari’s debug menu" -defaults write com.apple.Safari IncludeInternalDebugMenu -bool true -ok +# IncludeInternalDebugMenu: Non-functional since Safari 15+. The Develop menu +# (IncludeDevelopMenu below) is what most people want. running "Make Safari’s search banners default to Contains instead of Starts With" defaults write com.apple.Safari FindOnPageMatchesWordStartsOnly -bool false @@ -85,25 +93,14 @@ running "Warn about fraudulent websites" defaults write com.apple.Safari WarnAboutFraudulentWebsites -bool true ok -running "Disable plug-ins" -defaults write com.apple.Safari WebKitPluginsEnabled -bool false -defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2PluginsEnabled -bool false -ok - -running "Disable Java" -defaults write com.apple.Safari WebKitJavaEnabled -bool false -defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaEnabled -bool false -defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaEnabledForLocalFiles -bool false -ok +# Plugins and Java removed from Safari since macOS Catalina running "Block pop-up windows" defaults write com.apple.Safari WebKitJavaScriptCanOpenWindowsAutomatically -bool false defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaScriptCanOpenWindowsAutomatically -bool false ok -running "Enable Do Not Track" -defaults write com.apple.Safari SendDoNotTrackHTTPHeader -bool true -ok +# Do Not Track removed from Safari since macOS Monterey running "Update extensions automatically" defaults write com.apple.Safari InstallExtensionUpdatesAutomatically -bool true diff --git a/macos/defaults-terminal.sh b/macos/defaults-terminal.sh index 8289a28..e333744 100644 --- a/macos/defaults-terminal.sh +++ b/macos/defaults-terminal.sh @@ -25,7 +25,7 @@ if [ "${CURRENT_PROFILE}" != "${TERM_PROFILE}" ]; then fi ok -running "Enable “focus follows mouse” for Terminal.app and all X11 apps" +running "Enable 'focus follows mouse' for Terminal.app and all X11 apps" defaults write com.apple.terminal FocusFollowsMouse -bool true ok @@ -39,7 +39,7 @@ mkdir -p "$CUSTOM_THEME_DIR/" 2>/dev/null ok running "Install themes for Warp" -rm -rf "$CUSTOM_THEME_DIR/*.yaml" 2>/dev/null +rm -rf "$CUSTOM_THEME_DIR"/*.yaml 2>/dev/null cp "$DOTFILES_DIR/apps/warp/themes/standard"/*.yaml "$CUSTOM_THEME_DIR/" 2>/dev/null cp "$DOTFILES_DIR/apps/warp/themes/base16"/*.yaml "$CUSTOM_THEME_DIR/" 2>/dev/null ok diff --git a/macos/defaults-textedit.sh b/macos/defaults-textedit.sh index 794be14..b5260f8 100644 --- a/macos/defaults-textedit.sh +++ b/macos/defaults-textedit.sh @@ -2,7 +2,7 @@ source "$DOTFILES_DIR/scripts/echos.sh" source "$DOTFILES_DIR/scripts/requirers.sh" ############################################################################### -bot "TextEdit and Disk Utility" +bot "TextEdit" ############################################################################### running "Use plain text mode for new documents" defaults write com.apple.TextEdit RichText -int 0 diff --git a/macos/defaults-transmission.sh b/macos/defaults-transmission.sh index 2943f4c..2f7cba3 100644 --- a/macos/defaults-transmission.sh +++ b/macos/defaults-transmission.sh @@ -44,9 +44,8 @@ defaults write org.m0k.transmission WarningLegal -bool false ok running "Setting IP block list" -# Source: https://giuliomac.wordpress.com/2014/02/19/best-blocklist-for-transmission/ defaults write org.m0k.transmission BlocklistNew -bool true -defaults write org.m0k.transmission BlocklistURL -string "http://john.bitsurge.net/public/biglist.p2p.gz" +defaults write org.m0k.transmission BlocklistURL -string "https://github.com/Naunter/BT_BlockLists/raw/master/bt_blocklists.gz" defaults write org.m0k.transmission BlocklistAutoUpdate -bool true ok diff --git a/macos/defaults-xcode.sh b/macos/defaults-xcode.sh index d7ff70f..5e94a79 100644 --- a/macos/defaults-xcode.sh +++ b/macos/defaults-xcode.sh @@ -33,9 +33,8 @@ running "Show line numbers" defaults write com.apple.dt.Xcode DVTTextShowLineNumbers -bool true ok -running "Reduce the number of compile tasks and stop indexing" -defaults write com.apple.dt.XCode IDEIndexDisable 1 -ok +# Note: IDEIndexDisable removed - disabling indexing breaks code completion and navigation +# If you need faster builds, use derived data RAM disk instead running "Show ruler at 80 chars" defaults write com.apple.dt.Xcode DVTTextShowPageGuide -bool true @@ -50,16 +49,8 @@ running "Show build time" defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES ok -running "Improve performance" -defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 5 -ok - running "Improve performance by leveraging multi-core CPU" -defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks $(sysctl -n hw.ncpu) -ok - -running "Delete these settings" -defaults delete com.apple.dt.XCode IDEIndexDisable +defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks "$(sysctl -n hw.ncpu)" ok killall "Xcode" >/dev/null 2>&1 diff --git a/macos/defaults.sh b/macos/defaults.sh index 422fc86..bb3d590 100644 --- a/macos/defaults.sh +++ b/macos/defaults.sh @@ -25,31 +25,32 @@ bot "Configuring System" # Close any open System Preferences panes, to prevent them from overriding # settings we’re about to change running "closing any system preferences to prevent issues with automated changes" -osascript -e 'tell application "System Preferences" to quit' +# Use "System Settings" for macOS Ventura+ or fall back to "System Preferences" +osascript -e 'tell application "System Settings" to quit' 2>/dev/null || osascript -e 'tell application "System Preferences" to quit' 2>/dev/null ok ############################################################################### bot "Security" ############################################################################### running "Enable install from Anywhere" -sudo spctl --master-disable +# On macOS 15+ (Sequoia), this requires manual confirmation in System Settings > Privacy & Security +if [[ $(sw_vers -productVersion | cut -d. -f1) -lt 15 ]]; then + sudo spctl --master-disable +else + echo " NOTE: On macOS 15+, enable 'Anywhere' manually in System Settings > Privacy & Security" +fi ok running "Disable remote apple events" -sudo systemsetup -setremoteappleevents off +sudo systemsetup -setremoteappleevents off 2>/dev/null || true ok running "Disable remote login" -sudo systemsetup -setremotelogin off +sudo systemsetup -setremotelogin off 2>/dev/null || true ok -running "Disable wake-on modem" -sudo systemsetup -setwakeonmodem off -ok - -# Disable wake-on LAN running "Disable wake-on LAN" -sudo systemsetup -setwakeonnetworkaccess off +sudo pmset -a womp 0 ok running "Disable guest account login" @@ -77,33 +78,29 @@ running "Set timezone to $TIMEZONE;" #see `sudo systemsetup -listtimezones` for sudo systemsetup -settimezone "$TIMEZONE" >/dev/null ok -running "Disable the sound effects on boot" -sudo nvram SystemAudioVolume=" " -sudo nvram StartupMute=%01 -ok +# Boot sound: On macOS 11+ (Big Sur), control via System Settings > Sound > "Play sound on startup" +# The nvram commands only worked on Intel Macs running macOS 10.15 or earlier running "Restart automatically if the computer freezes" -sudo systemsetup -setrestartfreeze on 2>/dev/null +sudo systemsetup -setrestartfreeze on 2>/dev/null || true ok running "Set standby delay to 24 hours (default is 1 hour)" sudo pmset -a standbydelay 86400 ok -running "Disable Sudden Motion Sensor" -sudo pmset -a sms 0 -ok +# Note: Sudden Motion Sensor (sms) setting removed - only relevant for HDDs, not SSDs +# All modern Macs use SSDs, so this setting is obsolete running "Disable audio feedback when volume is changed" defaults write com.apple.sound.beep.feedback -bool false ok -running "Show battery percentage" -defaults write com.apple.menuextra.battery ShowPercent YES -ok +# Note: Battery percentage setting removed - deprecated in macOS Big Sur+ +# Now controlled via System Settings > Control Center > Battery running "Set highlight color to steel blue" -defaults write NSGlobalDomain AppleHighlightColor -string "0.172549019607843 0.349019607843137 0,501960784313725" +defaults write NSGlobalDomain AppleHighlightColor -string "0.172549019607843 0.349019607843137 0.501960784313725" ok running "Set sidebar icon size to medium" @@ -145,14 +142,15 @@ running "Automatically quit printer app once the print jobs complete" defaults write com.apple.print.PrintingPrefs "Quit When Finished" -bool true ok -running "Disable the “Are you sure you want to open this application?” dialog" +running "Disable the 'Are you sure you want to open this application?' dialog" defaults write com.apple.LaunchServices LSQuarantine -bool false ok -running "Remove duplicates in the “Open With” menu (also see 'lscleanup' alias)" +running "Remove duplicates in the 'Open With' menu (also see 'lscleanup' alias)" /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user ok +running "Show control characters" defaults write NSGlobalDomain NSTextShowsControlCharacters -bool true ok @@ -160,9 +158,8 @@ running "Disable Resume system-wide" defaults write NSGlobalDomain NSQuitAlwaysKeepsWindows -bool false ok -running "Disable automatic termination of inactive apps" -defaults write NSGlobalDomain NSDisableAutomaticTermination -bool true -ok +# Note: NSDisableAutomaticTermination removed - disabling automatic termination +# prevents macOS from freeing RAM and negatively impacts system performance running "Set Help Viewer windows to non-floating mode" defaults write com.apple.helpviewer DevMode -bool true @@ -201,7 +198,7 @@ defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false ok running "Enable full keyboard access for all controls (e.g. enable Tab in modal dialogs)" -defaults write NSGlobalDomain AppleKeyboardUIMode -int 3 +defaults write NSGlobalDomain AppleKeyboardUIMode -int 2 ok running "Disable press-and-hold for keys in favor of key repeat" @@ -213,13 +210,8 @@ defaults write NSGlobalDomain KeyRepeat -int 1 defaults write NSGlobalDomain InitialKeyRepeat -int 15 ok -running "Automatically illuminate built-in MacBook keyboard in low light" -defaults write com.apple.BezelServices kDim -bool true -ok - -running "Turn off keyboard illumination when computer is not used for 5 minutes" -defaults write com.apple.BezelServices kDimTime -int 300 -ok +# Note: BezelServices keyboard illumination settings removed - deprecated in modern macOS +# Keyboard backlight is now managed automatically by the system ############################################################################### bot "Trackpad, mouse, Bluetooth accessories" @@ -257,10 +249,10 @@ ok bot "Screen" ############################################################################### -running "Require password immediately after sleep or screen saver begins" -defaults write com.apple.screensaver askForPassword -int 1 -defaults write com.apple.screensaver askForPasswordDelay -int 0 -ok +# Screen lock password: Broken since macOS 10.13 (High Sierra). +# Use System Settings > Lock Screen to configure, or: +# sysadminctl -screenLock immediate -password - +# (requires interactive password entry) running "Save screenshots to the desktop" mkdir -p "${SCREENSHOTS_FOLDER}" @@ -275,10 +267,8 @@ running "Disable shadow in screenshots" defaults write com.apple.screencapture disable-shadow -bool true ok -running "Enable subpixel font rendering on non-Apple LCDs" -# Reference: https://github.com/kevinSuttle/macOS-Defaults/issues/17#issuecomment-266633501 -defaults write NSGlobalDomain AppleFontSmoothing -int 2 -ok +# Note: AppleFontSmoothing (subpixel rendering) removed - deprecated since macOS Mojave +# Retina displays don't benefit from subpixel antialiasing #running "Enable HiDPI display modes (requires restart)" #sudo defaults write /Library/Preferences/com.apple.windowserver DisplayResolutionEnabled -bool true @@ -325,16 +315,14 @@ running "Show path bar" defaults write com.apple.finder ShowPathbar -bool true ok -running "Allow text selection in Quick Look" -defaults write com.apple.finder QLEnableTextSelection -bool true -ok +# Note: QLEnableTextSelection removed - text selection is now enabled by default in Quick Look -running "Display full POSIX path as Finder window title" -defaults write com.apple.finder _FXShowPosixPathInTitle -bool true -ok +# POSIX path in title: Broken on Sequoia (Finder title bar redesign). +# Use ShowPathbar instead (enabled above). running "Keep folders on top when sorting by name" defaults write com.apple.finder _FXSortFoldersFirst -bool true +ok running "When performing a search, search the current folder by default" defaults write com.apple.finder FXDefaultSearchScope -string "SCcf" @@ -382,9 +370,8 @@ running "Disable the warning before emptying the Trash" defaults write com.apple.finder WarnOnEmptyTrash -bool false ok -running "Empty Trash securely by default" -defaults write com.apple.finder EmptyTrashSecurely -bool true -ok +# Note: EmptyTrashSecurely removed - secure delete was removed in El Capitan +# SSDs don't benefit from secure erase due to wear leveling running "Show the ~/Library folder" chflags nohidden ~/Library && xattr -d com.apple.FinderInfo ~/Library @@ -394,7 +381,7 @@ running "Show the /Volumes folder" sudo chflags nohidden /Volumes ok -running "Expand the following File Info panes: “General”, “Open with”, and “Sharing & Permissions”" +running "Expand the following File Info panes: General, Open with, and Sharing & Permissions" defaults write com.apple.finder FXInfoPanesExpanded -dict \ General -bool true \ OpenWith -bool true \ @@ -402,7 +389,7 @@ defaults write com.apple.finder FXInfoPanesExpanded -dict \ ok ############################################################################### -bot "Dock & Dashboard" +bot "Dock" ############################################################################### running "Set the icon size of Dock items to 36 pixels" @@ -429,9 +416,7 @@ running "Show indicator lights for open applications in the Dock" defaults write com.apple.dock show-process-indicators -bool true ok -running "Speed up Mission Control animations" -defaults write com.apple.dock expose-animation-duration -float 0.1 -ok +# Mission Control animation speed: Unreliable since Sierra (animations moved to WindowServer) running "Remove the auto-hiding Dock delay" defaults write com.apple.dock autohide-delay -float 0 @@ -445,9 +430,10 @@ running "Reset Launchpad, but keep the desktop wallpaper intact" find "${HOME}/Library/Application Support/Dock" -name "*-*.db" -maxdepth 1 -delete ok -running "Add iOS & Watch Simulator to Launchpad" -sudo ln -sf "/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app" "/Applications/Simulator.app" -sudo ln -sf "/Applications/Xcode.app/Contents/Developer/Applications/Simulator (Watch).app" "/Applications/Simulator (Watch).app" +running "Add iOS Simulator to Launchpad" +if [[ -d "/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app" ]]; then + sudo ln -sf "/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app" "/Applications/Simulator.app" +fi ok bot "Hot corners" @@ -458,20 +444,23 @@ bot "Hot corners" # 4: Desktop # 5: Start screen saver # 6: Disable screen saver -# 7: Dashboard # 10: Put display to sleep # 11: Launchpad # 12: Notification Center # 13: Lock Screen +# 14: Quick Note (added in Monterey) running "Top left screen corner → Mission Control" defaults write com.apple.dock wvous-tl-corner -int 2 defaults write com.apple.dock wvous-tl-modifier -int 0 +ok running "Top right screen corner → Desktop" defaults write com.apple.dock wvous-tr-corner -int 4 defaults write com.apple.dock wvous-tr-modifier -int 0 +ok running "Bottom left screen corner → Start screen saver" defaults write com.apple.dock wvous-bl-corner -int 5 defaults write com.apple.dock wvous-bl-modifier -int 0 +ok ############################################################################### bot "Spotlight" diff --git a/modules/stevenblack-hosts b/modules/stevenblack-hosts index 5da10a6..5885502 160000 --- a/modules/stevenblack-hosts +++ b/modules/stevenblack-hosts @@ -1 +1 @@ -Subproject commit 5da10a61afc297307c489903bfc35b1eb8dac674 +Subproject commit 588550252c26c8b9b4286f0d655d349030610ddb diff --git a/modules/zsh/zsh-autocomplete b/modules/zsh/zsh-autocomplete deleted file mode 160000 index 316c588..0000000 --- a/modules/zsh/zsh-autocomplete +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 316c588a92e3444e919ca9a341fc8894c82800a2 diff --git a/profiles/README.md b/profiles/README.md new file mode 100644 index 0000000..62d2983 --- /dev/null +++ b/profiles/README.md @@ -0,0 +1,53 @@ +# Machine Profiles + +This directory contains machine-specific configurations that are loaded based on the hostname or a profile name. + +## How It Works + +1. Create a profile file named after your machine's hostname or a custom name: + - `profiles/work.zsh` - Work machine profile + - `profiles/personal.zsh` - Personal machine profile + - `profiles/MacBook-Pro.zsh` - Hostname-based profile + +2. Set the `DOTFILES_PROFILE` environment variable (optional): + ```bash + export DOTFILES_PROFILE="work" + ``` + +3. If `DOTFILES_PROFILE` is not set, the system will try to load a profile matching your hostname. + +## Profile Loading Order + +1. `profiles/default.zsh` (always loaded if exists) +2. `profiles/$DOTFILES_PROFILE.zsh` OR `profiles/$(hostname -s).zsh` +3. `profiles/local.zsh` (always loaded if exists, for machine-specific overrides) + +## Example Profile + +```zsh +# profiles/work.zsh + +# Work-specific Git configuration +export GIT_AUTHOR_EMAIL="you@company.com" +export GIT_COMMITTER_EMAIL="you@company.com" + +# Work-specific aliases +alias vpn="open /Applications/CompanyVPN.app" +alias jira="open https://company.atlassian.net" + +# Work proxy settings +# export http_proxy="http://proxy.company.com:8080" + +# Additional PATH entries +prepend-path "$HOME/work/scripts" + +# Load work-specific Brewfile +# export HOMEBREW_BUNDLE_FILE="$DOTFILES_DIR/profiles/Brewfile.work" +``` + +## Files + +- `default.zsh` - Base settings loaded on all machines +- `personal.zsh` - Personal machine settings +- `work.zsh` - Work machine settings +- `local.zsh` - Machine-specific overrides (gitignored) diff --git a/profiles/default.zsh b/profiles/default.zsh new file mode 100644 index 0000000..ff3703f --- /dev/null +++ b/profiles/default.zsh @@ -0,0 +1,4 @@ +# Default Profile +# This file is loaded on all machines before the machine-specific profile + +# Nothing here by default - add settings that should apply everywhere diff --git a/profiles/personal.zsh.example b/profiles/personal.zsh.example new file mode 100644 index 0000000..ccf9292 --- /dev/null +++ b/profiles/personal.zsh.example @@ -0,0 +1,15 @@ +# Personal Machine Profile +# Copy to personal.zsh and customize + +# Personal Git configuration (if different from global) +# export GIT_AUTHOR_EMAIL="personal@email.com" +# export GIT_COMMITTER_EMAIL="personal@email.com" + +# Personal aliases +alias projects="cd ~/Projects" + +# Personal paths +# prepend-path "$HOME/personal/scripts" + +# Load personal Brewfile additions +# export HOMEBREW_BUNDLE_FILE_PERSONAL="$DOTFILES_DIR/profiles/Brewfile.personal" diff --git a/profiles/work.zsh.example b/profiles/work.zsh.example new file mode 100644 index 0000000..1315403 --- /dev/null +++ b/profiles/work.zsh.example @@ -0,0 +1,26 @@ +# Work Machine Profile +# Copy to work.zsh and customize + +# Work Git configuration +# export GIT_AUTHOR_EMAIL="you@company.com" +# export GIT_COMMITTER_EMAIL="you@company.com" + +# Work-specific aliases +# alias vpn="open /Applications/CompanyVPN.app" +# alias jira="open https://company.atlassian.net" +# alias slack="open -a Slack" + +# Work proxy settings (if needed) +# export http_proxy="http://proxy.company.com:8080" +# export https_proxy="http://proxy.company.com:8080" +# export no_proxy="localhost,127.0.0.1,.company.com" + +# Work-specific paths +# prepend-path "$HOME/work/scripts" +# prepend-path "$HOME/work/bin" + +# Work Node.js version +# fnm use 18 + +# Load work Brewfile additions +# export HOMEBREW_BUNDLE_FILE_WORK="$DOTFILES_DIR/profiles/Brewfile.work" diff --git a/runcom/.huskyrc b/runcom/.huskyrc deleted file mode 100644 index 1323c04..0000000 --- a/runcom/.huskyrc +++ /dev/null @@ -1,2 +0,0 @@ -eval "$(/opt/homebrew/bin/brew shellenv)" -eval "$(fnm env --use-on-cd)" diff --git a/runcom/.profile b/runcom/.profile index f359c5c..594e9fb 100644 --- a/runcom/.profile +++ b/runcom/.profile @@ -11,11 +11,24 @@ fi PATH="$DOTFILES_DIR/bin:$PATH" -for DOTFILE in "$DOTFILES_DIR"/system/.{function,function_*,path,env,alias,starship,fnm,fzf,grep,fix,pnpm}; do +for DOTFILE in "$DOTFILES_DIR"/system/.{function,function_*,path,env,alias,fnm,fzf,grep,fix,pnpm}; do [[ -f "$DOTFILE" ]] && . "$DOTFILE" done -eval "$(dircolors -b "$DOTFILES_DIR"/system/.dir_colors)" +# dircolors - cached for performance +_dircolors_cache="${XDG_CACHE_HOME:-$HOME/.cache}/dircolors.zsh" +_dircolors_src="$DOTFILES_DIR/system/.dir_colors" +if [[ ! -f "$_dircolors_cache" ]] || [[ "$_dircolors_src" -nt "$_dircolors_cache" ]]; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + dircolors -b "$_dircolors_src" > "$_dircolors_cache" 2>/dev/null +fi +[[ -f "$_dircolors_cache" ]] && source "$_dircolors_cache" +unset _dircolors_cache _dircolors_src unset DOTFILE export DOTFILES_DIR + +# Load machine-specific profile +[[ -f "$DOTFILES_DIR/system/.profile_loader" ]] && source "$DOTFILES_DIR/system/.profile_loader" + +[[ -f "$HOME/.local/bin/env" ]] && . "$HOME/.local/bin/env" diff --git a/runcom/.zpreztorc b/runcom/.zpreztorc index b7349b5..63a078f 100644 --- a/runcom/.zpreztorc +++ b/runcom/.zpreztorc @@ -27,28 +27,33 @@ zstyle ':prezto:load' pmodule-dirs $DOTFILES_DIR/modules/prezto-contrib $DOTFILE # Set the Prezto modules to load (browse modules). # The order matters. zstyle ':prezto:load' pmodule \ - 'zsh-lazy-load' \ 'environment' \ 'terminal' \ 'editor' \ 'history' \ - 'homebrew' \ 'directory' \ 'spectrum' \ 'utility' \ 'git' \ - 'zsh-thefuck' \ + 'homebrew' \ 'osx' \ 'ssh' \ - 'ruby' \ 'python' \ - 'node' \ 'completion' \ + 'prompt' \ 'syntax-highlighting' \ 'history-substring-search' \ - 'prompt' \ 'autosuggestions' +# +# Prompt +# + +# Set the prompt theme to load. +# Setting it to 'random' loads a random theme. +# Auto set to 'off' on dumb terminals. +zstyle ':prezto:module:prompt' theme 'powerlevel10k' + # # Autosuggestions # @@ -121,25 +126,6 @@ zstyle ':prezto:module:history-substring-search' globbing-flags 'i' # Set the Pacman frontend. # zstyle ':prezto:module:pacman' frontend 'yaourt' -# -# Prompt -# - -# Set the prompt theme to load. -# Setting it to 'random' loads a random theme. -# Auto set to 'off' on dumb terminals. -zstyle ':prezto:module:prompt' theme 'powerlevel10k' -# zstyle ':prezto:module:prompt' theme 'starship' - -# Set the working directory prompt display length. -# By default, it is set to 'short'. Set it to 'long' (without '~' expansion) -# for longer or 'full' (with '~' expansion) for even longer prompt display. -zstyle ':prezto:module:prompt' pwd-length 'short' - -# Set the prompt to display the return code along with an indicator for non-zero -# return codes. This is not supported by all prompts. -# zstyle ':prezto:module:prompt' show-return-val 'yes' - # # Python # @@ -186,10 +172,7 @@ zstyle ':prezto:module:syntax-highlighting' color 'yes' zstyle ':prezto:module:syntax-highlighting' highlighters \ 'main' \ 'brackets' \ - 'pattern' \ - 'line' \ - 'cursor' \ - 'root' + 'pattern' # Set syntax highlighting styles. # zstyle ':prezto:module:syntax-highlighting' styles \ diff --git a/runcom/.zshrc b/runcom/.zshrc index d25703f..73dd98a 100644 --- a/runcom/.zshrc +++ b/runcom/.zshrc @@ -6,40 +6,44 @@ # Sorin Ionescu # -################################################################################################## -# Enable Instant Prompt -################################################################################################## -# if [[ -s "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then -# source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" -# fi - ################################################################################################## # Hide Username from Prompt ################################################################################################## -export DEFAULT_USER=$(whoami) +export DEFAULT_USER="$USER" ################################################################################################## -# Source Powerlevel Settings +# Source Prezto ################################################################################################## -[[ -f "$DOTFILES_DIR/system/.prompt" ]] && . "$DOTFILES_DIR/system/.prompt" +# Add completions to fpath BEFORE compinit (Prezto handles compinit) +fpath=("$DOTFILES_DIR/completions" $fpath) + +[[ -s "$DOTFILES_DIR/modules/prezto/init.zsh" ]] && . "$DOTFILES_DIR/modules/prezto/init.zsh" ################################################################################################## -# Source Prezto +# Prompt configuration (Powerlevel10k) ################################################################################################## -[[ -s "$DOTFILES_DIR/modules/prezto/init.zsh" ]] && . "$DOTFILES_DIR/modules/prezto/init.zsh" +# Source p10k config if it exists +[[ -f "$DOTFILES_DIR/system/.prompt" ]] && source "$DOTFILES_DIR/system/.prompt" ################################################################################################## # Completion settings ################################################################################################## +# Source completion config (without redundant compinit) source "$DOTFILES_DIR/system/.completion" -fpath+="$DOTFILES_DIR/completions" -compinit + +# Zoxide (lazy-loaded in .zoxide for performance) source "$DOTFILES_DIR/system/.zoxide" +################################################################################################## +# Key bindings (must be after Prezto for history-substring-search) +################################################################################################## + +source "$DOTFILES_DIR/system/.bindings" + ################################################################################################## # Recursive globbing with "**" ################################################################################################## diff --git a/scripts/echos.sh b/scripts/echos.sh index 5957b10..abb112c 100644 --- a/scripts/echos.sh +++ b/scripts/echos.sh @@ -16,19 +16,19 @@ COL_MAGENTA=$ESC_SEQ"35;01m" COL_CYAN=$ESC_SEQ"36;01m" function bot() { - echo -e "\n${COL_GREEN}\[._.]/${COL_RESET} - "$1 + echo -e "\n${COL_GREEN}\[._.]/${COL_RESET} - $1" } function ok() { - echo -e "[${COL_GREEN}ok${COL_RESET}] "$1 + echo -e "[${COL_GREEN}ok${COL_RESET}] $1" } function skip() { - echo -e "[${COL_MAGENTA}skipped${COL_RESET}] "$1 + echo -e "[${COL_MAGENTA}skipped${COL_RESET}] $1" } function running() { - echo -en "${COL_YELLOW} ⇒ ${COL_RESET}"$1": " + echo -en "${COL_YELLOW} ⇒ ${COL_RESET}$1: " } function action() { @@ -36,9 +36,9 @@ function action() { } function warn() { - echo -e "[${COL_YELLOW}warning${COL_RESET}] "$1 + echo -e "[${COL_YELLOW}warning${COL_RESET}] $1" } function error() { - echo -e "[${COL_RED}error${COL_RESET}] "$1 + echo -e "[${COL_RED}error${COL_RESET}] $1" } diff --git a/scripts/install_prezto.zsh b/scripts/install_prezto.zsh index 5f21e31..e0b5ee1 100644 --- a/scripts/install_prezto.zsh +++ b/scripts/install_prezto.zsh @@ -1,4 +1,4 @@ -#!/bin/zsh +#!/usr/bin/env zsh setopt EXTENDED_GLOB diff --git a/scripts/requirers.sh b/scripts/requirers.sh index b3b565c..2605db5 100644 --- a/scripts/requirers.sh +++ b/scripts/requirers.sh @@ -17,8 +17,7 @@ function require_tap() { running "tap $1" if [[ $(brew tap | grep -x "$1") != "$1" ]]; then action "brew tap $1" - brew tap "$1" - if [[ $? != 0 ]]; then + if ! brew tap "$1"; then error "failed to tap $1!" fi fi @@ -27,11 +26,9 @@ function require_tap() { function require_cask() { running "cask $1" - brew list --cask "$1" >/dev/null 2>&1 | true - if [[ ${PIPESTATUS[0]} != 0 ]]; then + if ! brew list --cask "$1" >/dev/null 2>&1; then action "brew install --cask $1 $2" - brew install --cask $1 - if [[ $? != 0 ]]; then + if ! brew install --cask "$1"; then error "failed to install $1!" fi fi @@ -40,11 +37,9 @@ function require_cask() { function require_brew() { running "brew $1 $2" - brew list "$1" >/dev/null 2>&1 | true - if [[ ${PIPESTATUS[0]} != 0 ]]; then + if ! brew list "$1" >/dev/null 2>&1; then action "brew install $1 $2" - brew install $1 $2 - if [[ $? != 0 ]]; then + if ! brew install "$1" "$2"; then error "failed to install $1!" fi fi @@ -63,7 +58,7 @@ function require_code() { function require_mas() { running "mas $1" - if [[ $(mas list | grep $1 | head -1 | cut -d' ' -f1) != "$1" ]]; then + if [[ $(mas list | grep "$1" | head -1 | cut -d' ' -f1) != "$1" ]]; then action "mas install $1" mas install "$1" fi @@ -72,7 +67,7 @@ function require_mas() { function require_gem() { running "gem $1" - if [[ $(gem list --local | grep $1 | head -1 | cut -d' ' -f1) != "$1" ]]; then + if [[ $(gem list --local | grep "$1" | head -1 | cut -d' ' -f1) != "$1" ]]; then action "gem install $1" gem install "$1" fi @@ -95,8 +90,7 @@ function require_npm() { source_fnm fnm use default >/dev/null 2>&1 running "npm $*" - npm list -g --depth 0 | grep "$1"@ >/dev/null - if [[ $? != 0 ]]; then + if ! npm list -g --depth 0 | grep "$1"@ >/dev/null; then action "npm install -g $*" npm install -g "$@" fi diff --git a/system/.alias b/system/.alias index ef23395..4e7286f 100644 --- a/system/.alias +++ b/system/.alias @@ -3,51 +3,47 @@ ############################################################# alias _="sudo" +alias clauded="claude --dangerously-skip-permissions" alias rr="rm -rf" alias g="git" alias library="cd $HOME/Library" -alias gdrive="cd $HOME/Google Drive" +alias gdrive='cd "$HOME/Library/CloudStorage/GoogleDrive-"* 2>/dev/null || echo "Google Drive not found"' alias xcode="open -a Xcode" alias reload="exec $SHELL -l" alias pbcopynn='tr -d "\n" | pbcopy' alias jsonfix="pbpaste | jq . | pbcopy" alias reloadshell="source $HOME/.zshrc" alias copyssh="pbcopy < $HOME/.ssh/id_ed25519.pub" -alias lookbusy="cat /dev/urandom | hexdump -C | grep \"34 32\"" -alias afk="/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend" +alias lookbusy="hexdump -C < /dev/urandom | grep '34 32'" +alias afk="pmset displaysleepnow" ############################################################# # Default options ############################################################# -alias vtop="vtop --theme nord" alias rsync="rsync -vh" -alias json="json -c" -alias psgrep="psgrep -i" +alias psgrep="ps aux | grep -i" alias mzsh="arch -arm64 zsh" alias izsh="arch -x86_64 zsh" -if [ "$(command -v bat)" ]; then +# bat as cat replacement (using zsh built-in check, no subprocess) +(( $+commands[bat] )) && { unalias -m 'cat' alias cat='bat -pp --theme="Nord"' -fi +} ############################################################# -# Global aliases +# Global aliases (zsh-only features, no subprocess check needed) ############################################################# -if $(is-supported "alias -g"); then - alias -g G="| grep -i" - alias -g H="| head" - alias -g T="| tail" - alias -g L="| less" -fi +alias -g G="| grep -i" +alias -g H="| head" +alias -g T="| tail" +alias -g L="| less" -if $(is-supported "alias -s"); then - alias -s git="git clone --depth 1" - alias -s html="cat" - alias -s {js,jsx,ts,tsx}="code" -fi +alias -s git="git clone --depth 1" +alias -s html="cat" +alias -s {js,jsx,ts,tsx}="code" ############################################################# # List declared aliases, functions, paths @@ -62,32 +58,22 @@ alias mpath='echo -e ${MANPATH//:/\\n}' # Directory listing/traversal ############################################################# -if [ "$(command -v eza)" ]; then - LS_COLORS_ENABLE=$(is-supported "eza --color auto" --color) - LS_TIMESTYLEISO=$(is-supported "eza --time-style=long-iso" --time-style=long-iso) - LS_GROUPDIRSFIRST=$(is-supported "eza --group-directories-first" --group-directories-first) - - unalias -m "ls" - unalias -m "ll" - unalias -m "la" - unalias -m "lr" - unalias -m "ld" - - alias ls="eza -Ga -s type --icons $LS_COLORS_ENABLE auto" - alias ll="eza -la --icons $LS_COLORS_ENABLE always" - alias la="eza -la --icons $LS_COLORS_ENABLE always $LS_TIMESTYLEISO $LS_GROUPDIRSFIRST" - alias lr="eza -lr -s time --icons $LS_COLORS_ENABLE always $LS_TIMESTYLEISO $LS_GROUPDIRSFIRST" - alias ld="eza -ld --icons $LS_COLORS_ENABLE always */" +# Using zsh built-in $+commands check (no subprocess) +if (( $+commands[eza] )); then + # eza supports these options on all platforms + unalias -m "ls" "ll" "la" "lr" "ld" 2>/dev/null + alias ls="eza -Ga -s type --icons --color=auto" + alias ll="eza -la --icons --color=always" + alias la="eza -la --icons --color=always --time-style=long-iso --group-directories-first" + alias lr="eza -lr -s time --icons --color=always --time-style=long-iso --group-directories-first" + alias ld="eza -ld --icons --color=always */" else - LS_COLORS_ENABLE=$(is-supported "ls -G" -G) - LS_TIMESTYLEISO=$(is-supported "ls --time-style=long-iso" --time-style=long-iso) - LS_GROUPDIRSFIRST=$(is-supported "ls --group-directories-first" --group-directories-first) - - alias ls="ls $LS_COLORS_ENABLE" - alias ll="ls -lA $LS_COLORS_ENABLE" - alias la="ls -lahA $LS_COLORS_ENABLE $LS_TIMESTYLEISO $LS_GROUPDIRSFIRST" - alias lr="ls -lhAtr $LS_COLORS_ENABLE $LS_TIMESTYLEISO $LS_GROUPDIRSFIRST" - alias ld="ls -ld $LS_COLORS_ENABLE */" + # Fallback to ls with GNU coreutils options (installed via brew) + alias ls="ls --color=auto" + alias ll="ls -lA --color=auto" + alias la="ls -lahA --color=auto --time-style=long-iso --group-directories-first" + alias lr="ls -lhAtr --color=auto --time-style=long-iso --group-directories-first" + alias ld="ls -ld --color=auto */" fi alias ..="cd .." @@ -116,32 +102,18 @@ alias update='sudo softwareupdate -i -a; brew update; brew upgrade; brew upgrade alias ip="curl -s ipinfo.io | jq -r '.ip'" alias ipl="ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'" -# Request using GET, POST, etc. method -for METHOD in GET HEAD POST PUT DELETE TRACE OPTIONS; do - alias "$METHOD"="lwp-request -m '$METHOD'" -done -unset METHOD - ############################################################# -#Spotlight +# Spotlight ############################################################# alias spoton="sudo mdutil -a -i on" alias spotoff="sudo mdutil -a -i off" -############################################################# -# Notification Center -############################################################# - -alias notioff="launchctl unload -w /System/Library/LaunchAgents/com.apple.notificationcenterui.plist && killall NotificationCenter" -alias notion="launchctl load -w /System/Library/LaunchAgents/com.apple.notificationcenterui.plist && open /System/Library/CoreServices/NotificationCenter.app/" - ############################################################# # Npm ############################################################# alias npmri="rm -r node_modules package-lock.json && npm install" -alias ncd="npm-check -su" ############################################################# # Git @@ -157,5 +129,5 @@ alias gitdev='git checkout develop; git up; git branch --merged develop | grep - alias hosts="sudo $EDITOR /etc/hosts" alias quit="exit" alias week="date +%V" -alias speedtest="wget -O /dev/null http://speed.transip.nl/100mb.bin" +alias speedtest="curl -o /dev/null -w 'Speed: %{speed_download} bytes/sec\n' https://speed.cloudflare.com/__down?bytes=100000000" alias grip="grip -b" diff --git a/system/.bindings b/system/.bindings index 942c896..3d81b72 100644 --- a/system/.bindings +++ b/system/.bindings @@ -23,9 +23,8 @@ bindkey '^?' backward-delete-char # Delete word with ctrl+backspace bindkey '^[[3;5~' backward-delete-word -# Search history with fzf if installed, default otherwise -if [ "$(command -v fzf)" ]; then - source "$HOMEBREW_PREFIX/opt/fzf/shell/key-bindings.zsh" -else - bindkey '^R' history-incremental-search-backward +# fzf key-bindings are loaded via .fzf (fzf --zsh includes them) +# Fallback for systems without fzf +if ! command -v fzf &>/dev/null; then + bindkey '^R' history-incremental-search-backward fi diff --git a/system/.completion b/system/.completion index a8dd509..7cec1e1 100644 --- a/system/.completion +++ b/system/.completion @@ -1,37 +1,37 @@ -if [ -z "$HOMEBREW_PREFIX" ] && is-executable brew; then - HOMEBREW_PREFIX=$(brew --prefix) +# Completion configuration +# Note: compinit is handled by Prezto's completion module + +# Cache HOMEBREW_PREFIX if not set (avoid repeated brew calls) +if [[ -z "$HOMEBREW_PREFIX" ]]; then + if [[ -x /opt/homebrew/bin/brew ]]; then + HOMEBREW_PREFIX="/opt/homebrew" + elif [[ -x /usr/local/bin/brew ]]; then + HOMEBREW_PREFIX="/usr/local" + fi fi -# Dotfiles +# Dotfiles completion _dotfiles_completions() { - compadd 'clean configure dock edit help install link macos open unlink update' + compadd 'clean' 'configure' 'dock' 'edit' 'help' 'install' 'link' 'macos' 'open' 'unlink' 'update' } - compdef _dotfiles_completions dotfiles -# npm -if command-exists npm; then - . <(npm completion) -fi - -# fzf -if command-exists fzf; then - export FZF_COMPLETION_TRIGGER=',,' - . "$HOMEBREW_PREFIX/opt/fzf/shell/completion.zsh" +# npm completion - cached for performance +# Regenerate with: npm completion > ~/.cache/npm-completion.zsh +_npm_completion_file="${XDG_CACHE_HOME:-$HOME/.cache}/npm-completion.zsh" +if [[ ! -f "$_npm_completion_file" ]] && command -v npm &>/dev/null; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + npm completion > "$_npm_completion_file" 2>/dev/null fi +[[ -f "$_npm_completion_file" ]] && source "$_npm_completion_file" +unset _npm_completion_file -# Git +# fzf completion trigger (completions loaded via .fzf using fzf --zsh) +export FZF_COMPLETION_TRIGGER=',,' -if is-executable git; then - if [ -f "/usr/share/git/completion/git-completion.bash" ]; then - . "/usr/share/git/completion/git-completion.bash" - elif [ -n "$HOMEBREW_PREFIX" ] && [ -f "$HOMEBREW_PREFIX/etc/bash_completion.d/git-completion.bash" ]; then - . "$HOMEBREW_PREFIX/etc/bash_completion.d/git-completion.bash" - fi -fi - -# pnpm (https://pnpm.io/completion) +# Git completion is handled by Prezto's git module - removed redundant loading +# pnpm completion _pnpm_completion() { local reply local si=$IFS @@ -45,12 +45,4 @@ _pnpm_completion() { _describe 'values' reply fi } -# When called by the Zsh completion system, this will end with -# "loadautofunc" when initially autoloaded and "shfunc" later on, otherwise, -# the script was "eval"-ed so use "compdef" to register it with the -# completion system -if [[ $zsh_eval_context == *func ]]; then - _pnpm_completion "$@" -else - compdef _pnpm_completion pnpm -fi +compdef _pnpm_completion pnpm diff --git a/system/.env b/system/.env index d82fe64..d82f703 100644 --- a/system/.env +++ b/system/.env @@ -2,7 +2,7 @@ export XDG_CONFIG_HOME="$HOME/.config" export XDG_CACHE_HOME="$HOME/.cache" export XDG_DATA_HOME="$HOME/.local/share" export XDG_STATE_HOME="$HOME/.local/state" -export XDG_RUNTIME_DIR="$HOME/Library/Caches" +export XDG_RUNTIME_DIR="${HOME}/.local/runtime" # Prefer US English and use UTF-8 export LC_ALL="en_US.UTF-8" @@ -19,7 +19,7 @@ export DOTFILES_GIT_GUI="stree" export CLICOLOR=1 # Highlight section titles in man pages -export LESS_TERMCAP_md="${yellow}" +export LESS_TERMCAP_md=$'\e[1;33m' # Keep showing man page after exit export MANPAGER='less -X' diff --git a/system/.fix b/system/.fix index a034eea..cba3a01 100644 --- a/system/.fix +++ b/system/.fix @@ -1 +1,14 @@ -eval $(thefuck --alias fix) +# thefuck - command correction +# Uses cached output to avoid running thefuck --alias on every shell start + +_thefuck_cache="${XDG_CACHE_HOME:-$HOME/.cache}/thefuck-alias.zsh" + +if command -v thefuck &>/dev/null; then + # Regenerate cache if missing or thefuck binary is newer + if [[ ! -f "$_thefuck_cache" ]] || [[ "$commands[thefuck]" -nt "$_thefuck_cache" ]]; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + thefuck --alias fix > "$_thefuck_cache" 2>/dev/null + fi + [[ -f "$_thefuck_cache" ]] && source "$_thefuck_cache" +fi +unset _thefuck_cache diff --git a/system/.fnm b/system/.fnm index 91e2b4f..8cd51d8 100644 --- a/system/.fnm +++ b/system/.fnm @@ -1,7 +1,27 @@ -eval "$(fnm env --use-on-cd --corepack-enabled --shell zsh)" +# fnm (Fast Node Manager) - optimized initialization +# Uses cached output to avoid running fnm env on every shell start -fnm_upgrade () { +_fnm_cache="${XDG_CACHE_HOME:-$HOME/.cache}/fnm-env.zsh" + +# Regenerate cache if fnm exists and cache is missing or older than 1 day +if command -v fnm &>/dev/null; then + if [[ ! -f "$_fnm_cache" ]] || [[ $(find "$_fnm_cache" -mtime +1 2>/dev/null) ]]; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + fnm env --use-on-cd --corepack-enabled --shell zsh > "$_fnm_cache" 2>/dev/null + fi + [[ -f "$_fnm_cache" ]] && source "$_fnm_cache" +fi +unset _fnm_cache + +# Helper function to upgrade global npm packages +fnm_upgrade() { fnm exec --using=$1 npm ls --global --json \ | jq -r '.dependencies | to_entries[] | .key+"@"+.value.version' \ | xargs npm i -g } + +# Force cache refresh +fnm_refresh() { + rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/fnm-env.zsh" + exec "$SHELL" +} diff --git a/system/.function b/system/.function index d634f03..39110a7 100644 --- a/system/.function +++ b/system/.function @@ -1,28 +1,28 @@ # Add to path prepend-path() { - [ -d $1 ] && PATH="$1:$PATH" + [ -d "$1" ] && PATH="$1:$PATH" } -# Get named var +# Get named var (zsh-compatible indirect expansion) # usage: get "VAR_NAME" get() { - echo "${!1}" + echo "${(P)1}" } # Generate .gitignore file function gen-git-ignore() { - curl -sL https://www.toptal.com/developers/gitignore/api/$@ + curl -sL "https://www.toptal.com/developers/gitignore/api/$*" } # Get weather information for a given country/city # usage: weather Cyprus function weather() { - curl wttr.in/$1 + curl "wttr.in/${1:-}" } # Use Mac OS Preview to open a man page in a more handsome format function manp() { - man -t $1 | open -f -a /Applications/Preview.app + man -t "$1" | open -f -a /Applications/Preview.app } # Hide shadow under screenshots @@ -53,20 +53,14 @@ function hide-hidden-files() { osascript -e 'tell application "Finder" to activate' } -# Deduplicate path variables +# Deduplicate path variables using zsh native typeset (no subprocess) +# usage: dedup-pathvar PATH function dedup-pathvar() { - function get-var() { - eval 'printf "%s\n" "${'"$1"'}"' - } - - function set-var() { - eval "$1=\"\$2\"" - } - - pathvar_name="$1" - pathvar_value="$(get-var "$pathvar_name")" - deduped_path="$(echo -n $PATH | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}')" - set-var "$pathvar_name" "$deduped_path" + local pathvar_name="$1" + # In zsh, PATH is tied to 'path' array, MANPATH to 'manpath', etc. + # Use typeset -gU on the lowercase array version to deduplicate + local pathvar_array="${(L)pathvar_name}" + typeset -gU "$pathvar_array" } # Show 256 TERM colors @@ -75,6 +69,6 @@ colors() { local Y=$(printf %$((COLUMNS-6))s) for i in {0..256}; do o=00$i - echo -e ${o:${#o}-3:3} $(tput setaf $i;tput setab $i)${Y// /=}$X + echo -e "${o:${#o}-3:3}" "$(tput setaf "$i";tput setab "$i")${Y// /=}${X}" done } diff --git a/system/.function_fs b/system/.function_fs index 941c70a..67bbf97 100644 --- a/system/.function_fs +++ b/system/.function_fs @@ -5,8 +5,8 @@ function mkd() { # Create a new git repo with one README commit and CD into it function gitnr() { - mkdir $1 - cd $1 + mkdir "$1" + cd "$1" git init touch README.md git add README.md @@ -38,8 +38,8 @@ function fo() { } # Fuzzy find file/dir -ff() { find . -type f -iname "*$1*";} -fd() { find . -type d -iname "*$1*";} +ff() { find . -type f -iname "*${1}*";} +finddir() { find . -type d -iname "*${1}*";} # Show disk usage of current folder, or list with depth duf() { diff --git a/system/.function_fun b/system/.function_fun index 99e47c9..27ce0d1 100644 --- a/system/.function_fun +++ b/system/.function_fun @@ -1,12 +1,12 @@ # Hammer a service with curl for a given number of times # usage: curlhammer $url function curlhammer() { - bot "about to hammer $1 with $2 curls ⇒" + echo "About to hammer $1 with $2 curls ⇒" echo "curl -k -s -D - $1 -o /dev/null | grep 'HTTP/1.1' | sed 's/HTTP\/1.1 //'" for i in {1..$2}; do - curl -k -s -D - $1 -o /dev/null | grep 'HTTP/1.1' | sed 's/HTTP\/1.1 //' + curl -k -s -D - "$1" -o /dev/null | grep 'HTTP/1.1' | sed 's/HTTP\/1.1 //' done - bot "done" + echo "Done" } # Do a Matrix movie effect of falling characters diff --git a/system/.function_network b/system/.function_network index ddc7fe6..bd3dc92 100644 --- a/system/.function_network +++ b/system/.function_network @@ -1,5 +1,9 @@ # Webserver srv() { + if ! command -v superstatic &>/dev/null; then + echo "superstatic not found. Install with: npm i -g superstatic" >&2 + return 1 + fi local DIR=${1:-.} local AVAILABLE_PORT=$(get-port) local PORT=${2:-$AVAILABLE_PORT} @@ -19,26 +23,17 @@ function ipinfo() { # Get IP from hostname # usage: hostname2ip google.com hostname2ip() { - ping -c 1 "$1" | egrep -m1 -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -} - -# Upload file to transfer.sh -# https://github.com/dutchcoders/transfer.sh/ -transfer() { - tmpfile=$( mktemp -t transferXXX ) - curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename $1) >> $tmpfile; - cat $tmpfile; - rm -f $tmpfile; + ping -c 1 "$1" | grep -E -m1 -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' } # Find real from shortened url unshorten() { - curl -sIL $1 | sed -n 's/Location: *//p' + curl -sIL "$1" | sed -n 's/Location: *//p' } # Download youtube videos in a breeze function yt-download() { - youtube-dl -o "${HOME}/Desktop/%(title)s.%(ext)s" "$1" + yt-dlp -o "${HOME}/Desktop/%(title)s.%(ext)s" "$1" } # Curlheader will return only a specific response header or all response headers for a given URL @@ -47,10 +42,10 @@ function yt-download() { function curlheader() { if [[ -z "$2" ]]; then echo "curl -k -s -D - $1 -o /dev/null" - curl -k -s -D - $1 -o /dev/null: + curl -k -s -D - "$1" -o /dev/null else echo "curl -k -s -D - $2 -o /dev/null | grep $1:" - curl -k -s -D - $2 -o /dev/null | grep $1: + curl -k -s -D - "$2" -o /dev/null | grep "$1:" fi } diff --git a/system/.function_text b/system/.function_text index 1ba217a..860c294 100644 --- a/system/.function_text +++ b/system/.function_text @@ -2,15 +2,15 @@ line() { local LINE_NUMBER=$1 local LINES_AROUND=${2:-0} - sed -n "`expr $LINE_NUMBER - $LINES_AROUND`,`expr $LINE_NUMBER + $LINES_AROUND`p" + sed -n "$((LINE_NUMBER - LINES_AROUND)),$((LINE_NUMBER + LINES_AROUND))p" } # Show duplicate/unique lines # Source: https://github.com/ain/.dotfiles/commit/967a2e65a44708449b6e93f87daa2721929cb87a duplines() { - sort $1 | uniq -d + sort "$1" | uniq -d } uniqlines() { - sort $1 | uniq -u + sort "$1" | uniq -u } diff --git a/system/.fzf b/system/.fzf index 589723d..4817f70 100644 --- a/system/.fzf +++ b/system/.fzf @@ -1,5 +1,19 @@ -source <(fzf --zsh) +# fzf - fuzzy finder configuration +# Cached initialization for performance +_fzf_cache="${XDG_CACHE_HOME:-$HOME/.cache}/fzf-init.zsh" + +if command -v fzf &>/dev/null; then + # Regenerate cache if missing or fzf binary is newer + if [[ ! -f "$_fzf_cache" ]] || [[ "$commands[fzf]" -nt "$_fzf_cache" ]]; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + fzf --zsh > "$_fzf_cache" 2>/dev/null + fi + [[ -f "$_fzf_cache" ]] && source "$_fzf_cache" +fi +unset _fzf_cache + +# fzf options export FZF_CTRL_R_OPTS=" --bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort' --color header:italic diff --git a/system/.grep b/system/.grep index 0d2c525..f6e9562 100644 --- a/system/.grep +++ b/system/.grep @@ -1,20 +1,2 @@ -# Tell grep to highlight matches -if is-supported "grep --color a <<< a"; then - GREP_OPTIONS+=" --color=auto" -fi - -# Avoid VCS folders -if is-supported "echo | grep --exclude-dir=.cvs ''"; then - for PATTERN in .cvs .git .hg .svn; do - GREP_OPTIONS+=" --exclude-dir=$PATTERN" - done -elif is-supported "echo | grep --exclude=.cvs ''"; then - for PATTERN in .cvs .git .hg .svn; do - GREP_OPTIONS+=" --exclude=$PATTERN" - done -fi - -unset PATTERN - -alias grep="grep $GREP_OPTIONS" -export GREP_COLOR='1;32' +# Grep defaults +alias grep="grep --color=auto --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.svn --exclude-dir=.hg" diff --git a/system/.path b/system/.path index 63acc6b..3586466 100644 --- a/system/.path +++ b/system/.path @@ -1,12 +1,24 @@ -# Start with system path -# Retrieve it from getconf, otherwise it's just current $PATH +# PATH configuration - optimized for performance -command-exists getconf && PATH=$($(command -v getconf) PATH) +# Detect Homebrew prefix without subprocess +if [[ -x /opt/homebrew/bin/brew ]]; then + export HOMEBREW_PREFIX="/opt/homebrew" +elif [[ -x /usr/local/bin/brew ]]; then + export HOMEBREW_PREFIX="/usr/local" +fi -export HOMEBREW_PREFIX=$($DOTFILES_DIR/bin/is-supported $DOTFILES_DIR/bin/is-apple-silicon /opt/homebrew /usr/local) -eval "$("$HOMEBREW_PREFIX"/bin/brew shellenv)" +# Cache brew shellenv output (rarely changes, expensive to compute) +if [[ -n "$HOMEBREW_PREFIX" ]]; then + _brew_cache="${XDG_CACHE_HOME:-$HOME/.cache}/brew-shellenv.zsh" + if [[ ! -f "$_brew_cache" ]] || [[ "$HOMEBREW_PREFIX/bin/brew" -nt "$_brew_cache" ]]; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + "$HOMEBREW_PREFIX/bin/brew" shellenv 2>/dev/null | grep -v 'path_helper' > "$_brew_cache" + fi + [[ -f "$_brew_cache" ]] && source "$_brew_cache" + unset _brew_cache +fi -# Default +# Prepend paths (function defined in .function) prepend-path "/bin" prepend-path "/sbin" prepend-path "/usr/bin" @@ -21,12 +33,13 @@ prepend-path "$HOMEBREW_PREFIX/opt/go/libexec/bin" prepend-path "$HOMEBREW_PREFIX/opt/ruby/bin" prepend-path "$HOMEBREW_PREFIX/opt/openssl/bin" prepend-path "$HOME/bin" +prepend-path "$HOME/.local/bin" prepend-path "$HOME/.cargo/bin" prepend-path "$HOME/.local/share/pnpm" -prepend-path "/Applications/Postgres.app/Contents/Versions/16/bin/psql" +prepend-path "$HOME/.cache/.bun/bin" +prepend-path "/Applications/Postgres.app/Contents/Versions/16/bin" -# Remove duplicates (preserving prepended items) -PATH=$(echo -n $PATH | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}') +# Remove duplicates using zsh native typeset (no subprocess) +typeset -U PATH path -# Wrap up export PATH diff --git a/system/.profile_loader b/system/.profile_loader new file mode 100644 index 0000000..ab22490 --- /dev/null +++ b/system/.profile_loader @@ -0,0 +1,26 @@ +# Machine Profile Loader +# Loads machine-specific configurations from profiles/ + +_load_profile() { + local profile_dir="$DOTFILES_DIR/profiles" + local hostname_short="${HOST:-$(hostname -s 2>/dev/null)}" + + # Load default profile (always) + [[ -f "$profile_dir/default.zsh" ]] && source "$profile_dir/default.zsh" + + # Determine which profile to load + local profile_name="${DOTFILES_PROFILE:-$hostname_short}" + local profile_file="$profile_dir/$profile_name.zsh" + + # Load machine-specific profile + if [[ -f "$profile_file" ]]; then + source "$profile_file" + export DOTFILES_LOADED_PROFILE="$profile_name" + fi + + # Load local overrides (always, for machine-specific secrets/settings) + [[ -f "$profile_dir/local.zsh" ]] && source "$profile_dir/local.zsh" +} + +_load_profile +unset -f _load_profile diff --git a/system/.starship b/system/.starship index 873886f..c4aed2b 100644 --- a/system/.starship +++ b/system/.starship @@ -1 +1,6 @@ export STARSHIP_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/starship/config.toml" + +# Initialize Starship prompt +if command -v starship &>/dev/null; then + eval "$(starship init zsh)" +fi diff --git a/system/.zoxide b/system/.zoxide index 2acda37..9cc9483 100644 --- a/system/.zoxide +++ b/system/.zoxide @@ -1 +1,14 @@ -eval "$(zoxide init zsh)" +# zoxide - smarter cd command +# Uses cached output to avoid running zoxide init on every shell start + +_zoxide_cache="${XDG_CACHE_HOME:-$HOME/.cache}/zoxide-init.zsh" + +if command -v zoxide &>/dev/null; then + # Regenerate cache if missing or zoxide binary is newer + if [[ ! -f "$_zoxide_cache" ]] || [[ "$commands[zoxide]" -nt "$_zoxide_cache" ]]; then + mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}" + zoxide init zsh > "$_zoxide_cache" 2>/dev/null + fi + [[ -f "$_zoxide_cache" ]] && source "$_zoxide_cache" +fi +unset _zoxide_cache