WieldingLs.psm1

Import-Module WieldingAnsi

enum DisplayFormat {
    Long;
    Short;
}

enum SortProperty {
    Name;
    Attributes;
    LastWriteTime;
    Length;    
}

class GDCOptions {
    [string]$Path
    [string]$SortProperty
    [bool]$ShowHeader
    [bool]$ShowTotal
    [bool]$ShowColor
    [bool]$ShowHidden
    [bool]$ShowSystem
    [DisplayFormat]$Format
    [int]$Width
    [int]$MaxNameLength
}

class Style {
    [string]$Codes
}

class FileItem {
    [object]$File
    [string]$Style
    [string]$AdjustedName
}

class GdcTheme {
    [string]$SourceCodeColor
    $SourceCodeExtensions = @()
    [string]$DataFileColor
    $DataFileExtensions = @()
    [string]$LogFileColor
    $LogFileExtensions = @()
    [string]$DocumentFileColor
    $DocumentFileExtensions = @()
    [string]$ExecutableFileColor
    $ExecutableFileExtensions = @()
    [string]$CompressedFileColor
    $CompressedFileExtensions = @()
    $FileAttributes = @()
    $FileAttributesColors = @{}
    $FileAttributeCombinationColors = @{}
    [string]$HiddenFileColor
    [string]$HiddenFolderColor
    [string]$NakedFileColor
    [string]$DefaultFileColor
    $ExtensionColors = @{}
    [string]$DefaultDisplayFormat
    [string]$TruncationIndicator
}


$GdcTheme = New-Object -TypeName GdcTheme

$GdcTheme.SourceCodeColor = "{:F82:}"
$GdcTheme.SourceCodeExtensions = @(
    ".ahk",
    ".awk",
    ".c", ".h",
    ".cc", ".cpp", ".cxx", ".c++", ".hh", ".hpp", ".hxx", ".h++",
    ".cs",
    ".go",
    ".groovy",
    ".html", ".htm", ".hta", ".css", ".scss", ".sass",
    ".java",
    ".js", ".mjs", ".ts", ".tsx"
    ".pl", ".pm", ".t", ".pod",
    ".php", ".phtml", ".php3", ".php4", ".php5", ".php7", ".phps", ".php-s", ".pht",
    ".pp", ".pas", ".inc",
    ".psm1", ".ps1xml", ".psc1", ".psd1", ".pssc", ".cdxml",
    ".py", ".pyx", ".pyc", ".pyd", ".pyo", ".pyw", ".pyz",
    ".r", ".RData", ".rds", ".rda",
    ".rb",
    ".rs", ".rlib",
    ".scala", ".sc",
    ".scm", ".ss",
    ".swift",
    ".sql",
    ".vbs", ".vbe", ".wsf", ".wsc", ".asp"
)

$GdcTheme.DataFileColor = "{:F14:}"
$GdcTheme.DataFileExtensions = @(
    ".csv",
    ".dat",
    ".dxf",
    ".geojson"
    ".gpx",
    ".json",
    ".kml",    
    ".shp",    
    ".xml"
)

$GdcTheme.LogFileColor = "{:F9:}"
$GdcTheme.LogFileExtensions = @(
    ".log"
)

$GdcTheme.DocumentFileColor = "{:F12:}"
$GdcTheme.DocumentFileExtensions = @(
    ".doc",
    ".docx",
    ".md",
    ".xls"
)    

$GdcTheme.ExecutableFileColor = "{:F2:}"
$GdcTheme.ExecutableFileExtensions = @(
    ".exe",
    ".bat",
    ".ps1",
    ".sh"
)    

$GdcTheme.CompressedFileColor = "{:F129:}"
$GdcTheme.CompressedFileExtensions = @(
    ".zip",
    ".tz",
    ".gz",
    ".7z",
    ".deb",
    ".rpm"
)


$GdcTheme.FileAttributesColors = @{    
    [System.IO.FileAttributes]::Directory = "{:F11:}"
    ([System.IO.FileAttributes]::Directory + [System.IO.FileAttributes]::ReparsePoint) = "{:InverseOn:}"
}

$GdcTheme.HiddenFileColor = "{:F240:}"
$GdcTheme.HiddenFolderColor = "{:F136:}"
$GdcTheme.NakedFileColor = "{:F28:}"
$GdcTheme.DefaultFileColor = "{:R:}"
$GdcTheme.TruncationIndicator = "{:F15:}...{:R:}"
[DisplayFormat]$GdcTheme.DefaultDisplayFormat = [DisplayFormat]::Short

