Last active
August 18, 2025 08:01
-
-
Save ChrisColeTech/010a3a1f313fa39a10566a328c32424b to your computer and use it in GitHub Desktop.
Windows Toast Notifications - Bash Installation Script for WSL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Windows Toast Notifications - Bash Installation Script for WSL | |
| # WSL Toast Notification Installer | |
| # This script installs a toast() function for WSL that triggers Windows notifications | |
| # Uses C:\ProgramData\Toast\ for system-wide installation - no user detection needed | |
| set -euo pipefail | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' # No Color | |
| # Function to print colored output | |
| print_status() { | |
| echo -e "${GREEN}[INFO]${NC} $1" | |
| } | |
| print_warning() { | |
| echo -e "${YELLOW}[WARN]${NC} $1" | |
| } | |
| print_error() { | |
| echo -e "${RED}[ERROR]${NC} $1" | |
| } | |
| # Configuration | |
| SNIPPET_START="# -- wsl toast helper start --" | |
| SNIPPET_END="# -- wsl toast helper end --" | |
| echo "=== WSL Toast Notification Installer ===" | |
| echo | |
| # Cleanup old installations | |
| print_status "Performing comprehensive cleanup of old toast installations..." | |
| # Remove old toast scripts from common locations | |
| OLD_TOAST_LOCATIONS=( | |
| ~/bin/toast | |
| ~/.local/bin/toast | |
| /usr/local/bin/toast | |
| /tmp/toast | |
| /var/tmp/toast | |
| ) | |
| for location in "${OLD_TOAST_LOCATIONS[@]}"; do | |
| if [[ -f "$location" ]]; then | |
| print_status "Removing old toast script from: $location" | |
| rm -f "$location" | |
| fi | |
| done | |
| # Remove old toast directories if they exist and are empty | |
| OLD_TOAST_DIRS=( | |
| ~/bin | |
| ~/.local/bin | |
| ) | |
| for dir in "${OLD_TOAST_DIRS[@]}"; do | |
| if [[ -d "$dir" ]] && [[ -z "$(ls -A "$dir" 2>/dev/null)" ]]; then | |
| print_status "Removing empty directory: $dir" | |
| rmdir "$dir" 2>/dev/null || true | |
| fi | |
| done | |
| # Clean up old toast functions and environment variables from shell configs | |
| RC_FILES=(~/.bashrc ~/.zshrc ~/.bash_profile ~/.profile ~/.fish) | |
| for rc in "${RC_FILES[@]}"; do | |
| rc_expanded="$(eval echo "${rc}")" | |
| if [[ -f "$rc_expanded" ]]; then | |
| print_status "Cleaning up toast artifacts from: $rc_expanded" | |
| # Create backup | |
| cp "$rc_expanded" "$rc_expanded.toast_backup.$(date +%s)" 2>/dev/null || true | |
| # Remove old toast helper blocks (any variation) | |
| sed -i '/# -- .*[Tt]oast.*helper start --/,/# -- .*[Tt]oast.*helper end --/d' "$rc_expanded" | |
| # Remove standalone toast functions | |
| sed -i '/^[[:space:]]*toast()[[:space:]]*{/,/^}/d' "$rc_expanded" | |
| # Remove PowerShell PATH additions (common variations) | |
| sed -i '/export PATH.*WindowsPowerShell.*PATH/d' "$rc_expanded" | |
| sed -i '/export PATH.*powershell.*PATH/d' "$rc_expanded" | |
| sed -i '/export PATH.*PowerShell.*PATH/d' "$rc_expanded" | |
| # Remove toast-related PATH additions | |
| sed -i '/export PATH.*toast.*PATH/d' "$rc_expanded" | |
| sed -i '/export PATH.*Toast.*PATH/d' "$rc_expanded" | |
| # Remove duplicate PATH entries and clean up | |
| if command -v awk >/dev/null 2>&1; then | |
| # Use awk to deduplicate PATH exports while preserving order | |
| awk '!seen[$0]++ || !/^export PATH=/' "$rc_expanded" > "$rc_expanded.tmp" && mv "$rc_expanded.tmp" "$rc_expanded" | |
| fi | |
| # Remove toast-related environment variables | |
| sed -i '/^export.*[Tt]oast/d' "$rc_expanded" | |
| sed -i '/^[Tt]oast.*=/d' "$rc_expanded" | |
| # Clean up empty lines (more than 2 consecutive) | |
| sed -i '/^$/N;/^\n$/d' "$rc_expanded" | |
| print_status "Cleaned up: $rc_expanded" | |
| fi | |
| done | |
| # Verify cleanup by checking for remaining artifacts | |
| print_status "Verifying cleanup completion..." | |
| CLEANUP_VERIFIED=true | |
| # Check for remaining toast functions in shell configs | |
| for rc in "${RC_FILES[@]}"; do | |
| rc_expanded="$(eval echo "${rc}")" | |
| if [[ -f "$rc_expanded" ]] && (grep -q "toast()" "$rc_expanded" || grep -q "toast helper" "$rc_expanded"); then | |
| print_warning "Found remaining toast artifacts in: $rc_expanded" | |
| CLEANUP_VERIFIED=false | |
| fi | |
| done | |
| # Check for remaining toast scripts | |
| for location in "${OLD_TOAST_LOCATIONS[@]}"; do | |
| if [[ -f "$location" ]]; then | |
| print_warning "Found remaining toast script: $location" | |
| CLEANUP_VERIFIED=false | |
| fi | |
| done | |
| if [[ "$CLEANUP_VERIFIED" == "true" ]]; then | |
| print_status "✓ Comprehensive cleanup completed successfully" | |
| else | |
| print_warning "Some artifacts may remain - manual cleanup may be required" | |
| fi | |
| print_status "Cleanup phase complete." | |
| # Claude Code specific cleanup | |
| print_status "Checking for Claude Code shell snapshots..." | |
| claude_snapshots_dir="$HOME/.claude/shell-snapshots" | |
| if [[ -d "$claude_snapshots_dir" ]]; then | |
| snapshot_count=$(ls -1 "$claude_snapshots_dir"/*.sh 2>/dev/null | wc -l) | |
| if [[ "$snapshot_count" -gt 0 ]]; then | |
| print_warning "Found $snapshot_count Claude Code shell snapshots that may contain cached toast functions" | |
| print_status "Clearing old shell snapshots to prevent function caching issues..." | |
| rm -f "$claude_snapshots_dir"/*.sh 2>/dev/null || true | |
| print_status "Cleared Claude Code shell snapshots" | |
| fi | |
| fi | |
| # Force unload any cached toast functions in current shell | |
| print_status "Clearing cached toast functions from current shell..." | |
| unset -f toast 2>/dev/null || true | |
| unset toast 2>/dev/null || true | |
| echo | |
| # Debug information to help understand environment differences | |
| print_status "Environment debugging information:" | |
| echo " - System: $(uname -r)" | |
| # Detect environment and set Windows mount path | |
| WIN_MOUNT="" | |
| POWERSHELL_PATH="" | |
| if [[ -d "/mnt/c" ]]; then | |
| WIN_MOUNT="/mnt/c" | |
| POWERSHELL_PATH="/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" | |
| print_status "Detected: WSL environment" | |
| elif [[ -d "/c" ]] || mount | grep -q "/c"; then | |
| WIN_MOUNT="/c" | |
| # Use PowerShell 7 if available for better module support in MSYS2 | |
| if [[ -f "/c/Program Files/PowerShell/7/pwsh.exe" ]]; then | |
| POWERSHELL_PATH="/c/Program Files/PowerShell/7/pwsh.exe" | |
| print_status "Detected: MSYS2/Git Bash environment (using PowerShell 7)" | |
| else | |
| POWERSHELL_PATH="powershell.exe" | |
| print_status "Detected: MSYS2/Git Bash environment (using PowerShell 5.1)" | |
| fi | |
| else | |
| print_error "Neither /mnt/c nor /c Windows mount points found" | |
| print_error "This script requires WSL or MSYS2/Git Bash environment" | |
| exit 1 | |
| fi | |
| echo " - Windows mount point: $(ls -d "$WIN_MOUNT" 2>/dev/null && echo "✓ Available at $WIN_MOUNT" || echo "✗ Not found")" | |
| echo " - PowerShell availability: $(which "$POWERSHELL_PATH" &>/dev/null && echo "✓ Available" || echo "✗ Not found")" | |
| echo | |
| # Remove old Windows toast installations from user directories | |
| print_status "Cleaning up old Windows toast installations from user directories..." | |
| OLD_WIN_TOAST_LOCATIONS=( | |
| "$WIN_MOUNT/Users/*/bin/toast.ps1" | |
| "$WIN_MOUNT/Users/*/AppData/Local/Toast" | |
| "$WIN_MOUNT/Users/*/AppData/Roaming/Toast" | |
| "$WIN_MOUNT/temp/install_toast_windows.ps1" | |
| "$WIN_MOUNT/tmp/install_toast_windows.ps1" | |
| ) | |
| for pattern in "${OLD_WIN_TOAST_LOCATIONS[@]}"; do | |
| for location in $pattern; do | |
| if [[ -e "$location" ]]; then | |
| print_status "Removing old Windows toast artifact: $location" | |
| rm -rf "$location" 2>/dev/null || true | |
| fi | |
| done | |
| done | |
| # Check if Windows toast is installed at system-wide location | |
| print_status "Checking Windows toast installation..." | |
| if [[ -f "$WIN_MOUNT/ProgramData/Toast/toast.ps1" ]]; then | |
| print_status "Found existing Windows toast installation at system-wide location" | |
| else | |
| print_warning "Windows toast not found at system-wide location, installing..." | |
| # Download and run Windows installer for system-wide installation | |
| TEMP_SCRIPT="/tmp/install_toast_windows.ps1" | |
| print_status "Downloading Windows installer..." | |
| if curl -fsSL 'https://gist.githubusercontent.com/ChrisColeTech/1f79919c60bf210982a04bc607a1a1d7/raw/windows_toast_install.ps1' -o "$TEMP_SCRIPT"; then | |
| print_status "Downloaded Windows installer successfully" | |
| print_status "File size: $(ls -lh "$TEMP_SCRIPT" | awk '{print $5}')" | |
| # Convert Unix path to Windows path for PowerShell | |
| WIN_TEMP_SCRIPT="C:\\temp\\install_toast_windows.ps1" | |
| mkdir -p "$WIN_MOUNT/temp" | |
| cp "$TEMP_SCRIPT" "$WIN_MOUNT/temp/install_toast_windows.ps1" | |
| print_status "Running Windows installer for system-wide installation..." | |
| if "$POWERSHELL_PATH" -ExecutionPolicy Bypass -File "$WIN_TEMP_SCRIPT"; then | |
| print_status "✓ Windows toast installation completed" | |
| rm -f "$TEMP_SCRIPT" "$WIN_MOUNT/temp/install_toast_windows.ps1" | |
| else | |
| print_error "Failed to install Windows toast automatically" | |
| print_error "Please install manually in Windows PowerShell:" | |
| print_error " curl.exe -fsSL 'https://gist.githubusercontent.com/ChrisColeTech/1f79919c60bf210982a04bc607a1a1d7/raw/windows_toast_install.ps1' -o install_toast.ps1" | |
| print_error " powershell -ExecutionPolicy Bypass -File install_toast.ps1" | |
| rm -f "$TEMP_SCRIPT" "$WIN_MOUNT/temp/install_toast_windows.ps1" | |
| exit 1 | |
| fi | |
| else | |
| print_error "Failed to download Windows installer" | |
| print_error "Please install manually in Windows PowerShell" | |
| exit 1 | |
| fi | |
| fi | |
| # Define the toast function snippet using system-wide ProgramData installation | |
| SNIPPET="# -- wsl toast helper start -- | |
| toast() { | |
| local title=\"\${1:-Notification}\" | |
| shift | |
| local body=\"\${*:-}\" | |
| \"$POWERSHELL_PATH\" -ExecutionPolicy Bypass -Command \"& 'C:\\ProgramData\\Toast\\toast.ps1' '\$title' '\$body'\" | |
| } | |
| # -- wsl toast helper end --" | |
| # Test Windows toast functionality | |
| print_status "Testing Windows toast functionality..." | |
| if "$POWERSHELL_PATH" -ExecutionPolicy Bypass -Command "& 'C:\ProgramData\Toast\toast.ps1' 'Test' 'WSL Installer Test'" 2>/dev/null; then | |
| print_status "✓ Windows toast test successful" | |
| else | |
| print_warning "Windows toast test failed, but continuing with installation..." | |
| fi | |
| print_status "Installing WSL toast helper..." | |
| # Install to shell configuration files (only bashrc and zshrc for the new function) | |
| INSTALL_RC_FILES=(~/.bashrc ~/.zshrc) | |
| for rc in "${INSTALL_RC_FILES[@]}"; do | |
| rc_expanded="$(eval echo "${rc}")" | |
| # Create file if it doesn't exist | |
| if [[ ! -f "$rc_expanded" ]]; then | |
| print_status "Creating $rc_expanded" | |
| touch "$rc_expanded" | |
| fi | |
| # Remove existing snippets first to avoid duplicates | |
| if grep -Fq "$SNIPPET_START" "$rc_expanded"; then | |
| print_status "Removing old toast helper from $rc_expanded" | |
| # Use sed to remove lines between markers (inclusive) | |
| sed -i "/$SNIPPET_START/,/$SNIPPET_END/d" "$rc_expanded" | |
| fi | |
| print_status "Adding toast helper to $rc_expanded" | |
| echo "" >> "$rc_expanded" | |
| printf "%s\n" "$SNIPPET" >> "$rc_expanded" | |
| done | |
| echo | |
| print_status "Installation complete!" | |
| echo | |
| echo "To enable the toast function in your current session, run:" | |
| echo " source ~/.bashrc # if you use bash" | |
| echo " source ~/.zshrc # if you use zsh" | |
| echo | |
| echo "Or open a new terminal window." | |
| echo | |
| echo "Usage examples:" | |
| echo ' toast "Hello" "This is a test notification"' | |
| echo ' toast "Build Complete"' | |
| echo ' toast "Error" "Something went wrong"' | |
| echo | |
| # Create executable script in ~/bin for PATH access | |
| print_status "Creating executable toast script..." | |
| mkdir -p ~/bin | |
| cat > ~/bin/toast << EOF | |
| #!/bin/bash | |
| title="\${1:-Notification}" | |
| shift | |
| body="\${*:-}" | |
| "$POWERSHELL_PATH" -ExecutionPolicy Bypass -Command "& 'C:\\ProgramData\\Toast\\toast.ps1' '\$title' '\$body'" | |
| EOF | |
| chmod +x ~/bin/toast | |
| # Add ~/bin to PATH if not already there | |
| for rc in "${INSTALL_RC_FILES[@]}"; do | |
| rc_expanded="$(eval echo "${rc}")" | |
| if [[ -f "$rc_expanded" ]] && ! grep -q 'export PATH="$HOME/bin:$PATH"' "$rc_expanded"; then | |
| print_status "Adding ~/bin to PATH in $rc_expanded" | |
| echo 'export PATH="$HOME/bin:$PATH"' >> "$rc_expanded" | |
| fi | |
| done | |
| # Test in current session if possible | |
| print_status "Testing installation..." | |
| export PATH="$HOME/bin:$PATH" | |
| # Force reload the shell functions to ensure we're using the new installation | |
| unset -f toast 2>/dev/null || true | |
| source ~/.bashrc 2>/dev/null || true | |
| # Test the installation | |
| if ~/bin/toast "Installation Complete" "WSL toast notifications are ready" 2>/dev/null; then | |
| print_status "✓ Test notification sent successfully!" | |
| # Verify the function is using the correct path | |
| print_status "Verifying toast function configuration..." | |
| if declare -f toast >/dev/null 2>&1; then | |
| if declare -f toast | grep -q "C:\\ProgramData\\Toast\\toast.ps1"; then | |
| print_status "✓ Toast function is correctly configured for system-wide installation" | |
| else | |
| print_warning "⚠ Toast function may still be using old configuration" | |
| print_warning "Run: type toast to check the current configuration" | |
| fi | |
| else | |
| print_status "✓ Using ~/bin/toast executable (no cached function)" | |
| fi | |
| else | |
| print_warning "Test notification failed" | |
| print_warning "This may be due to cached functions in Claude Code shell snapshots" | |
| print_warning "Try restarting Claude Code or your terminal" | |
| # Provide diagnostic information | |
| if declare -f toast >/dev/null 2>&1; then | |
| print_warning "Current toast function definition:" | |
| declare -f toast | head -5 | |
| fi | |
| fi | |
| echo | |
| # Configure Claude Code hooks | |
| print_status "Configuring Claude Code hooks..." | |
| claude_settings_dir="$HOME/.claude" | |
| claude_settings_path="$claude_settings_dir/settings.json" | |
| # Important warning about hooks replacement | |
| print_warning "IMPORTANT: This installer will replace any existing Claude Code hooks" | |
| print_warning "with toast notification hooks for optimal functionality." | |
| echo | |
| if [[ ! -d "$claude_settings_dir" ]]; then | |
| print_status "Creating Claude settings directory: $claude_settings_dir" | |
| mkdir -p "$claude_settings_dir" | |
| fi | |
| # Check for existing settings and preserve them | |
| existing_settings="" | |
| if [[ -f "$claude_settings_path" ]]; then | |
| print_status "Found existing Claude Code settings file" | |
| # Create timestamped backup | |
| timestamp=$(date +%Y%m%d_%H%M%S) | |
| backup_path="${claude_settings_path}.backup_${timestamp}" | |
| if cp "$claude_settings_path" "$backup_path" 2>/dev/null; then | |
| print_status "Created backup: $backup_path" | |
| else | |
| print_warning "Failed to create backup of existing settings" | |
| fi | |
| # Try to extract existing settings (excluding hooks) using simple parsing | |
| if command -v jq >/dev/null 2>&1; then | |
| existing_settings=$(jq 'del(.hooks)' "$claude_settings_path" 2>/dev/null | jq -r 'to_entries | map(" \"" + .key + "\": " + (.value | tostring | @json)) | join(",\n")' 2>/dev/null) | |
| else | |
| # Fallback: simple grep-based extraction for basic key-value pairs | |
| print_status "jq not available, using basic parsing for common settings" | |
| existing_content=$(cat "$claude_settings_path") | |
| # Extract model setting if present | |
| if echo "$existing_content" | grep -q '"model"'; then | |
| model_value=$(echo "$existing_content" | grep '"model"' | sed 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') | |
| if [[ -n "$model_value" ]]; then | |
| existing_settings=" \"model\": \"$model_value\"" | |
| fi | |
| fi | |
| # Extract other common settings (can be extended) | |
| if echo "$existing_content" | grep -q '"someOtherSetting"'; then | |
| other_value=$(echo "$existing_content" | grep '"someOtherSetting"' | sed 's/.*"someOtherSetting"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') | |
| if [[ -n "$other_value" ]]; then | |
| if [[ -n "$existing_settings" ]]; then | |
| existing_settings="$existing_settings,\n \"someOtherSetting\": \"$other_value\"" | |
| else | |
| existing_settings=" \"someOtherSetting\": \"$other_value\"" | |
| fi | |
| fi | |
| fi | |
| fi | |
| if [[ -n "$existing_settings" ]]; then | |
| print_status "Preserved existing settings (non-hook configuration)" | |
| fi | |
| fi | |
| # Create the JSON with preserved settings and hooks (keeping emojis) | |
| if [[ -n "$existing_settings" ]]; then | |
| cat > "$claude_settings_path" << EOF | |
| { | |
| $existing_settings, | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "matcher": "startup", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"New session started - Ready to help!\"" | |
| } | |
| ] | |
| }, | |
| { | |
| "matcher": "resume", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Session resumed - Continuing where we left off\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "Notification": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"User input needed to continue\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "Stop": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Task completed successfully\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "SubagentStop": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Subagent task finished\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "PreCompact": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Compacting context to continue\"" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| EOF | |
| else | |
| cat > "$claude_settings_path" << 'EOF' | |
| { | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "matcher": "startup", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"New session started - Ready to help!\"" | |
| } | |
| ] | |
| }, | |
| { | |
| "matcher": "resume", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Session resumed - Continuing where we left off\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "Notification": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"User input needed to continue\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "Stop": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Task completed successfully\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "SubagentStop": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Subagent task finished\"" | |
| } | |
| ] | |
| } | |
| ], | |
| "PreCompact": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "toast \"Claude Code\" \"Compacting context to continue\"" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| EOF | |
| fi | |
| print_status "✓ Claude Code critical hooks configured successfully!" | |
| print_status "Settings saved to: $claude_settings_path" | |
| echo | |
| print_status "=== INSTALLATION COMPLETE ===" | |
| echo | |
| print_status "IMPORTANT POST-INSTALLATION STEPS:" | |
| echo "1. Restart Claude Code to ensure shell snapshots use the new toast configuration" | |
| echo "2. Or restart your terminal to clear any cached functions" | |
| echo "3. Run: type toast to verify it shows C:\\ProgramData\\Toast\\toast.ps1" | |
| echo "4. Test notifications with: toast 'Test' 'Message'" | |
| echo | |
| print_warning "If toast notifications don't work immediately:" | |
| echo "- Restart Claude Code (recommended)" | |
| echo "- Or run: unset -f toast && source ~/.bashrc" | |
| echo "- Or open a new terminal session" | |
| echo | |
| print_status "For troubleshooting, check that:" | |
| echo "- ~/bin/toast script exists and is executable" | |
| echo "- C:/ProgramData/Toast/toast.ps1 exists" | |
| echo "- PowerShell path is correct in your environment" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment