# ------------------------------------------------------------------- # PowerShell Profile for Windows PowerShell 5.1 # ------------------------------------------------------------------- # This profile customizes the PowerShell experience with best practices: # - Leaves the current directory unchanged in normal sessions # - Redirects away from C:\Windows\System32 to the user's home directory # - Provides a clean, colored prompt indicating elevation status # - Includes useful functions with proper help and parameter validation # - Loads Chocolatey tab completion if available # ------------------------------------------------------------------- # If PowerShell starts in C:\Windows\System32, move to the user's home directory $currentPath = (Get-Location).ProviderPath $system32Path = (Join-Path $env:WINDIR 'System32') if ($currentPath -ieq $system32Path) { Set-Location -Path $HOME } # Detect administrative privileges (calculated once for efficiency) $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(` [Security.Principal.WindowsBuiltInRole]::Administrator) # Custom prompt function function Prompt { # Set prompt prefix color: Red for admin, Green for normal user $color = if ($IsAdmin) { 'Red' } else { 'Green' } Write-Host 'PS' -NoNewline -ForegroundColor $color Write-Host " $((Get-Location).Path)" -NoNewline -ForegroundColor Cyan Write-Host '>' -NoNewline -ForegroundColor $color # Important: Return a string (with a trailing space) to complete the prompt return ' ' } # ------------------------------------------------------------------- # Function: Get-FolderSize # ------------------------------------------------------------------- <# .SYNOPSIS Calculates the total size of files in a directory and its subdirectories. .DESCRIPTION Recursively enumerates files under the specified path, sums their sizes, and returns a custom object with file count, total size in bytes, and formatted size in MB/GB (auto-scaled for readability). .PARAMETER Path The directory path to measure. Defaults to the current location. .EXAMPLE Get-FolderSize -Path "C:\Users\$env:USERNAME\Documents" .EXAMPLE Get-FolderSize | Format-Table #> function Get-FolderSize { [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [ValidateNotNullOrEmpty()] [string]$Path = (Get-Location).Path ) $files = Get-ChildItem -Path $Path -Recurse -File -ErrorAction SilentlyContinue if (-not $files) { Write-Warning "No files found in '$Path' or access denied to some subfolders." return } $measure = $files | Measure-Object -Property Length -Sum $totalBytes = $measure.Sum # Auto-scale to MB or GB if ($totalBytes -ge 1GB) { $formattedSize = '{0:N2} GB' -f ($totalBytes / 1GB) } elseif ($totalBytes -ge 1MB) { $formattedSize = '{0:N2} MB' -f ($totalBytes / 1MB) } else { $formattedSize = '{0:N2} KB' -f ($totalBytes / 1KB) } [PSCustomObject]@{ Path = (Resolve-Path -Path $Path).Path FileCount = $measure.Count TotalBytes = $totalBytes Size = $formattedSize } } # ------------------------------------------------------------------- # Chocolatey Tab Completion # ------------------------------------------------------------------- # Load Chocolatey profile for tab completion if installed $ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" if (Test-Path -Path $ChocolateyProfile) { Import-Module -Name $ChocolateyProfile } # ------------------------------------------------------------------- # Function: Remove-PSReadLineHistoryEntry # ------------------------------------------------------------------- <# .SYNOPSIS Deletes entries from the PSReadLine persistent history file by line number. .DESCRIPTION Removes one or more specific lines from the file at (Get-PSReadLineOption).HistorySavePath. This is the same numbering you see in `rh`, which shows the 1-based line number in the PSReadLine history file (not the session history Id used by Get-History/Clear-History). .PARAMETER LineNumber One or more 1-based line numbers to delete. .PARAMETER PassThru If set, outputs objects describing what was deleted. .EXAMPLE Remove-PSReadLineHistoryEntry 1947 .EXAMPLE 1947,1952 | Remove-PSReadLineHistoryEntry -PassThru #> function Remove-PSReadLineHistoryEntry { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateRange(1, [int]::MaxValue)] [int[]]$LineNumber, [switch]$PassThru ) $historyPath = (Get-PSReadLineOption).HistorySavePath if (-not (Test-Path -Path $historyPath)) { Write-Error "History file not found at '$historyPath'." return } $targets = $LineNumber | Sort-Object -Unique $lines = [System.IO.File]::ReadAllLines($historyPath) $max = $lines.Count $bad = $targets | Where-Object { $_ -lt 1 -or $_ -gt $max } if ($bad) { Write-Warning ("Ignoring out-of-range line number(s): {0}. Valid range is 1..{1}." -f ($bad -join ', '), $max) $targets = $targets | Where-Object { $_ -ge 1 -and $_ -le $max } if (-not $targets) { return } } $toDelete = foreach ($n in $targets) { [PSCustomObject]@{ LineNumber = $n Command = $lines[$n - 1] } } # Always write a backup alongside the history file. $stamp = Get-Date -Format "yyyyMMdd-HHmmss" $backup = "$historyPath.bak.$stamp" Copy-Item -Path $historyPath -Destination $backup -Force if (-not $PSCmdlet.ShouldProcess($historyPath, "Delete PSReadLine history line(s): $($targets -join ', ')")) { return } # Remove from bottom-up to keep indexes stable. $list = New-Object 'System.Collections.Generic.List[string]' (, $lines) foreach ($n in ($targets | Sort-Object -Descending)) { $list.RemoveAt($n - 1) } [System.IO.File]::WriteAllLines($historyPath, $list, [System.Text.UTF8Encoding]::new($false)) # Best-effort: refresh in-memory history for this session so UpArrow / Ctrl+R reflect the deletion. try { [Microsoft.PowerShell.PSConsoleReadLine]::ClearHistory() foreach ($l in $list) { if ($l -and $l.Trim()) { [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($l) } } } catch { # If PSReadLine isn't available for some reason, just leave it. } if ($PassThru) { return $toDelete } foreach ($d in $toDelete) { Write-Host ("Deleted {0}: {1}" -f $d.LineNumber, $d.Command) } Write-Host ("Backup written to: {0}" -f $backup) } # ------------------------------------------------------------------- # Function: Invoke-HistoryCommand # ------------------------------------------------------------------- <# .SYNOPSIS Searches PowerShell history for unique commands matching a term and re-runs a selected one. .DESCRIPTION Displays unique commands from history that start with the optional search term, numbered for selection. At the prompt you can: - Enter a number to re-run the command - Enter `d ` (or `del `) to delete that history line .PARAMETER SearchTerm Optional prefix to filter history commands. .PARAMETER Delete Deletes one or more PSReadLine history lines by number (the same numbers shown by `rh`). .EXAMPLE rh clear .EXAMPLE rh -Delete 1947 #> function Invoke-HistoryCommand { [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [string]$SearchTerm = '', [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int[]]$Delete ) if ($Delete) { Remove-PSReadLineHistoryEntry -LineNumber $Delete return } # Ensure PSReadLine is available (built-in for PS 5.1) $historyPath = (Get-PSReadLineOption).HistorySavePath if (-not (Test-Path -Path $historyPath)) { Write-Error "History file not found at '$historyPath'." return } $historyLines = Get-Content -Path $historyPath # Filter and collect unique commands (preserving most recent occurrence) $unique = [ordered]@{} for ($i = $historyLines.Count - 1; $i -ge 0; $i--) { $line = $historyLines[$i].Trim() if ($line -and ($SearchTerm -eq '' -or $line -like "$SearchTerm*") -and -not $unique.Contains($line)) { $unique[$line] = $i + 1 # 1-based line number for display } } if ($unique.Count -eq 0) { Write-Host "No matching commands found in history." return } # Display numbered list (most recent first) $unique.GetEnumerator() | ForEach-Object { Write-Host ("{0,4}: {1}" -f $_.Value, $_.Key) } # Prompt for selection $selection = Read-Host -Prompt "Enter number to re-run, or 'd ' to delete (empty to cancel)" if (-not $selection) { return } # Delete mode: d 1947 / del 1947,1950 $deleteMatch = [regex]::Match($selection, '^\s*(d|del|delete)\s+(.+?)\s*$', 'IgnoreCase') if ($deleteMatch.Success) { $raw = $deleteMatch.Groups[2].Value $tokens = $raw -split '[,\s]+' | Where-Object { $_ } $nums = New-Object 'System.Collections.Generic.List[int]' foreach ($t in $tokens) { $n = 0 if ([int]::TryParse($t, [ref]$n) -and $n -ge 1) { [void]$nums.Add($n) } else { Write-Warning "Ignoring invalid line number token: '$t'" } } if ($nums.Count -gt 0) { Remove-PSReadLineHistoryEntry -LineNumber ($nums.ToArray()) } return } # Run mode: just a number $chosenNumber = 0 if (-not [int]::TryParse($selection, [ref]$chosenNumber)) { Write-Warning "Invalid input: '$selection' is not a number (or 'd ')." return } $match = $unique.GetEnumerator() | Where-Object { $_.Value -eq $chosenNumber } | Select-Object -First 1 if (-not $match) { Write-Warning "No command found for number '$chosenNumber'." return } $chosenLine = $match.Key Write-Host "Executing: $chosenLine" -ForegroundColor Yellow Invoke-Expression -Command $chosenLine } # ------------------------------------------------------------------- # Convenience Functions # ------------------------------------------------------------------- <# .SYNOPSIS Opens the current PowerShell profile in Notepad++ when available. #> function Edit-Profile { [CmdletBinding()] param() if (-not (Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force | Out-Null } $editor = Get-Command notepad++ -ErrorAction SilentlyContinue if ($editor) { & $editor.Source $PROFILE } else { notepad $PROFILE } } <# .SYNOPSIS Creates a directory if needed, then enters it. .PARAMETER Path Directory path to create and enter. #> function New-AndEnterDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string]$Path ) if (-not (Test-Path -Path $Path)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null } Set-Location -Path $Path } <# .SYNOPSIS Changes to the dedicated Codex workspace. #> function Enter-CodexWorkspace { [CmdletBinding()] param() Set-Location -Path 'C:\Users\J2897\Codex' } <# .SYNOPSIS Launches Codex in the dedicated workspace. #> function Start-CodexWorkspace { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & codex --cd 'C:\Users\J2897\Codex' @Arguments } <# .SYNOPSIS Launches Codex in the dedicated workspace with live web search enabled. #> function Start-CodexWorkspaceSearch { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & codex --cd 'C:\Users\J2897\Codex' --search @Arguments } <# .SYNOPSIS Resumes the most recent Codex session for the dedicated workspace. #> function Resume-CodexWorkspace { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) Push-Location 'C:\Users\J2897\Codex' try { & codex resume --last @Arguments } finally { Pop-Location } } <# .SYNOPSIS Opens the user-level Codex configuration file in Notepad++ when available. #> function Edit-CodexConfig { [CmdletBinding()] param() $configPath = Join-Path $HOME '.codex\config.toml' $configDir = Split-Path -Parent $configPath if (-not (Test-Path -Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null } if (-not (Test-Path -Path $configPath)) { New-Item -ItemType File -Path $configPath -Force | Out-Null } $editor = Get-Command notepad++ -ErrorAction SilentlyContinue if ($editor) { & $editor.Source $configPath } else { notepad $configPath } } <# .SYNOPSIS Shows concise Git status for the current repository. #> function Get-GitShortStatus { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git status --short --branch @Arguments } <# .SYNOPSIS Stages files for commit. #> function Add-GitItem { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git add @Arguments } <# .SYNOPSIS Shows the current Git diff. #> function Get-GitDiff { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git diff @Arguments } <# .SYNOPSIS Shows the staged Git diff. #> function Get-GitCachedDiff { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git diff --cached @Arguments } <# .SYNOPSIS Creates a Git commit. #> function Invoke-GitCommit { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git commit @Arguments } <# .SYNOPSIS Shows a compact decorated Git log for the current repository. #> function Get-GitOneLineLog { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git log --oneline --graph --decorate -12 @Arguments } <# .SYNOPSIS Shows a commit or object with summary statistics. #> function Get-GitShow { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git show --stat @Arguments } <# .SYNOPSIS Shows local branches with verbose tracking information. #> function Get-GitBranchList { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git branch -vv @Arguments } <# .SYNOPSIS Switches branches. #> function Switch-GitBranch { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git switch @Arguments } <# .SYNOPSIS Creates and switches to a new branch. #> function New-GitBranchSwitch { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git switch -c @Arguments } <# .SYNOPSIS Restores files in the working tree or index. #> function Restore-GitItem { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git restore @Arguments } <# .SYNOPSIS Shows line-by-line authorship for a file. #> function Get-GitBlame { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git blame @Arguments } <# .SYNOPSIS Lists tracked files. #> function Get-GitTrackedFiles { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git ls-files @Arguments } <# .SYNOPSIS Fetches updates from the remote. #> function Invoke-GitFetch { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git fetch @Arguments } <# .SYNOPSIS Pulls updates with fast-forward only. #> function Invoke-GitPull { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git pull --ff-only @Arguments } <# .SYNOPSIS Pushes commits to the remote. #> function Invoke-GitPush { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) & git push @Arguments } <# .SYNOPSIS Shows a short reference for custom Git aliases. #> function Show-GitAliasReference { [CmdletBinding()] param() @( 'ga git add' 'gst git status --short --branch' 'gd git diff' 'gdc git diff --cached' 'glg git log --oneline --graph --decorate -12' 'gcmt git commit' 'gsh git show --stat' 'gbr git branch -vv' 'gsw git switch' 'gcb git switch -c' 'grs git restore' 'gbl git blame' 'gls git ls-files' 'gft git fetch' 'gpl git pull --ff-only' 'gpu git push' ) | ForEach-Object { Write-Host $_ } } # Aliases for convenience ### History and profile Set-Alias -Name rh -Value Invoke-HistoryCommand -Description "Search unique history and re-run selected command" Set-Alias -Name rhd -Value Remove-PSReadLineHistoryEntry -Description "Delete PSReadLine history by line number" Set-Alias -Name eprof -Value Edit-Profile -Description "Open the current PowerShell profile" Set-Alias -Name mkcd -Value New-AndEnterDirectory -Description "Create a directory and enter it" ### Codex Set-Alias -Name croot -Value Enter-CodexWorkspace -Description "Change to C:\Users\J2897\Codex" Set-Alias -Name cdx -Value Start-CodexWorkspace -Description "Launch Codex in C:\Users\J2897\Codex" Set-Alias -Name cdxs -Value Start-CodexWorkspaceSearch -Description "Launch Codex in C:\Users\J2897\Codex with live web search" Set-Alias -Name cdxr -Value Resume-CodexWorkspace -Description "Resume the latest Codex session in C:\Users\J2897\Codex" Set-Alias -Name ecodex -Value Edit-CodexConfig -Description "Open ~/.codex/config.toml" ### Git Set-Alias -Name ga -Value Add-GitItem -Description "Stage files with Git add" Set-Alias -Name gst -Value Get-GitShortStatus -Description "Show short Git status with branch" Set-Alias -Name gd -Value Get-GitDiff -Description "Show Git diff" Set-Alias -Name gdc -Value Get-GitCachedDiff -Description "Show staged Git diff" Set-Alias -Name glg -Value Get-GitOneLineLog -Description "Show a compact decorated Git log" Set-Alias -Name gcmt -Value Invoke-GitCommit -Description "Create a Git commit" Set-Alias -Name gsh -Value Get-GitShow -Description "Show commit details with stats" Set-Alias -Name gbr -Value Get-GitBranchList -Description "Show branches with tracking info" Set-Alias -Name gsw -Value Switch-GitBranch -Description "Switch branches" Set-Alias -Name gcb -Value New-GitBranchSwitch -Description "Create and switch to a new branch" Set-Alias -Name grs -Value Restore-GitItem -Description "Restore files with Git restore" Set-Alias -Name gbl -Value Get-GitBlame -Description "Show Git blame for a file" Set-Alias -Name gls -Value Get-GitTrackedFiles -Description "List tracked Git files" Set-Alias -Name gft -Value Invoke-GitFetch -Description "Fetch from remote" Set-Alias -Name gpl -Value Invoke-GitPull -Description "Pull with fast-forward only" Set-Alias -Name gpu -Value Invoke-GitPush -Description "Push to remote" Set-Alias -Name gref -Value Show-GitAliasReference -Description "Show custom Git alias reference"