function Get-WieldingLsInfo {
    $moduleName = (Get-ChildItem "$PSScriptRoot/*.psd1").Name

    Import-PowerShellDataFile -Path "$PSScriptRoot/$moduleName"
}

function Update-GDCColors {
    foreach ($extension in $GdcTheme.SourceCodeExtensions) {
        $GdcTheme.ExtensionColors[$extension] = $GdcTheme.SourceCodeColor
    }

    foreach ($extension in $GdcTheme.DataFileExtensions) {
        $GdcTheme.ExtensionColors[$extension] = $GdcTheme.DataFileColor
    }

    foreach ($extension in $GdcTheme.LogFileExtensions) {
        $GdcTheme.ExtensionColors[$extension] = $GdcTheme.LogFileColor
    }    

    foreach ($extension in $GdcTheme.CompressedFileExtensions) {
        $GdcTheme.ExtensionColors[$extension] = $GdcTheme.CompressedFileColor
    }        

    foreach ($extension in $GdcTheme.ExecutableFileExtensions) {
        $GdcTheme.ExtensionColors[$extension] = $GdcTheme.ExecutableFileColor
    }            

    foreach ($extension in $GdcTheme.DocumentFileExtensions) {
        $GdcTheme.ExtensionColors[$extension] = $GdcTheme.DocumentFileColor
    }                
}

function Write-FileLength {
    Param ($Length)

    If ($null -eq $Length) {
        Return ""
    }
    ElseIf ($Length -ge 1GB) {
        Return ($Length / 1GB).ToString("F") + 'GB'
    }
    ElseIf ($Length -ge 1MB) {
        Return ($Length / 1MB).ToString("F") + 'MB'
    }
    ElseIf ($Length -ge 1KB) {
        Return ($Length / 1KB).ToString("F") + 'KB'
    }

    Return $Length.ToString() + ' '
}

function Get-FileColor([Object]$file) {
    $fileStyle = $GdcTheme.DefaultFileColor
    $isDir = ($file.Attributes -band [System.IO.FileAttributes]::Directory) -eq [System.IO.FileAttributes]::Directory

    foreach ($attribute in $GdcTheme.FileAttributesColors.Keys) {
        if (($file.Attributes -band $attribute) -eq $attribute ) {
            $style = $GdcTheme.FileAttributesColors[$attribute]

            if ($style.Length -gt 0) {
                if ($style.SubString(0, 1) -eq "!") {
                    return $style.SubString(1, $style.Length - 1)
                }
            }

            $fileStyle += $GdcTheme.FileAttributesColors[$attribute]
        }        
    }

    if ($isDir -and $file.Name.StartsWith(".")) {
        if ($GdcTheme.HiddenFolderColor.SubString(0, 1) -eq "!") {
            return $GdcTheme.HiddenFolderColor.SubString(1, $GdcTheme.HiddenFolderColor.Length - 1)
        }
        return $fileStyle + $GdcTheme.HiddenFolderColor
    }

    if (!$isDir -and $file.Extension.Length -lt 1) {
        return $fileStyle + $GdcTheme.NakedFileColor
    }

    if ($file.Name.StartsWith(".")) {
        return $fileStyle + $GdcTheme.HiddenFileColor
    }

    if ($GdcTheme.ExtensionColors.ContainsKey($file.Extension)) {
        return $fileStyle + $GdcTheme.ExtensionColors[$file.Extension]
    }

    return $fileStyle
}

