modified README
This commit is contained in:
+170
@@ -0,0 +1,170 @@
|
||||
# cursor-lang
|
||||
|
||||
[English version](README.md)
|
||||
|
||||
Приложение WPF показывает раскладку клавиатуры у курсора.
|
||||
|
||||
## Клонирование
|
||||
|
||||
Двоичные ресурсы — иконка exe и логотипы MSIX — хранятся в Git LFS, поэтому
|
||||
перед клонированием нужно установить [git-lfs](https://git-lfs.com):
|
||||
|
||||
```powershell
|
||||
git lfs install
|
||||
git clone git@git.alrakis.kz:alrakis/cursor-lang.git
|
||||
```
|
||||
|
||||
Клонирование без него проходит успешно, но вместо изображений остаются
|
||||
текстовые файлы-указатели, и сборка затем падает на нечитаемой иконке.
|
||||
Существующий клон чинится командой `git lfs install`, а затем `git lfs pull`.
|
||||
|
||||
## Права администратора
|
||||
|
||||
Приложение работает от имени обычного пользователя. От повышения прав
|
||||
отказались, чтобы приложение можно было опубликовать в Microsoft Store: пакеты
|
||||
MSIX всегда выполняются в контексте вошедшего пользователя, а политика Store
|
||||
отклоняет приложения, которым права администратора нужны для любой части
|
||||
функциональности.
|
||||
|
||||
Цена этого — необязательная горячая клавиша Caps Lock. Пока фокусом владеет
|
||||
окно с более высоким уровнем целостности — Диспетчер задач, Редактор реестра,
|
||||
запросы UAC, — Windows не доставляет нажатия низкоуровневому хуку и не принимает
|
||||
запрос на смену раскладки, так что горячая клавиша там ничего не делает. Чтение
|
||||
раскладки чужого окна не ограничено, поэтому сама подсказка продолжает работать
|
||||
везде.
|
||||
|
||||
## Автозапуск
|
||||
|
||||
Автозапуск включается из настроек приложения тем способом, который доступен
|
||||
сборке. Пакет объявляет его в манифесте как `windows.startupTask`. Сборка,
|
||||
распакованная в папку, прописывается так, как это всегда делали программы для
|
||||
рабочего стола, — в `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, и прав
|
||||
администратора для этого не нужно.
|
||||
|
||||
И в том, и в другом случае Windows показывает приложение в разделе Параметры —
|
||||
Приложения — Автозагрузка; если пользователь выключит его там, приложение уже не
|
||||
сможет включить его обратно и скажет об этом вместо молчаливого отказа. Запись в
|
||||
реестре при этом остаётся на месте: решение пользователя Windows хранит отдельно
|
||||
от неё, в `StartupApproved`, и приложение с ним считается.
|
||||
|
||||
## Настройки
|
||||
|
||||
Расположение `settings.json` зависит от способа установки приложения. Отдельная
|
||||
установка хранит его в `%APPDATA%\CursorLang`. Пакетная сборка хранит его в
|
||||
собственной папке данных пакета, которую Windows удаляет вместе с приложением —
|
||||
предполагается, что приложения из Store не оставляют после себя ничего.
|
||||
|
||||
При первом запуске пакетная сборка подхватывает настройки, оставленные отдельной
|
||||
установкой, и копирует их себе. Исходный файл остаётся на месте: обе сборки могут
|
||||
быть установлены рядом, и приложение не вправе удалять настройки, которые ему не
|
||||
принадлежат.
|
||||
|
||||
## Обновления
|
||||
|
||||
Приложение ищет новые версии среди выпусков собственного репозитория. Выпуск
|
||||
годится, если его тег — это просто версия (`v1.2.3` или `1.2.3`) и к нему
|
||||
приложен пакет MSIX. Тег, в котором есть что-то ещё, — в том числе `v1.2.3-beta`
|
||||
— пропускается: предварительную версию берут намеренно, приложение её не
|
||||
предлагает.
|
||||
|
||||
Из приложенных файлов предпочитается `.msixbundle` — он несёт обе архитектуры.
|
||||
Если его нет, берётся пакет, в имени которого стоит архитектура этой машины:
|
||||
`CursorLang-1.2.3.0-x64.msix`. Такие имена даёт `build-msix.ps1`, так что выпуск
|
||||
делается прикладыванием того, что он собрал.
|
||||
|
||||
Пакет скачивается во временную папку и передаётся установщику приложений
|
||||
Windows: тот показывает издателя, спрашивает подтверждение и заменяет
|
||||
установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску
|
||||
пакет должен быть подписан — неподписанный установится только на машине в режиме
|
||||
разработчика. Работающее приложение до перезапуска продолжает жить на старых
|
||||
файлах.
|
||||
|
||||
У приложения, установленного из Store, раздела обновлений нет вовсе: его
|
||||
обновляет Store, а пакет со стороны Windows поверх него всё равно не примет.
|
||||
|
||||
Само по себе приложение спрашивает о выпусках раз в сутки, при запуске, и хранит
|
||||
дату последней удачной проверки в настройках. Там же её можно выключить — тогда
|
||||
остаётся кнопка в окне настроек, делающая то же самое по требованию.
|
||||
|
||||
Где искать выпуски, задаётся в `UpdateOptions`: репозиторий принадлежит тому, кто
|
||||
выпускает приложение, а не пользователю, поэтому значения живут в сборке, а не в
|
||||
`settings.json`:
|
||||
|
||||
```csharp
|
||||
services.AddSingleton(new UpdateOptions
|
||||
{
|
||||
ServiceUri = new Uri("https://git.alrakis.kz/"), // сам сервер Gitea
|
||||
Project = "alrakis/cursor-lang",
|
||||
});
|
||||
```
|
||||
|
||||
Выпуски берутся из Gitea, а её API живёт на самом сервере: адрес — тот же, по
|
||||
которому репозиторий открывают в браузере, и под ним приложение спрашивает
|
||||
`/api/v1/repos/{владелец}/{репозиторий}/releases`.
|
||||
|
||||
Закрытому репозиторию нужен токен. Он читается из переменной окружения
|
||||
`CURSORLANG_UPDATE_TOKEN`, а не хранится в исходниках: секрет, встроенный в
|
||||
сборку, — это секрет, отданный всем, кто эту сборку получил.
|
||||
|
||||
## Тесты
|
||||
|
||||
```powershell
|
||||
dotnet test
|
||||
```
|
||||
|
||||
Тесты лежат в `CursorLang.Tests` и работают на xUnit. Половина приложения —
|
||||
окна, таймеры диспетчера, перехват клавиатуры — живёт только на потоке STA
|
||||
с очередью сообщений, поэтому тесты держат один такой поток на весь прогон
|
||||
и выполняют на нём всё, что этого требует.
|
||||
|
||||
Части проверок нужно настоящее окно переднего плана: положение каретки и
|
||||
просьбу сменить раскладку видно только там. Право вывести окно вперёд Windows
|
||||
даёт не всегда, и такие проверки сообщают о себе как о пропущенных, а не как
|
||||
о провалившихся — без окна переднего плана проверять нечего. Сквозные проверки
|
||||
запускают собранное приложение отдельным процессом и пропускают себя, если
|
||||
приложение уже работает: вмешиваться в чужой запущенный экземпляр они не вправе.
|
||||
|
||||
Покрытие снимается так:
|
||||
|
||||
```powershell
|
||||
dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage.runsettings
|
||||
```
|
||||
|
||||
Непокрытым остаётся то, до чего тестовому процессу не дотянуться: пути, которым
|
||||
нужен установленный пакет MSIX — `StartupTask` и папка данных пакета, — и
|
||||
композиционный корень в `App.xaml.cs`, который вместо этого проверяется сквозным
|
||||
запуском приложения.
|
||||
|
||||
## Сборка пакета MSIX
|
||||
|
||||
Ни Visual Studio, ни Windows SDK не требуются — `makeappx` и `signtool`
|
||||
поставляются из пакета NuGet. Порядок публикации целиком — от регистрации
|
||||
разработчика до отправки на проверку — описан в
|
||||
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md).
|
||||
|
||||
```powershell
|
||||
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
|
||||
powershell -File Packaging\New-Assets.ps1
|
||||
|
||||
# Сборка и подпись для локальной проверки
|
||||
powershell -File Packaging\build-msix.ps1 -Architectures x64 -Sign
|
||||
|
||||
# Сборка для Store — identity берётся из Partner Center
|
||||
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||
-IdentityName 12345Alrakis.CursorLang -Publisher "CN=ABCD1234-..."
|
||||
```
|
||||
|
||||
Результат — `artifacts\packages\CursorLang-<version>.msixbundle`, покрывающий x64
|
||||
и arm64. Загружать в Partner Center его нужно неподписанным — Store подписывает
|
||||
пакет собственным сертификатом.
|
||||
|
||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||
MSIX не может установить среду выполнения как зависимость пакета.
|
||||
|
||||
Чтобы попробовать пакет без установки, зарегистрируйте опубликованный layout —
|
||||
для этого нужен режим разработчика и не нужна подпись вовсе:
|
||||
|
||||
```powershell
|
||||
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
|
||||
Remove-AppxPackage (Get-AppxPackage -Name Alrakis.CursorLang).PackageFullName
|
||||
```
|
||||
@@ -1,10 +1,168 @@
|
||||
# cursor-lang
|
||||
|
||||
The WPF application displays the keyboard layout at the cursor
|
||||
[Русская версия](README.RU.md)
|
||||
|
||||
The app requests administrator rights on startup. Windows does not deliver
|
||||
keyboard input to a low-level hook while an app of a higher integrity level
|
||||
holds the focus, so without elevation the Caps Lock hotkey would silently stop
|
||||
working in Task Manager, Registry Editor and anything else started as
|
||||
administrator. Note that a plain shortcut in the startup folder cannot launch an
|
||||
elevated app — use a Task Scheduler task with the "highest privileges" flag.
|
||||
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.
|
||||
|
||||
## Building the MSIX package
|
||||
|
||||
Neither Visual Studio nor the Windows SDK is required — `makeappx` and
|
||||
`signtool` come 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
|
||||
|
||||
# Build and sign for local testing
|
||||
powershell -File Packaging\build-msix.ps1 -Architectures x64 -Sign
|
||||
|
||||
# Build for the Store — identity comes from Partner Center
|
||||
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||
-IdentityName 12345Alrakis.CursorLang -Publisher "CN=ABCD1234-..."
|
||||
```
|
||||
|
||||
The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64
|
||||
and arm64. Upload it to Partner Center unsigned — the Store signs it with its own
|
||||
certificate.
|
||||
|
||||
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 Alrakis.CursorLang).PackageFullName
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user