DirColors.psm1

If ($PSEdition -Eq "Desktop" -Or $IsWindows -Eq $True) {
    $ExecutableSuffixes = $Env:PATHEXT -Split ";"
}

$script:IgnoredDirColorsTokens = ("COLOR", "TERM", "EIGHTBIT")

$script:ESC = [char]27
$script:ws = [char[]]" `t`r"

$script:LSColorsTokensToSchemeProperties = @{
    "no" = "Default";
    "rs" = "Reset";
    "fi" = "File";
    "di" = "Directory";
    "ln" = "Link";
    "mh" = "MultiHardLink";
    "pi" = "Pipe";
    "so" = "Socket";
    "do" = "Door";
    "bd" = "BlockDevice";
    "cd" = "CharacterDevice";
    "or" = "Orphan";
    "mi" = "Missing";
    "su" = "SetUid";
    "sg" = "SetGid";
    "ca" = "Capability";
    "tw" = "StickyOtherWritable";
    "ow" = "OtherWritable";
    "st" = "Sticky";
    "ex" = "Executable";
}

$script:SchemePropertiesToLSColors =  @{
    "Default"             = "no";
    "Reset"               = "rs";
    "File"                = "fi";
    "Directory"           = "di";
    "Link"                = "ln";
    "MultiHardLink"       = "mh";
    "Pipe"                = "pi";
    "Socket"              = "so";
    "Door"                = "do";
    "BlockDevice"         = "bd";
    "CharacterDevice"     = "cd";
    "Orphan"              = "or";
    "Missing"             = "mi";
    "SetUid"              = "su";
    "SetGid"              = "sg";
    "Capability"          = "ca";
    "StickyOtherWritable" = "tw";
    "OtherWritable"       = "ow";
    "Sticky"              = "st";
    "Executable"          = "ex";
}

$script:DirColorsTokensToSchemeProperties = @{
    "NORMAL"                = "Default";
    "RESET"                 = "Reset";
    "FILE"                  = "File";
    "DIR"                   = "Directory";
    "LINK"                  = "Link";
    "MULTIHARDLINK"         = "MultiHardLink";
    "FIFO"                  = "Pipe";
    "SOCK"                  = "Socket";
    "DOOR"                  = "Door";
    "BLK"                   = "BlockDevice";
    "CHR"                   = "CharacterDevice";
    "ORPHAN"                = "Orphan";
    "MISSING"               = "Missing";
    "SETUID"                = "SetUid";
    "SETGID"                = "SetGid";
    "CAPABILITY"            = "Capability";
    "STICKY_OTHER_WRITABLE" = "StickyOtherWritable";
    "OTHER_WRITABLE"        = "OtherWritable";
    "STICKY"                = "Sticky";
    "EXEC"                  = "Executable";
}

Function New-ColorScheme {
    Return [PSCustomObject]@{
        PSTypeName = "ColorScheme";

        Default = "0";
        Reset = "0";
        File = "0";
        Directory = "01;34";
        Link = "01;36";
        MultiHardLink = "0";
        Pipe = "00;33";
        Socket = "01;35";
        Door = "01;35";
        BlockDevice = "01;33";
        CharacterDevice = "01;33";
        Orphan = "01;36";
        Missing = "0";
        SetUid = "37;41";
        SetGid = "30;43";
        Capability = "30;41";
        StickyOtherWritable = "30;42";
        OtherWritable = "34;42";
        Sticky = "37;44";
        Executable = "01;32";

        Extensions = @{};
        Matches = @{};
    }
}

$DefaultColors = New-ColorScheme
$DirColors = $DefaultColors

Function Import-DirColors() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string[]]$Path
    )

    $out = New-ColorScheme

    ForEach ($_ In (Get-Content -Path:$Path -ReadCount 0) -Split "`n") {
        If ([string]::IsNullOrWhitespace($_)) {
            Continue
        }
        $param, $arg = $_.Split($script:ws, 3, [System.StringSplitOptions]::RemoveEmptyEntries)[0, 1]

        If ($param -In $script:IgnoredDirColorsTokens) {
            Continue
        }

        $canon = $script:DirColorsTokensToSchemeProperties[$param.ToUpper()]
        If ($null -Eq $canon) {
            $i = $param.IndexOf('.')
            If ($i -Le 1 -And $i -Eq $param.LastIndexOf('.')) {
                # *.x with no other periods: fast path
                If ($param[0] -Eq '*') {
                    $param = $param.Substring(1)
                }
                $out.Extensions[$param] = $arg
            } Else {
                If (!$param.Contains('*')) {
                    # dircolors enforces a leading * when generating LS_COLORS
                    $param = '*' + $param
                }
                $out.Matches[$param] = $arg
            }
        } Else {
            $out.$canon = $arg
        }
    }

    Return $out
}

Function ConvertFrom-LSColors {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$LSColors
    )

    $out = New-ColorScheme

    ForEach ($_ In $LSColors -Split ":") {
        $param, $arg = $_ -Split "="
        $canon = $script:LSColorsTokensToSchemeProperties[$param.ToLower()]
        If ($null -Eq $canon) {
            $i = $param.IndexOf('.')
            If ($i -Gt -1 -And $i -Eq $param.LastIndexOf('.')) {
                # *.x with no other periods: fast path
                If ($param[0] -Eq '*') {
                    $param = $param.Substring(1)
                }
                $out.Extensions[$param] = $arg
            } Else {
                # dircolors enforces a leading * when generating LS_COLORS
                $out.Matches[$param] = $arg
            }
        } Else {
            $out.$canon = $arg
        }
    } # $_

    Return $out
}

