NobGit
public imalexnord read

ms.open

Adds Mac "Open ." command to Windows

Languages

Repository composition by tracked source files.

PowerShell
PowerShell 100%
Create file Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/ms.open.git
ssh://[email protected]:2222/user/imalexnord/ms.open.git
MSOpen.ps1
[CmdletBinding()]
param(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$Command,

    [Parameter(Position = 1, ValueFromRemainingArguments = $true)]
    [string[]]$Arguments
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Arguments = @($Arguments)

function Resolve-MSOpenPath {
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop
    return $resolved.Path
}

function Invoke-Open {
    param([string[]]$Args)

    $target = if ($Args.Count -gt 0) {
        $Args[0]
    }
    else {
        (Get-Location).Path
    }

    if (Test-Path -LiteralPath $target) {
        $target = Resolve-MSOpenPath -Path $target
    }

    Start-Process -FilePath $target
}

function Invoke-Reveal {
    param([string[]]$Args)

    if ($Args.Count -lt 1) {
        throw 'Usage: reveal <file-or-folder>'
    }

    $target = Resolve-MSOpenPath -Path $Args[0]

    if (Test-Path -LiteralPath $target -PathType Container) {
        Start-Process -FilePath 'explorer.exe' -ArgumentList "`"$target`""
        return
    }

    Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$target`""
}

function Invoke-CopyPath {
    param([string[]]$Args)

    $target = if ($Args.Count -gt 0) {
        Resolve-MSOpenPath -Path $Args[0]
    }
    else {
        (Get-Location).Path
    }

    Set-Clipboard -Value $target
    Write-Output $target
}

function Invoke-Paths {
    $entries = $env:Path -split ';' |
        ForEach-Object { $_.Trim() } |
        Where-Object { $_ }

    $index = 1
    foreach ($entry in $entries) {
        [pscustomobject]@{
            Number = $index
            Path   = $entry
            Exists = Test-Path -LiteralPath $entry
        }
        $index++
    }
}

function Invoke-Which {
    param([string[]]$Args)

    if ($Args.Count -lt 1) {
        throw 'Usage: which <command>'
    }

    $commands = Get-Command -Name $Args[0] -All -ErrorAction Stop

    foreach ($item in $commands) {
        $location = $null

        if ($item.PSObject.Properties.Name -contains 'Path' -and $item.Path) {
            $location = $item.Path
        }
        elseif ($item.Source) {
            $location = $item.Source
        }
        elseif ($item.Definition) {
            $location = $item.Definition
        }

        [pscustomobject]@{
            CommandType = $item.CommandType
            Name        = $item.Name
            Location    = $location
        }
    }
}

function Get-ListeningPorts {
    $connections = Get-NetTCPConnection -State Listen -ErrorAction Stop |
        Sort-Object LocalPort, LocalAddress

    foreach ($connection in $connections) {
        $processName = '<unknown>'

        if ($connection.OwningProcess -eq 4) {
            $processName = 'System'
        }
        else {
            try {
                $processName = (Get-Process -Id $connection.OwningProcess -ErrorAction Stop).ProcessName
            }
            catch {
                $processName = '<unavailable>'
            }
        }

        [pscustomobject]@{
            Protocol = 'TCP'
            Address  = $connection.LocalAddress
            Port     = $connection.LocalPort
            PID      = $connection.OwningProcess
            Process  = $processName
        }
    }
}

function Invoke-Ports {
    Get-ListeningPorts | Format-Table -AutoSize
}

function Invoke-KillPort {
    param([string[]]$Args)

    if ($Args.Count -lt 1) {
        throw 'Usage: killport <port> [--force]'
    }

    $port = 0
    if (-not [int]::TryParse($Args[0], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
        throw "Invalid port: $($Args[0])"
    }

    $force = $Args -contains '--force'
    $connections = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue
    $processIds = @($connections | Select-Object -ExpandProperty OwningProcess -Unique)

    if ($processIds.Count -eq 0) {
        throw "Nothing is listening on port $port."
    }

    $targets = foreach ($processId in $processIds) {
        $name = '<unknown>'
        try {
            $name = (Get-Process -Id $processId -ErrorAction Stop).ProcessName
        }
        catch {
            $name = '<unavailable>'
        }

        [pscustomobject]@{
            Port    = $port
            PID     = $processId
            Process = $name
        }
    }

    $targets | Format-Table -AutoSize

    $protected = @($targets | Where-Object { $_.PID -in 0, 4 })
    if ($protected.Count -gt 0) {
        throw 'Refusing to terminate a protected Windows system process.'
    }

    if (-not $force) {
        $answer = Read-Host "Terminate the process owning port $port? [y/N]"
        if ($answer -notmatch '^(y|yes)$') {
            Write-Host 'Cancelled.'
            return
        }
    }

    foreach ($target in $targets) {
        Stop-Process -Id $target.PID -Force -ErrorAction Stop
        Write-Host "Stopped $($target.Process) (PID $($target.PID))."
    }
}

function Invoke-Sha256 {
    param([string[]]$Args)

    if ($Args.Count -lt 1) {
        throw 'Usage: sha256 <file>'
    }

    $target = Resolve-MSOpenPath -Path $Args[0]

    if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
        throw "Not a file: $target"
    }

    $hash = Get-FileHash -LiteralPath $target -Algorithm SHA256
    Write-Output "$($hash.Hash.ToLowerInvariant())  $target"
}

function Invoke-Pbcopy {
    param([string[]]$Args)

    $text = if ($Args.Count -gt 0) {
        $Args -join ' '
    }
    elseif ([Console]::IsInputRedirected) {
        [Console]::In.ReadToEnd()
    }
    else {
        ''
    }

    Set-Clipboard -Value $text
}

function Invoke-Pbpaste {
    $value = Get-Clipboard -Raw
    if ($null -ne $value) {
        [Console]::Out.Write($value)
    }
}

function Get-MimeType {
    param([string]$Path)

    switch ([IO.Path]::GetExtension($Path).ToLowerInvariant()) {
        '.html' { 'text/html; charset=utf-8' }
        '.htm'  { 'text/html; charset=utf-8' }
        '.css'  { 'text/css; charset=utf-8' }
        '.js'   { 'text/javascript; charset=utf-8' }
        '.json' { 'application/json; charset=utf-8' }
        '.xml'  { 'application/xml; charset=utf-8' }
        '.txt'  { 'text/plain; charset=utf-8' }
        '.md'   { 'text/markdown; charset=utf-8' }
        '.svg'  { 'image/svg+xml' }
        '.png'  { 'image/png' }
        '.jpg'  { 'image/jpeg' }
        '.jpeg' { 'image/jpeg' }
        '.gif'  { 'image/gif' }
        '.webp' { 'image/webp' }
        '.ico'  { 'image/x-icon' }
        '.pdf'  { 'application/pdf' }
        '.wasm' { 'application/wasm' }
        '.zip'  { 'application/zip' }
        default { 'application/octet-stream' }
    }
}

function Send-TextResponse {
    param(
        [Parameter(Mandatory)]
        [System.Net.HttpListenerResponse]$Response,

        [Parameter(Mandatory)]
        [int]$StatusCode,

        [Parameter(Mandatory)]
        [string]$Text,

        [string]$ContentType = 'text/plain; charset=utf-8'
    )

    $bytes = [Text.Encoding]::UTF8.GetBytes($Text)
    $Response.StatusCode = $StatusCode
    $Response.ContentType = $ContentType
    $Response.ContentLength64 = $bytes.Length
    $Response.OutputStream.Write($bytes, 0, $bytes.Length)
}

function Get-DirectoryListingHtml {
    param(
        [Parameter(Mandatory)]
        [string]$Directory,

        [Parameter(Mandatory)]
        [string]$RequestPath
    )

    $encodedTitle = [Net.WebUtility]::HtmlEncode("Index of $RequestPath")
    $builder = [Text.StringBuilder]::new()

    [void]$builder.AppendLine('<!doctype html>')
    [void]$builder.AppendLine('<html lang="en"><head><meta charset="utf-8">')
    [void]$builder.AppendLine("<title>$encodedTitle</title>")
    [void]$builder.AppendLine('<style>body{font-family:system-ui,sans-serif;max-width:900px;margin:40px auto;padding:0 20px}li{margin:8px 0}a{text-decoration:none}a:hover{text-decoration:underline}</style>')
    [void]$builder.AppendLine('</head><body>')
    [void]$builder.AppendLine("<h1>$encodedTitle</h1><ul>")

    if ($RequestPath -ne '/') {
        [void]$builder.AppendLine('<li><a href="../">../</a></li>')
    }

    foreach ($item in Get-ChildItem -LiteralPath $Directory | Sort-Object @{ Expression = { -not $_.PSIsContainer } }, Name) {
        $displayName = $item.Name + $(if ($item.PSIsContainer) { '/' } else { '' })
        $hrefName = [Uri]::EscapeDataString($item.Name) + $(if ($item.PSIsContainer) { '/' } else { '' })
        $encodedName = [Net.WebUtility]::HtmlEncode($displayName)
        [void]$builder.AppendLine("<li><a href=`"$hrefName`">$encodedName</a></li>")
    }

    [void]$builder.AppendLine('</ul></body></html>')
    return $builder.ToString()
}

function Invoke-Serve {
    param([string[]]$Args)

    $port = 8000
    if ($Args.Count -gt 0) {
        if (-not [int]::TryParse($Args[0], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
            throw "Invalid port: $($Args[0])"
        }
    }

    $root = [IO.Path]::GetFullPath((Get-Location).Path)
    $rootWithSeparator = $root.TrimEnd('\') + '\'
    $prefix = "http://127.0.0.1:$port/"
    $listener = [System.Net.HttpListener]::new()
    $listener.Prefixes.Add($prefix)

    try {
        $listener.Start()
        Write-Host "Serving $root"
        Write-Host $prefix
        Write-Host 'Press Ctrl+C to stop.'

        while ($listener.IsListening) {
            $context = $listener.GetContext()
            $response = $context.Response

            try {
                $requestPath = [Uri]::UnescapeDataString($context.Request.Url.AbsolutePath)
                $relativePath = $requestPath.TrimStart('/').Replace('/', '\')
                $candidate = [IO.Path]::GetFullPath((Join-Path $root $relativePath))

                $insideRoot = $candidate.Equals($root, [StringComparison]::OrdinalIgnoreCase) -or
                    $candidate.StartsWith($rootWithSeparator, [StringComparison]::OrdinalIgnoreCase)

                if (-not $insideRoot) {
                    Send-TextResponse -Response $response -StatusCode 403 -Text 'Forbidden'
                    continue
                }

                if (Test-Path -LiteralPath $candidate -PathType Container) {
                    $indexFile = @('index.html', 'index.htm') |
                        ForEach-Object { Join-Path $candidate $_ } |
                        Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
                        Select-Object -First 1

                    if ($indexFile) {
                        $candidate = $indexFile
                    }
                    else {
                        $html = Get-DirectoryListingHtml -Directory $candidate -RequestPath $requestPath
                        Send-TextResponse -Response $response -StatusCode 200 -Text $html -ContentType 'text/html; charset=utf-8'
                        continue
                    }
                }

                if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
                    Send-TextResponse -Response $response -StatusCode 404 -Text 'Not found'
                    continue
                }

                $fileInfo = Get-Item -LiteralPath $candidate
                $response.StatusCode = 200
                $response.ContentType = Get-MimeType -Path $candidate
                $response.ContentLength64 = $fileInfo.Length

                $stream = [IO.File]::OpenRead($candidate)
                try {
                    $stream.CopyTo($response.OutputStream)
                }
                finally {
                    $stream.Dispose()
                }
            }
            catch {
                if ($response.OutputStream.CanWrite) {
                    Send-TextResponse -Response $response -StatusCode 500 -Text 'Internal server error'
                }
            }
            finally {
                $response.OutputStream.Close()
            }
        }
    }
    finally {
        if ($listener.IsListening) {
            $listener.Stop()
        }
        $listener.Close()
    }
}

function Show-Help {
    @'
MS.Open commands

  open [path]          Open a folder, file, or URL
  reveal <path>        Show a file in Explorer
  copypath [path]      Copy a full path to the clipboard
  which <command>      Explain which command will run
  paths                Print PATH entries one per row
  serve [port]         Serve the current folder at 127.0.0.1
  ports                Show listening TCP ports
  killport <port>      Stop the process listening on a port
  sha256 <file>        Calculate a SHA-256 hash
  pbcopy [text]        Copy arguments or piped input
  pbpaste              Print clipboard text

PowerShell functions installed separately:

  mkcd <folder>        Create a directory and enter it
  reload               Reload the current PowerShell profile
'@
}

try {
    switch ($Command.ToLowerInvariant()) {
        'open'     { Invoke-Open -Args $Arguments }
        'reveal'   { Invoke-Reveal -Args $Arguments }
        'copypath' { Invoke-CopyPath -Args $Arguments }
        'which'    { Invoke-Which -Args $Arguments }
        'paths'    { Invoke-Paths }
        'serve'    { Invoke-Serve -Args $Arguments }
        'ports'    { Invoke-Ports }
        'killport' { Invoke-KillPort -Args $Arguments }
        'sha256'   { Invoke-Sha256 -Args $Arguments }
        'pbcopy'   { Invoke-Pbcopy -Args $Arguments }
        'pbpaste'  { Invoke-Pbpaste }
        'help'     { Show-Help }
        '--help'   { Show-Help }
        '-h'       { Show-Help }
        default    { throw "Unknown command: $Command`n`n$(Show-Help)" }
    }
}
catch {
    Write-Error $_.Exception.Message
    exit 1
}