Files
cursor-lang/README.RU.md
T
2026-08-12 04:59:22 +05:00

16 KiB

cursor-lang

English version

Приложение 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 не доставляет нажатия низкоуровневому хуку и не принимает запрос на смену раскладки, так что горячая клавиша там ничего не делает. Чтение раскладки чужого окна не ограничено, поэтому сама подсказка продолжает работать везде.

Трей

Приложение работает в фоне и живёт в области уведомлений. Окно настроек — гость на экране, а не само приложение: оно показывается после установки и всякий раз, когда его просят иконка или её меню. Обе кнопки в заголовке окна — свернуть и закрыть — убирают окно в трей и ничего не завершают; выход из приложения — пункт «Выход» в меню трея.

Окно скрывается, а не закрывается. Закрытие унесло бы с собой его дескриптор, и всё, что на окне держится, — тема, место, где его оставили, вычисленная разметка, — строилось бы заново при каждом показе.

Меню иконки сделано средствами WPF, а не системное: так оно держится темы и языка, выбранных в настройках, как и остальной интерфейс.

Запущенное самой Windows, приложение не показывает окна вовсе и сразу уходит в трей — пользователь просил, чтобы оно было на месте при входе в систему, а не чтобы окно встречало его каждое утро. Такой запуск две сборки распознают по-разному: запись в реестре у распакованной сборки несёт аргумент --startup, а пакет своей командной строкой не распоряжается, и вместо неё Windows спрашивают об активации.

Если Windows откажет иконке — в сеансе без рабочего стола области уведомлений нет, — окно возвращает себе обычные обязанности: показывается при любом запуске, а его закрытие завершает приложение. Иначе у пользователя осталось бы приложение, которое он не может ни увидеть, ни закрыть.

Автозапуск

Автозапуск включается из настроек приложения тем способом, который доступен сборке. Пакет объявляет его в манифесте как windows.startupTask. Сборка, распакованная в папку, прописывается так, как это всегда делали программы для рабочего стола, — в HKCU\Software\Microsoft\Windows\CurrentVersion\Run, и прав администратора для этого не нужно. Записанная там команда заканчивается на --startup: так приложение отличает запуск, устроенный самой Windows, от запуска пользователем.

И в том, и в другом случае 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 проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом с приложенными пакетами. Номер версии берётся только из тега — тег любого другого вида останавливает прогон в самом начале. Версия пакета получается 1.2.3.0: Store принимает четыре числа и последнее оставляет себе, так что тег на него не влияет.

Обоим пайплайнам нужен runner под Windows с меткой windows-x64, на нём — .NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка остаётся текстовой заглушкой и сборка на ней падает. Работать runner лучше в интерактивном сеансе рабочего стола: тесты поднимают настоящие окна, и те проверки, которым нужен свой рабочий стол — сквозные, а также те, что просят окно на переднем плане или каретку, — на 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