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:
+91
-21
@@ -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.
|
||||
|
||||
## Непрерывная сборка
|
||||
|
||||
|
||||
Reference in New Issue
Block a user