power-git.psm1

#requires -Version 5.1
# power-git.psm1 - Module entry point
# A faster, richer alternative to posh-git for Windows PowerShell 5.1+

# ──────────────────────────────────────────────────────────────────────────────
# Prerequisite check
# ──────────────────────────────────────────────────────────────────────────────

if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
    Write-Warning 'power-git: git executable not found in PATH. Module functionality will be unavailable.'
    return
}

# ──────────────────────────────────────────────────────────────────────────────
# Load source files
# ──────────────────────────────────────────────────────────────────────────────

$srcDir = Join-Path $PSScriptRoot 'src'

foreach ($file in @('Settings.ps1', 'GitUtils.ps1', 'GitStatus.ps1', 'GitPrompt.ps1', 'GitCompletion.ps1')) {
    $path = Join-Path $srcDir $file
    if (Test-Path $path) {
        . $path
    } else {
        Write-Warning "power-git: Missing source file: $path"
    }
}

# ──────────────────────────────────────────────────────────────────────────────
# Initialize global settings
# ──────────────────────────────────────────────────────────────────────────────

if (-not (Get-Variable -Name PowerGitSettings -Scope Global -ErrorAction SilentlyContinue)) {
    $global:PowerGitSettings = New-PowerGitSettings
}

# ──────────────────────────────────────────────────────────────────────────────
# Prompt integration
# ──────────────────────────────────────────────────────────────────────────────

# Capture and save the user's existing prompt function
if (-not (Get-Variable -Name __OriginalPrompt -Scope Global -ErrorAction SilentlyContinue)) {
    $global:__OriginalPrompt = (Get-Item Function:\prompt).ScriptBlock
}

# Install the enhanced prompt
function global:prompt {
    $origPrompt = & $global:__OriginalPrompt
    $settings   = $global:PowerGitSettings

    if (-not $settings -or -not $settings.EnablePrompt) { return $origPrompt }

    # Split the original prompt into everything before the trailing '>' and the '> ' itself.
    # e.g. "PS C:\foo> " -> prefix="PS C:\foo", suffix="> "
    # The greedy .* backtracks so that >+\s* always captures the last '>' group.
    if ($origPrompt -match '^(.*)(>+\s*)$') {
        $promptPrefix = $Matches[1]
        $promptSuffix = $Matches[2]
    } else {
        $promptPrefix = $origPrompt
        $promptSuffix = ''
    }

    try {
        $status = Get-GitStatus
        if (-not $status) { return $origPrompt }

        # Update window title
        try {
            $repoName = Split-Path $status.WorkingDir -Leaf
            $Host.UI.RawUI.WindowTitle = "$((Get-Location).Path) [$repoName/$($status.Branch.Name)]"
        } catch {}

        if ($settings.WriterMode -ne 'Legacy') {
            # ANSI mode: build the full string and return it
            $gitStr = Write-GitPrompt -Status $status -Settings $settings
            return "${promptPrefix}${gitStr}${promptSuffix}"
        } else {
            # Legacy mode: write prefix and git info via Write-Host, return only the suffix
            # so '>' is always the last printed character
            Write-Host -NoNewline $promptPrefix
            Write-GitPrompt -Status $status -Settings $settings
            return $promptSuffix
        }
    } catch {
        return $origPrompt
    }
}

# ──────────────────────────────────────────────────────────────────────────────
# ANSI virtual terminal processing (Windows 10 classic console)
# ──────────────────────────────────────────────────────────────────────────────