Function ConvertTo-LSColors {
    [CmdletBinding()]
    Param (
        [PSTypeName("ColorScheme")]
        [Parameter(ValueFromPipeline=$true)]
        $ColorScheme
    )

    $tokens = ForEach($_ In $script:SchemePropertiesToLSColors.GetEnumerator()) {
        "{0}={1}" -F $_.Value, $ColorScheme.($_.Name)
    }

    $tokens += ForEach($_ in $ColorScheme.Extensions.GetEnumerator()) {
        "*{0}={1}" -F $_.Name, $_.Value
    }

    $tokens += ForEach($_ in $ColorScheme.Matches.GetEnumerator()) {
        "{0}={1}" -F $_.Name, $_.Value
    }

    Return $tokens -Join ":"
}

Function Update-DirColors {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string[]]$Path
    )

    $script:DirColors = Import-DirColors -Path:$Path
    $Env:LS_COLORS = ConvertTo-LSColors $script:DirColors
}

Function Get-ContainingDirectoryInfo($fi) {
    If ($fi -Is [System.IO.DirectoryInfo]) {
        Return $fi.Parent
    }

    Return $fi.Directory
}

Function Get-ColorCode($fi) {
    If ($fi.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
        If ($fi.LinkType -Eq "SymbolicLink" -Or $fi.LinkType -Eq "Junction") {
            $tfn = [System.IO.Path]::Combine((Get-ContainingDirectoryInfo($fi)).FullName, $fi.Target)
            $tfi = (Get-Item $tfn -EA Ignore)
            If ($null -Eq $tfi) {
                Return $script:DirColors.Orphan
            } ElseIf ($script:DirColors.Link -Eq "target") {
                Return Get-ColorCode($tfi)
            }

            Return $script:DirColors.Link
        }
        Return $script:DirColors.BlockDevice
    }

    If ($fi -Is [System.IO.DirectoryInfo]) {
        Return $script:DirColors.Directory
    } Else {
        $ext = $fi.Extension

        # This is likely to be wrong: Extensions are quicker since we've mapped
        # them all to colors, but ls probably matches wildcards more strongly
        # than extensions (since they're more expressive, and therefore more
        # specific)
        If (-Not [String]::IsNullOrEmpty($ext)) {
            # Fast path: extension matching (pre-hashed)
            If ($ext -In $script:ExecutableSuffixes) {
                return $script:DirColors.Executable
            }

            $cc = $script:DirColors.Extensions[$ext]
            If ($cc) {
                Return $cc
            }
        }

        ForEach($k in $script:DirColors.Matches.Keys) {
            # Slow path: wildcard matching
            If ($fi.Name -Like $k) {
                Return $script:DirColors.Matches.Item($k)
            }
        }

    }

    Return $script:DirColors.Default
}

Function Format-ColorizedFilename() {
    Param (
        [Parameter(ValueFromPipeline=$true)]
        [System.IO.FileSystemInfo]$FileInfo
    )
    $cc = Get-ColorCode($FileInfo)
    Return "$ESC[${cc}m$($FileInfo.Name)$ESC[$($script:DirColors.Reset)m"
}

Function Format-ColorizedLinkTarget() {
    Param (
        [Parameter(ValueFromPipeline=$true)]
        [System.IO.FileSystemInfo]$FileInfo
    )
    # Looking up LinkType requires opening the file; this is expensive.
    If ($FileInfo.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
        If ($FileInfo.LinkType -Eq "SymbolicLink" -Or $FileInfo.LinkType -Eq "Junction") {
            $tfn = [System.IO.Path]::Combine((Get-ContainingDirectoryInfo($FileInfo)).FullName, $FileInfo.Target)
            $tfi = (Get-Item $tfn -EA Ignore)
            If ($null -Eq $tfi) {
                Return "$ESC[$($script:DirColors.Missing)m$($FileInfo.Target)$ESC[$($script:DirColors.Reset)m"
            } Else {
                $tcc = Get-ColorCode($tfi)
                Return "$ESC[${tcc}m$($FileInfo.Target)$ESC[$($script:DirColors.Reset)m"
            }
        }
    }
    Return $null
}

function Format-ColorizedFilenameAndLinkTarget() {
    Param (
        [Parameter(ValueFromPipeline=$true)]
        [System.IO.FileSystemInfo]$FileInfo
    )

    $lt = Format-ColorizedLinkTarget $FileInfo
    If ($null -Ne $lt) {
        Return (Format-ColorizedFilename $FileInfo) + " -> " + $lt
    }

    Return (Format-ColorizedFilename $FileInfo)
}

Update-FormatData -Prepend (Join-Path $PSScriptRoot "DirColors.format.ps1xml")

If (-Not [String]::IsNullOrEmpty($Env:LS_COLORS)) {
    $script:DirColors = ConvertFrom-LSColors $Env:LS_COLORS
}

# vim: ts=4 sw=4 et