J2897 icon

PowerShell Profile for Windows PowerShell 5.1

J2897 | PRO | 04/03/26 09:36:35 PM UTC (Edited) | 0 ⭐ | 268 👁️ | Never ⏰ | []
PowerShell |

20.72 KB

|

None

|

0 👍

/

0 👎

# -------------------------------------------------------------------
# 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