function Enable-AnsiProcessing {
    # Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) on the stdout handle.
    # Required for ANSI colours in the classic Windows console host on Win10.
    # Silently ignored on non-Windows or if already enabled.
    try {
        if (-not ([System.Management.Automation.PSTypeName]'PwrGit.Win32').Type) {
            Add-Type -Namespace PwrGit -Name Win32 -MemberDefinition @'
[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int h);
[DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out int m);
[DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, int m);
'@

        }
        $hOut = [PwrGit.Win32]::GetStdHandle(-11)  # STD_OUTPUT_HANDLE
        $mode = 0
        $null = [PwrGit.Win32]::GetConsoleMode($hOut, [ref]$mode)
        $null = [PwrGit.Win32]::SetConsoleMode($hOut, ($mode -bor 0x0004))
    } catch { <# Non-critical; fall back to Legacy mode #> }
}

# Try to enable ANSI. If it fails (e.g. console doesn't support it) switch to Legacy.
if ($global:PowerGitSettings -and $global:PowerGitSettings.WriterMode -ne 'Legacy') {
    # Only attempt on Windows PS 5.1 ConsoleHost without a known good ANSI terminal
    if ($Host.Name -eq 'ConsoleHost' -and -not $env:WT_SESSION -and -not $env:TERM_PROGRAM) {
        Enable-AnsiProcessing
        # Quick smoke-test: if ANSI doesn't render, fall back
        # (We trust Win10 >= 1511 supports VT after enabling; no test needed)
    }
}

# ──────────────────────────────────────────────────────────────────────────────
# Tab completion
# ──────────────────────────────────────────────────────────────────────────────

Register-GitCompletion

# ──────────────────────────────────────────────────────────────────────────────
# Convenience functions exposed to users
# ──────────────────────────────────────────────────────────────────────────────

function Set-GitTheme {
    <#
    .SYNOPSIS
        Apply a built-in power-git prompt theme.
    .DESCRIPTION
        Applies one of the built-in themes to $global:PowerGitSettings.
        Available themes: Default, Minimal, ASCII, Powerline
    .PARAMETER Name
        Name of the theme to apply.
    .EXAMPLE
        Set-GitTheme Powerline
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Default','Minimal','ASCII','Powerline')]
        [string]$Name
    )
    if (-not $global:PowerGitSettings) {
        $global:PowerGitSettings = New-PowerGitSettings -Theme $Name
    } else {
        $global:PowerGitSettings.ApplyTheme($Name)
    }
    Write-Verbose "power-git: Applied theme '$Name'."
}

function Enable-GitPrompt {
    <#
    .SYNOPSIS
        Enable the power-git prompt integration.
    #>

    [CmdletBinding()]
    param()
    if ($global:PowerGitSettings) { $global:PowerGitSettings.EnablePrompt = $true }
}

function Disable-GitPrompt {
    <#
    .SYNOPSIS
        Disable the power-git prompt integration (reverts to original prompt).
    #>

    [CmdletBinding()]
    param()
    if ($global:PowerGitSettings) { $global:PowerGitSettings.EnablePrompt = $false }
}

function Enable-GitFileStatus {
    <#
    .SYNOPSIS
        Enable file-level status counts in the prompt for the current repo.
    #>

    [CmdletBinding()]
    param()
    if ($global:PowerGitSettings) {
        $root = Get-GitRoot
        if ($root) { $global:PowerGitSettings.DisableFileStatusRepos.Remove($root) | Out-Null }
        $global:PowerGitSettings.ShowFileStatus = $true
    }
}

function Disable-GitFileStatus {
    <#
    .SYNOPSIS
        Disable file-level status counts for the current repo (useful for large monorepos).
    #>

    [CmdletBinding()]
    param()
    if ($global:PowerGitSettings) {
        $root = Get-GitRoot
        if ($root) {
            $global:PowerGitSettings.DisableFileStatusRepos.Add($root) | Out-Null
            Write-Host "power-git: File status disabled for '$root'."
        } else {
            $global:PowerGitSettings.ShowFileStatus = $false
        }
    }
}

