Implement native Windows KSwitcher with configurable hotkeys and clipboard history

This commit is contained in:
2026-10-27 22:14:06 +02:00
commit f683e4178c
37 changed files with 31288 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
"""Regenerate the native multi-resolution icon (development only; requires Pillow)."""
from pathlib import Path
from PIL import Image, ImageDraw
root = Path(__file__).resolve().parents[1] / 'resources'
# Geometric K: white stem/upper arm, mint lower arm on an indigo keycap.
polygons = [('#FFFFFF', [(8, 7), (12, 7), (12, 25), (8, 25)]),
('#FFFFFF', [(11, 15), (22, 6), (25, 9), (14, 18)]),
('#5EEAD4', [(11, 14), (25, 24), (22, 27), (10, 18)])]
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 32 32">\n'
svg += '<rect x="1" y="1" width="30" height="30" rx="7" fill="#4F46E5"/>\n'
for color, points in polygons:
svg += '<polygon fill="' + color + '" points="' + ' '.join(f'{x},{y}' for x,y in points) + '"/>\n'
(root / 'kswitcher.svg').write_text(svg + '</svg>\n', encoding='utf-8')
scale = 32
im = Image.new('RGBA', (32*scale, 32*scale))
draw = ImageDraw.Draw(im)
draw.rounded_rectangle((scale, scale, 31*scale, 31*scale), radius=7*scale, fill='#4F46E5')
for color, points in polygons:
draw.polygon([(x*scale,y*scale) for x,y in points], fill=color)
im = im.resize((256,256),Image.Resampling.LANCZOS)
im.save(root / 'kswitcher.png')
im.save(root / 'kswitcher.ico', sizes=[(s,s) for s in (16,20,24,32,40,48,64,128,256)])
+29
View File
@@ -0,0 +1,29 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot
$resultPath = Join-Path $root 'build\autostart-install-result.json'
try {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not ([Security.Principal.WindowsPrincipal]::new($identity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Administrator approval is required.' }
$exe = Join-Path $root 'build\KSwitcher.exe'
$staged = Join-Path $root 'build\staged\KSwitcher.exe'
$running = @(Get-Process KSwitcher -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $exe })
$exitProcess = Start-Process -FilePath $exe -ArgumentList '--exit' -WindowStyle Hidden -PassThru
if (-not $exitProcess.WaitForExit(5000)) { throw 'KSwitcher exit command timed out.' }
foreach ($process in $running) { if (-not $process.WaitForExit(10000)) { throw 'KSwitcher did not stop; no executable was replaced.' } }
Copy-Item -LiteralPath $exe -Destination (Join-Path $root 'build\staged\KSwitcher.previous.exe') -Force
Copy-Item -LiteralPath $staged -Destination $exe -Force
$helper = Start-Process -FilePath $exe -ArgumentList @('--configure-autostart=2', $identity.User.Value) -WindowStyle Hidden -PassThru
$helper.WaitForExit()
if ($helper.ExitCode -ne 0) { throw 'The native autostart helper failed.' }
$settingsPath = Join-Path $root 'build\data\settings.json'
$settings = Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json
$settings.startWithSystem = $true
$settings | Add-Member -NotePropertyName autostartAsAdministrator -NotePropertyValue $true -Force
[IO.File]::WriteAllText($settingsPath, ($settings | ConvertTo-Json -Depth 12), [Text.UTF8Encoding]::new($false))
Start-ScheduledTask -TaskName 'KSwitcher'
$task = Get-ScheduledTask -TaskName 'KSwitcher'
[pscustomobject]@{success=$true; task=$task.TaskName; runLevel=[string]$task.Principal.RunLevel; logonType=[string]$task.Principal.LogonType; user=$task.Principal.UserId; executable=$task.Actions.Execute; state=[string]$task.State} | ConvertTo-Json | Set-Content -LiteralPath $resultPath -Encoding UTF8
} catch {
[pscustomobject]@{success=$false; error=$_.Exception.Message} | ConvertTo-Json | Set-Content -LiteralPath $resultPath -Encoding UTF8
exit 1
}
+12
View File
@@ -0,0 +1,12 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot
try {
$exe = Join-Path $root 'build\KSwitcher.exe'
$running = @(Get-Process KSwitcher -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $exe })
$stop = Start-Process -FilePath $exe -ArgumentList '--exit' -WindowStyle Hidden -PassThru
if (-not $stop.WaitForExit(5000)) { throw 'Exit command timed out.' }
foreach ($process in $running) { if (-not $process.WaitForExit(10000)) { throw 'KSwitcher did not stop.' } }
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.exe') -Destination $exe -Force
Start-Process -FilePath $exe -WindowStyle Hidden
'OK' | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt')
} catch { $_.Exception.Message | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt'); exit 1 }