From b451a80b5767bb4849208db659ddf506e8090e12 Mon Sep 17 00:00:00 2001 From: srdusr Date: Tue, 23 Sep 2025 21:09:05 +0200 Subject: Minor fixes --- windows/Documents/PowerShell/bootstrap.ps1 | 949 +++++++++++++++++++---------- 1 file changed, 611 insertions(+), 338 deletions(-) (limited to 'windows/Documents/PowerShell/bootstrap.ps1') diff --git a/windows/Documents/PowerShell/bootstrap.ps1 b/windows/Documents/PowerShell/bootstrap.ps1 index 73d53b5..d2f4369 100644 --- a/windows/Documents/PowerShell/bootstrap.ps1 +++ b/windows/Documents/PowerShell/bootstrap.ps1 @@ -1,396 +1,669 @@ -# Requires -RunAsAdministrator - -# Set execution policy to remote signed -Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force - -# Set network category to private -Set-NetConnectionProfile -NetworkCategory Private - -# Variables -$dotfiles_url = 'https://github.com/srdusr/dotfiles.git' -$dotfiles_dir = "$HOME\.cfg" - -# Function to handle errors -function handle_error { - param ($message) - Write-Host $message -ForegroundColor Red - exit 1 +#!/usr/bin/env pwsh + +# Created By: srdusr +# Created On: Windows PowerShell Bootstrap Script +# Project: Dotfiles installation script for Windows + +# Dependencies: git, powershell + +param( + [string]$Profile = "essentials", + [switch]$Force = $false, + [switch]$Ask = $false, + [switch]$Help = $false +) + +# Color definitions for pretty UI +$Script:Colors = @{ + Reset = "`e[0m" + Red = "`e[0;31m" + Green = "`e[0;32m" + Yellow = "`e[0;33m" + Blue = "`e[0;34m" + Cyan = "`e[0;36m" + White = "`e[0;37m" + Bold = "`e[1m" } -# Logs -New-Item -Path $Env:USERPROFILE\Logs -ItemType directory -Force -Start-Transcript -Path $Env:USERPROFILE\Logs\Bootstrap.log -$ErrorActionPreference = 'SilentlyContinue' -Write-Host "Bootstrap.log generated in Logs\" - -# Function to check if the current session is elevated -function Test-IsAdmin { - $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) - return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +# Prompt helper: Yes/No with default; in non-Ask mode, returns default immediately +function Prompt-YesNo { + param( + [Parameter(Mandatory=$true)][string]$Question, + [ValidateSet('Y','N')][string]$Default = 'Y' + ) + if (-not $Script:AskPreference) { + return $Default -eq 'Y' + } + $suffix = if ($Default -eq 'Y') { '[Y/n]' } else { '[y/N]' } + while ($true) { + Write-Host -NoNewline "$Question $suffix: " -ForegroundColor Yellow + $resp = Read-Host + if ([string]::IsNullOrWhiteSpace($resp)) { $resp = $Default } + switch ($resp.ToUpper()) { + 'Y' { return $true } + 'YES' { return $true } + 'N' { return $false } + 'NO' { return $false } + default { Write-Warning "Please answer Y/yes or N/no" } + } + } } -# Ensure the script is run as administrator -if (-not (Test-IsAdmin)) { - handle_error "This script must be run as an administrator." +# Configuration +$Script:Config = @{ + DotfilesUrl = 'https://github.com/srdusr/dotfiles.git' + DotfilesDir = "$HOME\.cfg" + LogFile = "$HOME\AppData\Local\dotfiles_install.log" + StateFile = "$HOME\AppData\Local\dotfiles_install_state" + BackupDir = "$HOME\.dotfiles-backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + PackagesFile = "packages.yml" + OS = "windows" } -# Imports -. $HOME\.config\powershell\initialize.ps1 -. $HOME\.config\powershell\bloatware.ps1 - -# Configure PowerShell -Write-Host "Configuring PowerShell" -Write-Host "----------------------------------------" - -# Get the "MyDocuments" path for the current user, excluding OneDrive -$UserMyDocumentsPath = [System.Environment]::GetFolderPath('MyDocuments').Replace("OneDrive", "") - -$PowerShellProfileDirectory = "$UserMyDocumentsPath\PowerShell" -$PowerShellLegacySymlink = "$UserMyDocumentsPath\WindowsPowerShell" - -$PowerShellProfileTemplate = "$PSScriptRoot\$USERNAME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1" -$env:PSModulePath = $env:PSModulePath -replace "\\OneDrive\\Documents\\WindowsPowerShell\\","\.powershell\" - -# Set documents path to user's local Documents folder -$documentsPath = "$UserMyDocumentsPath" -$powerShellProfileDir = "$documentsPath\PowerShell" - -# Output the chosen PowerShell profile directory -$PROFILE = "$powerShellProfileDir\Microsoft.PowerShell_profile.ps1" -Write-Host "PowerShell profile directory set to: $powerShellProfileDir" - -if (-not (Test-Path -Path $powerShellProfileDir)) { - New-Item -ItemType Directory -Path $powerShellProfileDir -Force +# Logging functions +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $logEntry = "[$timestamp] [$Level] $Message" + Add-Content -Path $Script:Config.LogFile -Value $logEntry -Force } -New-Item -ItemType HardLink -Force ` - -Path "$powerShellProfileDir\Microsoft.PowerShell_profile.ps1" ` - -Target "$home\.config\powershell\Microsoft.PowerShell_profile.ps1" - -# Set environment variable -[System.Environment]::SetEnvironmentVariable('PowerShellProfileDir', $powerShellProfileDir, [System.EnvironmentVariableTarget]::User) - -Write-Host "PowerShell profile directory set to: $powerShellProfileDir" -Write-Host "Environment variable 'PowerShellProfileDir' set to: $powerShellProfileDir" - -# Verify profile sourcing -if (!(Test-Path -Path "$home\.config\powershell\Microsoft.PowerShell_profile.ps1")) { - handle_error "PowerShell profile does not exist. Please create it at $home\.config\powershell\Microsoft.PowerShell_profile.ps1" +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + Write-Host $Message -ForegroundColor $Color + Write-Log $Message } -# Install Chocolatey if not installed -Write-Host "Installing Chocolatey" -Write-Host "----------------------------------------" - -if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) - - # Check if Chocolatey installed successfully - if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { - handle_error "Chocolatey installation failed." - } -} else { - Write-Host "Chocolatey is already installed." +function Write-Success { param([string]$Message) Write-ColorOutput "✓ $Message" "Green" } +function Write-Info { param([string]$Message) Write-ColorOutput "ℹ $Message" "Cyan" } +function Write-Warning { param([string]$Message) Write-ColorOutput "⚠ $Message" "Yellow" } +function Write-Error { param([string]$Message) Write-ColorOutput "✗ $Message" "Red" } + +function Write-Header { + param([string]$Title) + Write-Host "" + Write-Host "=" * 60 -ForegroundColor Blue + Write-Host " $Title" -ForegroundColor Bold + Write-Host "=" * 60 -ForegroundColor Blue + Write-Host "" } -# Install Applications -Write-Host "Installing Applications" -Write-Host "----------------------------------------" - -# Check if the powershell-yaml module is installed, if not, install it -if (-not (Get-Module powershell-yaml -ListAvailable)) { - $policy = Get-PSRepository -Name 'PSGallery' | Select-Object -ExpandProperty InstallationPolicy - Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted - Install-Module powershell-yaml - Set-PSRepository -Name 'PSGallery' -InstallationPolicy $policy +# Utility functions +function Test-CommandExists { + param([string]$Command) + return [bool](Get-Command $Command -ErrorAction SilentlyContinue) } -Import-Module powershell-yaml - -# Load packages.yml -$packagesFile = "$HOME\packages.yml" -$packages = Get-Content $packagesFile | ConvertFrom-Yaml - -# Ensure 'windows' section exists and has applications listed -if ($packages.windows) { - foreach ($app in $packages.windows) { - # Check if the application is already installed - if (-not (choco list --local-only | Select-String -Pattern "^$app\s")) { - Write-Host "Installing $app" - choco install $app -y - - if ($LASTEXITCODE -ne 0) { - handle_error "Installation of $app failed." - } else { - Write-Host "$app installed successfully." - } - } else { - Write-Host "$app is already installed." - } - } -} else { - Write-Host "No applications specified under the 'windows' section in $packagesFile." +function Test-IsAdmin { + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } -# Set Chrome as default browser ------------------------ -#Add-Type -AssemblyName 'System.Windows.Forms' -#Start-Process $env:windir\system32\control.exe -ArgumentList '/name Microsoft.DefaultPrograms /page pageDefaultProgram\pageAdvancedSettings?pszAppName=google%20chrome' -#Sleep 2 -#[System.Windows.Forms.SendKeys]::SendWait("{TAB} {TAB}{TAB} ") -SetDefaultBrowser firefox - -# Refresh the environment variables -Write-Host "Refreshing environment variables" - -# Update the current session environment variables -Write-Host "Setting environment variables" -ForegroundColor Cyan -$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") -[System.Environment]::SetEnvironmentVariable("Path", $env:Path, [System.EnvironmentVariableTarget]::Process) -[Environment]::SetEnvironmentVariable("HOME", "$env:USERPROFILE", "User") -[Environment]::SetEnvironmentVariable("LC_ALL", "C.UTF-8", "User") -refreshenv - -# Add Git to PATH if it's installed via Chocolatey -Write-Host "Checking for Git installation" -$gitBinPath = "C:\Program Files\Git\bin" -$gitCmdPath = "C:\Program Files\Git\cmd" -$gitPaths = @($gitBinPath, $gitCmdPath) - -foreach ($path in $gitPaths) { - if (Test-Path $path) { - Write-Host "Adding $path to PATH" - [System.Environment]::SetEnvironmentVariable("Path", "$env:Path;$path", [System.EnvironmentVariableTarget]::Machine) - [System.Environment]::SetEnvironmentVariable("Path", "$env:Path;$path", [System.EnvironmentVariableTarget]::User) - [System.Environment]::SetEnvironmentVariable("Path", "$env:Path;$path", [System.EnvironmentVariableTarget]::Process) +function Invoke-AdminCommand { + param([string]$Command) + if (-not (Test-IsAdmin)) { + Write-Warning "Elevating privileges for: $Command" + Start-Process powershell.exe -ArgumentList "-NoProfile", "-Command", $Command -Verb RunAs -Wait } else { - Write-Host "$path does not exist." + Invoke-Expression $Command } } -# Check if Git is installed -Write-Host "Checking for Git installation" -if (-not (Get-Command git -ErrorAction SilentlyContinue)) { - handle_error "Git is not installed or not found in PATH after installation." -} else { - Write-Host "Git is installed and available in PATH." +# Package management functions +function Get-PackageManager { + if (Test-CommandExists "choco") { return "chocolatey" } + if (Test-CommandExists "winget") { return "winget" } + if (Test-CommandExists "scoop") { return "scoop" } + return $null } -# Define the `config` alias in the current session -function global:config { - git --git-dir="$env:USERPROFILE\.cfg" --work-tree="$env:USERPROFILE" $args -} - -# Add .gitignore entries -Add-Content -Path "$HOME\.gitignore" -Value ".cfg" -Add-Content -Path "$HOME\.gitignore" -Value "install.bat" -Add-Content -Path "$HOME\.gitignore" -Value ".config/powershell/bootstrap.ps1" - -# Create symbolic links -Write-Host "Create symbolic links" -Write-Host "----------------------------------------" - -# Visual Studio Code settings.json -New-Item -Force -ItemType SymbolicLink $HOME\AppData\Roaming\Code\User\ -Name settings.json -Value $HOME\.config\Code\User\settings.json - -# Visual Studio Code keybindings -New-Item -Force -ItemType SymbolicLink $HOME\AppData\Roaming\Code\User\ -Name keybindings.json -Value $HOME\.config\Code\User\keybindings.json - -# Function to install dotfiles -function install_dotfiles { - if (Test-Path -Path $dotfiles_dir) { - config pull | Out-Null - $update = $true - } else { - git clone --bare $dotfiles_url $dotfiles_dir | Out-Null - $update = $false - } - - $std_err_output = config checkout 2>&1 - - if ($std_err_output -match "following untracked working tree files would be overwritten") { - if (-not $update) { - config checkout | Out-Null +# Return $true if a package appears to be installed for the given manager +function Test-PackageInstalled { + param( + [Parameter(Mandatory=$true)][string]$Manager, + [Parameter(Mandatory=$true)][string]$Name + ) + switch ($Manager) { + "chocolatey" { + $out = choco list --local-only --exact $Name 2>$null + return ($out | Select-String -Pattern "^\s*$([regex]::Escape($Name))\s").Length -gt 0 + } + "winget" { + # Winget list may return multiple rows; use --exact name match when possible + $out = winget list --name $Name 2>$null + return ($out | Select-String -SimpleMatch $Name).Length -gt 0 } + "scoop" { + # scoop list returns 0 when installed + scoop list $Name *> $null + return $LASTEXITCODE -eq 0 + } + default { return $false } } - config config status.showUntrackedFiles no - - git config --global include.path "$HOME\.gitconfig.aliases" +} - if ($update -or (Read-Host "Do you want to overwrite existing files and continue with the dotfiles setup? [Y/n]" -eq "Y")) { - config fetch origin main:main | Out-Null - config reset --hard main | Out-Null - config checkout -f - if ($?) { - Write-Host "Successfully imported $dotfiles_dir." +function Install-PackageManager { + Write-Header "Installing Package Manager" + + if (-not (Test-CommandExists "choco")) { + Write-Info "Installing Chocolatey..." + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + if (Test-CommandExists "choco") { + Write-Success "Chocolatey installed successfully" } else { - handle_error "Mission failed." + Write-Error "Failed to install Chocolatey" + return $false } } else { - handle_error "Aborted by user. Exiting..." + Write-Info "Chocolatey already installed" } + return $true } -install_dotfiles - -# Install python -Write-Host "Updating python packages" -ForegroundColor Cyan -python -m pip install --upgrade pip - -# Enable WSL feature -dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart -Write-Host "Enable WSL feature" -wsl --install -d ubuntu -wsl --set-default-version 2 - -# Enable Virtual Machine feature -#dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart -#Write-Host "Enable Virtual Machine feature" - -Write-Header "Installing Hyper-V" - -# Install Hyper-V -Write-Host "Installing Hyper-V and restart" -Enable-WindowsOptionalFeature -Online -FeatureName Containers -All -NoRestart -Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart -Install-WindowsFeature -Name Hyper-V -IncludeAllSubFeature -IncludeManagementTools -NoRestart - -# Configure Neovim -Write-Host "Configuring Neovim" -Write-Host "----------------------------------------" - -$neovimLocalPath = "$home\AppData\Local\nvim" -$neovimConfigPath = "$home\.config\nvim" +function Install-Packages { + param([string]$PackagesFile, [string]$Profile) + + if (-not (Test-Path $PackagesFile)) { + Write-Warning "Packages file not found: $PackagesFile" + return + } + + Write-Header "Installing Packages" + + # Install powershell-yaml if not available + if (-not (Get-Module powershell-yaml -ListAvailable)) { + Write-Info "Installing powershell-yaml module..." + $policy = Get-PSRepository -Name 'PSGallery' | Select-Object -ExpandProperty InstallationPolicy + Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + Install-Module powershell-yaml -Force + Set-PSRepository -Name 'PSGallery' -InstallationPolicy $policy + } + + Import-Module powershell-yaml + + # Helper: run custom_installs..windows if condition passes + function Invoke-CustomInstallsWindows { + param([Parameter(Mandatory=$true)]$Yaml) + if (-not $Yaml.custom_installs) { return } + foreach ($name in $Yaml.custom_installs.PSObject.Properties.Name) { + $entry = $Yaml.custom_installs.$name + if (-not $entry) { continue } + $winCmd = $entry.windows + if (-not $winCmd) { continue } + $shouldRun = $true + if ($entry.condition) { + $cond = [string]$entry.condition + if ($cond -match "!\s*command\s+-v\s+([A-Za-z0-9._-]+)") { + $shouldRun = -not (Test-CommandExists $Matches[1]) + } elseif ($cond -match "command\s+-v\s+([A-Za-z0-9._-]+)") { + $shouldRun = (Test-CommandExists $Matches[1]) + } + } + if (-not $shouldRun) { Write-Info "Skipping custom install: $name"; continue } + Write-Info "Running custom install: $name" + try { Invoke-Expression $winCmd; Write-Success "Custom install completed: $name" } + catch { Write-Warning "Custom install failed for '$name': $_" } + } + } + + try { + $packages = Get-Content $PackagesFile | ConvertFrom-Yaml + $packageManager = Get-PackageManager + + if (-not $packageManager) { + Write-Error "No package manager available" + return + } + + # Get packages for current profile and OS + $profilePackages = @() + if ($packages.profiles.$Profile.windows) { + $profilePackages += $packages.profiles.$Profile.windows + } + if ($packages.profiles.$Profile.common) { + $profilePackages += $packages.profiles.$Profile.common + } + + foreach ($package in $profilePackages) { + $packageName = if ($packages.packages.$package.windows) { + $packages.packages.$package.windows + } else { + $package + } -# Check if nvim directory already exists in AppData\Local -if (-not (Test-Path -Path $neovimLocalPath)) { - New-Item -ItemType Junction -Force -Path $neovimLocalPath -Target $neovimConfigPath -} else { - Write-Host "Neovim directory ($neovimLocalPath) already exists." -} + if (Test-PackageInstalled -Manager $packageManager -Name $packageName) { + Write-Info "Already installed: $packageName" + continue + } -# Install Windows Terminal, and configure -Write-Host "Install Windows Terminal, and configure" -Write-Host "----------------------------------------" + Write-Info "Installing package: $packageName" + + switch ($packageManager) { + "chocolatey" { + if (-not (choco list --local-only | Select-String -Pattern "^$packageName\s")) { + choco install $packageName -y + if ($LASTEXITCODE -eq 0) { + Write-Success "Installed: $packageName" + } else { + Write-Error "Failed to install: $packageName" + } + } else { + Write-Info "Already installed: $packageName" + } + } + "winget" { + winget install $packageName --accept-package-agreements --accept-source-agreements + } + "scoop" { + scoop install $packageName + } + } + } -$windowsTerminalSettingsPath = "$home\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" -$windowsTerminalConfigPath = "$home\.config\windows-terminal\settings.json" + # Also install top-level Windows packages list if present + if ($packages.windows) { + foreach ($pkg in $packages.windows) { + if ([string]::IsNullOrWhiteSpace($pkg)) { continue } + if (Test-PackageInstalled -Manager $packageManager -Name $pkg) { Write-Info "Already installed: $pkg"; continue } + Write-Info "Installing package: $pkg" + switch ($packageManager) { + "chocolatey" { + if (-not (choco list --local-only | Select-String -Pattern "^$([regex]::Escape($pkg))\s")) { choco install $pkg -y } + } + "winget" { winget install --id $pkg --silent --accept-package-agreements --accept-source-agreements } + "scoop" { scoop install $pkg } + } + } + } -# Check if Windows Terminal settings.json already exists -if (Test-Path -Path $windowsTerminalSettingsPath) { - # Backup existing settings.json - Move-Item -Force $windowsTerminalSettingsPath "$windowsTerminalSettingsPath.old" -} else { - Write-Host "Windows Terminal settings.json not found, no need to backup." + # Execute Windows custom installs from packages.yml + Invoke-CustomInstallsWindows -Yaml $packages + } catch { + Write-Error "Error processing packages: $_" + } } -# Create a hard link to the settings.json file in .config\windows-terminal -New-Item -ItemType HardLink -Force -Path $windowsTerminalSettingsPath -Target $windowsTerminalConfigPath - -# Function to check if a registry key exists -function Test-RegistryKeyExists { - param ($path) - return (Test-Path $path -PathType Container) +# Dotfiles management functions +function Install-Dotfiles { + Write-Header "Installing Dotfiles" + + if (Test-Path $Script:Config.DotfilesDir) { + Write-Info "Updating existing dotfiles repository..." + & git --git-dir="$($Script:Config.DotfilesDir)" --work-tree="$($Script:Config.DotfilesDir)" pull origin main + } else { + Write-Info "Cloning dotfiles repository..." + git clone --bare $Script:Config.DotfilesUrl $Script:Config.DotfilesDir + + if (-not (Test-Path $Script:Config.DotfilesDir)) { + Write-Error "Failed to clone dotfiles repository" + return $false + } + } + + # Set up config alias for this session + function script:config { + git --git-dir="$($Script:Config.DotfilesDir)" --work-tree="$($Script:Config.DotfilesDir)" @args + } + + # Configure repository + config config --local status.showUntrackedFiles no + + # Checkout files to restore directory structure + Write-Info "Checking out dotfiles..." + try { + config checkout 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Info "Forcing checkout to overwrite existing files..." + config checkout -f + } + Write-Success "Dotfiles checked out successfully" + } catch { + Write-Error "Failed to checkout dotfiles: $_" + return $false + } + + return $true } -# Function to check if a registry property exists -function Test-RegistryPropertyExists { - param ($keyPath, $propertyName) - if (Test-Path $keyPath) { - $properties = Get-ItemProperty -Path $keyPath - return $properties.PSObject.Properties.Name -contains $propertyName +function Deploy-Dotfiles { + Write-Header "Deploying Dotfiles" + + if (-not (Test-Path $Script:Config.DotfilesDir)) { + Write-Error "Dotfiles directory not found. Run Install-Dotfiles first." + return $false + } + + # Source the config command from profile if available + $profilePath = "$HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1" + if (Test-Path $profilePath) { + Write-Info "Loading config command from profile..." + . $profilePath + } + + # Deploy using config command if available, otherwise manual deployment + if (Get-Command config -ErrorAction SilentlyContinue) { + Write-Info "Deploying dotfiles using config command..." + config deploy + } else { + Write-Warning "Config command not available, using manual deployment..." + # Manual deployment fallback would go here } - return $false + + Write-Success "Dotfiles deployment completed" + return $true } -# Registry Tweaks -Write-Host "Registry Tweaks" -Write-Host "----------------------------------------" - -# Show hidden files -$advancedKeyPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" -if (-not (Test-RegistryPropertyExists $advancedKeyPath "Hidden")) { - Set-ItemProperty -Path $advancedKeyPath -Name Hidden -Value 1 +# Locate profile-specific packages.yml similar to Linux installer +function Get-ProfilePackagesFile { + param([string]$Profile) + $candidates = @( + Join-Path $HOME ".cfg/profile/$Profile/packages.yml", + Join-Path $HOME "profile/$Profile/packages.yml", + Join-Path $HOME "dot_setup/profile/$Profile/packages.yml", + Join-Path $Script:Config.DotfilesDir "common/$($Script:Config.PackagesFile)" + ) + foreach ($pf in $candidates) { + if (Test-Path $pf) { return $pf } + } + return $null } -# Show file extensions in Windows Explorer -$hideFileExtPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" -if (-not (Test-RegistryPropertyExists $hideFileExtPath "HideFileExt")) { - Set-ItemProperty -Path $hideFileExtPath -Name HideFileExt -Value 0 +# System configuration functions +function Set-WindowsConfiguration { + param( + [string]$PackagesFile + ) + + Write-Header "Configuring Windows Settings" + + if (-not $PackagesFile -or -not (Test-Path $PackagesFile)) { + Write-Warning "Packages file not found, skipping Windows configuration" + return + } + + try { + # Load YAML content + $yamlContent = Get-Content $PackagesFile -Raw | ConvertFrom-Yaml + $registrySettings = $yamlContent.system_tweaks.windows.registry + + if (-not $registrySettings) { + Write-Warning "No Windows registry settings found in packages.yml" + return + } + + Write-Info "Applying registry settings from packages.yml..." + + foreach ($setting in $registrySettings) { + try { + $path = $setting.path + $name = $setting.name + $value = $setting.value + $type = $setting.type + $description = $setting.description + + Write-Info "Setting: $description" + + # Ensure the registry path exists + $pathParts = $path -split '\\' + $currentPath = $pathParts[0] + for ($i = 1; $i -lt $pathParts.Length; $i++) { + $currentPath = "$currentPath\$($pathParts[$i])" + if (-not (Test-Path $currentPath)) { + New-Item -Path $currentPath -Force | Out-Null + } + } + + # Set the registry value + Set-ItemProperty -Path $path -Name $name -Value $value -Type $type -Force + Write-Success "Applied: $description" + + } catch { + Write-Warning "Failed to apply setting '$($setting.description)': $_" + } + } + + Write-Success "Windows configuration applied" + + # Restart explorer to apply changes + Write-Info "Restarting Windows Explorer..." + Stop-Process -Name explorer -Force + Start-Process explorer.exe + + } catch { + Write-Warning "Failed to process Windows configuration: $_" + } } -# Never Combine taskbar buttons when the taskbar is full -$taskbarGlomLevelPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" -if (-not (Test-RegistryPropertyExists $taskbarGlomLevelPath "TaskbarGlomLevel")) { - Set-ItemProperty -Path $taskbarGlomLevelPath -Name TaskbarGlomLevel -Value 2 +function Enable-WindowsFeatures { + param( + [string]$PackagesFile + ) + + Write-Header "Enabling Windows Features" + + if (-not $PackagesFile -or -not (Test-Path $PackagesFile)) { + Write-Warning "Packages file not found, skipping Windows features" + return + } + + try { + # Load YAML content + $yamlContent = Get-Content $PackagesFile -Raw | ConvertFrom-Yaml + $features = $yamlContent.system_tweaks.windows.features + + if (-not $features) { + Write-Warning "No Windows features found in packages.yml" + return + } + + foreach ($feature in $features) { + $featureName = $feature.name + $description = $feature.description + $requiresAdmin = $feature.requires_admin + + if ($requiresAdmin -and -not (Test-IsAdmin)) { + Write-Warning "Skipping '$description' - requires administrator privileges" + continue + } + + try { + Write-Info "Enabling: $description" + dism.exe /online /enable-feature /featurename:$featureName /all /norestart + Write-Success "Enabled: $description" + } catch { + Write-Warning "Failed to enable '$description': $_" + } + } + + Write-Success "Windows features processing complete (restart may be required)" + + } catch { + Write-Warning "Failed to process Windows features: $_" + } } -# Taskbar small icons -$taskbarSmallIconsPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" -if (-not (Test-RegistryPropertyExists $taskbarSmallIconsPath "TaskbarSmallIcons")) { - Set-ItemProperty -Path $taskbarSmallIconsPath -Name TaskbarSmallIcons -Value 1 +function Install-PowerShellProfile { + Write-Header "Setting up PowerShell Profile" + + $documentsPath = [System.Environment]::GetFolderPath('MyDocuments') + $powerShellProfileDir = "$documentsPath\PowerShell" + $profilePath = "$powerShellProfileDir\Microsoft.PowerShell_profile.ps1" + + Write-Info "PowerShell profile directory: $powerShellProfileDir" + + if (-not (Test-Path $powerShellProfileDir)) { + New-Item -ItemType Directory -Path $powerShellProfileDir -Force | Out-Null + Write-Success "Created PowerShell profile directory" + } + + # Copy profile from dotfiles if it exists + $dotfilesProfile = "$($Script:Config.DotfilesDir)\windows\Documents\PowerShell\Microsoft.PowerShell_profile.ps1" + if (Test-Path $dotfilesProfile) { + Copy-Item $dotfilesProfile $profilePath -Force + Write-Success "PowerShell profile installed from dotfiles" + } else { + Write-Warning "PowerShell profile not found in dotfiles" + } } -# Set Windows to use UTC time instead of local time for system clock -$timeZoneInfoPath = "HKLM:\SYSTEM\CurrentControlSet\Control\TimeZoneInformation" -if (-not (Test-RegistryPropertyExists $timeZoneInfoPath "RealTimeIsUniversal")) { - Set-ItemProperty -Path $timeZoneInfoPath -Name RealTimeIsUniversal -Value 1 +# Main execution function +function Start-Bootstrap { + param([string]$Profile, [switch]$Force, [switch]$Ask) + + Write-Header "Windows Dotfiles Bootstrap" + Write-Info "Profile: $Profile" + Write-Info "Force mode: $Force" + Write-Info "Interactive mode: $Ask" + + # Initialize logging + $logDir = Split-Path $Script:Config.LogFile + if (-not (Test-Path $logDir)) { + New-Item -ItemType Directory -Path $logDir -Force | Out-Null + } + + Write-Log "Bootstrap started with profile: $Profile" + + # Set Ask preference for all prompts + $Script:AskPreference = [bool]$Ask + + # Check dependencies + Write-Header "Checking Dependencies" + $requiredCommands = @("git", "powershell") + $missingCommands = @() + + foreach ($cmd in $requiredCommands) { + if (-not (Test-CommandExists $cmd)) { + $missingCommands += $cmd + Write-Error "Required command not found: $cmd" + } else { + Write-Success "Found: $cmd" + } + } + + if ($missingCommands.Count -gt 0) { + Write-Error "Missing required dependencies. Please install: $($missingCommands -join ', ')" + return $false + } + + # Install package manager (skippable) + if (Prompt-YesNo -Question "Install/check package manager?" -Default 'Y') { + if (-not (Install-PackageManager)) { + Write-Error "Failed to install package manager" + return $false + } + } else { + Write-Warning "Skipped package manager step by user choice" + } + + # Install dotfiles + if (Prompt-YesNo -Question "Install or update dotfiles?" -Default 'Y') { + if (-not (Install-Dotfiles)) { + Write-Error "Failed to install dotfiles" + return $false + } + } else { + Write-Warning "Skipped dotfiles installation by user choice" + } + + # Get packages file (profile-aware) + $packagesFile = Get-ProfilePackagesFile -Profile $Profile + if (-not $packagesFile) { + Write-Error "Failed to get packages file for profile '$Profile'" + return $false + } + + # Install packages + if (Prompt-YesNo -Question "Install profile packages?" -Default 'Y') { + Install-Packages -PackagesFile $packagesFile -Profile $Profile + } else { + Write-Warning "Skipped package installation by user choice" + } + + # Set up PowerShell profile + if (Prompt-YesNo -Question "Install PowerShell profile?" -Default 'Y') { + Install-PowerShellProfile + } else { + Write-Warning "Skipped PowerShell profile setup by user choice" + } + + # Deploy dotfiles + if (Prompt-YesNo -Question "Deploy dotfiles to system locations?" -Default 'Y') { + if (-not (Deploy-Dotfiles)) { + Write-Error "Failed to deploy dotfiles" + return $false + } + } else { + Write-Warning "Skipped dotfiles deployment by user choice" + } + + # Configure Windows + if (Prompt-YesNo -Question "Apply Windows configuration from packages.yml?" -Default 'N') { + Set-WindowsConfiguration -PackagesFile $packagesPath + } else { + Write-Warning "Skipped Windows configuration by user choice" + } + + # Enable Windows features (if admin) + if (Prompt-YesNo -Question "Enable Windows optional features?" -Default 'N') { + Enable-WindowsFeatures -PackagesFile $packagesPath + } else { + Write-Warning "Skipped enabling Windows features by user choice" + } + + Write-Header "Bootstrap Complete" + Write-Success "Windows dotfiles bootstrap completed successfully!" + Write-Info "Please restart your computer to apply all changes." + Write-Log "Bootstrap completed successfully" + + return $true } -# Disable the search in taskbar -$searchBoxTaskbarPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" -if (-not (Test-RegistryPropertyExists $searchBoxTaskbarPath "SearchBoxTaskbarMode")) { - New-ItemProperty -Path $searchBoxTaskbarPath -Name SearchBoxTaskbarMode -Value 0 -Type DWord -Force +# Help function +function Show-Help { + Write-Host @" +Windows Dotfiles Bootstrap Script + +USAGE: + .\bootstrap.ps1 [-Profile ] [-Force] [-Ask] [-Help] + +PARAMETERS: + -Profile Installation profile (default: essentials) + Available: essentials, minimal, dev, server, full, or a custom profile. + Custom profile files are resolved from: + - %USERPROFILE%\.cfg\profile\\packages.yml + - %USERPROFILE%\profile\\packages.yml + - %USERPROFILE%\dot_setup\profile\\packages.yml + -Force Force installation without prompts + -Ask Interactive mode with step-by-step prompts + -Help Show this help message + +EXAMPLES: + .\bootstrap.ps1 # Install with essentials profile + .\bootstrap.ps1 -Profile dev # Install development profile + .\bootstrap.ps1 -Profile full -Force # Force install full profile + .\bootstrap.ps1 -Ask # Interactive installation + +"@ -ForegroundColor Cyan } -# Dark mode: -$personalizePath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" -if (-not (Test-RegistryPropertyExists $personalizePath "AppsUseLightTheme")) { - Set-ItemProperty -Path $personalizePath -Name AppsUseLightTheme -Value 0 -Type Dword -Force -} -if (-not (Test-RegistryPropertyExists $personalizePath "SystemUsesLightTheme")) { - Set-ItemProperty -Path $personalizePath -Name SystemUsesLightTheme -Value 0 -Type Dword -Force +# Script entry point +if ($Help) { + Show-Help + exit 0 } -# Restart explorer so the rest of the settings take effect: -Stop-Process -f -ProcessName explorer -Start-Process explorer.exe - -# Function to disable the Windows key -function Disable-WindowsKey { - $regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Keyboard Layout" - $regName = "Scancode Map" - - # Binary data to remap the Windows key to F24 (an unused key) - $binaryValue = [byte[]]( - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, - 0x3A, 0x00, 0x5B, 0xE0, - 0x00, 0x00, 0x00, 0x00 - ) - - # Create the registry key if it doesn't exist - if (-not (Test-RegistryKeyExists $regPath)) { - New-Item -Path $regPath -Force | Out-Null - } - - # Set the Scancode Map value if it doesn't exist - if (-not (Test-RegistryPropertyExists $regPath $regName)) { - Set-ItemProperty -Path $regPath -Name $regName -Value $binaryValue +# Run bootstrap +try { + $result = Start-Bootstrap -Profile $Profile -Force:$Force -Ask:$Ask + if (-not $result) { + exit 1 } - - Write-Output "Windows key has been disabled from opening the start menu. Please restart your computer for the changes to take effect." +} catch { + Write-Error "Bootstrap failed: $_" + Write-Log "Bootstrap failed: $_" "ERROR" + exit 1 } - -#Disable-WindowsKey - -Write-Host "Bootstrap script completed." -Write-Host "Please Restart." - -# Clean up Bootstrap.log -Write-Host "Clean up Bootstrap.log" -Stop-Transcript -$logSuppress = Get-Content $Env:USERPROFILE\Logs\Bootstrap.log | Where-Object { $_ -notmatch "Host Application: powershell.exe" } -$logSuppress | Set-Content $Env:USERPROFILE\Logs\Bootstrap.log -Force -- cgit v1.2.3