diff --git a/config/scripts/wsl-notify.ps1 b/config/scripts/wsl-notify.ps1 index 3be5eeb6..2fb5e485 100644 --- a/config/scripts/wsl-notify.ps1 +++ b/config/scripts/wsl-notify.ps1 @@ -1,6 +1,7 @@ param( [Parameter(Mandatory = $true)][string] $Title, - [Parameter(Mandatory = $true)][string] $Message + [Parameter(Mandatory = $true)][string] $Message, + [string] $DistroName = '' ) $ErrorActionPreference = 'Stop' @@ -74,6 +75,135 @@ function Get-NotificationAppId { Select-Object -First 1 -ExpandProperty AppID } +function Get-WslDistroIconPath { + param([string] $Name) + + if ([string]::IsNullOrWhiteSpace($Name)) { + return $null + } + + $roots = @( + ('\\wsl.localhost\{0}' -f $Name), + ('\\wsl$\{0}' -f $Name) + ) + + foreach ($root in $roots) { + $configPath = Join-Path -Path $root -ChildPath 'etc\wsl-distribution.conf' + if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { + continue + } + + try { + $section = '' + $linuxIconPath = $null + + foreach ($rawLine in Get-Content -LiteralPath $configPath -ErrorAction Stop) { + $line = ($rawLine -split '[#;]', 2)[0].Trim() + if ($line.Length -eq 0) { + continue + } + + if ($line -match '^\[(?
[^\]]+)\]$') { + $section = $Matches['Section'].Trim() + continue + } + + if ($section -ieq 'shortcut' -and $line -match '^(?i:icon)\s*=\s*(?.+)$') { + $linuxIconPath = $Matches['Icon'].Trim() + if (($linuxIconPath.StartsWith('"') -and $linuxIconPath.EndsWith('"')) -or + ($linuxIconPath.StartsWith("'") -and $linuxIconPath.EndsWith("'"))) { + $linuxIconPath = $linuxIconPath.Substring(1, $linuxIconPath.Length - 2) + } + break + } + } + + if ([string]::IsNullOrWhiteSpace($linuxIconPath) -or -not $linuxIconPath.StartsWith('/')) { + continue + } + + $relativePath = $linuxIconPath.TrimStart('/').Replace('/', '\') + $candidate = Join-Path -Path $root -ChildPath $relativePath + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + return $candidate + } + } + catch { + continue + } + } + + return $null +} + +function Get-WslDistroIconUri { + param([string] $Name) + + $sourcePath = Get-WslDistroIconPath -Name $Name + if ([string]::IsNullOrWhiteSpace($sourcePath)) { + return $null + } + + $extension = [IO.Path]::GetExtension($sourcePath).ToLowerInvariant() + if ($extension -in @('.png', '.jpg', '.jpeg', '.svg')) { + return ([Uri] $sourcePath).AbsoluteUri + } + + if ($extension -ne '.ico') { + return $null + } + + try { + Add-Type -AssemblyName System.Drawing + + $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + if ([string]::IsNullOrWhiteSpace($localAppData)) { + return $null + } + + $cacheDirectory = Join-Path -Path $localAppData -ChildPath 'wsl-notify\icons' + [void] [IO.Directory]::CreateDirectory($cacheDirectory) + + $sha256 = [Security.Cryptography.SHA256]::Create() + try { + $cacheKey = [Text.Encoding]::UTF8.GetBytes("$Name`0$sourcePath") + $hash = [BitConverter]::ToString($sha256.ComputeHash($cacheKey)).Replace('-', '').ToLowerInvariant() + } + finally { + $sha256.Dispose() + } + + $cachePath = Join-Path -Path $cacheDirectory -ChildPath "$hash.png" + $sourceInfo = Get-Item -LiteralPath $sourcePath -ErrorAction Stop + $refreshCache = -not (Test-Path -LiteralPath $cachePath -PathType Leaf) + if (-not $refreshCache) { + $cacheInfo = Get-Item -LiteralPath $cachePath -ErrorAction Stop + $refreshCache = $cacheInfo.LastWriteTimeUtc -lt $sourceInfo.LastWriteTimeUtc + } + + if ($refreshCache) { + $icon = New-Object -TypeName System.Drawing.Icon -ArgumentList @($sourcePath, 48, 48) + try { + $bitmap = $icon.ToBitmap() + try { + $bitmap.Save($cachePath, [System.Drawing.Imaging.ImageFormat]::Png) + } + finally { + $bitmap.Dispose() + } + } + finally { + $icon.Dispose() + } + } + + return ([Uri] $cachePath).AbsoluteUri + } + catch { + return $null + } +} + $foregroundProcess = Get-ForegroundProcessName if ([string]::IsNullOrWhiteSpace($foregroundProcess)) { exit $ExitFocusUnknown @@ -100,6 +230,18 @@ try { [void] $textNodes.Item(0).AppendChild($document.CreateTextNode($Title)) [void] $textNodes.Item(1).AppendChild($document.CreateTextNode($Message)) + $iconUri = Get-WslDistroIconUri -Name $DistroName + if (-not [string]::IsNullOrWhiteSpace($iconUri)) { + $binding = $document.SelectSingleNode('/toast/visual/binding') + $image = $document.CreateElement('image') + $image.SetAttribute('placement', 'appLogoOverride') + $image.SetAttribute('src', $iconUri) + if (-not [string]::IsNullOrWhiteSpace($DistroName)) { + $image.SetAttribute('alt', $DistroName) + } + [void] $binding.AppendChild($image) + } + $toast = [Windows.UI.Notifications.ToastNotification]::new($document) [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast) exit $ExitToastShown diff --git a/config/tests/wsl-notify.zsh b/config/tests/wsl-notify.zsh index eb141ee3..d92e0cbf 100644 --- a/config/tests/wsl-notify.zsh +++ b/config/tests/wsl-notify.zsh @@ -44,6 +44,7 @@ export WSL_NOTIFY_TEST_EXIT=20 output=$(bgnotify 'build finished' 'command completed' '') [[ "$output" == $'\a' ]] [[ "$(wc -l <"$WSL_NOTIFY_TEST_LOG")" -eq 4 ]] +[[ "$(grep -c -- '-DistroName ci' "$WSL_NOTIFY_TEST_LOG")" -eq 4 ]] # Exercise the Ghostty hook-removal path without depending on the runner's zsh # function installation. The fixture is loaded through zsh's real autoload diff --git a/home/.config/sheldon/plugins/wsl-notify.plugin.zsh b/home/.config/sheldon/plugins/wsl-notify.plugin.zsh index a1e0955c..6447ff81 100644 --- a/home/.config/sheldon/plugins/wsl-notify.plugin.zsh +++ b/home/.config/sheldon/plugins/wsl-notify.plugin.zsh @@ -49,7 +49,8 @@ function bgnotify { if powershell.exe -NoLogo -NoProfile -NonInteractive \ -ExecutionPolicy Bypass -File "$windows_notify_script" \ - -Title "$title" -Message "$message" >/dev/null 2>&1; then + -Title "$title" -Message "$message" \ + -DistroName "${WSL_DISTRO_NAME:-}" >/dev/null 2>&1; then notify_status=0 else notify_status=$?