lightweight variant (#1)

Reviewed-on: #1
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-08-12 13:37:30 +00:00
committed by alex
parent a0d3098fe4
commit 55ac8e6556
147 changed files with 4055 additions and 2349 deletions
+91 -21
View File
@@ -18,6 +18,38 @@ git clone git@git.alrakis.kz:alrakis/cursor-lang.git
текстовые файлы-указатели, и сборка затем падает на нечитаемой иконке.
Существующий клон чинится командой `git lfs install`, а затем `git lfs pull`.
## Запуск во время работы над приложением
```powershell
dotnet run --project CursorLang.Agent
```
Это целое приложение, а не одна фоновая половина: сборка агента кладёт окно настроек
рядом с ним, потому что именно там агент его и ищет — меню трея запускает окно по
пути, а в установленном приложении они лежат в одной папке. Собери агента отдельно —
и пункт «Настройки» молча ничего бы не делал.
Rider и Visual Studio берут профили запуска из
`CursorLang.Agent/Properties/launchSettings.json` и
`CursorLang.Settings/Properties/launchSettings.json`, так что в списке конфигураций
появляются три:
| Профиль | Что делает |
|---|---|
| `Agent` | Запуск как у пользователя: иконка в трее и сразу окно настроек |
| `Agent (started by Windows)` | Добавляет `--startup`: только трей, без окна — случай входа в систему |
| `Settings` | Окно настроек само по себе, без агента за спиной |
Обе половины читают `%APPDATA%\CursorLang\settings.json` — тот же файл, что и у
установленного приложения: путь вычисляется в одном месте, и обе половины зовут
именно его. Значит, отладка правит настоящие настройки; так и задумано — смотреть,
как агент подхватывает правку из окна, это и есть большая часть того, на что тут
стоит смотреть.
Чтобы шагать по обеим сразу, включи в отладчике присоединение к дочерним процессам:
окно настроек агент запускает отдельным процессом, и без этого отладчик останется
на агенте.
## Права администратора
Приложение работает от имени обычного пользователя. От повышения прав
@@ -33,20 +65,44 @@ MSIX всегда выполняются в контексте вошедшег
раскладки чужого окна не ограничено, поэтому сама подсказка продолжает работать
везде.
## Два процесса
Приложение — это два исполняемых файла в одной папке.
`CursorLang.exe` — агент: перехват клавиатуры, опрос раскладки, подсказка и
иконка в трее. Именно его Windows запускает при входе в систему и именно он
целый день сидит в трее, поэтому он собран без WPF: фоновый процесс,
таскающий за собой движок отрисовки, стоит около ста мегабайт ради подсказки,
которую средствами Win32 рисуют за восемь. Так он держится ниже 9 МБ.
`CursorLang.Settings.exe` — окно настроек и ничего больше. Агент запускает его
из меню трея; закрытие окна завершает процесс, и занятая WPF память целиком
возвращается системе. Плата — холодный старт около полусекунды при следующем
открытии окна.
`CursorLang.Core.dll` — общее для обоих: модели, файл настроек, слежение за
раскладкой, обновления. Без UI, и так должно остаться: всё, что попадёт туда,
попадёт и в фоновый процесс.
Связь между ними — только `settings.json`. Окно пишет его целиком, во временный
файл, который одним движением встаёт на место, и посылает агенту
зарегистрированное оконное сообщение, по которому тот перечитывает файл.
Сообщение не несёт данных: пересылка самих изменений лишила бы файл роли
единственного источника правды. Работающий агент необязателен — без него окно
работает так же. За файлом никто не следит: писатель у него один, и он сам
сообщает о записи.
## Трей
Приложение работает в фоне и живёт в области уведомлений. Окно настроек —
гость на экране, а не само приложение: оно показывается после установки и
всякий раз, когда его просят иконка или её меню. Обе кнопки в заголовке окна —
свернуть и закрыть — убирают окно в трей и ничего не завершают; выход из
приложения — пункт «Выход» в меню трея.
Окно настроек — гость на экране, а не само приложение: оно показывается после
установки и всякий раз, когда его просят иконка или её меню. Обе кнопки в
заголовке окна означают ровно то, что написано: окно закрывается, а его процесс
завершается. Выход из самого приложения — пункт «Выход» в меню трея.
Окно скрывается, а не закрывается. Закрытие унесло бы с собой его дескриптор, и
всё, что на окне держится, тема, место, где его оставили, вычисленная
разметка, — строилось бы заново при каждом показе.
Меню иконки сделано средствами WPF, а не системное: так оно держится темы и
языка, выбранных в настройках, как и остальной интерфейс.
Меню иконки системное, его рисует Windows. Надписи по-прежнему следуют языку,
выбранному в настройках, а тема до меню больше не дотягивается: меню на WPF
означало бы весь движок отрисовки в фоновом процессе — ровно то, чего этот
процесс и создан избегать.
Запущенное самой Windows, приложение не показывает окна вовсе и сразу уходит в
трей — пользователь просил, чтобы оно было на месте при входе в систему, а не
@@ -56,9 +112,8 @@ MSIX всегда выполняются в контексте вошедшег
спрашивают об активации.
Если Windows откажет иконке — в сеансе без рабочего стола области уведомлений
нет, — окно возвращает себе обычные обязанности: показывается при любом запуске,
а его закрытие завершает приложение. Иначе у пользователя осталось бы
приложение, которое он не может ни увидеть, ни закрыть.
нет, — агент открывает окно настроек при любом запуске. Иначе у пользователя
осталось бы приложение, которое он не может ни увидеть, ни закрыть.
## Автозапуск
@@ -90,6 +145,10 @@ MSIX всегда выполняются в контексте вошедшег
## Обновления
Проверка выполняется при открытии окна настроек, а не при включении машины:
фоновая половина в сеть больше не ходит вовсе, да и показать ответ ей нечем.
Настройка в окне так и написана.
Приложение ищет новые версии среди выпусков собственного репозитория. Выпуск
годится, если его тег — это просто версия (`v1.2.3` или `1.2.3`) и к нему
приложен пакет MSIX. Тег, в котором есть что-то ещё, — в том числе `v1.2.3-beta`
@@ -141,10 +200,19 @@ services.AddSingleton(new UpdateOptions
dotnet test
```
Тесты лежат в `CursorLang.Tests` и работают на xUnit. Половина приложения —
окна, таймеры диспетчера, перехват клавиатуры — живёт только на потоке STA
с очередью сообщений, поэтому тесты держат один такой поток на весь прогон
и выполняют на нём всё, что этого требует.
На каждый проект свой набор — `CursorLang.Core.Tests`, `CursorLang.Agent.Tests`
и `CursorLang.Settings.Tests`, — все на xUnit, а подделки и вспомогательный код
общие и вынесены в отдельную библиотеку `CursorLang.Tests.Shared`. Сама библиотека
про xUnit не знает: единственному месту, которому нужно было утверждение, хватило
исключения. У набора для Core нет ссылки на WPF, и это само по себе проверка: то,
что затянуло бы WPF в фоновый процесс, там попросту не скомпилируется.
Половина приложения — окна, таймеры, перехват клавиатуры — живёт только на
потоке STA с очередью сообщений, поэтому тесты держат один такой поток на весь
прогон и выполняют на нём всё, что этого требует. Для Core и агента на этом
потоке крутится обычный цикл Win32 (`CursorLang.Tests.Shared/Pump.cs`), для окна
настроек —
диспетчер WPF.
Части проверок нужно настоящее окно переднего плана: положение каретки и
просьбу сменить раскладку видно только там. Право вывести окно вперёд Windows
@@ -156,13 +224,15 @@ dotnet test
Покрытие снимается так:
```powershell
dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage.runsettings
dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
```
Непокрытым остаётся то, до чего тестовому процессу не дотянуться: пути, которым
нужен установленный пакет MSIX — `StartupTask` и папка данных пакета, — и
композиционный корень в `App.xaml.cs`, который вместо этого проверяется сквозным
запуском приложения.
композиционные корни `App.xaml.cs` и `Agent.cs`, которые вместо этого
проверяются сквозным запуском приложения. Одна из таких проверок читает список
модулей работающего агента и падает, если среди них окажется хоть что-то от
движка отрисовки WPF.
## Непрерывная сборка