# -------------------------------------------------------------------
# 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 <number>` (or `del <number>`) 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 <number>' 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 <number>')."
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"
Comments