Files
cursor-lang/README.md
T
alex 08192d317e
Release / release (push) Successful in 8m58s
fixed tests
2026-08-11 16:34:28 +05:00

191 lines
8.4 KiB
Markdown

# cursor-lang
[Русская версия](README.RU.md)
The WPF application displays the keyboard layout at the cursor.
## Cloning
Binary assets — the exe icon and the MSIX logos — are stored in Git LFS, so
[git-lfs](https://git-lfs.com) has to be installed before cloning:
```powershell
git lfs install
git clone git@git.alrakis.kz:alrakis/cursor-lang.git
```
Cloning without it succeeds but leaves text pointer files in place of the
images, and the build then fails on an unreadable icon. An existing clone is
repaired with `git lfs install` followed by `git lfs pull`.
## Administrator rights
The app runs as the ordinary user. Elevation was dropped so that the app can be
published in the Microsoft Store: MSIX packages always run in the context of the
signed-in user, and Store policy rejects apps that need administrator rights for
any part of their functionality.
The cost is the optional Caps Lock hotkey. While a window of a higher integrity
level holds the focus — Task Manager, Registry Editor, UAC prompts — Windows
neither delivers keystrokes to the low-level hook nor accepts the layout change
request, so the hotkey does nothing there. Reading the layout of a foreign window
is not restricted, so the tooltip itself keeps working everywhere.
## Startup
Startup is switched on from the app's settings by whichever means the build has.
A package declares it in the manifest as a `windows.startupTask`. A build unpacked
into a folder registers itself the way desktop programs always have — under
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, which needs no administrator
rights.
Either way Windows lists the app in Settings — Apps — Startup; if the user turns
it off there, the app can no longer turn it back on and says so instead of
silently failing. The registry entry stays where it is in that case: Windows keeps
the user's verdict apart from it, under `StartupApproved`, and the app obeys it.
## Settings
Where `settings.json` lives depends on how the app was installed. A separate
install keeps it in `%APPDATA%\CursorLang`. The packaged build keeps it in the
package's own data folder, which Windows removes together with the app — Store
apps are expected to leave nothing behind.
On its first run the packaged build picks up the settings left by a separate
install and copies them over. The original file stays where it is: both builds
may be installed side by side, and the app has no business deleting settings it
does not own.
## Updates
The app looks for new versions among the releases of its own repository. A
release counts when its tag is a plain version — `v1.2.3` or `1.2.3` — and an
MSIX package is attached to it. A tag with anything else in it, `v1.2.3-beta`
among them, is passed over: a pre-release version is asked for on purpose, not
offered by the app.
Out of the attached files the `.msixbundle` is preferred — it carries both
architectures. Failing that, the package whose name holds the architecture of
this machine is taken: `CursorLang-1.2.3.0-x64.msix`. Those are the names
`build-msix.ps1` produces, so a release is made by attaching what it built.
The package is downloaded to the temp folder and handed to the Windows app
installer: it shows the publisher, asks for a confirmation and replaces the
installed version. Windows checks the signature, so the package attached to a
release has to be signed — an unsigned one installs nowhere but a machine in
developer mode. The running app keeps working off the old files until it is
restarted.
An app installed from the Store has no updates section at all: the Store
updates it, and a package from the side is something Windows would not accept
over it anyway.
By itself the app asks about releases once a day, at startup, and remembers the
date of the last successful check in the settings. The check can be turned off
there, which leaves the button in the settings window doing the same on demand.
Where the releases are looked for is set in `UpdateOptions` — the repository
belongs to whoever publishes the app, not to the user, so the values live in the
build rather than in `settings.json`:
```csharp
services.AddSingleton(new UpdateOptions
{
ServiceUri = new Uri("https://git.alrakis.kz/"), // the Gitea server itself
Project = "alrakis/cursor-lang",
});
```
The releases come from Gitea, and its API sits on the server itself: the address
is the one the repository is opened at in a browser, and the app asks
`/api/v1/repos/{owner}/{repo}/releases` under it.
A closed repository needs a token. It is read from the `CURSORLANG_UPDATE_TOKEN`
environment variable rather than kept in the source: a secret built into the app
is a secret handed to everyone who got the app.
## Tests
```powershell
dotnet test
```
The suite lives in `CursorLang.Tests` and runs on xUnit. Half of the app —
windows, dispatcher timers, the keyboard hook — only works on an STA thread with
a message loop, so the tests keep one such thread for the whole run and drive
everything through it.
A few checks need a real foreground window: the caret position and the layout
switch request are only observable there. Windows does not always grant the
right to bring a window forward, and those checks report themselves as skipped
rather than as failures — there is nothing to verify without a foreground
window. The end-to-end checks start the built application as a separate process
and skip themselves if the app is already running: interfering with someone
else's running instance is not their business.
Coverage is collected with:
```powershell
dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage.runsettings
```
What stays uncovered is what a test process cannot reach: the code paths that
require an MSIX package identity — `StartupTask` and the package data folder —
and the composition root in `App.xaml.cs`, which is exercised end-to-end instead.
## Continuous integration
The pipelines live in `.gitea/workflows` and run on Gitea Actions. A pull
request into `master` is built and tested; a tag of the form `v1.2.3` is built,
tested, packed into an MSIX and published as a release with the packages
attached. The version is taken from the tag alone — a tag shaped any other way
stops the run right at the start. The package version ends up as `1.2.3.0`: the
Store takes four numbers and keeps the last one for itself, so the tag has no
say in it.
Both pipelines ask for a Windows runner labelled `windows-x64` with the .NET 10
SDK and git-lfs on it. The checkout pulls LFS files: without them the icon is a
text pointer and the build fails on it. The runner is better off working in an
interactive desktop session: the tests raise real windows, and the checks that
need a desktop of their own — the end-to-end ones, and those that ask for the
foreground window or the caret — skip themselves on a runner that lives as a
service in session 0, where there is no desktop to show a window on.
The package the release carries goes to Partner Center as it is. The identity
comes from repository variables and falls back to the defaults of the script when
unset: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and `MSIX_PUBLISHER_DISPLAY_NAME`.
## Building the MSIX package
Neither Visual Studio nor the Windows SDK is required — `makeappx` comes from a
NuGet package. The full publishing procedure — from opening a developer account
to submitting for certification — is written up in
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md) (Russian only).
```powershell
# One-off: draw the logos and the exe icon (already committed, rerun after edits)
powershell -File Packaging\New-Assets.ps1
# A check on your own machine: your architecture alone
powershell -File Packaging\build-msix.ps1 -Architectures x64
# For Partner Center — the identity is the one reserved there
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
```
The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64
and arm64; next to it lie the packages of single architectures. Upload the bundle
to Partner Center as it is.
The app ships with its own copy of .NET: Windows does not include .NET 10, and
MSIX cannot install a runtime as a package dependency.
To try the package without installing it, register the published layout — this
needs Developer Mode and no signature at all:
```powershell
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
```