191 lines
8.4 KiB
Markdown
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
|
|
```
|