Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion config/hk.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,45 @@ local checks = new Mapping<String, Step> {
glob = List("scripts/*.sh")
check = "sh -n {{files}}"
}
["powershell-syntax"] {
glob = List("scripts/*.ps1")
check = """
set -e

if command -v pwsh >/dev/null 2>&1; then
ps_cmd=pwsh
scripts_dir="$PWD/scripts"
elif command -v powershell.exe >/dev/null 2>&1 && command -v wslpath >/dev/null 2>&1; then
ps_cmd=powershell.exe
scripts_dir="$(wslpath -w "$PWD/scripts")"
else
echo 'warning: PowerShell unavailable; skipping PowerShell syntax validation' >&2
exit 0
fi

"$ps_cmd" -NoLogo -NoProfile -NonInteractive -Command '& {
param([string] $ScriptsDir)
$failed = $false

Get-ChildItem -LiteralPath $ScriptsDir -Filter "*.ps1" | ForEach-Object {
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$_.FullName,
[ref] $tokens,
[ref] $errors
) | Out-Null

if ($errors.Count -gt 0) {
$failed = $true
$errors | ForEach-Object { Write-Error "$($_.Extent.File): $_" }
}
}

if ($failed) { exit 1 }
}' "$scripts_dir"
"""
}
["mise-tool-disables"] {
glob = List("conf.d/packages.pacman.toml", "conf.d/platform.omarchy.toml")
check = """
Expand All @@ -23,9 +62,22 @@ local checks = new Mapping<String, Step> {
check = "bash -n {{files}} && shellcheck -x {{files}}"
}
["zsh-syntax"] {
glob = List("../home/.zshrc", "../home/.p10k.zsh")
glob = List(
"../home/.zshrc",
"../home/.p10k.zsh",
"../home/.config/sheldon/plugins/*.zsh",
"tests/*.zsh"
)
check = "zsh -n {{files}}"
}
["wsl-notify"] {
glob = List(
"../home/.config/sheldon/plugins/wsl-notify.plugin.zsh",
"scripts/wsl-notify.ps1",
"tests/wsl-notify.zsh"
)
check = "zsh tests/wsl-notify.zsh"
}
}

hooks {
Expand Down
110 changes: 110 additions & 0 deletions config/scripts/wsl-notify.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
param(
[Parameter(Mandatory = $true)][string] $Title,
[Parameter(Mandatory = $true)][string] $Message
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# Exit codes consumed by wsl-notify.plugin.zsh:
# 0 toast displayed
# 10 Windows Terminal is focused; notification intentionally suppressed
# 11 foreground state could not be determined; fail closed
# 20 native toast delivery failed; the shell may fall back to BEL
$ExitToastShown = 0
$ExitFocused = 10
$ExitFocusUnknown = 11
$ExitDeliveryFailed = 20

function Get-ForegroundProcessName {
try {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public static class ForegroundWindow {
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
}
"@

$window = [ForegroundWindow]::GetForegroundWindow()
if ($window -eq [IntPtr]::Zero) {
return $null
}

$foregroundProcessId = [uint32]0
$threadId = [ForegroundWindow]::GetWindowThreadProcessId($window, [ref] $foregroundProcessId)
if ($threadId -eq 0 -or $foregroundProcessId -eq 0) {
return $null
}

return (Get-Process -Id $foregroundProcessId -ErrorAction Stop).ProcessName
}
catch {
return $null
}
}

function Get-NotificationAppId {
$startApps = @(Get-StartApps)

foreach ($candidate in @(
'Microsoft.WindowsTerminal_8wekyb3d8bbwe!App',
'Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe!App'
)) {
if ($startApps.AppID -contains $candidate) {
return $candidate
}
}

$appId = $startApps |
Where-Object { $_.AppID -match '^Microsoft\.WindowsTerminal.*!App$' } |
Select-Object -First 1 -ExpandProperty AppID

if (-not [string]::IsNullOrWhiteSpace($appId)) {
return $appId
}

return $startApps |
Where-Object { $_.AppID -match 'PowerShell' } |
Select-Object -First 1 -ExpandProperty AppID
}

$foregroundProcess = Get-ForegroundProcessName
if ([string]::IsNullOrWhiteSpace($foregroundProcess)) {
exit $ExitFocusUnknown
}

if ($foregroundProcess -like 'WindowsTerminal*') {
exit $ExitFocused
}

try {
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]

$appId = Get-NotificationAppId
if ([string]::IsNullOrWhiteSpace($appId)) {
throw 'No Start-menu AUMID suitable for WSL toast notifications was found.'
}

$document = [Windows.Data.Xml.Dom.XmlDocument]::new()
$document.LoadXml('<toast><visual><binding template="ToastGeneric"><text/><text/></binding></visual><audio src="ms-winsoundevent:Notification.Default"/></toast>')
$textNodes = $document.GetElementsByTagName('text')
[void] $textNodes.Item(0).AppendChild($document.CreateTextNode($Title))
[void] $textNodes.Item(1).AppendChild($document.CreateTextNode($Message))

$toast = [Windows.UI.Notifications.ToastNotification]::new($document)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
exit $ExitToastShown
}
catch {
[Console]::Error.WriteLine("WSL notification failed: $($_.Exception.Message)")
exit $ExitDeliveryFailed
}
42 changes: 0 additions & 42 deletions config/scripts/wsl-toast.ps1

This file was deleted.

75 changes: 75 additions & 0 deletions config/tests/wsl-notify.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env zsh
set -eu

repo_root=${0:A:h:h:h}
plugin="$repo_root/home/.config/sheldon/plugins/wsl-notify.plugin.zsh"
test_root=$(mktemp -d)
trap 'rm -rf "$test_root"' EXIT
mkdir -p "$test_root/bin"

cat >"$test_root/bin/wslpath" <<'EOF'
#!/bin/sh
printf '%s\n' 'C:\\wsl-notify.ps1'
EOF

cat >"$test_root/bin/powershell.exe" <<'EOF'
#!/bin/sh
printf '%s\n' "$*" >>"$WSL_NOTIFY_TEST_LOG"
exit "${WSL_NOTIFY_TEST_EXIT:-0}"
EOF

chmod +x "$test_root/bin/wslpath" "$test_root/bin/powershell.exe"

export PATH="$test_root/bin:$PATH"
export MISE_CONFIG_DIR="$repo_root/config"
export WSL_DISTRO_NAME=ci
export WT_SESSION=ci
export WSL_NOTIFY_TEST_LOG="$test_root/powershell.log"
unset TERM_PROGRAM
rehash

source "$plugin"

[[ "$(bgnotify_appid)" == '__wsl_notify_dispatch__' ]]
[[ "$bgnotify_termid" == '__wsl_notify_terminal_foreground__' ]]
[[ ! -e "$WSL_NOTIFY_TEST_LOG" ]]

for exit_code in 0 10 11; do
export WSL_NOTIFY_TEST_EXIT=$exit_code
output=$(bgnotify 'build finished' 'command completed' '')
[[ -z $output ]]
done

export WSL_NOTIFY_TEST_EXIT=20
output=$(bgnotify 'build finished' 'command completed' '')
[[ "$output" == $'\a' ]]
[[ "$(wc -l <"$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
# mechanism, just like add-zsh-hook is in an interactive shell.
mkdir -p "$test_root/fpath"
cat >"$test_root/fpath/add-zsh-hook" <<'EOF'
local mode=$1 hook=$2 callback=$3
[[ $mode == -d ]] || return 2

case $hook in
preexec) preexec_functions=(${preexec_functions:#$callback}) ;;
precmd) precmd_functions=(${precmd_functions:#$callback}) ;;
*) return 2 ;;
esac
EOF

zsh -f -c '
typeset -ga preexec_functions precmd_functions
preexec_functions=(bgnotify_begin)
precmd_functions=(bgnotify_end)
function bgnotify_begin {}
function bgnotify_end {}
fpath=("$2" $fpath)
export WSL_DISTRO_NAME=ci
export TERM_PROGRAM=ghostty
source "$1"
[[ -z ${preexec_functions[(r)bgnotify_begin]-} ]]
[[ -z ${precmd_functions[(r)bgnotify_end]-} ]]
' zsh "$plugin" "$test_root/fpath"
Loading