function Test-PowerGit {
    <#
    .SYNOPSIS
        Diagnose power-git installation and show what is happening in the current directory.
    #>

    [CmdletBinding()]
    param()

    Write-Host "power-git diagnostic" -ForegroundColor Cyan
    Write-Host "----------------------------------------"

    # Git availability
    $gitCmd = Get-Command git -ErrorAction SilentlyContinue
    $gitPath = if ($gitCmd) { $gitCmd.Source } else { $null }
    Write-Host "git executable : $(if ($gitPath) { $gitPath } else { 'NOT FOUND' })"

    # Settings
    if ($global:PowerGitSettings) {
        Write-Host "Settings : loaded (WriterMode=$($global:PowerGitSettings.WriterMode), EnablePrompt=$($global:PowerGitSettings.EnablePrompt))"
    } else {
        Write-Host "Settings : NOT initialized (run: `$global:PowerGitSettings = New-PowerGitSettings)" -ForegroundColor Red
    }

    # Prompt installed
    $promptSrc = (Get-Item Function:\prompt).ScriptBlock.ToString()
    $installed = $promptSrc -match 'Get-GitStatus'
    Write-Host "Prompt hook : $(if ($installed) { 'installed' } else { 'NOT installed' })"

    # Repo detection
    $status = Get-GitStatus -ErrorAction SilentlyContinue
    if ($status) {
        Write-Host "In git repo : YES"
        Write-Host " Branch : $($status.Branch.Name)$(if ($status.Branch.IsDetached) { ' (detached)' })"
        Write-Host " Upstream : $(if ($status.Branch.Upstream) { $status.Branch.Upstream } else { '(none)' })"
        Write-Host " Ahead/Behind : +$($status.Branch.AheadBy) / -$($status.Branch.BehindBy)"
        Write-Host " Staged : A$($status.Index.Added) M$($status.Index.Modified) D$($status.Index.Deleted)"
        Write-Host " Working : M$($status.Working.Modified) D$($status.Working.Deleted) ?$($status.Working.Untracked)"
        Write-Host " State : $(if ($status.RepoState.State) { $status.RepoState.State } else { 'clean' })"
        Write-Host " StashCount : $($status.StashCount)"
    } else {
        Write-Host "In git repo : NO (not in a git repository)"
    }

    # Prompt string preview
    $str = Get-GitPromptString -ErrorAction SilentlyContinue
    if ($str) {
        Write-Host "Prompt string : '$str' (length=$($str.Length))"
    } else {
        Write-Host "Prompt string : (empty - check above for issues)" -ForegroundColor Yellow
    }

    # ANSI support
    Write-Host "WT_SESSION : $(if ($env:WT_SESSION) { 'set (Windows Terminal)' } else { 'not set' })"
    Write-Host "TERM_PROGRAM : $(if ($env:TERM_PROGRAM) { $env:TERM_PROGRAM } else { 'not set' })"
    Write-Host "ConsoleHost : $($Host.Name)"
}

function Add-PowerGitToProfile {
    <#
    .SYNOPSIS
        Append the power-git import line to your PowerShell profile.
    .PARAMETER AllHosts
        Add to the profile shared by all hosts (default: current host profile).
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param([switch]$AllHosts)

    $profilePath = if ($AllHosts) { $PROFILE.CurrentUserAllHosts } else { $PROFILE }
    $importLine  = 'Import-Module power-git'

    if (-not (Test-Path $profilePath)) {
        if ($PSCmdlet.ShouldProcess($profilePath, 'Create profile and add power-git import')) {
            $null = New-Item -ItemType File -Path $profilePath -Force
            Add-Content -Path $profilePath -Value $importLine
        }
        return
    }

    $content = Get-Content $profilePath -Raw
    if ($content -match 'power-git') {
        Write-Host 'power-git: Profile already contains power-git import.'
        return
    }

    if ($PSCmdlet.ShouldProcess($profilePath, 'Add power-git import')) {
        Add-Content -Path $profilePath -Value "`n$importLine"
        Write-Host "power-git: Added import to '$profilePath'."
    }
}

