13 KiB
cursor-lang
Приложение WPF показывает раскладку клавиатуры у курсора.
Клонирование
Двоичные ресурсы — иконка exe и логотипы MSIX — хранятся в Git LFS, поэтому перед клонированием нужно установить git-lfs:
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:
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, а не хранится в исходниках: секрет, встроенный в
сборку, — это секрет, отданный всем, кто эту сборку получил.
Тесты
dotnet test
Тесты лежат в CursorLang.Tests и работают на xUnit. Половина приложения —
окна, таймеры диспетчера, перехват клавиатуры — живёт только на потоке STA
с очередью сообщений, поэтому тесты держат один такой поток на весь прогон
и выполняют на нём всё, что этого требует.
Части проверок нужно настоящее окно переднего плана: положение каретки и просьбу сменить раскладку видно только там. Право вывести окно вперёд Windows даёт не всегда, и такие проверки сообщают о себе как о пропущенных, а не как о провалившихся — без окна переднего плана проверять нечего. Сквозные проверки запускают собранное приложение отдельным процессом и пропускают себя, если приложение уже работает: вмешиваться в чужой запущенный экземпляр они не вправе.
Покрытие снимается так:
dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage.runsettings
Непокрытым остаётся то, до чего тестовому процессу не дотянуться: пути, которым
нужен установленный пакет MSIX — StartupTask и папка данных пакета, — и
композиционный корень в App.xaml.cs, который вместо этого проверяется сквозным
запуском приложения.
Непрерывная сборка
Пайплайны лежат в .gitea/workflows и работают на Gitea Actions. Запрос
на слияние в master собирается и проверяется тестами; по тегу вида v1.2.3.0
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
с приложенными пакетами. Номер версии берётся только из тега — тег любого
другого вида останавливает прогон в самом начале.
Обоим пайплайнам нужен runner под Windows с меткой windows-x64, на нём —
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
остаётся текстовой заглушкой и сборка на ней падает. Работать runner должен
в интерактивном сеансе рабочего стола — тесты поднимают настоящие окна,
а службе в нулевом сеансе ждать нечего.
Пакет, который несёт релиз, загружается в Partner Center как есть. Identity
берётся из переменных репозитория, а если те не заданы — из значений по
умолчанию в скрипте: MSIX_IDENTITY_NAME, MSIX_PUBLISHER
и MSIX_PUBLISHER_DISPLAY_NAME.
Сборка пакета MSIX
Ни Visual Studio, ни Windows SDK не требуются — makeappx поставляется из
пакета NuGet. Порядок публикации целиком — от регистрации разработчика
до отправки на проверку — описан в
Packaging/PUBLISHING.RU.md.
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
powershell -File Packaging\New-Assets.ps1
# Проверка на своей машине: только своя архитектура
powershell -File Packaging\build-msix.ps1 -Architectures x64
# Для Partner Center — identity та, что зарезервирована там
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
Результат — artifacts\packages\CursorLang-<версия>.msixbundle, покрывающий x64
и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
Center как есть.
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а MSIX не может установить среду выполнения как зависимость пакета.
Чтобы попробовать пакет без установки, зарегистрируйте опубликованный layout — для этого нужен режим разработчика и не нужна подпись вовсе:
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName