Skip to content

Instantly share code, notes, and snippets.

@ChrisColeTech
Last active August 18, 2025 08:01
Show Gist options
  • Select an option

  • Save ChrisColeTech/010a3a1f313fa39a10566a328c32424b to your computer and use it in GitHub Desktop.

Select an option

Save ChrisColeTech/010a3a1f313fa39a10566a328c32424b to your computer and use it in GitHub Desktop.
Windows Toast Notifications - Bash Installation Script for WSL
#!/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