function Remove-PowerGitFromProfile {
    <#
    .SYNOPSIS
        Remove the power-git import line from your PowerShell profile.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param([switch]$AllHosts)

    $profilePath = if ($AllHosts) { $PROFILE.CurrentUserAllHosts } else { $PROFILE }

    if (-not (Test-Path $profilePath)) {
        Write-Host 'power-git: Profile not found.'
        return
    }

    $content = Get-Content $profilePath
    $newContent = $content | Where-Object { $_ -notmatch '^\s*Import-Module\s+power-git\s*$' }

    if ($content.Count -ne $newContent.Count) {
        if ($PSCmdlet.ShouldProcess($profilePath, 'Remove power-git import')) {
            Set-Content -Path $profilePath -Value $newContent
            Write-Host "power-git: Removed import from '$profilePath'."
        }
    } else {
        Write-Host 'power-git: No power-git import found in profile.'
    }
}

function Show-PowerGitHelp {
    <#
    .SYNOPSIS
        Show a quick-reference guide for the power-git prompt symbols and commands.
    #>

    [CmdletBinding()]
    param()

    $theme = $global:PowerGitSettings
    if (-not $theme) {
        Write-Warning 'power-git: Settings not initialized. Run: $global:PowerGitSettings = New-PowerGitSettings'
        return
    }
    $s = $theme.Symbols

    Write-Host ''
    Write-Host ' power-git quick reference' -ForegroundColor Cyan
    Write-Host ' ----------------------------------------------------------' -ForegroundColor DarkGray

    # Prompt anatomy
    $bt = $theme.BeforeText
    $at = $theme.AfterText
    Write-Host ''
    Write-Host ' PROMPT ANATOMY' -ForegroundColor Yellow
    Write-Host " PS C:\repo${bt}$($s.Branch) main $($s.Ahead)2 |$($s.Staged)+1 $($s.Staged)~3 $($s.Stash)1${at}>" -ForegroundColor White
    Write-Host ' |__________________________________|'
    Write-Host ' git segment (only shown inside a git repo)'

    # Branch & upstream
    Write-Host ''
    Write-Host ' BRANCH & UPSTREAM' -ForegroundColor Yellow
    Write-Host " $($s.Branch) branch name - current branch (Powerline glyph; requires Nerd Font)" -ForegroundColor White
    Write-Host " $($s.Detached) detached HEAD - short commit hash shown instead of branch name" -ForegroundColor White
    Write-Host " $($s.UpToDate) up to date - in sync with upstream" -ForegroundColor White
    Write-Host " $($s.Ahead)N ahead - N commits ahead of upstream (not yet pushed)" -ForegroundColor White
    Write-Host " $($s.Behind)N behind - N commits behind upstream (need to pull)" -ForegroundColor White
    Write-Host " $($s.Diverged) +N -M diverged - N ahead and M behind simultaneously" -ForegroundColor White
    Write-Host " $($s.Gone) upstream gone - tracking branch was deleted on the remote" -ForegroundColor White

    # Index
    Write-Host ''
    Write-Host ' INDEX - staged changes (shown left of |)' -ForegroundColor Yellow
    Write-Host " $($s.Staged)N added - N new files staged" -ForegroundColor White
    Write-Host " $($s.Staged)~N modified - N modified files staged" -ForegroundColor White
    Write-Host " $($s.Staged)-N deleted - N deleted files staged" -ForegroundColor White
    Write-Host " $($s.Staged)RN renamed - N renames staged" -ForegroundColor White

    # Working dir
    Write-Host ''
    Write-Host ' WORKING DIR - unstaged changes (shown right of |)' -ForegroundColor Yellow
    Write-Host " $($s.Unstaged)N modified - N files modified but not staged" -ForegroundColor White
    Write-Host " $($s.Unstaged)-N deleted - N files deleted but not staged" -ForegroundColor White
    Write-Host " $($s.Untracked)N untracked - N untracked files" -ForegroundColor White
    Write-Host " $($s.Conflict)N conflicts - N merge conflicts (shown in red)" -ForegroundColor White

    # Repo state
    Write-Host ''
    Write-Host ' REPO STATE & STASH' -ForegroundColor Yellow
    Write-Host " $($s.Merging) - a merge is in progress" -ForegroundColor White
    Write-Host " $($s.Rebasing) N/M - rebase in progress, step N of M" -ForegroundColor White
    Write-Host " $($s.CherryPicking) - cherry-pick in progress" -ForegroundColor White
    Write-Host " $($s.Reverting) - revert in progress" -ForegroundColor White
    Write-Host " $($s.Bisecting) - bisect session active" -ForegroundColor White
    Write-Host " $($s.Stash)N stash - N stashed changesets" -ForegroundColor White

    # Commands
    Write-Host ''
    Write-Host ' COMMANDS' -ForegroundColor Yellow
    Write-Host ' Test-PowerGit - diagnose installation / show current repo info' -ForegroundColor White
    Write-Host ' Show-PowerGitHelp - this help screen' -ForegroundColor White
    Write-Host ''
    Write-Host ' Set-GitTheme <name> - Default | Minimal | ASCII | Powerline' -ForegroundColor White
    Write-Host ' Enable-GitPrompt - turn prompt integration on' -ForegroundColor White
    Write-Host ' Disable-GitPrompt - turn prompt integration off' -ForegroundColor White
    Write-Host ''
    Write-Host ' Enable-GitFileStatus - show file counts in prompt (default)' -ForegroundColor White
    Write-Host ' Disable-GitFileStatus - hide file counts for current repo (large monorepos)' -ForegroundColor White
    Write-Host ''
    Write-Host ' Install-PowerGitFont - download & install a Nerd Font (for Powerline theme)' -ForegroundColor White
    Write-Host ' Add-PowerGitToProfile - auto-load power-git in every new session' -ForegroundColor White
    Write-Host ' Remove-PowerGitFromProfile - undo the above' -ForegroundColor White
    Write-Host ''
    Write-Host ' $global:PowerGitSettings - live config object; edit properties directly, e.g.:' -ForegroundColor White
    Write-Host ' .StatusCacheMs = 500 - prompt cache lifetime in ms (0 = no cache)' -ForegroundColor DarkGray
    Write-Host ' .BranchMaxLength = 20 - truncate long branch names (0 = off)' -ForegroundColor DarkGray
    Write-Host ' .ShowStashCount = $true - show/hide the stash indicator' -ForegroundColor DarkGray

    # Current settings
    Write-Host ''
    Write-Host ' CURRENT SETTINGS' -ForegroundColor Yellow
    Write-Host " WriterMode=$($theme.WriterMode) EnablePrompt=$($theme.EnablePrompt) ShowFileStatus=$($theme.ShowFileStatus)" -ForegroundColor White
    Write-Host " StatusCacheMs=$($theme.StatusCacheMs) BranchMaxLength=$($theme.BranchMaxLength) ShowStashCount=$($theme.ShowStashCount)" -ForegroundColor White
    Write-Host ''
}

