diff --git a/Packaging/AppxManifest.xml b/Packaging/AppxManifest.xml
new file mode 100644
index 0000000..fabfd6c
--- /dev/null
+++ b/Packaging/AppxManifest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ CursorLang
+ {PublisherDisplayName}
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Packaging/Assets/Square150x150Logo.png b/Packaging/Assets/Square150x150Logo.png
new file mode 100644
index 0000000..e736297
--- /dev/null
+++ b/Packaging/Assets/Square150x150Logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5dd71d408e30fd30890253ddb0f18fffc4cdff00cfcedb369dc97018def7d1b1
+size 2371
diff --git a/Packaging/Assets/Square310x310Logo.png b/Packaging/Assets/Square310x310Logo.png
new file mode 100644
index 0000000..0f177f4
--- /dev/null
+++ b/Packaging/Assets/Square310x310Logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2090657c5f70a1bae62027662760b42935ed80ec72f490e79c23afd4aefe4bc3
+size 5208
diff --git a/Packaging/Assets/Square44x44Logo.png b/Packaging/Assets/Square44x44Logo.png
new file mode 100644
index 0000000..ba0a816
--- /dev/null
+++ b/Packaging/Assets/Square44x44Logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fdb6c1f43b74d2b026ba8190de9d6d0d5fdfd87a5b5c4bb66274b22a16719ab8
+size 681
diff --git a/Packaging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Packaging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000..64df46c
--- /dev/null
+++ b/Packaging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cd4d4d1acde872813d54db3fd675674e6b173a5c6ed28622021a05060676f028
+size 311
diff --git a/Packaging/Assets/Square44x44Logo.targetsize-32_altform-unplated.png b/Packaging/Assets/Square44x44Logo.targetsize-32_altform-unplated.png
new file mode 100644
index 0000000..b846abb
--- /dev/null
+++ b/Packaging/Assets/Square44x44Logo.targetsize-32_altform-unplated.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7159b6aab969ddd2086c2d258bacefd51da6fc7206061bf918a339221c650427
+size 363
diff --git a/Packaging/Assets/Square44x44Logo.targetsize-48_altform-unplated.png b/Packaging/Assets/Square44x44Logo.targetsize-48_altform-unplated.png
new file mode 100644
index 0000000..540ff22
--- /dev/null
+++ b/Packaging/Assets/Square44x44Logo.targetsize-48_altform-unplated.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:696c09cde205529561c0a19e8567916b2efe63eba70cf3059508a1905dd9fe38
+size 490
diff --git a/Packaging/Assets/Square71x71Logo.png b/Packaging/Assets/Square71x71Logo.png
new file mode 100644
index 0000000..5fad8c6
--- /dev/null
+++ b/Packaging/Assets/Square71x71Logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4d6c224ecb06e2956520c61df781734147bc8dc58c66ac4c08e112d16c7f87c
+size 1036
diff --git a/Packaging/Assets/StoreLogo.png b/Packaging/Assets/StoreLogo.png
new file mode 100644
index 0000000..76b9772
--- /dev/null
+++ b/Packaging/Assets/StoreLogo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:addf77784e8d3af26e0928b3655879d03186fe10fed519689e32ca1f9480fcb9
+size 792
diff --git a/Packaging/Assets/Wide310x150Logo.png b/Packaging/Assets/Wide310x150Logo.png
new file mode 100644
index 0000000..416bb2a
--- /dev/null
+++ b/Packaging/Assets/Wide310x150Logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed46b7353fb7d8ef4adb85c8a9f6c97be06ac0cf08640ef1c48e830fffcc96dd
+size 2729
diff --git a/Packaging/New-Assets.ps1 b/Packaging/New-Assets.ps1
new file mode 100644
index 0000000..3c89fa8
--- /dev/null
+++ b/Packaging/New-Assets.ps1
@@ -0,0 +1,232 @@
+<#
+.SYNOPSIS
+ Draws the application icons: the logos for MSIX and the .ico for the exe.
+
+.DESCRIPTION
+ The icons are drawn by code rather than kept as pictures so that the whole
+ set of sizes is rebuilt by a single command whenever another drawing is
+ wanted.
+
+ The current drawing is a placeholder: a dark rounded square with a white
+ "Aя". Before publishing to the Store it is worth replacing with a real one:
+ rewriting New-Logo is enough, the rest of the script does not depend on the
+ drawing.
+
+.EXAMPLE
+ pwsh -File Packaging\New-Assets.ps1
+#>
+[CmdletBinding()]
+param(
+ # Where to put the MSIX logos
+ [string] $AssetsPath,
+
+ # Where to put the .ico for the exe
+ [string] $IconPath
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+# The defaults are worked out here rather than in the parameter declarations:
+# in Windows PowerShell $PSScriptRoot is still empty inside param()
+$root = $PSScriptRoot
+if (-not $AssetsPath) { $AssetsPath = Join-Path $root 'Assets' }
+if (-not $IconPath) { $IconPath = Join-Path $root '..\CursorLang\Resources\CursorLang.ico' }
+
+Add-Type -AssemblyName System.Drawing
+
+$Background = [System.Drawing.Color]::FromArgb(255, 0x20, 0x20, 0x20)
+$Foreground = [System.Drawing.Color]::FromArgb(255, 0xFF, 0xFF, 0xFF)
+$Glyph = 'Aя'
+
+function New-Logo {
+ param(
+ [int] $Width,
+ [int] $Height,
+
+ # The plate under the glyph. The Windows taskbar draws its shortcut
+ # without one, so for those sizes only the white glyph remains
+ [bool] $WithPlate = $true
+ )
+
+ $bitmap = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
+
+ try {
+ $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
+ $graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
+ $graphics.Clear([System.Drawing.Color]::Transparent)
+
+ $side = [Math]::Min($Width, $Height)
+
+ if ($WithPlate) {
+ # The corner radius is taken as a share of the side so that small
+ # sizes do not turn into a circle and large ones do not look like a
+ # plain square
+ $radius = [Math]::Max(2, [int]($side * 0.18))
+ $plate = New-Object System.Drawing.Drawing2D.GraphicsPath
+
+ $plate.AddArc(0, 0, $radius * 2, $radius * 2, 180, 90)
+ $plate.AddArc($Width - $radius * 2 - 1, 0, $radius * 2, $radius * 2, 270, 90)
+ $plate.AddArc($Width - $radius * 2 - 1, $Height - $radius * 2 - 1, $radius * 2, $radius * 2, 0, 90)
+ $plate.AddArc(0, $Height - $radius * 2 - 1, $radius * 2, $radius * 2, 90, 90)
+ $plate.CloseFigure()
+
+ $brush = New-Object System.Drawing.SolidBrush($Background)
+ try { $graphics.FillPath($brush, $plate) } finally { $brush.Dispose(); $plate.Dispose() }
+ }
+
+ # Typographic metrics leave out the side bearings MeasureString reports by
+ # default. Measured with those bearings the glyph looks wider than it is,
+ # the fitted type comes out too small, and at 16 px the strokes wash out
+ [System.Drawing.StringFormat] $format = [System.Drawing.StringFormat]::GenericTypographic.Clone()
+
+ try {
+ # The type size is found by trying: with different tile sizes the same
+ # share of the side gives text that is either cramped or too small
+ $target = $side * 0.80
+ $ceiling = $side * 0.86
+ $fontSize = $side * 0.9
+ $font = $null
+ $measured = $null
+
+ for ($attempt = 0; $attempt -lt 20; $attempt++) {
+ if ($font) { $font.Dispose() }
+ $font = New-Object System.Drawing.Font('Segoe UI', $fontSize, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Pixel)
+ $measured = $graphics.MeasureString($Glyph, $font, ([System.Drawing.PointF]::new(0, 0)), $format)
+
+ if ($measured.Width -le $target -and $measured.Height -le $ceiling) { break }
+
+ # Both dimensions are pulled in at once, otherwise a tall glyph
+ # keeps overflowing while the width already fits
+ $scale = [Math]::Min($target / $measured.Width, $ceiling / $measured.Height)
+ $fontSize = $fontSize * $scale * 0.98
+ }
+
+ try {
+ # AntiAliasGridFit snaps the stems onto the pixel grid, so the
+ # glyph is placed at a whole pixel: a fractional origin spreads
+ # every stem across two pixels, which is what blurred the small
+ # sizes when the text was centred inside a RectangleF
+ $x = [Math]::Round(($Width - $measured.Width) / 2.0)
+ $y = [Math]::Round(($Height - $measured.Height) / 2.0)
+
+ $brush = New-Object System.Drawing.SolidBrush($Foreground)
+ $origin = New-Object System.Drawing.PointF($x, $y)
+
+ try { $graphics.DrawString($Glyph, $font, $brush, $origin, $format) }
+ finally { $brush.Dispose() }
+ }
+ finally {
+ $font.Dispose()
+ }
+ }
+ finally {
+ $format.Dispose()
+ }
+ }
+ finally {
+ $graphics.Dispose()
+ }
+
+ return $bitmap
+}
+
+function Save-Logo {
+ param(
+ [string] $Path,
+ [int] $Width,
+ [int] $Height,
+ [bool] $WithPlate = $true
+ )
+
+ $bitmap = New-Logo -Width $Width -Height $Height -WithPlate $WithPlate
+ try { $bitmap.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) } finally { $bitmap.Dispose() }
+ Write-Host " $([System.IO.Path]::GetFileName($Path))"
+}
+
+function Save-Icon {
+ param(
+ [string] $Path,
+ [int[]] $Sizes
+ )
+
+ # The .ico is put together by hand: System.Drawing can only save a single
+ # size to that format, and the icon of the exe is needed in several at once
+ $images = @()
+
+ foreach ($size in $Sizes) {
+ $bitmap = New-Logo -Width $size -Height $size
+ try {
+ $stream = New-Object System.IO.MemoryStream
+ $bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
+ $images += , @{ Size = $size; Bytes = $stream.ToArray() }
+ $stream.Dispose()
+ }
+ finally {
+ $bitmap.Dispose()
+ }
+ }
+
+ $output = New-Object System.IO.MemoryStream
+ $writer = New-Object System.IO.BinaryWriter($output)
+
+ try {
+ $writer.Write([uint16]0) # reserved
+ $writer.Write([uint16]1) # type: icon
+ $writer.Write([uint16]$images.Count)
+
+ # The directory of entries comes before the pictures themselves, so the
+ # offsets are worked out beforehand: the header plus 16 bytes per entry
+ $offset = 6 + 16 * $images.Count
+
+ foreach ($image in $images) {
+ # 256 does not fit in a byte and is written as zero — the format
+ # means it that way
+ $dimension = if ($image.Size -ge 256) { 0 } else { $image.Size }
+
+ $writer.Write([byte]$dimension) # width
+ $writer.Write([byte]$dimension) # height
+ $writer.Write([byte]0) # palette colours: no palette
+ $writer.Write([byte]0) # reserved
+ $writer.Write([uint16]1) # planes
+ $writer.Write([uint16]32) # bits per pixel
+ $writer.Write([uint32]$image.Bytes.Length)
+ $writer.Write([uint32]$offset)
+
+ $offset += $image.Bytes.Length
+ }
+
+ foreach ($image in $images) {
+ $writer.Write($image.Bytes)
+ }
+
+ $writer.Flush()
+ [System.IO.File]::WriteAllBytes($Path, $output.ToArray())
+ }
+ finally {
+ $writer.Dispose()
+ $output.Dispose()
+ }
+
+ Write-Host " $([System.IO.Path]::GetFileName($Path))"
+}
+
+New-Item -ItemType Directory -Path $AssetsPath -Force | Out-Null
+New-Item -ItemType Directory -Path (Split-Path -Parent $IconPath) -Force | Out-Null
+
+Write-Host 'MSIX logos:'
+Save-Logo -Path (Join-Path $AssetsPath 'StoreLogo.png') -Width 50 -Height 50
+Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.png') -Width 44 -Height 44
+Save-Logo -Path (Join-Path $AssetsPath 'Square71x71Logo.png') -Width 71 -Height 71
+Save-Logo -Path (Join-Path $AssetsPath 'Square150x150Logo.png') -Width 150 -Height 150
+Save-Logo -Path (Join-Path $AssetsPath 'Square310x310Logo.png') -Width 310 -Height 310
+Save-Logo -Path (Join-Path $AssetsPath 'Wide310x150Logo.png') -Width 310 -Height 150
+
+# The taskbar and the application list take the shortcut without a plate
+Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-24_altform-unplated.png') -Width 24 -Height 24 -WithPlate $false
+Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-32_altform-unplated.png') -Width 32 -Height 32 -WithPlate $false
+Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-48_altform-unplated.png') -Width 48 -Height 48 -WithPlate $false
+
+Write-Host 'The exe icon:'
+Save-Icon -Path ([System.IO.Path]::GetFullPath($IconPath)) -Sizes @(16, 24, 32, 48, 64, 128, 256)
diff --git a/Packaging/Tools/SdkTools.csproj b/Packaging/Tools/SdkTools.csproj
new file mode 100644
index 0000000..cf72376
--- /dev/null
+++ b/Packaging/Tools/SdkTools.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+ net10.0
+ false
+
+
+
+
+
+ all
+ true
+
+
+
+
diff --git a/Packaging/build-msix.ps1 b/Packaging/build-msix.ps1
new file mode 100644
index 0000000..e2f4b22
--- /dev/null
+++ b/Packaging/build-msix.ps1
@@ -0,0 +1,203 @@
+<#
+.SYNOPSIS
+ Builds the CursorLang MSIX package for the Microsoft Store.
+
+.DESCRIPTION
+ Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a
+ NuGet package (Tools\SdkTools.csproj) and the application is built by the
+ plain .NET SDK.
+
+ The package is handed to Partner Center as it comes out of here — the Store
+ is where it gets everything else done to it.
+
+ The application is published with its own copy of .NET: Windows does not
+ carry one, and MSIX cannot install the runtime as a package dependency.
+
+.PARAMETER IdentityName
+ The identity of the package — from Partner Center, the "Product identity"
+ section. It is handed out there together with the reserved application name;
+ the default is only good enough for a check on your own machine.
+
+.EXAMPLE
+ # A check on your own machine: your architecture alone
+ pwsh -File Packaging\build-msix.ps1 -Architectures x64
+
+.EXAMPLE
+ # A build for the Store — the identity comes from Partner Center
+ pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
+ -IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
+#>
+[CmdletBinding()]
+param(
+ # Four numbers, the last one has to be 0: that is what the Store requires
+ [string] $Version = '1.0.0.0',
+
+ [string] $IdentityName = 'CursorLang',
+ [string] $Publisher = 'CN=Aleksandr Neychev',
+ [string] $PublisherDisplayName = 'Aleksandr Neychev',
+
+ [ValidateSet('x64', 'arm64')]
+ [string[]] $Architectures = @('x64', 'arm64'),
+
+ [string] $OutputPath
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$root = $PSScriptRoot
+$repository = Split-Path -Parent $root
+$project = Join-Path $repository 'CursorLang\CursorLang.csproj'
+$assets = Join-Path $root 'Assets'
+$manifestTemplate = Join-Path $root 'AppxManifest.xml'
+$toolsProject = Join-Path $root 'Tools\SdkTools.csproj'
+
+if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' }
+$layoutRoot = Join-Path $OutputPath 'layout'
+$packagesPath = Join-Path $OutputPath 'packages'
+
+if ($Version -notmatch '^\d+\.\d+\.\d+\.0$') {
+ throw "The version '$Version' does not fit: the Store takes four numbers ending in zero, for example 1.0.0.0."
+}
+
+if (-not (Test-Path $assets)) {
+ throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first."
+}
+
+function Invoke-Tool {
+ <#
+ .SYNOPSIS
+ Runs a program and fails the build if it returned an error.
+ .DESCRIPTION
+ A wrapper of our own is needed because PowerShell does not treat the
+ failure of an external program as an error and quietly moves on.
+ #>
+ param(
+ [string] $Path,
+ [string[]] $Arguments
+ )
+
+ & $Path @Arguments
+ if ($LASTEXITCODE -ne 0) {
+ throw "'$([System.IO.Path]::GetFileName($Path))' exited with code $LASTEXITCODE."
+ }
+}
+
+function Get-SdkToolsPath {
+ <#
+ .SYNOPSIS
+ Returns the folder of the restored Windows SDK Build Tools package.
+ .DESCRIPTION
+ The path is asked of MSBuild rather than searched for in the package
+ cache: the project pins one version of the package, and MSBuild
+ names exactly it. A search would also find the versions left over
+ from other builds.
+ #>
+ $path = (& dotnet msbuild $toolsProject -getProperty:PkgMicrosoft_Windows_SDK_BuildTools -nologo) |
+ Where-Object { $_ -and $_.Trim() } |
+ Select-Object -Last 1
+
+ if ($LASTEXITCODE -ne 0 -or -not $path -or -not (Test-Path $path.Trim())) {
+ throw 'The Windows SDK Build Tools package folder could not be found. Did the restore go through?'
+ }
+
+ return $path.Trim()
+}
+
+function Get-SdkTool {
+ <#
+ .SYNOPSIS
+ Returns the path of a program from the Windows SDK Build Tools package.
+ #>
+ param(
+ [string] $Name,
+ [string] $PackagePath
+ )
+
+ # The bitness of the program has nothing to do with the bitness of the
+ # package being built: we take the one that runs on this machine
+ $host64 = if ([Environment]::Is64BitOperatingSystem) { 'x64' } else { 'x86' }
+
+ $tool = Get-ChildItem (Join-Path $PackagePath 'bin') -Recurse -Filter $Name -ErrorAction SilentlyContinue |
+ Where-Object { $_.FullName -match "\\$host64\\" } |
+ Select-Object -First 1
+
+ if (-not $tool) {
+ throw "'$Name' was not found in the Windows SDK Build Tools package."
+ }
+
+ return $tool.FullName
+}
+
+Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan
+Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
+
+$sdkTools = Get-SdkToolsPath
+$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools
+
+Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
+New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
+
+$built = @()
+
+foreach ($architecture in $Architectures) {
+ Write-Host "Building $architecture..." -ForegroundColor Cyan
+
+ $layout = Join-Path $layoutRoot $architecture
+
+ Invoke-Tool -Path 'dotnet' -Arguments @(
+ 'publish', $project,
+ '--configuration', 'Release',
+ '--runtime', "win-$architecture",
+ '--self-contained', 'true',
+ "-p:Version=$($Version.Substring(0, $Version.LastIndexOf('.')))",
+ '--output', $layout,
+ '--nologo'
+ )
+
+ # Debug symbols have no place in the package: they take up room, the user
+ # has no use for them, and for crash reports the Store takes them separately
+ Get-ChildItem $layout -Recurse -Filter '*.pdb' | Remove-Item -Force
+
+ Copy-Item $assets -Destination (Join-Path $layout 'Assets') -Recurse -Force
+
+ $manifest = (Get-Content $manifestTemplate -Raw).
+ Replace('{IdentityName}', $IdentityName).
+ Replace('{Publisher}', $Publisher).
+ Replace('{PublisherDisplayName}', $PublisherDisplayName).
+ Replace('{Version}', $Version).
+ Replace('{Architecture}', $architecture)
+
+ Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8
+
+ $package = Join-Path $packagesPath "CursorLang-$Version-$architecture.msix"
+ Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package)
+
+ $built += $package
+}
+
+$result = $built[0]
+
+if ($built.Count -gt 1) {
+ Write-Host 'Bringing the architectures into one package...' -ForegroundColor Cyan
+
+ # makeappx bundle takes everything from a folder, so the separate packages
+ # are gathered into one of their own first — otherwise the results of
+ # earlier builds would end up in the bundle
+ $bundleInput = Join-Path $OutputPath 'bundle'
+ Remove-Item $bundleInput -Recurse -Force -ErrorAction SilentlyContinue
+ New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null
+ $built | ForEach-Object { Copy-Item $_ -Destination $bundleInput }
+
+ $result = Join-Path $packagesPath "CursorLang-$Version.msixbundle"
+ Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version)
+
+ Remove-Item $bundleInput -Recurse -Force
+}
+
+Write-Host ''
+Write-Host 'Done.' -ForegroundColor Green
+Write-Host " $result"
+
+Write-Host ''
+Write-Host ' This file is uploaded to Partner Center as it is.'