mdo-cli 0.2.0

A simple Rust CLI to convert Markdown to HTML5 (styled with simple.css), with optional file watching
<#
.SYNOPSIS
    Register Open as HTML with Windows Explorer for .md files (current user only).

.DESCRIPTION
    Adds a per-user Explorer integration for .md files:
      1. Registers an "Application" entry so "Open as HTML" shows up in
         "Open with -> Choose another app".
      2. Adds .md to its OpenWithProgids list so Explorer offers it.
      3. Adds an "Open as HTML" right-click verb on .md files.

    All changes are written under HKCU (HKEY_CURRENT_USER), so no admin
    rights are required and nothing system-wide is touched.

    To make Open as HTML the *default* handler for .md, after running this
    script: right-click a .md file -> Open with -> Choose another app ->
    pick Open as HTML -> tick "Always use this app". Windows requires that
    last step to be done interactively.

    Run scripts/uninstall-explorer.ps1 to undo everything this script does.

.PARAMETER ExePath
    Full path to mdo-open.exe. If omitted, the script tries
    `Get-Command mdo-open` and then falls back to
    ..\target\release\mdo-open.exe relative to this script.

    mdo-open.exe is a tiny windows-subsystem wrapper shipped alongside
    mdo.exe specifically so Explorer launches do not flash a console
    window. It must live in the same directory as mdo.exe.

.PARAMETER IconChar
    Single character to render into a .ico used by Explorer for the
    "Open with" entry and the right-click verb. Defaults to Ⓜ (U+24C2,
    "circled latin capital letter M"). Try "📄" for a page-curl emoji.
    The generated icon is written to %LOCALAPPDATA%\mdo\md.ico.

.PARAMETER IconColor
    Hex color (e.g. "#1E66E2" or "1E66E2") for the rendered glyph. The
    default is a mid-tone blue chosen so the glyph stays legible on both
    light and dark Explorer themes. Pure black ("#000000") will vanish
    on Windows dark mode.

.EXAMPLE
    powershell -ExecutionPolicy Bypass -File .\scripts\install-explorer.ps1

.EXAMPLE
    .\scripts\install-explorer.ps1 -ExePath "C:\Tools\mdo-open.exe"

.EXAMPLE
    .\scripts\install-explorer.ps1 -IconChar "📄" -IconColor "#E64A19"
#>
[CmdletBinding()]
param(
    [string]$ExePath,
    [string]$IconChar  = ([char]0x24C2),  # Ⓜ
    [string]$IconColor = '#1E66E2'        # legible on both light and dark
)

$ErrorActionPreference = 'Stop'

function Resolve-ExePath {
    param([string]$Hint)

    if ($Hint) {
        if (-not (Test-Path -LiteralPath $Hint)) {
            throw "mdo-open.exe not found at: $Hint"
        }
        return (Resolve-Path -LiteralPath $Hint).Path
    }

    $cmd = Get-Command mdo-open -ErrorAction SilentlyContinue
    if ($cmd) { return $cmd.Source }

    $local = Join-Path $PSScriptRoot '..\target\release\mdo-open.exe'
    if (Test-Path -LiteralPath $local) {
        return (Resolve-Path -LiteralPath $local).Path
    }

    throw "Could not locate mdo-open.exe. Build it with ``cargo build --release`` or pass -ExePath C:\path\to\mdo-open.exe"
}