function Install-PowerGitFont {
    <#
    .SYNOPSIS
        Download and install a Nerd Font so the Powerline theme renders correctly.
    .DESCRIPTION
        Downloads the chosen Nerd Font from the ryanoasis/nerd-fonts GitHub releases
        and installs it for the current user (no administrator rights required).
        After installation, set the font in your terminal application manually.
    .PARAMETER Font
        Which Nerd Font to install. Default: CascadiaCode (recommended for Windows).
        Other options: FiraCode, JetBrainsMono
    .EXAMPLE
        Install-PowerGitFont
    .EXAMPLE
        Install-PowerGitFont -Font FiraCode
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [ValidateSet('CascadiaCode', 'FiraCode', 'JetBrainsMono')]
        [string]$Font = 'CascadiaCode'
    )

    $userFontsDir = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\Fonts'
    if (-not (Test-Path $userFontsDir)) {
        $null = New-Item -ItemType Directory -Path $userFontsDir -Force
    }

    # Fetch latest release metadata
    Write-Host "Fetching latest nerd-fonts release info..." -ForegroundColor Cyan
    try {
        $apiUrl  = 'https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest'
        $release = Invoke-RestMethod -Uri $apiUrl
        $asset   = $release.assets | Where-Object { $_.name -eq "$Font.zip" } | Select-Object -First 1
        if (-not $asset) {
            Write-Error "Could not find '$Font.zip' in the latest nerd-fonts release."
            return
        }
    } catch {
        Write-Error "Failed to fetch release info: $_"
        return
    }

    $sizeMb   = [math]::Round($asset.size / 1MB, 1)
    $zipPath  = Join-Path $env:TEMP "$Font-NerdFont.zip"
    $unzipDir = Join-Path $env:TEMP "$Font-NerdFont"

    Write-Host "Downloading $Font Nerd Font (~${sizeMb} MB)..." -ForegroundColor Cyan
    if (-not $PSCmdlet.ShouldProcess($asset.browser_download_url, 'Download and install font')) { return }

    try {
        Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath -UseBasicParsing
    } catch {
        Write-Error "Download failed: $_"
        return
    }

    # Extract
    if (Test-Path $unzipDir) { Remove-Item $unzipDir -Recurse -Force }
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $unzipDir)

    # Install TTF files and register in the user font registry key
    $regPath  = 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts'
    $installed = 0

    Get-ChildItem $unzipDir -Filter '*.ttf' | ForEach-Object {
        $dest = Join-Path $userFontsDir $_.Name
        Copy-Item $_.FullName $dest -Force
        $displayName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
        Set-ItemProperty -Path $regPath -Name "$displayName (TrueType)" -Value $dest -ErrorAction SilentlyContinue
        $installed++
    }

    # Cleanup temp files
    Remove-Item $zipPath    -Force -ErrorAction SilentlyContinue
    Remove-Item $unzipDir   -Recurse -Force -ErrorAction SilentlyContinue

    Write-Host "Installed $installed font file(s) to '$userFontsDir'." -ForegroundColor Green
    Write-Host ''
    Write-Host 'Next step, set the font in your terminal (restart the terminal first):' -ForegroundColor Yellow
    Write-Host " Windows Terminal : Settings > Profiles > Appearance > Font face -> '$Font NF'" -ForegroundColor White
    Write-Host " Classic console : Right-click title bar > Properties > Font" -ForegroundColor White
    Write-Host " VS Code terminal : settings.json -> terminal.integrated.fontFamily: '$Font NF'" -ForegroundColor White
}