function Get-DirectoryContentsWithOptions {
    param (
        [GDCOptions]$options
    )

    $longestName = 0
    $totalSize = 0
    $index = 0
    $fileList = @()
    $attributes = "!System"
    

    if (-not $options.ShowColor) {
        $Wansi.Enabled = $false
    }
    
    if ($options.ShowHidden) {
        $attributes = "!System,Hidden+!System"
    }

    if ($options.ShowSystem) {
        $attributes = "!System,Hidden+System"
    }

    try {
        if ($options.SortProperty -ne "") {
            $files = Get-ChildItem $options.Path -Attributes $attributes -ErrorAction stop | Sort-Object -Property $options.SortProperty
        }
        else {
            $files = Get-ChildItem $options.Path -Attributes $attributes -ErrorAction stop
        }
    }
    catch {
        if ($options.Path -ne ".") {
            Write-Wansi "{:F9:}[$($options.Path)] was not found{:R:}`n"
            return 8
        }
        return 0
    }

    if ($options.ShowHeader) {
        $mode = ConvertTo-AnsiString "{:BoldOn:}{:F15:}{:UnderlineOn:}Mode{:R:}" -PadRight 8
        $lastWriteTime = ConvertTo-AnsiString "{:UnderlineOn:}{:F15:}LastWriteTime{:R:}" -PadLeft 19
        $Length = ConvertTo-AnsiString "{:UnderlineOn:}{:F15:}Length{:R:}" -PadLeft 15
        $Name = ConvertTo-AnsiString "{:UnderlineOn:}{:F15:}Name{:R:}" -PadLeft 10
        Write-Wansi ("{0}{1}{2}{3}`n" -f $mode.Value, $lastWriteTime.Value, $Length.Value, $Name.Value)
    }


    foreach ($file in $files) {
        ++$index

        if (!$options.ShowHidden) {
            if ($file.Name.StartsWith(".")) {
                continue
            }
        }

        if ($file.Name.Length + 3 -gt $longestName) {
            $longestName = $file.Name.Length + 3
            if ($longestName -ge $Host.Ui.RawUI.BufferSize.Width) {
                $longestName = $Host.Ui.RawUI.BufferSize.Width - 1
            }
        }

        $adjustedName = $file.Name

        $fileStyle = Get-FileColor $file

        if ($longestName -gt $options.MaxNameLength) {
            $longestName = $options.MaxNameLength
            $ti = ConvertTo-AnsiString $GdcTheme.TruncationIndicator
            $subLength = $options.MaxNameLength - ($ti.NakedLength + 1 + (ConvertTo-AnsiString $fileStyle).NakedLength)
            if ($subLength -lt 1) {
                $subLength = 1
            }
            $adjustedName = $file.Name.Substring(0, $subLength)
            $adjustedName += $ti.value
        }

        $isDir = ($file.Attributes -band [System.IO.FileAttributes]::Directory) -eq [System.IO.FileAttributes]::Directory

        if (!$isDir) {
            $totalSize += $file.Length
        }

        # Long Format
        if ($options.Format -eq [DisplayFormat]::Long) {
            Write-Wansi "$($file.Mode)`t" -NoNewline
            Write-Wansi ("{0, 10} {1, 8}`t" -f $($file.LastWriteTime.ToString("d"), $file.LastWriteTime.ToString("t"))) -NoNewline
            if ($isDir) {
                Write-Wansi ("{0, 9}`t" -f "-") -NoNewline
            }
            else {
                Write-Wansi ("{0, 10}`t" -f $(Write-FileLength $file.Length)) -NoNewline
            }
            Write-Wansi ("$($fileStyle)$($file.Name){:R:}`n")
        }

        # Build Short Format Array for display after the loop
        if ($options.Format -eq [DisplayFormat]::Short) {
            $t = New-Object -TypeName FileItem
            $t.File = $file
            $t.Style = $fileStyle
            $t.AdjustedName = $adjustedName + " "
            $fileList += $t
        }
    }

    # Display the short format
    if ($options.Format -eq [DisplayFormat]::Short) {
        $boundary = 0
        foreach ($i in $fileList) {
            if ($boundary + $longestName -ge $options.Width) {
                Write-Wansi "`n"
                $boundary = 0
            }

            $boundary += $longestName

            $ansiString = ConvertTo-AnsiString "$($i.Style)$($i.AdjustedName){:R:}" -PadRight $longestName
            Write-Wansi $ansiString.Value
        }     
    }
   
    if ($options.ShowTotal) {
        if ($totalSize -gt 0) {
            Write-Wansi ("`t`t`t`t{0, 10} total`n" -f $(Write-FileLength $totalSize))
        }
    }

    Write-Wansi "`n`n"
}
function Get-DirectoryContents {
    <#
 .SYNOPSIS
    Display files in specified directory
 
 .DESCRIPTION
    Display colorized list of files for current or specified directory with options for color and format.
 
 .PARAMETER DisplayFormat
    The display format for output. Must be one of "Long", "Short"
 
 .PARAMETER SortProperty
    The file property to sort on. Must be one of "Name", "Attributes", "LastWriteTime", "Length"
 
 .PARAMETER HideHeader
    Disables displaying the directory list header
 
 .PARAMETER MinColumns
    The minumum number of columns to display in short format before truncating filenames
 
 .PARAMETER HideTotal
    Disables displaying the file size total
 
 .PARAMETER ShowHidden
    Enables displaying files that start with "."
 
 .PARAMETER NoColor
    Disables colorizing output
 
 .EXAMPLE
    Get-DirectoryContents -ShowHidden ~
    Display all files in users home directory including hidden files and files starting with "."
 
 .EXAMPLE
    Get-DirectoryContents -SortProperty Name
    DISPLAY all files except for files with Hidden and System attributes in the current directory sorted by name
 
 .EXAMPLE
    Get-DirectoryContents -la
    DISPLAY all files in the current directory using the long format with hidden files shown
 
 .NOTES
    Author: Andrew Kunkel
    Inspired by and occasionally borrowed from https://github.com/joonro/Get-ChildItemColor
 
 .LINK
    https://github.com/Wielding/WieldingLs
     
#>
    
    param (
        [string]$Path = ".",
        [ValidateSet("Name", "Attributes", "LastWriteTime", "Length")]
        [SortProperty]$SortProperty,
        [ValidateSet("Long", "Short")]
        [DisplayFormat]$DisplayFormat = $GdcTheme.DefaultDisplayFormat,
        [switch]$HideHeader,
        [switch]$HideTotal,
        [switch]$NoColor,
        [int]$MinColumns = 4,
        [Alias("a")]
        [switch]$ShowHidden,
        [Alias("l")]
        [switch]$ShowLong,
        [Alias("w")]
        [switch]$ShowSystem,
        [switch]$Help
    )

    $returnCode = 0


    $options = New-Object -TypeName GDCOptions
    $options.Path = $Path
    $options.ShowHeader = !$HideHeader
    $options.ShowTotal = !$HideTotal
    $options.SortProperty = $SortProperty
    $options.ShowColor = !$NoColor
    $options.ShowHidden = $ShowHidden
    $options.ShowSystem = $ShowSystem
    $options.Format = $DisplayFormat
    $options.Width = $host.ui.RawUI.WindowSize.Width

    if ($MinColumns -lt 1) {
        $options.MaxNameLength = 256
    } else {
        $options.MaxNameLength = ($host.ui.RawUI.WindowSize.Width / $MinColumns)
    }
    
    if ($args.Length -gt 0) {
        foreach ($arg in $args) {
            if ($arg.StartsWith("-")) {
                foreach ($char in $arg.ToCharArray()) {
                    switch ($char) {
                        "l" {
                            $options.Format = [DisplayFormat]::Long
                            $DisplayFormat = [DisplayFormat]::Long
                            $options.ShowHeader = $true
                            $options.ShowTotal = $true
                        }
                        "w" {
                            $options.ShowSystem = $true
                        }
                        "a" {
                            $options.ShowHidden = $true
                        }
                        "-" {}
                        default {
                            Write-Wansi "{:F9:}[$char] unknown option{:R:}`n"
                            $returnCode = 8
                        }
                    }
                }
            }
            else {
                $options.Path = $arg
            }
        }
    }    

    if ($ShowLong) {
        $options.Format = [DisplayFormat]::Long
        $DisplayFormat = [DisplayFormat]::Long
    }

    if ($DisplayFormat -eq [DisplayFormat]::Short) {
        $options.ShowHeader = $false
        $options.ShowTotal = $false
    }
    
    if ($returnCode -gt 0) {
        return
    }

    $originalWansiEnabled = $Wansi.Enabled

    $returnCode = Get-DirectoryContentsWithOptions $options      

    $Wansi.Enabled = $originalWansiEnabled

    if ($returnCode -gt 0) {
        return
    }
   
}

Update-GDCColors

Export-ModuleMember -Function Out-Default, 'Get-DirectoryContents'
Export-ModuleMember -Function Out-Default, 'Update-GDCColors'
Export-ModuleMember -Function Out-Default, 'Get-AnsiCodes'
Export-ModuleMember -Function Out-Default, 'Get-WieldingLsInfo'
Export-ModuleMember -Function Out-Default, 'Get-FileColor'
Export-ModuleMember -Variable 'GdcTheme'