function New-CharIcon {
    <#
        Render a single Unicode character into a 256x256 PNG and wrap it
        in an ICO container (PNG-in-ICO, supported by Windows Vista+).
        Returns the path written.

        Note: GDI+ (System.Drawing) renders text via GDI, which on most
        installs draws color emoji as their monochrome fallback glyph.
        That's still a recognizable, scalable shape — fine for a 16/32px
        Explorer icon. For a glyph that *always* looks crisp regardless of
        platform, the default Ⓜ (a regular Unicode letter, not an emoji)
        is the safest choice.
    #>
    param(
        [Parameter(Mandatory)] [string]$Char,
        [Parameter(Mandatory)] [string]$OutPath,
        [string]$HexColor = '#1E66E2',
        [int]$Size = 256
    )

    Add-Type -AssemblyName System.Drawing

    # Parse "#RRGGBB" / "RRGGBB" into a Color, falling back to black on error.
    $hex = $HexColor.TrimStart('#')
    try {
        $r = [Convert]::ToInt32($hex.Substring(0, 2), 16)
        $g_ = [Convert]::ToInt32($hex.Substring(2, 2), 16)
        $b = [Convert]::ToInt32($hex.Substring(4, 2), 16)
        $color = [System.Drawing.Color]::FromArgb(255, $r, $g_, $b)
    } catch {
        Write-Warning "Invalid -IconColor '$HexColor', falling back to black"
        $color = [System.Drawing.Color]::Black
    }

    $bmp = New-Object System.Drawing.Bitmap($Size, $Size)
    $g   = [System.Drawing.Graphics]::FromImage($bmp)
    try {
        $g.SmoothingMode     = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
        $g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
        $g.Clear([System.Drawing.Color]::Transparent)

        # Segoe UI Emoji covers both the circled-M and emoji ranges.
        $font = New-Object System.Drawing.Font(
            'Segoe UI Emoji',
            [single]($Size * 0.78),
            [System.Drawing.FontStyle]::Regular,
            [System.Drawing.GraphicsUnit]::Pixel)

        $sf = New-Object System.Drawing.StringFormat
        $sf.Alignment     = [System.Drawing.StringAlignment]::Center
        $sf.LineAlignment = [System.Drawing.StringAlignment]::Center

        $brush = New-Object System.Drawing.SolidBrush($color)
        $rect = New-Object System.Drawing.RectangleF(0, 0, [single]$Size, [single]$Size)
        $g.DrawString($Char, $font, $brush, $rect, $sf)
        $brush.Dispose()
        $font.Dispose()
        $sf.Dispose()
    } finally {
        $g.Dispose()
    }

    # PNG bytes
    $ms = New-Object System.IO.MemoryStream
    $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
    $bmp.Dispose()
    $pngBytes = $ms.ToArray()

    # ICO container: 6-byte ICONDIR + 16-byte ICONDIRENTRY + PNG payload.
    # Width/height bytes set to 0 mean "256" per the ICO spec.
    $ico = New-Object System.IO.MemoryStream
    $bw  = New-Object System.IO.BinaryWriter($ico)
    $bw.Write([uint16]0)                  # reserved
    $bw.Write([uint16]1)                  # type: 1 = icon
    $bw.Write([uint16]1)                  # image count
    $bw.Write([byte]0)                    # width  (0 = 256)
    $bw.Write([byte]0)                    # height (0 = 256)
    $bw.Write([byte]0)                    # color count (0 for >=8bpp)
    $bw.Write([byte]0)                    # reserved
    $bw.Write([uint16]1)                  # planes
    $bw.Write([uint16]32)                 # bits per pixel
    $bw.Write([uint32]$pngBytes.Length)   # image size
    $bw.Write([uint32]22)                 # offset to image data (6 + 16)
    $bw.Write($pngBytes)
    $bw.Flush()

    $dir = Split-Path -Parent $OutPath
    if (-not (Test-Path -LiteralPath $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }
    [System.IO.File]::WriteAllBytes($OutPath, $ico.ToArray())
    return $OutPath
}

$exe = Resolve-ExePath -Hint $ExePath

# mdo-open.exe always implies --open, so the registry value is just
# `"<exe>" "%1"` — no extra flags. The wrapper itself spawns mdo.exe
# with CREATE_NO_WINDOW, which is what eliminates the Explorer console flash.
$cmd = '"{0}" "%1"' -f $exe

# Sanity check: mdo.exe must live next to mdo-open.exe so the
# wrapper can find it at runtime.
$sibling = Join-Path (Split-Path -Parent $exe) 'mdo.exe'
if (-not (Test-Path -LiteralPath $sibling)) {
    Write-Warning "mdo.exe not found next to mdo-open.exe at: $sibling"
    Write-Warning "Explorer integration will be registered, but double-clicking will fail until the main binary is in place."
}

# Render the requested glyph into a .ico used by every Explorer surface
# below (Open-with picker, ProgId, right-click verb).
$iconPath = Join-Path $env:LOCALAPPDATA 'mdo\md.ico'
New-CharIcon -Char $IconChar -HexColor $IconColor -OutPath $iconPath | Out-Null
$iconRef = '"{0}",0' -f $iconPath

Write-Host "Using mdo-open: $exe"
Write-Host "Command line       : $cmd"
Write-Host "Icon ($IconChar)            : $iconPath"
Write-Host ""

# 1. Register the application so Explorer's "Open with" picker can find it.
$appRoot = 'HKCU:\Software\Classes\Applications\mdo.exe'
New-Item -Path "$appRoot\shell\open\command" -Force | Out-Null
Set-ItemProperty -Path "$appRoot\shell\open\command" -Name '(Default)' -Value $cmd
Set-ItemProperty -Path $appRoot -Name 'FriendlyAppName' -Value 'Open as HTML'
# DefaultIcon under Applications\<exe> is what the Open-with picker shows.
New-Item -Path "$appRoot\DefaultIcon" -Force | Out-Null
Set-ItemProperty -Path "$appRoot\DefaultIcon" -Name '(Default)' -Value $iconRef

# 2. Offer mdo as a choice for .md files in the Open-with list.
$openWith = 'HKCU:\Software\Classes\.md\OpenWithProgids'
New-Item -Path $openWith -Force | Out-Null
New-ItemProperty -Path $openWith -Name 'mdo.md' -Value '' -PropertyType String -Force | Out-Null

$progid = 'HKCU:\Software\Classes\mdo.md'
New-Item -Path "$progid\shell\open\command" -Force | Out-Null
Set-ItemProperty -Path $progid -Name '(Default)' -Value 'Markdown document (Open as HTML)'
Set-ItemProperty -Path "$progid\shell\open\command" -Name '(Default)' -Value $cmd
# DefaultIcon under the ProgId is what Explorer shows for files whose
# default app is Open as HTML.
New-Item -Path "$progid\DefaultIcon" -Force | Out-Null
Set-ItemProperty -Path "$progid\DefaultIcon" -Name '(Default)' -Value $iconRef

# 3. Add an "Open as HTML" right-click verb on every .md file
#    (works alongside whatever the current default handler is). Remove older
#    mdo verbs so rerunning this installer upgrades the visible Explorer label
#    instead of leaving duplicate entries.
foreach ($oldVerb in @(
    'HKCU:\Software\Classes\SystemFileAssociations\.md\shell\Preview with mdo',
    'HKCU:\Software\Classes\SystemFileAssociations\.md\shell\Render with mdo'
)) {
    if (Test-Path -LiteralPath $oldVerb) {
        Remove-Item -LiteralPath $oldVerb -Recurse -Force
    }
}
$verb = 'HKCU:\Software\Classes\SystemFileAssociations\.md\shell\Open as HTML'
New-Item -Path "$verb\command" -Force | Out-Null
Set-ItemProperty -Path $verb -Name '(Default)' -Value 'Open as HTML'
Set-ItemProperty -Path "$verb\command" -Name '(Default)' -Value $cmd
Set-ItemProperty -Path $verb -Name 'Icon' -Value $iconRef

Write-Host "Done." -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:"
Write-Host "  - Right-click any .md file -> 'Open as HTML' (Win11: Show more options)."
Write-Host "  - To make it the default: Open with -> Choose another app -> Open as HTML -> Always."