# ──────────────────────────────────────────────────────────────────────────────
# Module cleanup on removal
# ──────────────────────────────────────────────────────────────────────────────

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    # Restore original prompt
    if (Get-Variable -Name __OriginalPrompt -Scope Global -ErrorAction SilentlyContinue) {
        Set-Item Function:\prompt $global:__OriginalPrompt
        Remove-Variable -Name __OriginalPrompt -Scope Global -Force -ErrorAction SilentlyContinue
    }
    # Clear settings
    Remove-Variable -Name PowerGitSettings -Scope Global -Force -ErrorAction SilentlyContinue
}

# ──────────────────────────────────────────────────────────────────────────────
# Exports
# ──────────────────────────────────────────────────────────────────────────────

Export-ModuleMember -Function @(
    # Diagnostics / help
    'Test-PowerGit'
    'Show-PowerGitHelp'
    # Status
    'Get-GitStatus'
    'Clear-GitStatusCache'
    # Prompt
    'Write-GitPrompt'
    'Get-GitPromptString'
    # Settings
    'New-PowerGitSettings'
    'Set-GitTheme'
    'Enable-GitPrompt'
    'Disable-GitPrompt'
    'Enable-GitFileStatus'
    'Disable-GitFileStatus'
    # Profile / setup
    'Add-PowerGitToProfile'
    'Remove-PowerGitFromProfile'
    'Install-PowerGitFont'
    # Utilities (re-export for users)
    'Get-GitDirectory'
    'Get-GitRoot'
    'Test-GitRepository'
    'Get-GitLocalBranches'
    'Get-GitRemoteBranches'
    'Get-GitTags'
    'Get-GitRemotes'
    'Get-GitStashes'
    'Get-GitBranchName'
    'Get-GitAliases'
    # Completion
    'Register-GitCompletion'
    'Unregister-GitCompletion'
) -Variable @('PowerGitSettings')