This commit is contained in:
@@ -18,6 +18,37 @@ 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`.
|
||||
|
||||
## Running it while working on it
|
||||
|
||||
```powershell
|
||||
dotnet run --project CursorLang.Agent
|
||||
```
|
||||
|
||||
That is the whole application, not just the background half: building the agent puts
|
||||
the settings window next to it, because that is where the agent looks for it — the
|
||||
tray menu starts it by path, and an installation has the two in one folder. Build the
|
||||
agent alone and the Settings item would silently do nothing.
|
||||
|
||||
Rider and Visual Studio pick the launch profiles up from
|
||||
`CursorLang.Agent/Properties/launchSettings.json` and
|
||||
`CursorLang.Settings/Properties/launchSettings.json`, so the run configuration
|
||||
dropdown offers three:
|
||||
|
||||
| Profile | What it does |
|
||||
|---|---|
|
||||
| `Agent` | The user's launch: tray icon and the settings window straight away |
|
||||
| `Agent (started by Windows)` | Adds `--startup`: tray only, no window — the sign-in case |
|
||||
| `Settings` | The settings window on its own, with no agent behind it |
|
||||
|
||||
Both halves read `%APPDATA%\CursorLang\settings.json`, the same file the installed
|
||||
application uses — there is one code path resolving it and both call it. Debugging
|
||||
therefore edits the real settings; that is deliberate, since watching the agent pick
|
||||
up a change made in the window is most of what there is to watch.
|
||||
|
||||
To step through both at once, turn on attaching to child processes in the debugger:
|
||||
the settings window is started by the agent as a separate process, and without that
|
||||
the debugger stays with the agent.
|
||||
|
||||
## Administrator rights
|
||||
|
||||
The app runs as the ordinary user. Elevation was dropped so that the app can be
|
||||
@@ -31,20 +62,43 @@ 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.
|
||||
|
||||
## Two processes
|
||||
|
||||
The app is two executables sharing one folder.
|
||||
|
||||
`CursorLang.exe` is the agent: the keyboard hook, the layout polling, the tooltip
|
||||
and the tray icon. It is what Windows starts at sign-in and what sits in the tray
|
||||
all day, so it is built without WPF — a background process that carries the
|
||||
rendering stack costs around a hundred megabytes for a tooltip it draws with Win32
|
||||
in eight. It holds under 9 MB instead.
|
||||
|
||||
`CursorLang.Settings.exe` is the settings window, and nothing else. The agent
|
||||
starts it from the tray menu; closing the window ends the process, and the memory
|
||||
WPF took goes back to the system. The price is a cold start of half a second or so
|
||||
the next time the window is asked for.
|
||||
|
||||
`CursorLang.Core.dll` is what both hold in common: the models, the settings file,
|
||||
the layout tracking, the updates. No UI, and it must stay that way — whatever lands
|
||||
there lands in the background process.
|
||||
|
||||
The connection between the two is `settings.json` and nothing else. The window
|
||||
writes it — whole, into a temporary file moved into place in one step — and then
|
||||
posts a registered window message to the agent, which re-reads. The message carries
|
||||
no data: sending the changed values along would make the file stop being the only
|
||||
source of truth. An agent that is not running is a normal case — the window works the
|
||||
same. Nobody watches the file: it has one writer, and that writer speaks up.
|
||||
|
||||
## The tray
|
||||
|
||||
The app works in the background and lives in the notification area. The settings
|
||||
window is a guest on the screen rather than the app itself: it shows up after the
|
||||
installation and whenever the icon or its menu is asked for. Both buttons in its
|
||||
title bar — minimise and close — put the window away into the tray and end nothing;
|
||||
the way out of the app is the Exit item of the tray menu.
|
||||
The settings window is a guest on the screen rather than the app itself: it shows
|
||||
up after the installation and whenever the icon or its menu is asked for. Both
|
||||
buttons in its title bar mean what they say — the window closes and its process
|
||||
ends. The way out of the app itself is the Exit item of the tray menu.
|
||||
|
||||
The window is hidden rather than closed. Closing it would take its handle with it,
|
||||
and everything hung on the window — the theme, the place it was left in, the
|
||||
computed layout — would have to be built anew on every show.
|
||||
|
||||
The menu of the icon is a WPF one rather than a system one: that way it keeps to
|
||||
the theme and the language chosen in the settings, like the rest of the interface.
|
||||
The menu of the icon is a system one, drawn by Windows. Its captions still follow
|
||||
the language chosen in the settings, but the theme no longer reaches it: a WPF
|
||||
menu would mean the whole rendering stack in the background process, which is the
|
||||
one thing that process is built to avoid.
|
||||
|
||||
Started by Windows itself, the app shows no window at all and goes straight to the
|
||||
tray — the user asked for it to be there when they sign in, not for a window to
|
||||
@@ -54,9 +108,8 @@ package has no say in its command line, and Windows is asked about the activatio
|
||||
instead.
|
||||
|
||||
Should Windows refuse the icon — there is no notification area in a session without
|
||||
a desktop — the window takes its usual duties back: it shows up whatever the launch
|
||||
was, and closing it ends the app. The alternative would be an app the user can
|
||||
neither see nor quit.
|
||||
a desktop — the agent opens the settings window whatever the launch was. The
|
||||
alternative would be an app the user can neither see nor quit.
|
||||
|
||||
## Startup
|
||||
|
||||
@@ -64,8 +117,9 @@ 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. The command written there ends with `--startup`: that is how the app tells
|
||||
a launch of Windows' own doing from a launch by the user.
|
||||
rights. The command names `CursorLang.exe` — the agent, not the settings window
|
||||
the checkbox was clicked in — and ends with `--startup`: that is how the agent
|
||||
tells a launch of Windows' own doing from a launch by the user.
|
||||
|
||||
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
|
||||
@@ -86,6 +140,10 @@ does not own.
|
||||
|
||||
## Updates
|
||||
|
||||
The check runs when the settings window is opened, not when the machine is
|
||||
switched on: the background half no longer goes to the network at all, and there
|
||||
would be nothing in it to show the answer. The setting in the window says as much.
|
||||
|
||||
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`
|
||||
@@ -138,10 +196,18 @@ is a secret handed to everyone who got the app.
|
||||
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.
|
||||
There is a suite per project — `CursorLang.Core.Tests`, `CursorLang.Agent.Tests`
|
||||
and `CursorLang.Settings.Tests` — all on xUnit, with the fakes and the helpers in a
|
||||
library of their own, `CursorLang.Tests.Shared`. That library knows nothing about
|
||||
xUnit: the one thing that wanted an assertion says so with an exception instead.
|
||||
Core's suite carries no WPF reference, which is a check in itself: anything that
|
||||
would drag WPF into the background process fails to compile there.
|
||||
|
||||
Half of the app — windows, 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. For Core and the agent that thread runs a plain Win32 loop
|
||||
(`CursorLang.Tests.Shared/Pump.cs`); the settings window gets a WPF dispatcher
|
||||
instead.
|
||||
|
||||
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
|
||||
@@ -154,12 +220,14 @@ else's running instance is not their business.
|
||||
Coverage is collected with:
|
||||
|
||||
```powershell
|
||||
dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage.runsettings
|
||||
dotnet test --collect:"XPlat Code Coverage" --settings 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.
|
||||
and the composition roots — `App.xaml.cs` and `Agent.cs` — which are exercised
|
||||
end-to-end instead. One of those end-to-end checks reads the module list of the
|
||||
running agent and fails if any part of the WPF renderer is in it.
|
||||
|
||||
## Continuous integration
|
||||
|
||||
|
||||
Reference in New Issue
Block a user