diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md
index d2c9a8c5..e4c3a99e 100644
--- a/.github/agents/copilot-instructions.md
+++ b/.github/agents/copilot-instructions.md
@@ -101,6 +101,16 @@ GitHub Pages is published from `.github/workflows/pages.yml`: build root `docfx.
- Mouse interaction, terminal/charset/font work, Wave 4, broad redesign, services, new dependencies, and runtime/product AI remain outside 019.
- The next prioritized intake is `Lastenheft_04_MouseSupportAndInteraction.md`.
+### 020-mouse-support-interaction
+- Current implementation status: bounded mouse support and interaction hardening is implemented locally; final evidence is in `specs/020-mouse-support-interaction/pr-evidence.md`.
+- `ConsoleMouseIngress` accepts only complete bounded SGR 1006 left press, pressed move, and release reports and publishes zero or one existing `TEvent`; malformed syntax, range, button, capability, and phase input is rejected atomically.
+- `TGroup` routes mouse down to one topmost visible target, transfers focus only to selectable targets, and preserves existing exactly-once control commands. Nested mouse coordinates traverse the full owner chain.
+- The only mouse drag contract is moving a `TWindow` from its title row. Owner bounds, release, Escape, capability loss, disable, removal, shutdown, and the existing `Ctrl+F5` keyboard fallback are proven.
+- Interactive macOS/Linux terminals and WSL use the SGR capability contract; native Windows Console and redirected/headless I/O remain honest `Unsupported` boundaries. Wheel, hover, touch, extra buttons, full protocol parity, and additional drag targets remain out of scope.
+- Primary proof runs through `TProgram.GetEvent` and `app.Run()` and combines concrete focus/command/drag state, target identity, visible text, and rendered buffer/cell assertions. Historical sources remain read-only.
+- The next prioritized intake is `Lastenheft_05_TerminalCharsetAndEmulation.md`.
+
+
### Autonomous Red-Proof Completeness
- Before the first red test batch, review imports, public XML docs, harness helpers, focus/ownership assertions, and linked-source assembly identity.
- Group independent negative cases only as a bounded project-local red matrix with explicit failure boundaries and shared ownership.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d10191e0..ae4b502d 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -227,6 +227,16 @@ On numbered Spec-Kit branches, align those three version fields before pushing.
- Mouse interaction, terminal/charset/font work, Wave 4, broad redesign, services, new dependencies, and runtime/product AI remain outside 019.
- The next prioritized intake is `Lastenheft_04_MouseSupportAndInteraction.md`.
+### 020-mouse-support-interaction
+- Current implementation status: bounded mouse support and interaction hardening is implemented locally; final evidence is in `specs/020-mouse-support-interaction/pr-evidence.md`.
+- `ConsoleMouseIngress` accepts only complete bounded SGR 1006 left press, pressed move, and release reports and publishes zero or one existing `TEvent`; malformed syntax, range, button, capability, and phase input is rejected atomically.
+- `TGroup` routes mouse down to one topmost visible target, transfers focus only to selectable targets, and preserves existing exactly-once control commands. Nested mouse coordinates traverse the full owner chain.
+- The only mouse drag contract is moving a `TWindow` from its title row. Owner bounds, release, Escape, capability loss, disable, removal, shutdown, and the existing `Ctrl+F5` keyboard fallback are proven.
+- Interactive macOS/Linux terminals and WSL use the SGR capability contract; native Windows Console and redirected/headless I/O remain honest `Unsupported` boundaries. Wheel, hover, touch, extra buttons, full protocol parity, and additional drag targets remain out of scope.
+- Primary proof runs through `TProgram.GetEvent` and `app.Run()` and combines concrete focus/command/drag state, target identity, visible text, and rendered buffer/cell assertions. Historical sources remain read-only.
+- The next prioritized intake is `Lastenheft_05_TerminalCharsetAndEmulation.md`.
+
+
### Autonomous Red-Proof Completeness
- Before the first red test batch, review imports, public XML docs, harness helpers, focus/ownership assertions, and linked-source assembly identity.
- Group independent negative cases only as a bounded project-local red matrix with explicit failure boundaries and shared ownership.
diff --git a/.specify/feature.json b/.specify/feature.json
index 362501f6..6d0c6382 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1,3 +1,3 @@
{
- "feature_directory": "specs/019-wave3-visual-component-porting"
+ "feature_directory": "specs/020-mouse-support-interaction"
}
diff --git a/AGENTS.md b/AGENTS.md
index cd7e612f..27ba7f9d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,6 +273,16 @@ npm run test:docfx
- Mouse interaction, terminal/charset/font work, Wave 4, broad redesign, services, new dependencies, and runtime/product AI remain outside 019.
- The next prioritized intake is `Lastenheft_04_MouseSupportAndInteraction.md`.
+### 020-mouse-support-interaction
+- Current implementation status: bounded mouse support and interaction hardening is implemented locally; final evidence is in `specs/020-mouse-support-interaction/pr-evidence.md`.
+- `ConsoleMouseIngress` accepts only complete bounded SGR 1006 left press, pressed move, and release reports and publishes zero or one existing `TEvent`; malformed syntax, range, button, capability, and phase input is rejected atomically.
+- `TGroup` routes mouse down to one topmost visible target, transfers focus only to selectable targets, and preserves existing exactly-once control commands. Nested mouse coordinates traverse the full owner chain.
+- The only mouse drag contract is moving a `TWindow` from its title row. Owner bounds, release, Escape, capability loss, disable, removal, shutdown, and the existing `Ctrl+F5` keyboard fallback are proven.
+- Interactive macOS/Linux terminals and WSL use the SGR capability contract; native Windows Console and redirected/headless I/O remain honest `Unsupported` boundaries. Wheel, hover, touch, extra buttons, full protocol parity, and additional drag targets remain out of scope.
+- Primary proof runs through `TProgram.GetEvent` and `app.Run()` and combines concrete focus/command/drag state, target identity, visible text, and rendered buffer/cell assertions. Historical sources remain read-only.
+- The next prioritized intake is `Lastenheft_05_TerminalCharsetAndEmulation.md`.
+
+
### Autonomous Red-Proof Completeness
- Before the first red test batch, review imports, public XML docs, harness helpers, focus/ownership assertions, and linked-source assembly identity.
- Group independent negative cases only as a bounded project-local red matrix with explicit failure boundaries and shared ownership.
diff --git a/CLAUDE.md b/CLAUDE.md
index cfe16c4d..53c9cc20 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -219,6 +219,16 @@ When a dedicated feature branch has implemented the requirements of a Lastenheft
- Mouse interaction, terminal/charset/font work, Wave 4, broad redesign, services, new dependencies, and runtime/product AI remain outside 019.
- The next prioritized intake is `Lastenheft_04_MouseSupportAndInteraction.md`.
+### 020-mouse-support-interaction
+- Current implementation status: bounded mouse support and interaction hardening is implemented locally; final evidence is in `specs/020-mouse-support-interaction/pr-evidence.md`.
+- `ConsoleMouseIngress` accepts only complete bounded SGR 1006 left press, pressed move, and release reports and publishes zero or one existing `TEvent`; malformed syntax, range, button, capability, and phase input is rejected atomically.
+- `TGroup` routes mouse down to one topmost visible target, transfers focus only to selectable targets, and preserves existing exactly-once control commands. Nested mouse coordinates traverse the full owner chain.
+- The only mouse drag contract is moving a `TWindow` from its title row. Owner bounds, release, Escape, capability loss, disable, removal, shutdown, and the existing `Ctrl+F5` keyboard fallback are proven.
+- Interactive macOS/Linux terminals and WSL use the SGR capability contract; native Windows Console and redirected/headless I/O remain honest `Unsupported` boundaries. Wheel, hover, touch, extra buttons, full protocol parity, and additional drag targets remain out of scope.
+- Primary proof runs through `TProgram.GetEvent` and `app.Run()` and combines concrete focus/command/drag state, target identity, visible text, and rendered buffer/cell assertions. Historical sources remain read-only.
+- The next prioritized intake is `Lastenheft_05_TerminalCharsetAndEmulation.md`.
+
+
### Autonomous Red-Proof Completeness
- Before the first red test batch, review imports, public XML docs, harness helpers, focus/ownership assertions, and linked-source assembly identity.
- Group independent negative cases only as a bounded project-local red matrix with explicit failure boundaries and shared ownership.
diff --git a/Directory.Build.props b/Directory.Build.props
index 02009726..72e7f634 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,9 +5,9 @@
enable
enable
true
- 1.19.5.124
- 1.19.5.124
- 1.19.5.124
+ 1.20.6.143
+ 1.20.6.143
+ 1.20.6.143
CS1591
diff --git a/GEMINI.md b/GEMINI.md
index e8965f71..2dc0bc43 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -246,6 +246,16 @@ Das Projekt folgt einer modularen Struktur gemäß .NET Best Practices:
- Mouse interaction, terminal/charset/font work, Wave 4, broad redesign, services, new dependencies, and runtime/product AI remain outside 019.
- The next prioritized intake is `Lastenheft_04_MouseSupportAndInteraction.md`.
+### 020-mouse-support-interaction
+- Current implementation status: bounded mouse support and interaction hardening is implemented locally; final evidence is in `specs/020-mouse-support-interaction/pr-evidence.md`.
+- `ConsoleMouseIngress` accepts only complete bounded SGR 1006 left press, pressed move, and release reports and publishes zero or one existing `TEvent`; malformed syntax, range, button, capability, and phase input is rejected atomically.
+- `TGroup` routes mouse down to one topmost visible target, transfers focus only to selectable targets, and preserves existing exactly-once control commands. Nested mouse coordinates traverse the full owner chain.
+- The only mouse drag contract is moving a `TWindow` from its title row. Owner bounds, release, Escape, capability loss, disable, removal, shutdown, and the existing `Ctrl+F5` keyboard fallback are proven.
+- Interactive macOS/Linux terminals and WSL use the SGR capability contract; native Windows Console and redirected/headless I/O remain honest `Unsupported` boundaries. Wheel, hover, touch, extra buttons, full protocol parity, and additional drag targets remain out of scope.
+- Primary proof runs through `TProgram.GetEvent` and `app.Run()` and combines concrete focus/command/drag state, target identity, visible text, and rendered buffer/cell assertions. Historical sources remain read-only.
+- The next prioritized intake is `Lastenheft_05_TerminalCharsetAndEmulation.md`.
+
+
### Autonomous Red-Proof Completeness
- Before the first red test batch, review imports, public XML docs, harness helpers, focus/ownership assertions, and linked-source assembly identity.
- Group independent negative cases only as a bounded project-local red matrix with explicit failure boundaries and shared ownership.
diff --git a/Lastenheft_04_MouseSupportAndInteraction.md b/Lastenheft_04_MouseSupportAndInteraction.020-mouse-support-interaction.md
similarity index 100%
rename from Lastenheft_04_MouseSupportAndInteraction.md
rename to Lastenheft_04_MouseSupportAndInteraction.020-mouse-support-interaction.md
diff --git a/Pflichtenheft.md b/Pflichtenheft.md
index 35c1460f..e8607c38 100644
--- a/Pflichtenheft.md
+++ b/Pflichtenheft.md
@@ -256,8 +256,10 @@ Statuscheckliste Beispielwellen:
Reihenfolgehinweis: abgeschlossen (Branch `013-wave2-visual-component-remediation`); die elf Wave-2-Beispiele besitzen echte sichtbare Hauptkomponenten, echte `TStatusLine`-Rueckmeldung, `Help -> Description` und primaere App-Loop-Smokes mit View-Baum- plus Buffer-/Cell-Nachweis.
- [x] **Welle 3 - Editor-/Hilfe-/Ressourcen-Vorhaertung**
Reihenfolgehinweis: abgeschlossen (Branch `018-editor-help-resources-hardening`); Editor-/Datei- und Runtime-Help-Flows sind zusammenhaengend nachgewiesen, der Help-Source-Compiler und sprachabhaengige Resource-Lookup sind wiederverwendbar gehaertet.
-- [ ] **Welle 3 - Visual Component Porting**
- Reihenfolgehinweis: nach der Wave-3-Vorhaertung ueber `Lastenheft_Wave3-Visual-Component-Porting.md`; `tvedit`, `bhelp`, `helpdemo`, `tvhc` und `i18n` muessen direkt als sichtbare Drei-Schichten-Demos portiert werden.
+- [x] **Welle 3 - Visual Component Porting**
+ Reihenfolgehinweis: abgeschlossen (Branch `019-wave3-visual-component-porting`); `tvedit`, `bhelp`, `helpdemo`, `tvhc` und `i18n` sind sichtbare Drei-Schichten-Demos mit App-Loop- und Buffer-/Cell-Proof.
+- [x] **Maussupport und Interaktions-Härtung**
+ Reihenfolgehinweis: abgeschlossen (Branch `020-mouse-support-interaction`); begrenzter SGR-1006-Ingress, Fokus/Aktivierung, Doppelklick, ein Titelzeilen-Drag und vollständige Tastaturfallbacks sind frameworkweit nachgewiesen.
- [ ] **Welle 4 - Terminal-/Charset-/Plattform-Vorhaertung**
Reihenfolgehinweis: nach Welle 3 ueber `Lastenheft_05_TerminalCharsetAndEmulation.md`; haertet Terminal-Session, Buffer-/Cell-Proof, Charset-/Font-Mapping, Resource-/Config-Fallbacks und Plattformgrenzen vor dem sichtbaren Wave-4-Port.
- [ ] **Welle 4 - Visual Component Porting**
@@ -655,9 +657,10 @@ Dieser Marker ist bei jeder wesentlichen Fortschreibung des Pflichtenhefts auf d
- `3.2b` = Welle 2: Sichtbarer Komponenten-Nachweis — ✓ ABGESCHLOSSEN (Branch `013-wave2-visual-component-remediation`)
- `3.3a` = Welle 3: Editor-/Hilfe-/Ressourcen-Vorhaertung — ✓ ABGESCHLOSSEN (Branch `018-editor-help-resources-hardening`)
- `3.3` = Welle 3: Editor, Dateien, Hilfe und Streams — ✓ ABGESCHLOSSEN (Branch `019-wave3-visual-component-porting`)
+ - `3.3b` = Maussupport und Interaktions-Härtung — ✓ ABGESCHLOSSEN (Branch `020-mouse-support-interaction`)
- `3.4` = Welle 4: Terminal-Emulation und erweiterte Zeichensaetze
->>> NAECHSTER SCHRITT <<< Mouse Support and Interaction starten: `Lastenheft_04_MouseSupportAndInteraction.md` umsetzen. Danach folgt `Lastenheft_05_TerminalCharsetAndEmulation.md`.
+>>> NAECHSTER SCHRITT <<< Terminal-/Charset-Härtung starten: `Lastenheft_05_TerminalCharsetAndEmulation.md` umsetzen. Danach folgt `Lastenheft_Wave4-Visual-Component-Porting.md`.
4. **MUSS-Testumfang und Beispiel-Smoke-Tests schliessen**
- Fuer alle 25 portierten Originalbeispiele automatisierte Smoke-Tests in CI bereitstellen.
diff --git a/README.md b/README.md
index c7716568..110adb11 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ Agentic-AI workflows. It is not an official Turbo Vision continuation.
- Multi-Mac workflow (MacBook Air M2 + Mac mini M4 Pro) with `gh` and `codex`:
[`docs/guides/multi-mac-workflow.md`](docs/guides/multi-mac-workflow.md)
+- Bounded SGR-1006 mouse support, host boundaries, keyboard fallback, and proof:
+ [`docs/guides/mouse-support.md`](docs/guides/mouse-support.md)
## Documentation Accessibility Checks
diff --git a/docs/architecture/runtime-view.md b/docs/architecture/runtime-view.md
index 0b35b89b..5b7a21fb 100644
--- a/docs/architecture/runtime-view.md
+++ b/docs/architecture/runtime-view.md
@@ -103,3 +103,37 @@ smokes inject commands into `app.Run()` and verify concrete state values,
view-tree types, and back-buffer regions. The new helper file under
`examples/Shared/` remains example-internal and creates no new runtime
dependency.
+
+## 020 Mouse Ingress Addendum
+
+```text
+Interactive SGR terminal or controlled observation
+ |
+ | complete bounded SGR 1006 report
+ v
+ConsoleMouseIngress in TuiVision.Drivers.Console
+ |
+ | syntax, range, capability, phase and monotonic click validation
+ | zero or one existing TEvent
+ v
+TProgram.GetEvent
+ |
+ | point-to-target-key delegate, no Driver-to-Controls reference
+ v
+TGroup topmost hit and focus -> existing control command or TWindow title drag
+ |
+ | visible status, view identity and buffer/cell proof
+ v
+Keyboard-complete application; shutdown disables SGR and clears transient state
+```
+
+Deutsch: Der Driver besitzt Protokoll und Hostzustand, Core bleibt der
+kanonische Eventvertrag, und Controls besitzen Fokus sowie den einzigen
+Titelzeilen-Drag. Ungültige Rohdaten erreichen den View-Baum nicht. Native
+Windows Console, Wheel, Hover, Touch und weitere Drag-Ziele bleiben außerhalb
+von 020.
+
+English: Driver owns protocol and host state, Core remains the canonical event
+contract, and Controls own focus plus the sole title-row drag. Invalid raw data
+never reaches the view tree. Native Windows Console, wheel, hover, touch, and
+additional drag targets remain outside 020.
diff --git a/docs/guides/mouse-support.md b/docs/guides/mouse-support.md
new file mode 100644
index 00000000..fcf344db
--- /dev/null
+++ b/docs/guides/mouse-support.md
@@ -0,0 +1,125 @@
+# Maussupport und Interaktion / Mouse Support and Interaction
+
+## Deutsch
+
+### Zweck und Grenze
+
+TuiVision führt reale Terminal-Mausmeldungen zentral über
+`TConsoleDriver` und `ConsoleMouseIngress` in das vorhandene `TEvent`-Modell.
+Controls und Beispiele lesen keine eigenen Escape-Sequenzen. Feature 020
+unterstützt nur vollständige SGR-1006-Meldungen für linkes Drücken, Bewegung
+bei gedrückter Taste und Loslassen.
+
+Nicht unterstützt sind native Windows-Console-Mausrecords, X10-Parität,
+Mausrad, Hover, Touch, weitere Buttons und beliebige Gesten. Diese Grenzen
+werden nicht durch stille Fallbacks verdeckt.
+
+### Host- und Capability-Matrix
+
+| Umgebung | Zustand beim Start | Bedingung |
+|---|---|---|
+| Interaktives macOS-Terminal | SGR 1006 kann aktiviert werden | Ein- und Ausgabe sind TTY; `TERM` ist gesetzt und nicht `dumb` |
+| Interaktives Linux-Terminal | SGR 1006 kann aktiviert werden | dieselbe Terminalbedingung |
+| WSL in einem passenden Terminal | SGR 1006 kann aktiviert werden | WSL und passende Terminalweiterleitung |
+| Native Windows Console | `Unsupported` | Ein eigener Backend-Vertrag fehlt |
+| Umgeleitete oder headless Ein-/Ausgabe | `Unsupported` | Tastatur- und Testpfade bleiben verfügbar |
+
+Die drei Zustände sind `Enabled`, `Disabled` und `Unsupported`. Beim Beenden
+wird SGR-Reporting abgeschaltet und jeder Press-, Klick- oder Drag-Zustand
+verworfen. Ein Aktivierungsfehler lässt die Anwendung im Tastaturmodus.
+
+### Interaktionen
+
+- Ein MouseDown fokussiert genau die oberste sichtbare, nicht deaktivierte und
+ selektierbare View am Zielpunkt.
+- Ein Button verwendet danach seinen vorhandenen Command-Pfad genau einmal.
+- Ein Doppelklick ist nur der zweite linke Press auf derselben Zelle und
+ demselben Ziel innerhalb von einschließlich 500 Millisekunden monotonic time.
+- Genau ein Drag-Pfad ist vorhanden: Ein Fenster mit `WindowFlags.Move` kann
+ an seiner oberen Titelzeile verschoben werden. Das vollständige Fenster
+ bleibt innerhalb seines Owners.
+- Release übernimmt die begrenzte Position. Escape, Capability-Verlust,
+ Deaktivierung, Entfernung und Shutdown brechen den Drag ab.
+
+### Tastatur und Barrierefreiheit
+
+Die Maus ergänzt die Tastatur, ersetzt sie aber nicht. Fokus und Aktivierung
+bleiben über die vorhandenen Tab-, Shortcut-, Enter- und Command-Pfade
+erreichbar. Fenster lassen sich weiterhin mit `Ctrl+F5`, Pfeiltasten und Enter
+verschieben; Escape stellt die Startposition wieder her.
+
+Capability, Fokus, Aktivierung, Doppelklick, Drag und Fallback müssen als Text
+verständlich bleiben. Farbe, Zeigerposition oder ein Screenshot sind kein
+alleiniger Nachweis.
+
+### Sicherheit und Nachweis
+
+Terminaleingaben gelten als nicht vertrauenswürdig. Erst eine vollständige,
+begrenzte und syntaktisch sowie zustandsbezogen gültige Sequenz erzeugt genau
+ein Framework-Ereignis. Fehlerhafte Beobachtungen erzeugen kein Teilereignis
+und beschädigen die nächste eigenständige Beobachtung nicht.
+
+Deterministische Tests beweisen Parser, Zustandsfolgen und App-Loop-Verhalten.
+Sie beweisen keinen nicht ausgeführten physischen Host. Die aktuelle lokale
+Codex-Session ist auf macOS headless mit `TERM=dumb`; deshalb ist der physische
+macOS-Spot-Check `NotRun`, nicht `Pass`.
+
+## English
+
+### Purpose and Boundary
+
+TuiVision routes real terminal mouse reports centrally through
+`TConsoleDriver` and `ConsoleMouseIngress` into the existing `TEvent` model.
+Controls and examples do not parse their own escape sequences. Feature 020
+supports only complete SGR 1006 reports for left press, movement while pressed,
+and release.
+
+Native Windows Console mouse records, X10 parity, wheel, hover, touch, extra
+buttons, and arbitrary gestures are unsupported. These boundaries are not
+hidden behind silent fallbacks.
+
+### Host and Capability Matrix
+
+| Environment | Startup state | Condition |
+|---|---|---|
+| Interactive macOS terminal | SGR 1006 can be enabled | Input and output are TTY; `TERM` is set and not `dumb` |
+| Interactive Linux terminal | SGR 1006 can be enabled | Same terminal condition |
+| WSL in a suitable terminal | SGR 1006 can be enabled | WSL and suitable terminal forwarding |
+| Native Windows Console | `Unsupported` | A dedicated backend contract is absent |
+| Redirected or headless I/O | `Unsupported` | Keyboard and test paths remain available |
+
+The three states are `Enabled`, `Disabled`, and `Unsupported`. Shutdown disables
+SGR reporting and clears every press, click, or drag state. Activation failure
+keeps the application in keyboard mode.
+
+### Interactions
+
+- MouseDown focuses exactly one topmost visible, enabled, selectable view at the target.
+- A button then uses its existing command path exactly once.
+- Double click means only the second left press on the same cell and target
+ within an inclusive 500 milliseconds of monotonic time.
+- Exactly one drag path exists: a window with `WindowFlags.Move` can move from
+ its top title row while the complete window remains inside its owner.
+- Release commits the clamped position. Escape, capability loss, disable,
+ removal, and shutdown cancel the drag.
+
+### Keyboard and Accessibility
+
+Mouse input augments the keyboard; it does not replace it. Existing Tab,
+shortcut, Enter, and command paths retain focus and activation. Windows still
+move with `Ctrl+F5`, arrow keys, and Enter; Escape restores the start position.
+
+Capability, focus, activation, double click, drag, and fallback remain
+understandable as text. Colour, pointer position, or a screenshot is not
+sufficient proof on its own.
+
+### Security and Proof
+
+Terminal input is untrusted. Only a complete, bounded, syntactically valid, and
+state-valid sequence creates exactly one framework event. Rejected observations
+create no partial event and do not damage the next independent observation.
+
+Deterministic tests prove parser, state, and app-loop contracts. They do not
+prove a physical host that was not exercised. The current local Codex session
+runs headless on macOS with `TERM=dumb`; therefore the physical macOS spot-check
+is `NotRun`, not `Pass`.
diff --git a/docs/project-statistics.md b/docs/project-statistics.md
index b093b9d3..1414fc9a 100644
--- a/docs/project-statistics.md
+++ b/docs/project-statistics.md
@@ -1,11 +1,15 @@
# Projektstatistik TuiVision
-Stand: 2026-07-11 (aktualisiert einschließlich der autonomen Spec-Kit-Standardisierung mit Runbook, Evidence-Vorlage, projektgebundenem Orchestrierungs-Skill und synchronisierter Agent-Guidance)
+Stand: 2026-07-12 (aktualisiert einschließlich Feature 020 mit begrenztem SGR-1006-Ingress, App-Loop-Proof und vollständigem Tastaturfallback)
Aktueller Zusatz: Feature 016 dokumentiert 157/157 Secure-Development-
Kontrollen, sechs behobene Medium-Funde, 498/498 grüne Release-Tests und eine
Coverage von mindestens 80,55 % in allen fünf Gate-Assemblies.
+Feature 020 ergänzt einen atomar validierten Host-Mauseingang, Topmost-Fokus,
+Exactly-once-Aktivierung, Doppelklick und genau einen begrenzten Titelzeilen-
+Drag. Physische Host-Prüfung und deterministische CI-Evidence bleiben getrennt.
+
## Zweck und Pflege
Diese Datei ist das fortlaufende Statistik-Register fuer TuiVision. Sie wird
@@ -82,29 +86,29 @@ fortgeschrieben.
| Kennzahl | Wert |
|---|---:|
-| Beobachtbarer Projektzeitraum | 2026-02-08 bis 2026-07-11 |
-| Git-Commits gesamt | 421 |
+| Beobachtbarer Projektzeitraum | 2026-02-08 bis 2026-07-12 |
+| Git-Commits gesamt | 432 |
| Autoren laut Git | 1 |
-| Sichtbare Aktivtage inkl. aktuellem Working Tree | 68 |
-| Produktionscode aktuell | 162 Dateien / 21491 Zeilen |
-| Testcode aktuell | 99 Dateien / 14407 Zeilen |
-| Dokumentation aktuell | 504 Dateien / 180064 Zeilen |
-| Davon Spec-Kit-Artefakte | 305 Dateien / 41204 Zeilen |
-| Davon zentrale Governance-/Agent-Dateien | 5 Dateien / 2350 Zeilen |
-| Davon projektgebundene Agent-Skills | 15 Dateien / 2404 Zeilen |
-| Gesamtbasis für Handschätzung (inkl. Dokumentation) | 215962 Zeilen |
-| Erfahrener Entwickler, konservative Untergrenze | 2699.5 Arbeitstage |
-| Erfahrener Entwickler, konservative Untergrenze in Stunden | 21056.3 Stunden (215962 / 80 * 7.8) |
-| Erfahrener Entwickler, brutto | 125.5 Arbeitsmonate (21.5 Tage/Monat) |
-| Erfahrener Entwickler, TVoeD-Annahme | 142.1 Kalendermonate bzw. 11.8 Jahre |
-| Thorsten solo, erfahrungsadjustierte Untergrenze | 1727.7 Arbeitstage |
-| Thorsten solo, erfahrungsadjustierte Untergrenze in Stunden | 13476.0 Stunden (215962 / 125 * 7.8) |
-| Thorsten solo, brutto | 80.3 Arbeitsmonate (21.5 Tage/Monat) |
-| Thorsten solo, TVoeD-Annahme | 90.9 Kalendermonate bzw. 7.6 Jahre |
-| Kleines Team (3 Personen, +20 % Koordination), Untergrenze | 1079.8 Arbeitstage |
-| Kleines Team (3 Personen, +20 % Koordination), TVoeD-Annahme | 56.8 Kalendermonate |
-| Repo-weiter Beschleunigungsfaktor vs. konservative Referenz | 39.7x (2699.5 / 68 sichtbare Aktivtage) |
-| Repo-weiter Beschleunigungsfaktor vs. Thorsten-Referenz | 25.4x (1727.7 / 68 sichtbare Aktivtage) |
+| Sichtbare Aktivtage inkl. aktuellem Working Tree | 69 |
+| Produktionscode aktuell | 177 Dateien / 24127 Zeilen |
+| Testcode aktuell | 113 Dateien / 16312 Zeilen |
+| Dokumentation aktuell | 560 Dateien / 186597 Zeilen |
+| Davon Spec-Kit-Artefakte | 318 Dateien / 45859 Zeilen |
+| Davon zentrale Governance-/Agent-Dateien | 5 Dateien / 2535 Zeilen |
+| Davon projektgebundene Agent-Skills | 15 Dateien / 2415 Zeilen |
+| Gesamtbasis für Handschätzung (inkl. Dokumentation) | 227036 Zeilen |
+| Erfahrener Entwickler, konservative Untergrenze | 2838.0 Arbeitstage |
+| Erfahrener Entwickler, konservative Untergrenze in Stunden | 22136.0 Stunden (227036 / 80 * 7.8) |
+| Erfahrener Entwickler, brutto | 132.0 Arbeitsmonate (21.5 Tage/Monat) |
+| Erfahrener Entwickler, TVoeD-Annahme | 149.4 Kalendermonate bzw. 12.4 Jahre |
+| Thorsten solo, erfahrungsadjustierte Untergrenze | 1816.3 Arbeitstage |
+| Thorsten solo, erfahrungsadjustierte Untergrenze in Stunden | 14167.0 Stunden (227036 / 125 * 7.8) |
+| Thorsten solo, brutto | 84.5 Arbeitsmonate (21.5 Tage/Monat) |
+| Thorsten solo, TVoeD-Annahme | 95.6 Kalendermonate bzw. 8.0 Jahre |
+| Kleines Team (3 Personen, +20 % Koordination), Untergrenze | 1135.2 Arbeitstage |
+| Kleines Team (3 Personen, +20 % Koordination), TVoeD-Annahme | 59.8 Kalendermonate |
+| Repo-weiter Beschleunigungsfaktor vs. konservative Referenz | 41.1x (2838.0 / 69 sichtbare Aktivtage) |
+| Repo-weiter Beschleunigungsfaktor vs. Thorsten-Referenz | 26.3x (1816.3 / 69 sichtbare Aktivtage) |
## Phasen und Haupt-Branches
@@ -630,6 +634,7 @@ fortgeschrieben.
| 2026-07-12 | `/speckit-autonomous` fuer `019-wave3-visual-component-porting` | Die fuenf historischen Wave-3-Beispiele `BHelp`, `HelpDemo`, `I18n`, `TvEdit` und `TvHc` sind als sichtbare Drei-Schichten-Anwendungen umgesetzt: reale Hauptkomponente, echte `TStatusLine` und tastaturerreichbares `Help -> Description`. Primaere Smokes laufen durch `app.Run()` und verbinden Zustand, View-Typ, Buffer-Zellen, Status und Beschreibung; die gemeinsame Matrix besteht lokal 14/14 Tests einschliesslich fuenf stabiler `48x16`-Ansichten. Vier Beispiele nutzen `UseExistingFramework`; BHelp dokumentiert `IntentionalDeviation`, weil kein proprietaerer ungepruefter Borland-`.tch`-Decoder portiert wird. Der finale Vor-Commit-Snapshot umfasst `+918` Produktions-/Beispielcodezeilen, `+369` Testzeilen, `+2056/-30` Dokumentations-/Evidence-/Guidance-Zeilen und `+83/-4` Metadatenzeilen, insgesamt `+3426/-34` beziehungsweise `3392` Nettozeilen bei Branch-Commitzahl `0`. Konservative Manualreferenz fuer 3426 hinzugefuegte oder aktualisierte Zeilen: `42,8` Tage beziehungsweise `334,0` Stunden bei 80 Zeilen/Tag; Thorsten-Solo: `27,4` Tage beziehungsweise `213,8` Stunden bei 125 Zeilen/Tag. Sichtbares Arbeitsfenster ist der autonome Lauf am 2026-07-12; `42,8x` und `27,4x` beschreiben Lieferdichte, keine Stoppuhrzeit. Lokale Abnahme: 14/14 Wave-3-Smokes, 550/550 Volltests, alle fuenf Coverage-Gates ueber 70 %, DocFX 0 Warnungen/0 Fehler, Playwright/axe 2/2 sowie saubere Secret- und Generated-Output-Scans. Das Lastenheft ist branch-suffigiert archiviert; Remote-Abschluss folgt in derselben Feature-Evidence, naechster Intake ist Mouse Support and Interaction. |
| 2026-07-12 | Post-Merge-Ledger-Abschluss fuer `019-wave3-visual-component-porting` | PR #45 bestand Ubuntu-/macOS-CI, macOS-/Linux-/Windows-Homogeneity, DocFX, Supply Chain, Gitleaks, Agent-Secret-Scan und Claude Review auf dem finalen Head. GraphQL meldete 0 Review-Threads und 0 Konversationskommentare; Copilot konnte wegen Nutzerquota keinen Review erzeugen und bleibt als fehlender Review dokumentiert. Ausschliesslich die Human-Approval-Regel blockierte den mergefaehigen PR, daher wurde der autorisierte Admin-Bypass eng auf diese Regel begrenzt. Merge-Commit ist `60f5951`; der Remote-Featurebranch wurde geloescht und lokales sauberes `main` entsprach danach exakt `origin/main`. Der nicht leere Closeout aktualisiert nur Taskstatus, Evidence und diese Statistikzeile. |
| 2026-07-12 | Lernende Nacharbeit nach autonomem Feature 019 | Die zweite Feldbeobachtung bestaetigt vollstaendige projektlokale Red-Matrizen als belastbare Effizienzregel. Runbook, `$speckit-autonomous`, Task-Template und alle fuenf Agentenflaechen verlangen nun vor dem ersten Red-Befehl einen Compile-Surface-Check fuer Imports, oeffentliche XML-Dokumentation, Harness-Helfer, Fokus-/Ownership-Assertionen und Linked-Source-Assembly-Identitaet. Negative Faelle duerfen nur bei expliziten Einzelgrenzen und gemeinsamer Ownership gebuendelt werden; mehrfach gelinkter Quellcode wird Cross-Projekt ueber oeffentliche Vertraege oder Zustandsdelegaten bewiesen. Aenderungsvolumen vor dieser Statistikzeile: `+89` reine Dokumentations-, Skill-, Template- und Guidance-Zeilen, kein Produktions- oder Testcode. Konservative Manualreferenz: `1,1` Tage beziehungsweise `8,7` Stunden bei 80 Zeilen/Tag; Thorsten-Solo: `0,7` Tage beziehungsweise `5,6` Stunden bei 125 Zeilen/Tag. Sichtbares Arbeitsfenster ist die lernende Nacharbeit am 2026-07-12; Validierung umfasst Skill-YAML, `specify check`, Agentenparitaet, Diff, Secrets sowie den DocFX-/A11Y-/lynx-Pfad. |
+| 2026-07-12 | `/speckit-autonomous` für `020-mouse-support-interaction` | Feature 020 liefert einen begrenzten SGR-1006-Ingress in `TuiVision.Drivers.Console`, rekursive globale View-Koordinaten, Topmost-Hit/Fokus, Exactly-once-Control-Aktivierung, deterministische 500-ms-Doppelklickklassifikation und genau einen `TWindow`-Titelzeilen-Drag mit vollständigen Abbruch- und Tastaturfallbackpfaden. Die lokale Umgebung ist macOS, aber headless mit `TERM=dumb`; physische Host-Evidence bleibt deshalb ehrlich `NotRun`. Der Vorvalidierungs-Snapshot ohne diese Statistikzeile umfasst `+986/-12` Produktionscode, `+933` Tests, `+1681/-4` Dokumentation/Evidence/Guidance und `+4/-4` Metadaten, zusammen `+3604/-20` beziehungsweise `3584` Nettozeilen. Konservative Manualreferenz für 3604 hinzugefügte oder aktualisierte Zeilen: `45,1` Tage beziehungsweise `351,4` Stunden bei 80 Zeilen/Tag; Thorsten-Solo: `28,8` Tage beziehungsweise `224,9` Stunden bei 125 Zeilen/Tag. Sichtbares Arbeitsfenster ist der autonome Lauf am 2026-07-12; `45,1x` und `28,8x` beschreiben Lieferdichte, keine Stoppuhrzeit. Lokale Slice-Abnahme: Driver 54/54, Fokusregressionen 81/81, Drag 6/6 und App-Loop 5/5; voller Release-, Coverage-, DocFX-/A11Y- und Remote-Abschluss folgt in derselben Feature-Evidence. Nächster Intake ist `Lastenheft_05_TerminalCharsetAndEmulation.md`. |
## Gesamtstatistik
@@ -639,21 +644,21 @@ nächsten Statistiklauf eine dokumentierte Momentaufnahme.
| Kennzahl | Verdichteter Gesamtblick |
|---|---:|
-| Artefaktbasis gesamt | 222266 Zeilen |
-| Produktions- und Testcode zusammen | 38532 Zeilen (17.3 %) |
-| Dokumentationsanteil | 183734 Zeilen (82.7 %) |
-| Spec-Kit-Anteil innerhalb der Doku | 44399 Zeilen (24.2 %) |
-| Zentrale Governance-/Agent-Dateien | 2455 Zeilen (1.3 % der Doku) |
-| Projektgebundene Agent-Skills | 2404 Zeilen (1.3 % der Doku) |
+| Artefaktbasis gesamt | 227036 Zeilen |
+| Produktions- und Testcode zusammen | 40439 Zeilen (17.8 %) |
+| Dokumentationsanteil | 186597 Zeilen (82.2 %) |
+| Spec-Kit-Anteil innerhalb der Doku | 45859 Zeilen (24.6 %) |
+| Zentrale Governance-/Agent-Dateien | 2535 Zeilen (1.4 % der Doku) |
+| Projektgebundene Agent-Skills | 2415 Zeilen (1.3 % der Doku) |
| Beobachtbarer Projektzeitraum | 2026-02-08 bis 2026-07-12 |
| Git-Commits / sichtbare Aktivtage | 432 / 69 |
-| Gesamtzeilen pro sichtbarem Aktivtag | 3221.2 |
-| Gesamtzeilen pro Commit | 514.5 |
-| Konservative Einzelentwickler-Untergrenze | 2778.3 Arbeitstage / 21670.9 Stunden |
-| Thorsten-Solo-Untergrenze | 1778.1 Arbeitstage / 13869.4 Stunden |
-| Kleines 3er-Team mit Koordinationsaufschlag | 1111.3 Arbeitstage |
-| Repo-Speedup gegen 80-Zeilen-Referenz | 40.3x |
-| Repo-Speedup gegen Thorsten-Referenz | 25.8x |
+| Gesamtzeilen pro sichtbarem Aktivtag | 3290.4 |
+| Gesamtzeilen pro Commit | 525.5 |
+| Konservative Einzelentwickler-Untergrenze | 2838.0 Arbeitstage / 22136.0 Stunden |
+| Thorsten-Solo-Untergrenze | 1816.3 Arbeitstage / 14167.0 Stunden |
+| Kleines 3er-Team mit Koordinationsaufschlag | 1135.2 Arbeitstage |
+| Repo-Speedup gegen 80-Zeilen-Referenz | 41.1x |
+| Repo-Speedup gegen Thorsten-Referenz | 26.3x |
Die hohe Dokumentationsquote enthält die umfangreiche Secure-Development-,
Governance- und Spec-Kit-Basis. Das autonome Runbook und der neue
@@ -672,9 +677,9 @@ compare visible scope with Git active days and do not measure personal time.
```text
Artefaktmix nach Snapshot (Zeilen)
-Produktion | #### | 23153 | 10.4 %
-Tests | ### | 15379 | 6.9 %
-Dokumentation | ##############################| 183734 | 82.7 %
+Produktion | #### | 24127 | 10.6 %
+Tests | ### | 16312 | 7.2 %
+Dokumentation | ##############################| 186597 | 82.2 %
```
Die Balken verwenden dieselbe Skala. Die Zahlen bleiben der genaue,
@@ -708,6 +713,7 @@ Branch-/Phasenvolumen nach dokumentierter Netto-Basis (Zeilen)
20 auto | # | 595
21 018i | ##### | 2912
22 019i | ##### | 3392
+23 020i | ##### | 3584
```
Feature 017 kombiniert einen kleinen beispielinternen Runtime-Anteil mit einer
@@ -728,6 +734,9 @@ persistence, and integration proof plus complete Spec-Kit evidence.
Feature 019 combines five visible applications with app-loop, view, cell,
status, description, controlled-I/O, and constrained-viewport proof.
+Feature 020 combines bounded host input, focus/activation, double-click and one
+window drag with deterministic Driver, Controls, and app-loop proof.
+
```text
Konservative Handarbeits-Referenz je dokumentierter Phase
0 main | ################# | 127.6 d
@@ -753,6 +762,7 @@ Konservative Handarbeits-Referenz je dokumentierter Phase
20 auto | # | 7.4 d
21 018i | ##### | 36.4 d
22 019i | ##### | 42.4 d
+23 020i | ##### | 44.8 d
```
Die Referenz rechnet den Netto-Phasenumfang mit 80 Zeilen pro Arbeitstag. Für
@@ -766,8 +776,8 @@ before statistics maintenance. The ledger also records added or updated lines.
```text
Dokumentierte Beschleunigungsfaktoren durch agentische KI + Spec-Kit/SDD
-Repo 80 | ############################## | 39.7x
-Repo125 | ################### | 25.4x
+Repo 80 | ############################## | 41.1x
+Repo125 | ################### | 26.3x
014i | ##### | 7.0x
015i-80 | #### | 5.4x
015i125 | ### | 3.4x
@@ -781,20 +791,22 @@ AUTO125 | ### | 5.0x
018i125 | ################ | 23.6x
019i-80 | ############################# | 42.8x
019i125 | ################### | 27.4x
+020i-80 | ############################## | 45.1x
+020i125 | ################### | 28.8x
```
-Die 017-, AUTO-, 018- und 019-Werte beziehen sich jeweils auf einen sichtbaren autonomen
+Die 017-, AUTO-, 018-, 019- und 020-Werte beziehen sich jeweils auf einen sichtbaren autonomen
Arbeitstag. Sie beschreiben Lieferdichte, nicht die Dauer einzelner Denk-,
Review-, CI- oder Wartephasen.
-The 017, AUTO, 018, and 019 values each use one visible autonomous workday. They describe
+The 017, AUTO, 018, 019, and 020 values each use one visible autonomous workday. They describe
delivery density, not the duration of individual thinking, review, CI, or
waiting phases.
```text
Vergleich Gesamtaufwand / sichtbares KI-Lieferfenster
-Erfahren | ############################## | 2778.3 d
-Thorsten | ################### | 1778.1 d
+Erfahren | ############################## | 2838.0 d
+Thorsten | ################### | 1816.3 d
KI sichtbar | # | 69.0 d
```
@@ -827,7 +839,7 @@ X/Y: Phasenvolumen 0..15 (Y ungefähr in Zeilen)
```text
X/Y: Phasenvolumen 16..31 (Y ungefähr in Zeilen)
-3500 | * * * |
+3500 | * * * * |
3000 | * |
1500 | |
600 |* * |
@@ -839,10 +851,10 @@ X/Y: Phasenvolumen 16..31 (Y ungefähr in Zeilen)
```
Die beiden Blöcke zeigen den Verlauf ohne eine zu breite Zeile. G bis J stehen
-für die Phasen 16 bis 19, K für die autonome Standardisierung und L fuer
-Feature 018 und M fuer Feature 019; alle späteren Slots sind bewusst leer.
+für die Phasen 16 bis 19, K für die autonome Standardisierung, L für Feature
+018, M für Feature 019 und N für Feature 020; alle späteren Slots sind bewusst leer.
The two blocks show progression without an overly wide line. G through J
represent phases 16 through 19, K represents autonomous standardization, L
-represents Feature 018, and M represents Feature 019; all later slots are
+represents Feature 018, M represents Feature 019, and N represents Feature 020; all later slots are
intentionally empty.
diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md
index 711ffb5a..54a0d05b 100644
--- a/docs/security/threat-model.md
+++ b/docs/security/threat-model.md
@@ -1,6 +1,6 @@
# Bedrohungsmodell / Threat Model: TuiVision
-**Stand / Current as of**: 2026-07-11
+**Stand / Current as of**: 2026-07-12
**Methode / Method**: STRIDE with CIA impact and selected CAPEC patterns
**Scope**: Local terminal UI framework, tests, examples, repository tooling, CI
@@ -38,6 +38,7 @@ Security-`N/A`.
| Bereich / Area | STRIDE/CIA | Relevante Angriffsmuster / Relevant patterns | Mitigation und Evidenz / Mitigation and evidence | Restrisiko / Residual risk |
|---|---|---|---|---|
| Terminal-/Eventeingabe | Tampering, DoS; I/A | CAPEC-10 Buffer Overflow, CAPEC-20 Input Data Manipulation | Begrenzte managed Buffer, Eventvalidierung, Tests in Core/Controls/Drivers | Low |
+| SGR-1006-Mauseingang | Spoofing, Tampering, DoS; I/A | CAPEC-20 Input Data Manipulation, CAPEC-130 Excessive Allocation | Feste Sequenz-/Zahlengrenzen, atomare Syntax-/Koordinaten-/Phasenprüfung, genau null oder ein Event, Capability-Reset und Driver-/App-Loop-Tests | Low; physische Terminalvarianten bleiben Host-Evidence |
| Datei-/Ressourcenpfade | Tampering, Information Disclosure; C/I/A | CAPEC-126 Path Traversal, CAPEC-153 Input Data Manipulation | Pfadvalidierung, kontrollierte Fixtures, sichere Ablehnung, keine beliebigen Dateiinhalte als Proof | Low |
| Serialisierte Daten | Tampering, DoS; I/A | CAPEC-130 Excessive Allocation, CAPEC-153 | Truncated/trailing/unknown/cyclic rejection tests, Typregistrierung | Low |
| Fehlermeldungen/Output | Information Disclosure; C | CAPEC-215 Fuzzing for sensitive output | Keine Secrets/Stack-Traces in nutzerseitiger Ausgabe, Secret-Scans | Low |
diff --git a/docs/toc.yml b/docs/toc.yml
index 240540e5..15832308 100644
--- a/docs/toc.yml
+++ b/docs/toc.yml
@@ -8,6 +8,8 @@
href: spec-kit-autonomous-retrospectives.md
- name: Multi-Mac-Workflow
href: guides/multi-mac-workflow.md
+ - name: Maussupport und Interaktion
+ href: guides/mouse-support.md
- name: Beispiel-Guides
items:
- name: Desklogo
diff --git a/specs/020-mouse-support-interaction/checklists/domain-acceptance.md b/specs/020-mouse-support-interaction/checklists/domain-acceptance.md
new file mode 100644
index 00000000..e649d2bb
--- /dev/null
+++ b/specs/020-mouse-support-interaction/checklists/domain-acceptance.md
@@ -0,0 +1,20 @@
+# Domain Requirements Checklist: Mouse Support and Interaction
+
+**Purpose**: Validate that the feature requirements are complete and unambiguous before planning and implementation.
+**Reviewed**: 2026-07-12
+
+- [X] Is the canonical mouse event contract named without introducing a parallel model? [Spec FR-001..FR-003]
+- [X] Are supported protocol, host families, and unsupported boundaries explicit? [Spec FR-006..FR-007]
+- [X] Are syntax, size, range, state, atomic rejection, and stream-recovery requirements defined? [Spec FR-004..FR-005, Edge Cases]
+- [X] Are click focus and activation defined with one target and exactly-once semantics? [Spec FR-008..FR-009]
+- [X] Are double-click time, cell, button, and target boundaries measurable? [Spec FR-010, SC-003]
+- [X] Is exactly one drag target named with start, bounds, completion, cancellation, and fallback? [Spec FR-011..FR-012, SC-004]
+- [X] Are wheel, hover, touch, arbitrary buttons, and full protocol parity excluded? [Spec FR-013]
+- [X] Is keyboard completeness required in enabled, disabled, and unsupported states? [Spec FR-014..FR-015]
+- [X] Does primary proof require app loop, state, view identity, status, and cells? [Spec FR-016..FR-017]
+- [X] Are Driver, Core, Controls, integration, and host evidence layers distinguished? [Spec FR-018..FR-019]
+- [X] Is deterministic injection prevented from becoming a false physical-host claim? [Clarifications, FR-019]
+- [X] Are framework decisions, historical intent, didactic comments, and evidence required? [Spec FR-021..FR-025]
+- [X] Are security, A11Y, cross-platform, and all six preset triggers explicit? [Spec CR-001..CR-015]
+
+**Result**: 13/13 passed. No implementation-blocking requirement gap remains.
diff --git a/specs/020-mouse-support-interaction/checklists/plan-quality.md b/specs/020-mouse-support-interaction/checklists/plan-quality.md
new file mode 100644
index 00000000..77df5212
--- /dev/null
+++ b/specs/020-mouse-support-interaction/checklists/plan-quality.md
@@ -0,0 +1,22 @@
+# Plan Quality Checklist: Mouse Support and Interaction
+
+**Purpose**: Review the complete design package before task generation.
+**Reviewed**: 2026-07-12
+
+- [X] Does component ownership keep raw protocol in Driver, canonical events in Core, and interaction in Controls? [Plan Structure]
+- [X] Does the vertical slice start with evidence, compile-surface review, and a project-local failing Driver matrix? [Plan Vertical Slice]
+- [X] Are every parser and state rejection boundary explicit rather than hidden in a broad robustness task? [Research R2-R4]
+- [X] Are double-click timing and target identity deterministic and testable? [Research R5, Data Model]
+- [X] Does hit routing select one topmost target and use existing focus/activation contracts? [Research R6]
+- [X] Is recursive coordinate ownership recognized before nested app-loop proof? [Research R7]
+- [X] Is the single title-drag contract bounded and cancellation-complete? [Research R8, Contract]
+- [X] Are keyboard/A11Y and host fallback first-class acceptance outcomes? [Research R3, R9]
+- [X] Are historical files named and kept read-only? [Research R10]
+- [X] Are injected, app-loop, remote CI, and physical-host evidence classes separated? [Research R11, Data Model]
+- [X] Does governance cover all six presets without inventing unrelated artifacts? [Plan Governance Matrix]
+- [X] Are shared evidence, version, documentation, statistics, and agent edits serialized? [Plan Implementation Order]
+- [X] Do remote tasks have one exact acceptance ledger and bounded bypass policy? [Contract Delivery]
+- [X] Are version increments required before every build/test and alignment before commit/push? [Plan Constitution Check]
+- [X] Is generic workflow promotion deferred to a separate non-empty retrospective PR? [Research R13]
+
+**Result**: 15/15 passed. Ready for task generation.
diff --git a/specs/020-mouse-support-interaction/checklists/plan-review.md b/specs/020-mouse-support-interaction/checklists/plan-review.md
new file mode 100644
index 00000000..ffa1931a
--- /dev/null
+++ b/specs/020-mouse-support-interaction/checklists/plan-review.md
@@ -0,0 +1,17 @@
+# Plan Execution Review: Mouse Support and Interaction
+
+**Purpose**: Execute each review instruction before generating `tasks.md`.
+**Reviewed**: 2026-07-12
+
+- [X] **Trace scope**: Map FR-001..FR-026 and SC-001..SC-013 to plan sections; all have an implementation or evidence boundary.
+- [X] **Inspect architecture**: Confirm project references allow Driver -> Core and Controls -> Driver without a cycle; current csproj graph supports the design.
+- [X] **Inspect existing behavior**: Confirm `TEvent` already represents press/move/release/double-click and Controls already consume mouse down; no parallel event type is needed.
+- [X] **Inspect drag choice**: Confirm `TWindow` already owns keyboard move mode and `WindowFlags.Move`; title drag is the smallest reusable slice.
+- [X] **Inspect host claim**: Confirm `System.Console` has no current native mouse contract; native Windows remains unsupported and WSL uses terminal SGR.
+- [X] **Inspect historical intent**: Locate `tevent.cc`, `tmouse.cc`, `tview.cc`, `twindow.cc`, `unix/xtermmouse.cc`, and matching headers; tasks must review them read-only.
+- [X] **Inspect test placement**: Driver parser/state proof belongs in Drivers.Tests; focus/coordinates/drag in Controls.Tests; end-to-end loop in Controls.Tests or existing smoke infrastructure.
+- [X] **Inspect compile surface**: Tasks must validate imports, public XML docs, harness helpers, focus ownership, and linked-source identity before first red command.
+- [X] **Inspect validation triggers**: Shared runtime changes require full Release and coverage; public XML/guide/toc changes require DocFX then axe.
+- [X] **Inspect delivery**: MergeAndSync authority is explicit; every remote task must update `pr-evidence.md`; no empty closeout PR.
+
+**Result**: 10/10 instructions executed. No corrective plan edit remains.
diff --git a/specs/020-mouse-support-interaction/checklists/requirements.md b/specs/020-mouse-support-interaction/checklists/requirements.md
new file mode 100644
index 00000000..3646193f
--- /dev/null
+++ b/specs/020-mouse-support-interaction/checklists/requirements.md
@@ -0,0 +1,39 @@
+# Specification Quality Checklist: Mouse Support and Interaction Hardening
+
+**Purpose**: Validate specification completeness and quality before planning
+**Created**: 2026-07-12
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [X] No implementation details beyond binding domain and governance contracts
+- [X] Focused on user value and business needs
+- [X] Written for non-technical stakeholders at CEFR-B2
+- [X] All mandatory sections completed
+
+## Requirement Completeness
+
+- [X] No `[NEEDS CLARIFICATION]` markers remain
+- [X] Requirements are testable and unambiguous
+- [X] Success criteria are measurable
+- [X] Success criteria are outcome-focused
+- [X] All acceptance scenarios are defined
+- [X] Edge cases are identified
+- [X] Scope is clearly bounded
+- [X] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [X] All functional requirements have clear acceptance criteria
+- [X] User scenarios cover primary flows
+- [X] Host capability and keyboard fallback are explicit
+- [X] Framework decision vocabulary is exact
+- [X] Governance applicability covers all six presets
+- [X] Compile-surface and grouped-red rules are explicit
+
+## Notes
+
+- Validation pass 1: 18/18 items complete.
+- The specification makes no unresolved protocol, drag, host, or permission
+ decision. Planning must choose the smallest implementation compatible with
+ these outcome boundaries.
diff --git a/specs/020-mouse-support-interaction/contracts/mouse-interaction-acceptance.md b/specs/020-mouse-support-interaction/contracts/mouse-interaction-acceptance.md
new file mode 100644
index 00000000..1f2dce3c
--- /dev/null
+++ b/specs/020-mouse-support-interaction/contracts/mouse-interaction-acceptance.md
@@ -0,0 +1,61 @@
+# Acceptance Contract: Mouse Support and Interaction
+
+## Ingress Contract
+
+- Accept only complete, bounded SGR-1006 left press, pressed move, and release reports.
+- Validate syntax, numeric size, one-based coordinates, buffer bounds, capability,
+ and phase order before creating any `TEvent`.
+- Publish exactly zero or one canonical event per observation.
+- Reject malformed or unsupported input atomically while preserving the next
+ independent valid observation.
+- Disable reporting and clear transient state on capability loss and shutdown.
+
+## Interaction Matrix
+
+| Area | Required route | Required outcome | Negative/fallback proof |
+|---|---|---|---|
+| Click focus | left press through app loop | one topmost eligible target becomes group focus | covered, hidden, disabled, or non-selectable target is not focused |
+| Activation | focused control receives normal mouse event | existing command fires exactly once | outside/invalid/duplicate input fires none |
+| Double click | second qualifying left press | one event carries `DoubleClick=true` | time >500 ms, other cell/button/target stay single |
+| Window drag | title press, moves, release | one movable window stays within owner bounds | Escape/capability loss/removal/shutdown clears drag |
+| Keyboard fallback | existing keys/commands | same mandatory task remains operable | disabled/unsupported mouse does not consume keys |
+
+## Host Contract
+
+- macOS/Linux interactive SGR terminals and WSL are the supported family.
+- Native Windows Console, redirected/headless sessions, and unknown terminals are
+ explicit `Unsupported` unless current evidence proves otherwise.
+- Deterministic injection proves parser and state contracts, not physical host I/O.
+- Each host row records terminal, capability, evidence class, result, risk, and trigger.
+
+## Primary Proof Contract
+
+At least one integration test runs the real app loop and combines:
+
+1. queued controlled host observations,
+2. two focusable controls and one movable window,
+3. concrete focus, activation-count, double-click, and drag assertions,
+4. concrete view-tree identity,
+5. visible status plus stable buffer/cell assertions,
+6. keyboard-equivalent operation with mouse disabled or unsupported.
+
+Direct parser calls and helper methods are setup or supplemental proof only.
+
+## Framework Usage Contract
+
+Each area receives exactly one decision: `UseExistingFramework`,
+`SmallFrameworkFix`, `IntentionalDeviation`, or `FollowUpHardening`. No parser,
+event model, or reusable interaction logic may live in an example project.
+
+## Security and A11Y Contract
+
+Raw input is untrusted and fail-safe. Essential state is text-visible and never
+depends only on pointer, color, or layout. Keyboard paths remain complete.
+Learner-facing docs are German-first/English-second CEFR-B2 and text-first.
+
+## Delivery Contract
+
+Every push, PR, check, review, merge, cleanup, closeout, and local-sync task names
+`specs/020-mouse-support-interaction/pr-evidence.md`. Missing reviewer capacity
+is missing review, never success. A bypass requires explicit authority, green
+required checks, zero actionable GraphQL threads, and only the named human rule.
diff --git a/specs/020-mouse-support-interaction/data-model.md b/specs/020-mouse-support-interaction/data-model.md
new file mode 100644
index 00000000..d1e139c9
--- /dev/null
+++ b/specs/020-mouse-support-interaction/data-model.md
@@ -0,0 +1,119 @@
+# Data Model: Mouse Support and Interaction Hardening
+
+The feature adds no persistent business data. These in-process and evidence
+entities define the bounded input, interaction, proof, and governance states.
+
+## MouseCapability
+
+| Field | Type | Rules |
+|---|---|---|
+| `State` | enum | `Enabled`, `Disabled`, or `Unsupported` |
+| `HostFamily` | enum | `MacOS`, `Linux`, `Wsl`, `WindowsConsole`, `Headless`, `Unknown` |
+| `Protocol` | enum | `Sgr1006` or `None` |
+| `Reason` | string | Non-empty bilingual/text-first status source |
+| `ReevaluationTrigger` | string | Required for `Unsupported` |
+
+Transitions: `Disabled -> Enabled` only after host checks; any failure or
+shutdown reaches `Disabled` or `Unsupported` and clears pressed/click state.
+
+## HostMouseObservation
+
+| Field | Type | Rules |
+|---|---|---|
+| `RawSequence` | bounded string | Complete SGR report within configured maximum |
+| `TimestampMs` | long | Monotonic, non-negative |
+| `BufferWidth/Height` | integer | Positive current dimensions |
+| `TargetId` | optional string | Stable key returned by caller-supplied point resolver; no Controls object crosses into Driver |
+| `Trust` | constant | Always `Untrusted` until validation completes |
+
+## MouseIngressState
+
+| Field | Type | Rules |
+|---|---|---|
+| `PressedButton` | enum | `None` or `Left` in Feature 020 |
+| `LastPosition` | point | Valid only while pressed |
+| `LastClickTimeMs` | optional long | Cleared on capability reset/clock regression |
+| `LastClickPosition` | optional point | Exact cell equality required |
+| `LastClickTargetId` | optional string | Exact target equality required |
+| `SequenceState` | enum | `Idle` or complete-observation processing only |
+
+## CanonicalMousePublication
+
+| Field | Type | Rules |
+|---|---|---|
+| `Kind` | enum | `MouseDown`, `MouseMove`, or `MouseUp` |
+| `Buttons` | enum | `Left` while pressed; release semantics documented |
+| `Where` | point | Zero-based and inside current buffer |
+| `DoubleClick` | bool | True only on qualifying second `MouseDown` |
+| `PublicationCount` | integer | Exactly `0` or `1` per observation |
+
+## DragSession
+
+| Field | Type | Rules |
+|---|---|---|
+| `Target` | `TWindow` reference | Movable, visible, owned, not disabled |
+| `StartPointer` | point | Press on title row |
+| `StartBounds` | rectangle | Used for delta and cancellation restore |
+| `CurrentBounds` | rectangle | Clamped fully inside owner extent |
+| `State` | enum | `Idle`, `Dragging`, `Committed`, `Cancelled` |
+| `EndReason` | enum | `Release`, `Escape`, `CapabilityLoss`, `Disabled`, `Removed`, `Shutdown` |
+
+## InteractionProofRecord
+
+| Field | Type | Rules |
+|---|---|---|
+| `ProofId` | string | Stable and unique |
+| `Route` | string | Raw sequence and/or keyboard events through `app.Run()` |
+| `TargetIdentity` | string | Concrete expected view/window |
+| `FocusBefore/After` | string | Explicit ownership assertion |
+| `CommandCount` | integer | Exactly-once where activation applies |
+| `DragState/Bounds` | string | Required for drag cases |
+| `KeyboardEquivalent` | string | Required for every mandatory operation |
+| `VisibleStatus` | string | Text-first assertion |
+| `RenderedRegion` | rectangle/text | Stable buffer/cell proof |
+| `Result` | enum | `Pending`, `Pass`, `Fail`, `Deferred` |
+| `ProofBoundary` | string | Distinguishes injected, app-loop, and physical-host proof |
+
+## HostEvidenceRecord
+
+| Field | Type | Rules |
+|---|---|---|
+| `Host` | enum | macOS, Linux, WSL, native Windows Console |
+| `Terminal` | string | Named when known |
+| `Capability` | enum | Capability state |
+| `EvidenceClass` | enum | `DeterministicInjection`, `PhysicalSpotCheck`, `RemoteCI`, `NotRun` |
+| `Result` | enum | `Pass`, `Unsupported`, `NotRun`, `FollowUpHardening` |
+| `ResidualRisk` | string | Always explicit |
+| `ReevaluationTrigger` | string | Required unless result is a physical pass |
+
+## FrameworkUsageDecision
+
+Allowed values are exactly `UseExistingFramework`, `SmallFrameworkFix`,
+`IntentionalDeviation`, and `FollowUpHardening`. Each contract area receives
+one value. Fixes require focused red and regression proof; deviations require
+historical intent and learner impact; follow-ups require owner and trigger.
+
+## GovernanceCheckpoint
+
+| Field | Type | Rules |
+|---|---|---|
+| `RunId` | string | `020-mouse-support-interaction` |
+| `PresetName/Version` | string/version | Exact installed preset |
+| `Checkpoint` | string | Named governance concern |
+| `Applicability` | enum | `Applicable`, `N/A`, or `Open` |
+| `Rationale/EvidencePath` | string/path | Always required |
+| `Owner/Reviewer/ReviewDate` | identity/date | Required before completion |
+| `Result/ResidualRisk` | string | Explicit |
+| `FollowUp/ReevaluationTrigger` | string | Required for `Open`; trigger required for `N/A` |
+
+## Relationships
+
+```text
+MouseCapability 1 --- 1 MouseIngressState
+HostMouseObservation 1 --- 0..1 CanonicalMousePublication
+CanonicalMousePublication * --- 0..1 DragSession
+InteractionProofRecord * --- 1 FeatureEvidence
+HostEvidenceRecord * --- 1 FeatureEvidence
+FrameworkUsageDecision * --- 1 FeatureEvidence
+GovernanceCheckpoint * --- 1 FeatureEvidence
+```
diff --git a/specs/020-mouse-support-interaction/plan.md b/specs/020-mouse-support-interaction/plan.md
new file mode 100644
index 00000000..93686110
--- /dev/null
+++ b/specs/020-mouse-support-interaction/plan.md
@@ -0,0 +1,214 @@
+# Implementation Plan: Mouse Support and Interaction Hardening
+
+**Branch**: `020-mouse-support-interaction` | **Date**: 2026-07-12 | **Spec**: [spec.md](spec.md)
+**Input**: `Lastenheft_04_MouseSupportAndInteraction.md` and the accepted Feature-019 baseline
+
+## Summary
+
+Add one bounded SGR-1006 host ingress to the existing console driver and route
+validated events through the existing `TEvent` and app-loop contracts. Harden
+click focus and exactly-once activation, deterministic double-click detection,
+and exactly one drag interaction: moving a movable `TWindow` by its title row.
+All required operations retain keyboard routes and visible text status. Native
+Windows Console, wheel, hover, touch, arbitrary buttons, and complete terminal
+protocol parity remain explicit follow-ups.
+
+## Technical Context
+
+**Language/Version**: C# 14 on .NET 10
+**Primary Dependencies**: Existing `TuiVision.Core`, `TuiVision.Controls`, and `TuiVision.Drivers.Console`; no new packages
+**Storage**: In-memory event, click, drag, and evidence state only
+**Testing**: MSTest 4, deterministic parser/state tests, real app-loop integration proof, Coverlet, DocFX, Playwright with axe
+**Target Platform**: SGR-1006 terminals on macOS/Linux and WSL; native Windows Console is an honest unsupported boundary
+**Project Type**: Multi-project terminal UI framework
+**Performance Goals**: One canonical event per accepted observation; bounded sequence size and no unbounded parser wait
+**Constraints**: Keyboard-first, no new dependencies, no example-local parser, no `tv203s/` edits, exactly one drag target, no Wave-4 scope
+**Scale/Scope**: Core event validation, console ingress/capability, group focus routing, window drag, one integration harness, guides/evidence/governance
+
+## Constitution Check
+
+*GATE: Passed before research and re-checked after design.*
+
+- **Level-2 environment**: .NET 10, MSTest, coverage, DocFX/A11Y,
+ statistics, versioning, and five agent surfaces remain binding.
+- **Memory-safe language**: C#/.NET is approved; historical C/C++ is read-only.
+- **Secure coding**: Raw terminal input is untrusted, size-bounded, fully
+ validated before publication, and atomically rejected on malformed state.
+- **Architecture**: Driver owns host protocol and click classification, Core
+ owns canonical events, Controls own focus/activation/drag semantics.
+- **NIST SSDF / CWE Top 25**: Applicable to parser boundaries, integer ranges,
+ state transitions, fail-safe deactivation, tests, and review evidence.
+- **ASVS**: `N/A`; no web/API/auth surface. Re-evaluate if one appears.
+- **Supply chain**: Existing SBOM/VEX/SLSA/OpenSSF evidence remains authoritative;
+ no package, distribution, lockfile, or provenance change is planned.
+- **AI-SBOM / regulation**: `N/A`; no runtime/product AI or regulated operated service.
+- **STRIDE/CIA/CAPEC**: Proportional review applies to terminal spoofing,
+ malformed input, duplicate dispatch, stale state, and availability limits.
+- **S-ADR / arc42**: Existing driver boundary is extended without a new
+ deployment or trust boundary; create no new artifact unless implementation deviates.
+- **Zero Trust / SAMM / BSI C3A / BSI C5**: `N/A`; no cloud, provider,
+ distributed service, or operations boundary changes.
+- **iSAQB**: Component ownership, quality goals, and deliberate protocol limits
+ are explicit; a parallel UI mouse abstraction is prohibited.
+- **A11Y**: Keyboard completeness, visible status, text-first guides, WCAG 2.2
+ AA review, and DocFX/axe proof are applicable.
+- **Didactic comments**: New parser, state-machine, dispatch, and proof logic is
+ reviewed for concise why/trade-off/proof-boundary comments.
+- **Cross-platform**: Host classification and fallbacks are applicable; script
+ governance is `N/A` because no scripts are planned.
+- **Agent parity**: Active feature context changes; all five maintained agent
+ surfaces are synchronized at completion.
+- **Autonomous task rule**: Every remote task names
+ `specs/020-mouse-support-interaction/pr-evidence.md` as acceptance ledger.
+- **Versioning**: Increment only the manual build component before each build
+ or test; align all fields to `1.20..` before commit/push.
+
+### Governance Checkpoint Matrix
+
+| Domain | Planned applicability | Evidence boundary |
+|---|---|---|
+| NIST SSDF / CWE Top 25 | Applicable | Parser, state, dispatch, tests, feature evidence |
+| OWASP ASVS | N/A unless web/API/auth appears | Existing ASVS ledger plus trigger row |
+| SBOM / VEX / SLSA / OpenSSF | Existing baseline; no new artifact | Supply-chain ledger plus feature row |
+| AI-SBOM / NIS2 / CRA / EU AI Act / DORA | N/A for local non-AI training framework | Feature row with re-evaluation trigger |
+| STRIDE / CIA / CAPEC | Applicable to untrusted host input and state/availability | Threat/evidence rows |
+| S-ADR / arc42 / Zero Trust / SAMM | N/A for new artifacts absent architecture change | Existing architecture/security evidence |
+| BSI C3A / BSI C5 | N/A; no cloud/provider boundary | Cloud applicability ledgers plus feature row |
+| iSAQB architecture | Applicable | Plan, research, component decisions |
+| A11Y | Applicable | Keyboard fallback, status, guides, DocFX/axe |
+| Cross-platform | Host/fallback proof applicable; script governance N/A | Host matrix and CI evidence |
+| Agent parity | Applicable | Five synchronized agent surfaces |
+
+## Project Structure
+
+### Documentation
+
+```text
+specs/020-mouse-support-interaction/
+├── spec.md
+├── plan.md
+├── research.md
+├── data-model.md
+├── quickstart.md
+├── contracts/
+│ └── mouse-interaction-acceptance.md
+├── checklists/
+├── tasks.md
+└── pr-evidence.md
+```
+
+### Runtime and Tests
+
+```text
+src/TuiVision.Core/TEvent.cs
+src/TuiVision.Drivers.Console/
+├── ConsoleMouseIngress.cs
+├── TConsoleDriver.cs
+└── DriverCapabilityMap.cs
+src/TuiVision.Controls/
+├── TProgram.cs
+├── TGroup.cs
+├── TView.cs
+└── TWindow.cs
+tests/TuiVision.Core.Tests/
+tests/TuiVision.Drivers.Tests/
+tests/TuiVision.Controls.Tests/
+docs/guides/mouse-support.md
+```
+
+**Structure Decision**: The console driver owns SGR framing, capability, and
+click classification. It emits only existing `TEvent` values. `TProgram` owns
+terminal lifecycle integration; `TGroup` owns hit/focus routing; `TWindow` owns
+the single title-drag session. No example project receives reusable mouse logic.
+
+## Phase 0: Research Decisions
+
+1. Use complete SGR-1006 sequences only; reject partial, oversized, unknown,
+ wheel, and invalid transition input atomically.
+2. Support interactive macOS/Linux terminals and WSL when SGR mode is available;
+ keep native Windows Console and redirected/headless input unsupported.
+3. Preserve `TEventKind` and `TMouseEvent` as the sole UI contract.
+4. Detect double-click in the ingress using injected monotonic milliseconds,
+ same left button, same cell, same target identity, and `<= 500 ms`.
+5. Route mouse down to the topmost visible eligible hit target and transfer
+ focus through `TGroup.SetFocus` before normal activation.
+6. Use movable `TWindow` title-row dragging as the only drag contract; clamp to
+ owner bounds and preserve `Ctrl+F5` plus arrows as keyboard fallback.
+7. Cancel drag on release, Escape, disable/capability loss, target removal, and shutdown.
+8. Use deterministic injected sequences as CI proof; label physical host checks
+ accurately and never infer hardware validation from parser tests.
+
+## Phase 1: Design
+
+### Runtime Design
+
+- `ConsoleMouseIngress` holds capability state, bounded SGR parsing, pressed
+ button state, and last-click state. It publishes no event until the complete
+ sequence and transition are valid.
+- `TConsoleDriver` owns the ingress and a controlled input queue used by the
+ real `TProgram.GetEvent` route in tests and by host sequence collection. A
+ caller-supplied point-to-target-key delegate lets the Driver compare click
+ targets without referencing Controls or exposing view objects.
+- `TProgram.Run` enables SGR reporting only for supported interactive hosts and
+ always disables it during cleanup. `GetEvent` checks mouse input before normal
+ keyboard translation without changing keyboard semantics.
+- `TView` global/local coordinate conversion includes its owner chain so nested
+ hit testing uses actual screen coordinates.
+- `TGroup` selects one topmost visible hit target. Selectable targets receive
+ group focus before the same event continues through their existing handler.
+- `TWindow` starts drag only on left `MouseDown` in the top title row, updates
+ on valid pressed movement, clamps inside its owner, and commits on release.
+
+### Vertical Slice
+
+1. Create evidence and complete the compile-surface review.
+2. Add one failing Driver matrix for valid press/move/release, malformed input,
+ and exact double-click boundaries.
+3. Implement the bounded ingress and prove one event per accepted observation.
+4. Add an app-loop harness with two controls and one movable window.
+5. Prove click focus/activation, title drag, visible state/cells, and keyboard fallback.
+6. Spread negative, host, cancellation, documentation, and governance proof.
+
+### Proof Design
+
+- Driver matrix records raw sequence, capability, timestamp, previous state,
+ result, event payload, rejection reason, and remaining-stream boundary.
+- Interaction matrix records target identity, focus before/after, command count,
+ drag state/bounds, keyboard equivalent, status text, view identity, cells, and result.
+- Host matrix distinguishes `Pass`, `Unsupported`, `NotRun`, and
+ `FollowUpHardening`; deterministic injection never becomes physical-host proof.
+- Primary integration proof runs `app.Run()` with queued host observations and
+ keyboard events, then asserts state, view tree, status, and rendered cells.
+
+### Documentation and Governance Design
+
+- Add one bilingual mouse-support guide and DocFX navigation entry.
+- Update driver capability evidence, security/architecture applicability,
+ Pflichtenheft next marker, all five agent contexts, and project statistics.
+- Archive the binding Lastenheft after all acceptance and validation gates pass.
+- Keep any generic autonomous-workflow refinement on a separate retrospective PR.
+
+## Phase 2: Implementation Order
+
+1. Evidence schema, historical inventory, framework/host/governance matrices.
+2. Compile-surface review and failing Driver vertical-slice matrix.
+3. SGR ingress, capability lifecycle, exactly-once mapping, and targeted proof.
+4. Failing Core/Controls focus and coordinate tests, then smallest framework fixes.
+5. Failing window-drag/cancellation tests, then the single drag implementation.
+6. Real app-loop integration, visible status, view/cell proof, and keyboard fallback.
+7. Host matrix, negative/fuzz-style bounded cases, comments, and framework decisions.
+8. Guide, DocFX navigation, security/architecture evidence, Pflichtenheft, agents, statistics.
+9. Static, targeted, full Release, coverage, DocFX/A11Y, secrets, and hygiene gates.
+10. Archive, version, commit, push, PR, review convergence, merge, cleanup, and main sync.
+
+## Post-Design Constitution Re-check
+
+Passed. The design adds no dependency, network, cloud, authentication, script,
+runtime AI, new example, or broad terminal-emulation scope. The only new raw
+input is bounded before publication. Keyboard and unsupported-host behavior are
+first-class. A material need for native Windows input, a second drag target, or
+a parallel event model stops local implementation and becomes follow-up work.
+
+## Complexity Tracking
+
+No Constitution violation or exceptional complexity is planned.
diff --git a/specs/020-mouse-support-interaction/pr-evidence.md b/specs/020-mouse-support-interaction/pr-evidence.md
new file mode 100644
index 00000000..2db3bbaa
--- /dev/null
+++ b/specs/020-mouse-support-interaction/pr-evidence.md
@@ -0,0 +1,260 @@
+# Autonomous Run Evidence: Mouse Support and Interaction Hardening
+
+**Branch**: `020-mouse-support-interaction`
+**Feature directory**: `specs/020-mouse-support-interaction`
+**Binding intake**: `Lastenheft_04_MouseSupportAndInteraction.020-mouse-support-interaction.md`
+**Delivery mode**: `MergeAndSync`
+**Authority source**: User instruction to implement Features 018-023 autonomously, including authorized non-empty PRs, merge, main sync, and narrowly bounded admin bypass
+
+## Scope
+
+### Included
+
+- Bounded SGR-1006 host ingress into the existing canonical `TEvent` contract.
+- Click focus, exactly-once activation, deterministic double click, one `TWindow` title drag, keyboard fallback, host evidence, docs, governance, and validation.
+
+### Excluded
+
+- Native Windows Console mouse backend, X10/full terminal parity, wheel, hover,
+ touch, extra buttons, second drag target, TP7 demo, Wave-4 terminal/charset
+ behavior, new dependencies, broad redesign, cloud/network/runtime AI, and `tv203s/` edits.
+
+## Run Gates
+
+| Phase | Attempt | Result | Evidence | Remaining action |
+|---|---:|---|---|---|
+| Preflight | 1 | Pass | Branch and `origin/main` baseline `c7964ab88b73184347d30e34813fcb6680d2307a`; `specify check`; prerequisites; presets; 56/56 checklists | None |
+| Specify | 1 | Pass | `spec.md`, requirements 18/18 | None |
+| Clarify | 2 | Pass | Four decisions in `spec.md`; second pass found no material ambiguity | None |
+| Checklists | 3 | Pass | `checklists/`, 56/56 complete | None |
+| Plan | 1 | Pass | `plan.md`, research, model, contract, quickstart | None |
+| Tasks | 1 | Pass | `tasks.md`, T001-T126 sequential | None |
+| Analyze | 3 | Pass | Final pass: 54/54 FR/CR/SC present and mapped, 126 tasks, 56/56 checklist items, no unmapped task, Critical/High/Medium 0 | None |
+| Implement | 1 | Pass | T001-T115 local tasks complete; runtime, tests, docs, governance, statistics, archive, and final consistency complete | Authorized delivery T116-T126 |
+| Validate | 1 | Pass | 120 targeted, 584 full, five coverage gates, DocFX/axe/lynx, format/diff/secrets | None |
+| Deliver | 1 | Open | Remote Delivery table | Execute T116-T126 |
+
+## Artifact Convergence
+
+| Artifact or pass | Result | Evidence |
+|---|---|---|
+| Requirements quality | Pass | `checklists/requirements.md` 18/18 |
+| Domain acceptance | Pass | `checklists/domain-acceptance.md` 13/13 |
+| Plan quality | Pass | `checklists/plan-quality.md` 15/15 |
+| Plan execution review | Pass | `checklists/plan-review.md` 10/10 |
+| Task IDs | Pass | T001-T126, no duplicates or gaps |
+| Requirement coverage | Pass | `tasks.md` maps FR-001..FR-026, CR-001..CR-015, SC-001..SC-013 |
+
+## Preflight Results
+
+| Check | Result | Evidence |
+|---|---|---|
+| Branch and ancestry | Pass | `020-mouse-support-interaction`; `origin/main` is ancestor; branch and origin baseline both `c7964ab88b73184347d30e34813fcb6680d2307a` before feature edits |
+| Feature metadata | Pass | `.specify/feature.json` -> `specs/020-mouse-support-interaction` |
+| `specify check` | Pass | CLI ready; required local agent/tool integration available |
+| PowerShell prerequisites | Pass | Feature directory resolved; research, model, contracts, quickstart, and tasks available |
+| Checklists | Pass | requirements 18/18, domain 13/13, plan quality 15/15, plan review 10/10 |
+| Presets | Pass | security 0.6.0/10; architecture 0.5.0/20; isaqb 0.2.0/30; a11y 0.4.0/40; cross-platform 0.2.0/50; agent-parity 0.3.0/60 |
+| Governance conflict | None | Accepted artifacts align with current Constitution and installed preset matrix |
+
+## Optional Command Disposition
+
+| Command | Result | Rationale |
+|---|---|---|
+| `speckit-constitution` | N/A | Current Constitution and six-preset matrix contain no material conflict requiring a constitutional edit |
+| `speckit-taskstoissues` | N/A | The accepted delivery unit is one dependency-ordered feature PR; splitting 126 single-writer tasks into remote issues would add no execution proof |
+
+## Ingress Observation Matrix
+
+| Case | Capability | Raw/condition | Previous state | Expected publication | Rejection/recovery boundary | Result |
+|---|---|---|---|---|---|---|
+| Valid left press | Enabled | Complete SGR 1006 | Idle | One `MouseDown` | None | Pass |
+| Valid pressed move | Enabled | Complete SGR 1006 move | Left pressed | One `MouseMove` | None | Pass |
+| Valid release | Enabled | Complete SGR 1006 release | Left pressed | One `MouseUp` | Clear press | Pass |
+| Invalid matrix | Any | Truncated/oversized/non-numeric/range/button/phase | Any | Zero | Next independent observation preserved | Pass |
+| Qualifying double | Enabled | Same cell/target/left within 500 ms | Prior click | Second down has double flag | Monotonic only | Pass |
+| Non-qualifying double | Enabled | 501 ms/cell/target/clock/reset mismatch | Any | Single click | Reset stale state | Pass |
+
+### Driver Red/Green Evidence
+
+| Attempt | Version | Result | Boundary |
+|---:|---|---|---|
+| Red 1 | `1.20.0.125` | Expected compile failure | Only missing `ConsoleMouseIngress`, capability, and rejection contracts |
+| Green attempt 1 | `1.20.0.126` | Compile failure | Definite assignment in bounded number parser; corrected explicitly |
+| Green attempt 2 | `1.20.0.127` | 52/53 | Existing capability-map test required the durable phrase `not reproduced` |
+| Green 1 | `1.20.0.128` | 53/53 | Initial parser/state matrix passed without analyzer warnings |
+| Host red | `1.20.0.129` | Expected compile failure | Missing deterministic host detector only |
+| Host green attempt | `1.20.0.130` | Compile failure | Namespace collision required `System.Console` qualification |
+| Driver final | `1.20.0.131` | 54/54 | Parser, state, recovery, double click, host classification, and existing Driver regressions passed |
+
+The grouped red matrix retained an explicit expected result for every malformed,
+range, phase, capability, recovery, and click-boundary case in one Driver-owned
+test file. No cross-project ownership or hidden aggregate assertion was used.
+
+## Interaction Matrix
+
+| Area | Route | Target/focus | Command/drag outcome | Keyboard equivalent | Status/view/cell proof | Result |
+|---|---|---|---|---|---|---|
+| Click focus | `app.Run()` | Topmost second button | One focus transfer | Existing focus/key routes retained | `TButton` identity and status/cells | Pass |
+| Activation | `app.Run()` | Focused second button | Command exactly once | Enter activates first button | Command count, identity, text/cells | Pass |
+| Double click | `app.Run()` | Same concrete proof view | Two downs, exactly one double flag | Existing command route remains separate | Status/cells and concrete counters | Pass |
+| Title drag | `app.Run()` | Movable `TWindow` | Clamped move/release plus focused cancellation tests | Ctrl+F5/arrows/Enter/Escape | Bounds, `TWindow`, final `┌` cell | Pass |
+| Fallback | Disabled/unsupported app loops | First button | One keyboard activation | Enter | `Keyboard activated First` cells | Pass |
+
+### Runtime/App-Loop Red/Green Evidence
+
+| Attempt | Version | Result | Boundary |
+|---:|---|---|---|
+| Runtime red | `1.20.0.138` | Expected compile failure | Missing `ConfigureMouseCapability` and queue-status contracts only |
+| Runtime green | `1.20.0.139` | 5/5 | Real GetEvent, focus/activation, double click, drag/cell, and fallback passed |
+| Runtime final | `1.20.0.140` | 5/5 | Added explicit enabled-to-disabled shutdown assertion; all primary proof passed |
+
+## Host Evidence
+
+| Host | Terminal/condition | Capability | Evidence class | Result | Residual risk | Re-evaluation trigger |
+|---|---|---|---|---|---|---|
+| macOS | Interactive SGR terminal | Disabled until runtime enables | DeterministicInjection; physical NotRun | Contract Pass; physical NotRun because session stdin/stdout are not TTY and `TERM=dumb` | Physical terminal diversity | Interactive macOS spot-check |
+| Linux | Interactive SGR terminal | Disabled until runtime enables | DeterministicInjection/RemoteCI pending | Contract Pass | No local physical host | Linux CI or manual host available |
+| WSL | Windows Terminal plus WSL | Disabled until runtime enables | DeterministicInjection/RemoteCI pending | Contract Pass | Backend configuration varies | WSL CI/manual host available |
+| Native Windows Console | No SGR contract in 020 | Unsupported | Contract review | Unsupported | No native mouse events | Dedicated native backend feature |
+| Headless/redirected | Non-interactive I/O | Unsupported | DeterministicInjection | Pass | None after fail-safe proof | I/O model changes |
+
+## Framework Decisions
+
+| Area | Decision | Existing component | Local logic | Rationale | Evidence | Follow-up boundary |
+|---|---|---|---|---|---|---|
+| Host ingress | SmallFrameworkFix | `TConsoleDriver` | Bounded SGR parser/state | Missing real host-to-event path | Driver tests | Other protocols/backends |
+| Canonical event | UseExistingFramework | `TEvent`/`TMouseEvent` | None | Existing contract is sufficient | Core tests | None |
+| Focus routing | SmallFrameworkFix | `TGroup.SetFocus`/dispatch | Topmost hit selection | Current all-child mouse dispatch lacks ownership | Controls tests | Complex capture/bubbling |
+| Activation | UseExistingFramework | Control handlers/commands | None | Existing command route must remain exactly once | App-loop proof | None |
+| Double click | SmallFrameworkFix | `TMouseEvent.DoubleClick` | Driver classifier | Existing payload lacks runtime classifier | Driver/app proof | Configurable tolerance |
+| Title drag | SmallFrameworkFix | `TWindow` keyboard move | One bounded mouse session | Reuses existing move capability | Window tests | Any second drag target |
+| Keyboard fallback | UseExistingFramework | Current key/command routes | Status only | Mouse augments keyboard | App-loop proof | None |
+
+## Didactic Comment Decisions
+
+| Area | Decision | Rationale | Change or boundary | Result |
+|---|---|---|---|---|
+| SGR parser | CommentNeeded | Atomic publication and protocol limit are non-trivial | Structure plus XML contract make complete-before-publication boundary explicit; no trivial inline narration | Pass |
+| Double-click state | CommentNeeded | Monotonic time and target key prevent false combining | Two-line bilingual clock-regression reason before reset | Pass |
+| Topmost focus routing | CommentNeeded | Z-order and ownership are not obvious | Two bilingual blocks explain container FirstClick and shared Topmost target | Pass |
+| Title drag cancellation | CommentNeeded | Multiple end paths protect state | Two-line bilingual release-versus-cancel boundary | Pass |
+| Obvious enum/property declarations | NoCommentNeeded | XML docs are sufficient | No inline restatement | Pass |
+
+## Historical Intent
+
+| Modern area | Historical source | Intent retained | Intentional deviation | Proof or rationale |
+|---|---|---|---|---|
+| Event/double click | `tv203s/contrib/tvision/classes/tevent.cc`, `tmouse.cc`, `include/tv/event.h` | State changes produce down/up/move; repeated same-button/same-position presses inside `doubleDelay` carry double-click | Managed monotonic milliseconds, stable target key, no polling auto-repeat | Reviewed read-only; Driver/Core tests own proof |
+| Host ingress | `tv203s/contrib/tvision/classes/unix/xtermmouse.cc`, Unix mouse headers | Explicit terminal reporting enable/disable and backend isolation | SGR 1006 only instead of historical 1000/1002/native matrix | Reviewed read-only; capability/lifecycle tests own proof |
+| Hit/focus | `tv203s/contrib/tvision/classes/tview.cc`, `include/tv/view.h` | Global hit testing and one selected receiver | Managed recursive owner-chain coordinates and one topmost target | Reviewed read-only; Controls tests own proof |
+| Window move | `tv203s/contrib/tvision/classes/twindow.cc`, `include/tv/window.h` | `wfMove`, owner bounds, resize/drag command, keyboard selection | Event-driven title drag plus retained managed keyboard mode; no grow/zoom | Reviewed read-only; window tests own proof |
+
+## Compile-Surface Review
+
+| Surface | Result | Decision |
+|---|---|---|
+| Project graph | Pass | Driver references Core; Controls references Driver/Core; no reverse reference or new project required |
+| Imports and public XML docs | Pass | New public Driver capability/ingress APIs require complete bilingual XML documentation; no undocumented public type is accepted |
+| Harness helpers | Pass | Existing queued `TProgram` patterns and buffer capture can be reused; new integration harness remains in Controls.Tests |
+| Focus and ownership assertions | Pass | Tests assert `TGroup.Current`, `TViewState.Focused`, exact target identity, and command count rather than text alone |
+| Linked-source identity | N/A | No new linked source file or cross-assembly type identity is planned |
+| Target resolver boundary | Pass | Controls supplies a `TPoint -> string?` delegate; Driver stores only a stable key and never references a Controls type |
+| Example-local parser scan | Pass | No current example contains SGR/1006/raw mouse parsing; future reusable logic remains prohibited |
+| Placeholder scan | Pass | No unresolved marker; checklist/task references to marker names are normative instructions only |
+| Historical diff | Pass | `git diff -- tv203s/` empty before and after read-only review |
+
+## Controls and Drag Red/Green Evidence
+
+| Slice | Version | Result | Boundary |
+|---|---|---|---|
+| Focus red | `1.20.0.132` | 1/5 pass, 4 expected failures | Nested coordinates and group focus ownership missing; outside negative already passed |
+| Focus attempt | `1.20.0.133` | 2/6 pass | Container's inherited FirstClick cleared MouseDown before child routing |
+| Focus green | `1.20.0.134` | 6/6 | Recursive coordinates, topmost hit, selectable focus, non-selectable no-focus passed |
+| Focus regressions | `1.20.0.135` | 81/81 | Button, dialog, list, group, program, and new focus tests passed |
+| Drag red | `1.20.0.136` | Expected compile failure | Missing mouse-drag state and capability-change contract only |
+| Drag green | `1.20.0.137` | 6/6 | Press/multiple move/release, four-edge clamp, invalid starts, Escape/capability/disable/removal/shutdown, keyboard fallback passed |
+
+## Governance Applicability
+
+| Preset | Version | Checkpoint | Applicability | Rationale | Evidence path | Owner | Reviewer | Result | Residual risk | Follow-up | Re-evaluation trigger |
+|---|---|---|---|---|---|---|---|---|---|---|---|
+| security-governance | 0.6.0 | NIST SSDF/CWE/input validation | Applicable | Untrusted host input and state transitions change | This file, Driver/Controls tests, `docs/security/threat-model.md` | Feature owner | Codex | Pass 2026-07-12 | Low after bounded tests | None | Parser or protocol scope changes |
+| security-governance | 0.6.0 | ASVS/supply chain/AI/regulation | N/A | No web/auth, dependency, distribution, product AI, or regulated operation | Existing ledgers plus this file | Feature owner | Codex | N/A reviewed 2026-07-12 | Trigger drift | None | Named trigger enters scope |
+| architecture-governance | 0.5.0 | STRIDE/CIA/CAPEC | Applicable | Host spoofing, malformed input, duplicate dispatch, stale state, availability | `docs/security/threat-model.md`, this file | Feature owner | Codex | Pass 2026-07-12 | Physical terminal variance | Host evidence follow-up | Trust boundary changes |
+| architecture-governance | 0.5.0 | S-ADR/arc42/Zero Trust/SAMM/C3A/C5 | N/A | Existing local component boundary; no cloud/distributed/provider boundary | Existing architecture/cloud ledgers | Feature owner | Codex | N/A reviewed 2026-07-12 | Architecture drift | None | New deployment/provider boundary |
+| isaqb-architecture-governance | 0.2.0 | Component ownership/quality goals | Applicable | Driver/Core/Controls ownership and fallbacks are central | `plan.md`, `docs/architecture/runtime-view.md`, this file | Feature owner | Codex | Pass 2026-07-12 | Low boundary leakage risk | None | Component dependency changes |
+| a11y-governance | 0.4.0 | Keyboard/text/WCAG/comments | Applicable | Mouse must not gate required operation; learner docs change | Tests, guide, DocFX/axe/lynx | Feature owner | Codex | Pass 2026-07-12 | Physical terminal variance | None | UI/docs change |
+| cross-platform-governance | 0.2.0 | Host matrix | Applicable | macOS/Linux/WSL/native Windows differ | Host table and guide | Feature owner | Codex | Contract Pass 2026-07-12 | Linux/WSL physical checks unavailable locally | Remote CI/manual evidence | Backend support changes |
+| cross-platform-governance | 0.2.0 | Script parity | N/A | No script changed | Diff review | Feature owner | Codex | N/A reviewed 2026-07-12 | Scope drift | None | Script enters diff |
+| agent-parity-governance | 0.3.0 | Five agent surfaces | Applicable | Active feature context changes | Five agent files | Feature owner | Codex | Pass 2026-07-12; block hash `c585bf142913e875fab53c770a715009cef9005dc742e9a7513b9526e06240e4` | None | None | Shared guidance changes |
+| agent-parity-governance | 0.3.0 | `.specify/templates/` | N/A | No generic workflow rule changed in feature implementation | Diff review | Feature owner | Codex | N/A reviewed 2026-07-12 | Later retrospective finding | Separate retro PR | Generic correction proven |
+
+## Validation
+
+| Command or review | Trigger | Result | Evidence or failure boundary |
+|---|---|---|---|
+| `git diff --check` | Always | Pass | No whitespace errors before and after generated cleanup/archive |
+| Placeholder/scope/generated/tv203s scans | Always | Pass | No unresolved markers, example parser, dependency change, generated tracked output, or historical diff |
+| `dotnet format --verify-no-changes --no-restore` | C# changes | Pass | Exit 0 at version `1.20.0.140` |
+| Targeted Driver/Core/Controls Release tests | Touched projects | Pass | Version `1.20.0.141`; Driver 17 plus Controls 103 = 120/120 |
+| Full Release tests | Shared runtime changes | Pass | Version `1.20.0.142`; Core 44, Serialization 44, Compatibility 18, Drivers 54, Controls 309, examples 115 = 584/584 |
+| Canonical Coverlet gate | Shared runtime changes | Pass | Version `1.20.0.143`; Core 89.78%, Controls 83.29%, Serialization 89.50%, Compatibility 80.55%, Drivers.Console 81.36% |
+| `docfx docfx.json` | Public XML/guide/toc changes | Pass | 254 models, 0 warnings, 0 errors |
+| Playwright/axe DocFX smoke | DocFX run | Pass | 2/2 Chromium tests; no serious representative-page violations |
+| UTF-8 lynx guide review | Learner guide | Pass | Generated mouse guide preserves headings, tables, lists, German/English text and skip links |
+| Secret scan | Always | Pass | PowerShell diff/tracked scanner: no secrets; extra directory scan found only six generated `_site` documentation examples, removed with `_site` |
+
+## Requirement and Success Coverage
+
+| Range | Primary evidence | Result |
+|---|---|---|
+| FR-001..FR-007 | Ingress/host matrices and Driver tests | Pass |
+| FR-008..FR-010 | Interaction matrix and Controls/app tests | Pass |
+| FR-011..FR-017 | Drag/fallback/app-loop proof | Pass |
+| FR-018..FR-026 | Project tests, decisions, history, docs, routing | Pass |
+| CR-001..CR-015 | Governance and delivery tables | Local Pass; remote rows pending authorized delivery |
+| SC-001..SC-013 | Validation, matrices, archive, remote closeout | Local Pass; SC-013 remote closeout pending |
+
+## Generated and Sensitive Output Hygiene
+
+| Surface | Required result | Result |
+|---|---|---|
+| `_site/` and generated `api/*.yml` | Untracked/absent from commit | Pass; removed after validation |
+| `TestResults/`, coverage, caches, logs | Untracked/absent from commit | Pass; removed after evidence extraction |
+| Credentials/secrets | No tracked or diff secret | Pass |
+| `tv203s/` | No diff | Pass |
+
+## Local Completion
+
+- Tasks T001-T122 are complete; only authorized GitHub delivery T123-T126 remains.
+- Binding intake archived by the repository PowerShell workflow as
+ `Lastenheft_04_MouseSupportAndInteraction.020-mouse-support-interaction.md` in commit `cfdd0bf`.
+- Changed runtime scope is limited to Driver ingress/capability, Program lifecycle,
+ recursive coordinates, topmost dispatch/focus, and one window title drag.
+- No package, example, script, cloud, network, persistence, runtime AI, Wave-4,
+ native Windows backend, generated output, or historical source entered scope.
+- Remaining follow-up: physical interactive macOS/Linux/WSL terminal observations
+ and any native Windows backend belong to host evidence or a later hardening feature.
+
+## Remote Delivery
+
+| Item | Result | Evidence |
+|---|---|---|
+| Staged scope | Pass | Intentional 020 source, tests, feature artifacts, docs, governance, metadata, statistics, and version only; `git diff --cached --check` and generated/sensitive path scan pass at `1.20.2.143` |
+| Implementation commit | Pass | `f76a0bf` (`feat: harden mouse support and interaction`) at version `1.20.2.143`; 39 files, 3,678 insertions, 69 deletions |
+| Evidence/version alignment | Pass | Bounded follow-up required because the implementation hash cannot be known inside its own commit; aligned to prospective third branch commit `1.20.3.143` without running another build/test |
+| Push | Pass | Initial and evidence-aligned pushes succeeded; remote branch `020-mouse-support-interaction` first observed at `0d99df0b37ef66fe2af2a077cc24a25e6a7183b4`; pre-push tracked-secret scan passed |
+| Pull request | Pass | Ready PR [#48](https://github.com/hindermath/TuiVision/pull/48), created from the evidence-derived scope and validation summary |
+| Required checks | Pass on reviewed head `c72c5b4` | Ubuntu/macOS CI, DocFX Pages, package/SBOM, Claude review, gitleaks, tracked-secret scan, and macOS/Ubuntu/Windows repository tooling passed; DocFX deployment correctly skipped for PR context |
+| Review threads | Pass on reviewed head `c72c5b4` | GraphQL-aware review fetch reported zero threads and zero conversation comments; no actionable remediation existed |
+| Unavailable reviews | Missing review recorded | Copilot reported requester quota exhaustion for both PR-request attempts; this is not counted as a passing review |
+| Merge | Open | Pending merge commit |
+| Local `main` sync | Open | Pending `HEAD == origin/main` |
+
+## Retrospective
+
+- **Effective**: Evidence-first task order and explicit Driver/Core/Controls ownership reduced ambiguity before red tests.
+- **Waste**: A repository commit that records the PR URL and reviewed-head facts retriggers expensive required checks; evaluate a generic causal-evidence checkpoint after delivery without weakening truthful evidence.
+- **Recurring blocker**: Copilot requester quota was unavailable for the third consecutive feature, while Claude completed without findings and GraphQL remained empty.
+- **Recommended refinement**: Decide after delivery; generic changes require a separate non-empty retrospective PR.
diff --git a/specs/020-mouse-support-interaction/quickstart.md b/specs/020-mouse-support-interaction/quickstart.md
new file mode 100644
index 00000000..b04ee3a9
--- /dev/null
+++ b/specs/020-mouse-support-interaction/quickstart.md
@@ -0,0 +1,58 @@
+# Quickstart: Mouse Support and Interaction Hardening
+
+## Preconditions
+
+```bash
+git switch 020-mouse-support-interaction
+specify check
+pwsh -NoProfile -File .specify/scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
+```
+
+Confirm `.specify/feature.json` points to
+`specs/020-mouse-support-interaction` and all feature checklists are complete.
+
+## Proof Order
+
+1. Create `pr-evidence.md` before runtime edits.
+2. Complete compile-surface and historical-source reviews.
+3. Add the bounded failing Driver parser/state matrix.
+4. Implement and validate the SGR-1006 vertical slice.
+5. Add failing focus, activation, coordinate, and title-drag proof.
+6. Implement the smallest Core/Controls corrections.
+7. Run the real app-loop matrix with enabled, disabled, and unsupported states.
+8. Complete host, governance, guide, and delivery evidence.
+
+## Runtime Boundary
+
+Supported sessions use SGR-1006 on interactive macOS/Linux terminals or WSL.
+Native Windows Console and redirected/headless sessions remain keyboard-only.
+The implementation does not claim wheel, hover, touch, extra-button, X10, or
+full emulator support.
+
+## Versioned Validation
+
+Before every `dotnet build` or `dotnet test`, increment only the manual build
+counter in `Directory.Build.props`. Use `1.20..`.
+
+```bash
+git diff --check
+dotnet format --verify-no-changes --no-restore
+dotnet test tests/TuiVision.Drivers.Tests/ --configuration Release
+dotnet test tests/TuiVision.Core.Tests/ --configuration Release
+dotnet test tests/TuiVision.Controls.Tests/ --configuration Release
+dotnet test --configuration Release
+dotnet test --configuration Release --collect:"XPlat Code Coverage" --settings coverlet.runsettings
+docfx docfx.json
+cd tests/web-a11y && npm run test:docfx
+```
+
+Record exact command, version, result, counts, coverage, trigger, and proof
+boundary in `pr-evidence.md`. Never commit `_site/`, generated `api/*.yml`, test
+results, caches, logs, terminal captures containing secrets, or credentials.
+
+## Completion
+
+- Ingress, focus, activation, double-click, drag, fallback, and host rows are complete.
+- Exactly one drag target is implemented and every required task has a keyboard route.
+- Lastenheft is archived with `.020-mouse-support-interaction.md` suffix.
+- Required checks pass, actionable threads are zero, and merge/main sync is recorded.
diff --git a/specs/020-mouse-support-interaction/research.md b/specs/020-mouse-support-interaction/research.md
new file mode 100644
index 00000000..5e2f4ac7
--- /dev/null
+++ b/specs/020-mouse-support-interaction/research.md
@@ -0,0 +1,112 @@
+# Research: Mouse Support and Interaction Hardening
+
+## R1 - Canonical Event Contract
+
+**Decision**: Keep `TEvent`, `TEventKind.MouseDown/MouseMove/MouseUp`, and
+`TMouseEvent` unchanged as the only UI mouse model.
+
+**Rationale**: Core and Controls already consume this contract. A host-specific
+payload outside the driver would duplicate semantics and leak protocol details.
+
+**Rejected**: A second event hierarchy or example-local mouse DTOs.
+
+## R2 - First Host Protocol
+
+**Decision**: Parse only complete SGR-1006 reports (`CSI < Cb ; Cx ; Cy M/m`)
+for left press, pressed movement, and release.
+
+**Rationale**: SGR uses decimal coordinates, has an explicit release terminator,
+and is widely available in modern Unix-like terminals and Windows Terminal/WSL.
+
+**Rejected**: X10 byte coordinates, wheel/hover coverage, arbitrary CSI parsing,
+or complete emulator compatibility.
+
+## R3 - Host Capability
+
+**Decision**: Classify SGR input as `Enabled`, `Disabled`, or `Unsupported`.
+Enable only for interactive macOS/Linux or WSL sessions with a usable terminal;
+keep redirected/headless and native Windows Console paths unsupported.
+
+**Rationale**: Explicit states prevent half-enabled input and false portability claims.
+
+## R4 - Atomic Validation
+
+**Decision**: Bound sequence length and numeric fields, require complete syntax,
+validate one-based host coordinates before zero-based conversion, clamp only to
+the current buffer contract, and reject invalid phase transitions without output.
+
+**Rationale**: Raw terminal input is untrusted. Partial publication risks ghost
+clicks, duplicate commands, and stale drag state.
+
+## R5 - Double Click
+
+**Decision**: Mark only the second left press as double-click when it has the
+same cell and target identity and occurs within 500 ms on a monotonic timeline.
+Controls supplies a point-to-target-key delegate; Driver stores only the stable
+key and never references a Controls type.
+
+**Rationale**: This matches historical same-position/tick intent while making
+time deterministic and preventing cross-target clicks from combining.
+
+**Rejected**: Wall-clock time, release-based classification, or spatial tolerance.
+
+## R6 - Focus and Hit Routing
+
+**Decision**: On mouse down, `TGroup` selects one topmost visible eligible hit
+target and transfers focus through `SetFocus` before normal target handling.
+
+**Rationale**: Current broadcast-to-all behavior can activate an older covered
+view and does not update group ownership of focus.
+
+## R7 - Coordinate Ownership
+
+**Decision**: Global/local conversion traverses the owner chain.
+
+**Rationale**: Canonical mouse coordinates are screen-global; subtracting only
+the immediate origin is incorrect for nested desktop/window/control trees.
+
+## R8 - Single Drag Contract
+
+**Decision**: Drag only movable `TWindow` instances from their top title row.
+Clamp within owner bounds, commit on release, and cancel on Escape, capability
+loss, disable/removal, or shutdown.
+
+**Rationale**: Window move already has a keyboard contract and proves the full
+press-move-release lifecycle without importing scrollbar or selection scope.
+
+## R9 - Keyboard and A11Y
+
+**Decision**: Preserve Tab/commands and `Ctrl+F5` plus arrows for all required
+operations, and expose capability and interaction state as text.
+
+**Rationale**: Mouse augments rather than gates the UI.
+
+## R10 - Historical Intent
+
+**Decision**: Review `tevent.cc`, `tmouse.cc`, `tview.cc`, `twindow.cc`,
+`unix/xtermmouse.cc`, and matching event/view/window/mouse headers read-only.
+
+**Rationale**: These sources define polling, double-click, hit, and window-move
+intent; the managed implementation remains event-driven and bounded.
+
+## R11 - Validation Boundary
+
+**Decision**: Use deterministic raw-sequence injection for parser/state CI and
+real app-loop proof for UI behavior. Record physical host checks separately.
+
+**Rationale**: CI can prove contracts without a pointing device but cannot claim
+an unexecuted terminal/backend observation.
+
+## R12 - Governance and Delivery
+
+**Decision**: Apply NIST/CWE, proportional STRIDE/CIA/CAPEC, iSAQB, A11Y,
+cross-platform host review, and agent parity. Keep unrelated cloud, web,
+supply-chain, AI, regulation, and script checkpoints trigger-based `N/A`.
+Use authorized `MergeAndSync`; every remote task writes the feature evidence.
+
+## R13 - Generic Promotion Boundary
+
+**Decision**: Record autonomous-run observations in feature evidence. Apply a
+generic workflow change only after merge on a separate non-empty retrospective PR.
+
+**Rationale**: Feature implementation must not absorb preset productization.
diff --git a/specs/020-mouse-support-interaction/spec.md b/specs/020-mouse-support-interaction/spec.md
new file mode 100644
index 00000000..bf88455a
--- /dev/null
+++ b/specs/020-mouse-support-interaction/spec.md
@@ -0,0 +1,356 @@
+# Feature Specification: Mouse Support and Interaction Hardening
+
+**Feature Branch**: `020-mouse-support-interaction`
+**Created**: 2026-07-12
+**Status**: Draft
+**Input**: Binding intake `Lastenheft_04_MouseSupportAndInteraction.md`
+
+## Clarifications
+
+### Session 2026-07-12
+
+- Q: Welcher erste Host-Protokollumfang ist verbindlich? -> A: SGR 1006 für interaktive macOS-/Linux-Terminals und WSL; native Windows Console bleibt ehrlich `Unsupported`.
+- Q: Welche Grenze gilt für einen Doppelklick? -> A: Zwei linke Press-Aktionen auf derselben Zelle und demselben Ziel innerhalb von 500 ms monotonic time.
+- Q: Welcher einzelne Drag-Pfad wird geliefert? -> A: Verschieben eines beweglichen `TWindow` am oberen Titelrahmen; `Ctrl+F5` plus Pfeile bleibt der Tastaturfallback.
+- Q: Wie werden Host- und CI-Nachweise getrennt? -> A: Parser, Zustandsfolgen und Fallbacks sind deterministisch injizierbar; physische Host-Evidence wird nur als verfügbarer manueller Spot-Check behauptet.
+
+## User Scenarios & Testing
+
+### User Story 1 - Kontrollierter Maus-Ingress / Controlled Mouse Ingress (Priority: P1)
+
+Als Benutzerin oder Benutzer eines unterstützten Terminals möchte ich, dass
+reale Mausaktionen als dieselben Framework-Ereignisse ankommen, die Controls
+bereits verstehen. Beispiele sollen keine eigenen Escape-Sequenzen oder
+Mausbefehle auswerten müssen.
+
+As a user of a supported terminal, I want real mouse actions to arrive as the
+same framework events already understood by controls. Examples must not parse
+their own escape sequences or mouse commands.
+
+**Why this priority**: Ohne einen zentralen Ingress bleiben alle weiteren
+Interaktionen nur Test-Hilfen oder beispielspezifische Sonderlogik.
+
+**Independent Test**: Eine kontrollierte Host-Eingabe wird durch den echten
+Ereignispfad geführt und erscheint einmal mit Position, Button, Phase und
+Capability-Zustand im kanonischen Mausereignis.
+
+**Acceptance Scenarios**:
+
+1. **Given** Maussupport ist aktiv und der Host meldet eine gültige Aktion,
+ **When** die Aktion den Runtime-Ingress erreicht, **Then** wird genau ein
+ kanonisches Framework-Ereignis mit begrenzten Koordinaten erzeugt.
+2. **Given** eine Eingabe ist unvollständig, unzulässig oder außerhalb des
+ darstellbaren Bereichs, **When** sie ausgewertet wird, **Then** entsteht kein
+ teilgültiges Mausereignis und der Eingabestrom bleibt nutzbar.
+3. **Given** ein Beispiel nutzt Mausinteraktion, **When** seine Quellen geprüft
+ werden, **Then** enthält es keinen eigenen Raw-Mausparser und keine lokale
+ Ersatzabstraktion.
+
+---
+
+### User Story 2 - Fokus, Aktivierung und Doppelklick / Focus, Activation, and Double Click (Priority: P1)
+
+Als Tastatur- und Mausbenutzer möchte ich Controls per Klick fokussieren und
+aktivieren sowie eine klar begrenzte Doppelklickaktion auslösen können, ohne
+dass Fokus oder Befehle doppelt zugestellt werden.
+
+As a keyboard and mouse user, I want to focus and activate controls by click
+and trigger one clearly bounded double-click action without duplicate focus or
+command delivery.
+
+**Why this priority**: Fokus und Aktivierung sind der kleinste sichtbare Nutzen
+des Ingress-Vertrags und müssen vor breiterer Gestenlogik stabil sein.
+
+**Independent Test**: Ein echter App-Loop mit mindestens zwei fokussierbaren
+Views erhält Klick- und Doppelklickereignisse und beweist Fokus, Aktivierung,
+Status und unveränderte Tastaturbedienung.
+
+**Acceptance Scenarios**:
+
+1. **Given** zwei fokussierbare Views sind sichtbar, **When** die zweite View
+ einmal geklickt wird, **Then** erhält nur sie den Fokus und der sichtbare
+ Textstatus nennt den Fokuswechsel.
+2. **Given** ein aktivierbares Control ist fokussiert, **When** ein vollständiger
+ Klick ausgeführt wird, **Then** wird seine normale Aktion genau einmal über
+ den bestehenden Befehlsweg ausgelöst.
+3. **Given** zwei passende Klicks liegen innerhalb der dokumentierten Zeit- und
+ Positionsgrenze, **When** der zweite Klick abgeschlossen wird, **Then** wird
+ genau eine Doppelklickbedeutung gemeldet; andere Folgen bleiben Einzelklicks.
+
+---
+
+### User Story 3 - Begrenzter Drag-Pfad / Bounded Drag Path (Priority: P2)
+
+Als Benutzerin oder Benutzer möchte ich genau einen dokumentierten einfachen
+Drag-Pfad verwenden können, der vorhandene Fenster- oder Scrollinteraktion
+nutzt und bei Abbruch in einem konsistenten Zustand endet.
+
+As a user, I want to use one documented simple drag path that reuses existing
+window or scrolling interaction and ends in a consistent state when cancelled.
+
+**Why this priority**: Ein realer Drag beweist Press-Move-Release-Zustand, ohne
+Hover, Wheel oder vollständige Terminalemulation in den Lauf zu ziehen.
+
+**Independent Test**: Ein App-Loop führt Press, begrenzte Bewegung und Release
+aus und prüft die sichtbare Endposition sowie Abbruch und Tastaturfallback.
+
+**Acceptance Scenarios**:
+
+1. **Given** der dokumentierte Drag-Handle ist sichtbar, **When** Press, Move und
+ Release innerhalb gültiger Grenzen eintreffen, **Then** ändert sich nur der
+ erlaubte Zielzustand und bleibt vollständig im Desktopbereich.
+2. **Given** ein Drag verliert Support oder wird abgebrochen, **When** kein
+ gültiger Release folgt, **Then** bleibt kein hängender Drag-Zustand zurück.
+3. **Given** Drag ist nicht verfügbar, **When** dieselbe Aufgabe per Tastatur
+ ausgeführt wird, **Then** bleibt sie vollständig erreichbar und der Status
+ erklärt den Fallback.
+
+---
+
+### User Story 4 - Ehrlicher Host- und Tastaturfallback / Honest Host and Keyboard Fallback (Priority: P1)
+
+Als Benutzerin oder Benutzer auf einem nicht unterstützten oder deaktivierten
+Host möchte ich weiterhin alle Pflichtaufgaben per Tastatur erledigen können
+und den Capability-Zustand als Text sehen.
+
+As a user on an unsupported or disabled host, I want to complete all required
+tasks by keyboard and see the capability state as text.
+
+**Why this priority**: Maus darf Barrierefreiheit und Plattformportabilität
+nicht zu einer stillen Teilfunktion verschlechtern.
+
+**Independent Test**: Derselbe Integrationspfad läuft mit aktivem, deaktiviertem
+und nicht unterstütztem Capability-Zustand; Tastaturaktionen bleiben identisch
+wirksam und der Status unterscheidet die drei Zustände.
+
+**Acceptance Scenarios**:
+
+1. **Given** der Host bietet keinen verlässlichen Maus-Ingress, **When** die App
+ startet, **Then** bleibt Maus deaktiviert und alle Pflichtpfade funktionieren
+ per Tastatur.
+2. **Given** Maussupport wurde bewusst deaktiviert, **When** eine potenzielle
+ Mausquelle Daten liefert, **Then** werden sie nicht als UI-Aktion zugestellt.
+3. **Given** Support ist verfügbar, deaktiviert oder nicht verfügbar, **When**
+ Hilfe oder Status angezeigt wird, **Then** ist der aktuelle Zustand
+ textorientiert und ohne Farbe oder Pointer verständlich.
+
+### Edge Cases
+
+- Leere, abgeschnittene, überlange oder nicht numerische Host-Eingaben.
+- Negative, extrem große oder außerhalb des aktuellen Buffers liegende
+ Koordinaten.
+- Unbekannte Buttons, ungültige Phasenfolgen und Bewegung ohne vorheriges Press.
+- Release nach Fokuswechsel, Fensterentfernung oder Capability-Deaktivierung.
+- Zwei Klicks mit zu großer Zeit- oder Positionsabweichung.
+- Doppelklickzustand nach Host-Uhrsprung oder monotonic-time-Grenze.
+- Drag über Desktopgrenzen, auf nicht ziehbare Views oder nach App-Shutdown.
+- Terminal-Resize zwischen Host-Eingang und View-Dispatch.
+- Nicht-interaktive, umgeleitete oder headless Ein-/Ausgabe.
+- Mehrere Eingaben in einem Read-Block ohne Verlust nach einer fehlerhaften
+ Sequenz.
+
+## Requirements
+
+### Functional Requirements
+
+- **FR-001**: Das System MUSS einen benannten Runtime-Ingress für reale oder
+ kontrolliert injizierte Host-Mausbeobachtungen bereitstellen.
+- **FR-002**: Der Ingress MUSS auf das bestehende kanonische Mausereignismodell
+ abbilden und DARF keine konkurrierende UI-Mausabstraktion einführen.
+- **FR-003**: Position, Button, Press/Move/Release-Phase und Doppelklickstatus
+ MÜSSEN eindeutig und höchstens einmal pro akzeptierter Aktion zugestellt
+ werden.
+- **FR-004**: Host-Eingaben MÜSSEN als nicht vertrauenswürdig gelten und vor
+ Veröffentlichung vollständig auf Syntax, Größe, Wertebereich und Zustand
+ geprüft werden.
+- **FR-005**: Ungültige oder unvollständige Eingaben MÜSSEN atomar abgelehnt
+ werden, ohne Teilereignis oder Verlust nachfolgender gültiger Eingaben.
+- **FR-006**: Maussupport MUSS explizit aktiviert, deaktiviert oder als nicht
+ unterstützt klassifiziert sein; ein halbaktiver Zustand ist unzulässig.
+- **FR-007**: Unterstützte Hostfamilien und erforderliche Terminalbedingungen
+ MÜSSEN dokumentiert werden. SGR 1006 ist der erste unterstützte Vertrag für
+ interaktive macOS-/Linux-Terminals und WSL; native Windows Console bleibt
+ `Unsupported`, bis ein eigener belegter Backend-Vertrag vorliegt.
+- **FR-008**: Click-to-focus MUSS genau eine geeignete sichtbare View fokussieren
+ und nicht fokussierbare oder verdeckte Views unverändert lassen.
+- **FR-009**: Click-to-activate MUSS die bestehende Control-/Command-Semantik
+ genau einmal verwenden und DARF keine lokale Beispielübersetzung benötigen.
+- **FR-010**: Doppelklick MUSS eine dokumentierte Zeit-, Button- und
+ Positions- und Zielgrenze besitzen. Nur zwei linke Press-Aktionen auf
+ derselben Zelle und demselben Ziel innerhalb von 500 ms monotonic time gelten
+ als Doppelklick; nicht passende Folgen bleiben Einzelklicks.
+- **FR-011**: Der Lauf MUSS genau einen einfachen Drag-Vertrag für eine
+ bestehende Fensterinteraktion liefern: Ein bewegliches `TWindow` wird am
+ oberen Titelrahmen gezogen. Desktopgrenzen, Release, Abbruch und der
+ bestehende `Ctrl+F5`-Pfeiltastenfallback MÜSSEN dokumentiert sein.
+- **FR-012**: Drag-Zustand MUSS bei Release, Abbruch, Capability-Verlust,
+ Zielentfernung und Shutdown vollständig beendet werden.
+- **FR-013**: Hover, Wheel, Touch, Mehrfinger, beliebige Buttons und vollständige
+ XTerm-/Raw-Protokollparität DÜRFEN nicht stillschweigend in Scope gelangen.
+- **FR-014**: Alle Pflichtinteraktionen MÜSSEN einen vollständigen
+ Tastaturfallback behalten.
+- **FR-015**: Capability-, Fokus-, Aktivierungs-, Doppelklick-, Drag- und
+ Fallbackzustände MÜSSEN als sichtbarer Text nachweisbar sein.
+- **FR-016**: Mindestens ein Integrationspfad MUSS den echten App-Loop, zwei
+ fokussierbare Views, Aktivierung, Doppelklick, den begrenzten Drag und
+ Tastaturfallback nachweisen.
+- **FR-017**: Primäre UI-Nachweise MÜSSEN konkreten Zustand, View-Identität und
+ gerenderte Buffer-/Cell-Regionen verbinden; direkte Helper sind nur Setup
+ oder ergänzender Proof.
+- **FR-018**: Core-, Controls- und Driver-Grenzen MÜSSEN fokussierte positive,
+ negative und Zustandsfolge-Tests besitzen.
+- **FR-019**: Für macOS, Linux und Windows/WSL MUSS jeweils reviewbare
+ Host-Evidence oder ein ehrlicher nicht ausgelöster/unsupported Nachweis
+ vorliegen. Deterministische CI-Injektion beweist Parser und Zustandsvertrag,
+ ersetzt aber keinen nicht ausgeführten physischen Host-Spot-Check.
+- **FR-020**: Beispiele DÜRFEN keine eigenen Raw-Mausparser, konkurrierenden
+ Mausmodelle oder wiederverwendbare lokale Ersatzlogik enthalten.
+- **FR-021**: Für jeden Interaktionsvertrag MUSS eine Entscheidung
+ `UseExistingFramework`, `SmallFrameworkFix`, `IntentionalDeviation` oder
+ `FollowUpHardening` mit Evidence-Pfad dokumentiert werden.
+- **FR-022**: Historisch abgeleitete Maussemantik MUSS anhand relevanter
+ `tv203s/`-Implementierungen und Header read-only geprüft werden; bewusste
+ Abweichungen werden dokumentiert.
+- **FR-023**: Neue oder geänderte nicht-triviale Ingress-, Dispatch-, Fokus-,
+ Doppelklick-, Drag- und Proof-Logik MUSS auf didaktischen Kommentarbedarf
+ geprüft werden.
+- **FR-024**: Benutzer- und Maintainer-Dokumentation MUSS Deutsch zuerst,
+ Englisch danach, CEFR-B2 und text-first zugänglich sein.
+- **FR-025**: Feature-Evidence MUSS Scope, Hostmatrix, Framework-Entscheidungen,
+ Parser-/Zustandsgrenzen, Tests, Governance, Remote-State und Follow-ups
+ vollständig erfassen.
+- **FR-026**: Pflichtenheft, Agent-Kontexte und Projektstatistik MÜSSEN nach
+ Abschluss auf `Lastenheft_05_TerminalCharsetAndEmulation.md` weitergeführt
+ werden.
+
+### Constitution Requirements
+
+- **CR-001**: Das Feature MUSS die TuiVision-Level-2-Registry-Zeile und C# als
+ Memory-Safe-Language-Kontext verwenden.
+- **CR-002**: NIST SSDF, CWE Top 25, sichere Eingabevalidierung und
+ fail-safe Zustände sind anwendbar und MÜSSEN Evidence erhalten.
+- **CR-003**: STRIDE/CIA/CAPEC MÜSSEN proportional für Host-Eingang,
+ Zustandsmaschine, Dispatch und Capability-Grenzen geprüft werden.
+- **CR-004**: OWASP ASVS ist `N/A`, solange keine Web-/API-/Auth-Fläche
+ hinzukommt; jede Scope-Änderung löst Neubewertung aus.
+- **CR-005**: Neue SBOM-, VEX-, SLSA-, OpenSSF- oder AI-SBOM-Evidence ist
+ `N/A`, solange keine Abhängigkeit, Distribution oder Produkt-AI hinzukommt.
+- **CR-006**: NIS2, CRA, EU AI Act und DORA bleiben `N/A` für das lokale
+ Trainingsframework; Distribution oder regulierter Betrieb löst Neubewertung
+ aus.
+- **CR-007**: S-ADR und arc42-Security-Updates sind nur bei neuer
+ Architektur-/Trust-Boundary-Entscheidung erforderlich; andernfalls ist die
+ bestehende Evidence mit Begründung wiederzuverwenden.
+- **CR-008**: Zero Trust, SAMM, BSI C3A und BSI C5 sind `N/A`, solange keine
+ verteilte, Cloud-, Provider- oder Betriebsgrenze geändert wird.
+- **CR-009**: WCAG 2.2 AA, Tastaturvollständigkeit, Textstatus und bilinguale
+ CEFR-B2-Dokumentation sind für sichtbare Laufzeit- und Guide-Flächen
+ anwendbar.
+- **CR-010**: Cross-Platform-Governance ist für Host- und Terminalunterschiede
+ anwendbar; Skriptparität ist `N/A`, solange kein Skript geändert wird.
+- **CR-011**: Agent-Parity ist bei Kontextänderung auf allen fünf gepflegten
+ Agent-Dateien anwendbar; `.specify/templates/` bleiben `N/A`, sofern keine
+ neue generische Workflow-Regel entsteht.
+- **CR-012**: Das Feature MUSS die sechs installierten Presets in den
+ akzeptierten Versionen und Prioritäten als Governance-Kontext verwenden.
+- **CR-013**: Vor dem ersten roten Testbefehl MUSS der Compile-Surface-Check
+ Imports, öffentliche XML-Dokumentation, Harness-Helfer,
+ Fokus-/Ownership-Assertionen und Linked-Source-Identität bewerten.
+- **CR-014**: Negative Fälle DÜRFEN nur als projektlokale Red-Matrix gebündelt
+ werden, wenn Einzelgrenzen und Ownership explizit bleiben.
+- **CR-015**: Operationales Commit-, Push-, PR- und Merge-Verhalten gehört in
+ Plan, Tasks und Feature-Evidence und darf keine Benutzeranforderung oder
+ implizite Remote-Autorität erzeugen.
+
+### Key Entities
+
+- **Mouse Capability State**: Aktiviert, deaktiviert oder nicht unterstützt,
+ einschließlich Hostfamilie, Bedingung und textorientierter Begründung.
+- **Host Mouse Observation**: Unveröffentlichte, nicht vertrauenswürdige
+ Eingabe mit Rohgrenze, Position, Button und Phase.
+- **Canonical Mouse Event**: Vollständig validiertes Framework-Ereignis mit
+ Position, Button, Phase und Doppelklickstatus.
+- **Click Sequence**: Begrenzter Zustand für Button, Position, Zeit und
+ Zielidentität zur Einzel-/Doppelklickentscheidung.
+- **Drag Session**: Begrenzter Press-Move-Release-Zustand mit Ziel, Handle,
+ Start, aktueller Position, End- oder Abbruchgrund.
+- **Host Evidence Record**: Host/Terminal, Capability, Aktivierung, Proof,
+ Ergebnis, Restrisiko und Neubewertungstrigger.
+- **Framework Decision Record**: Vertrag, bestehende Komponente, lokale Logik,
+ Entscheidung, Evidence und Follow-up.
+
+## Success Criteria
+
+### Measurable Outcomes
+
+- **SC-001**: 100 % der akzeptierten Host-Mausbeobachtungen erzeugen genau ein
+ kanonisches Ereignis; alle dokumentierten ungültigen Klassen erzeugen keines.
+- **SC-002**: Click-to-focus und Click-to-activate bestehen jeweils positive,
+ negative und Exactly-once-Nachweise im echten App-Loop.
+- **SC-003**: Mindestens drei Doppelklickgrenzen (Zeit, Position, Button/Ziel)
+ sind deterministisch bei exakt 500 ms, derselben Zelle und demselben linken
+ Button/Ziel getestet; nur die passende Folge wird Doppelklick.
+- **SC-004**: Genau ein begründeter Drag-Pfad beweist Press, mehrere Moves,
+ Release, Grenzen und mindestens zwei Abbruchpfade ohne hängenden Zustand.
+- **SC-005**: Alle Pflichtaufgaben bleiben bei deaktiviertem und nicht
+ unterstütztem Maussupport vollständig per Tastatur ausführbar.
+- **SC-006**: Der Integrationsproof verbindet für jede primäre Interaktion
+ App-Loop, konkreten Zustand, View-Identität, Status und gerenderte Cells.
+- **SC-007**: Core-, Controls- und Driver-Testgruppen bestehen vollständig;
+ der volle Repository-Testlauf bleibt grün.
+- **SC-008**: Jede der drei Hostfamilien macOS, Linux und Windows/WSL besitzt
+ einen überprüfbaren Pass-, Unsupported- oder nicht ausgelösten Nachweis mit
+ Neubewertungstrigger.
+- **SC-009**: 0 Beispieldateien enthalten einen lokalen Raw-Mausparser oder eine
+ konkurrierende Mausabstraktion.
+- **SC-010**: 100 % der Interaktionsverträge haben genau eine zulässige
+ Framework-Entscheidung und einen Evidence-Pfad.
+- **SC-011**: Alle ausgelösten Format-, Test-, Coverage-, Dokumentations-,
+ A11Y-, Secret-, Generated-Output- und Remote-Gates sind bestanden.
+- **SC-012**: Alle neuen oder geänderten nicht-trivialen Flows besitzen eine
+ dokumentierte Kommentarentscheidung ohne triviale Was-Kommentare.
+- **SC-013**: Der Lauf endet mit archiviertem Lastenheft, aktualisiertem
+ Folge-Intake, vollständiger Evidence und sauber synchronisiertem `main`.
+
+## Assumptions
+
+- Das vorhandene Framework-Mausmodell bleibt der kanonische UI-Vertrag.
+- Der erste Host-Ingress unterstützt nur vollständige SGR-1006-Sequenzen für
+ linkes Press, Move und Release; weitere Buttons, Wheel und Protokolle folgen
+ später.
+- Doppelklick verwendet eine monotone Zeitquelle oder kontrolliert injizierte
+ Zeit, nicht die veränderliche Wanduhr.
+- Das Verschieben eines beweglichen `TWindow` am Titelrahmen ist der einzige
+ Drag-Vertikalslice; zusätzliche Drag-Ziele benötigen eine neue Entscheidung.
+- Host-CI kann Capability-, Parser- und Fallbackverhalten deterministisch
+ beweisen, auch wenn kein physisches Zeigegerät verfügbar ist.
+- Runtime-/Produkt-AI, Datenbank, Netzwerkdienst und neue Abhängigkeiten sind
+ nicht Teil des Features.
+
+## Scope Boundaries
+
+### In Scope
+
+- Benannter, begrenzter Host-Maus-Ingress und Capability-Zustand.
+- Validierung und Abbildung auf das bestehende Mausereignismodell.
+- Click-to-focus, Click-to-activate und begrenzter Doppelklick.
+- Genau ein begründeter einfacher Drag-Pfad.
+- Sichtbarer Tastaturfallback und Hostmatrix.
+- Framework-, Integrations-, Host-, Guide- und Governance-Evidence.
+
+### Out of Scope
+
+- Vollständige XTerm-, Raw- oder plattformspezifische Protokollparität.
+- Hover, Wheel, Touch, Mehrfinger und beliebige zusätzliche Buttons.
+- TP7-`MOUSEDLG.PAS` oder neue Beispielportierung.
+- Wave-4-Terminal-/Charset-/Font-Implementierung.
+- Breite Framework-Revision oder konkurrierendes Mausmodell.
+- Neue Pakete, Dienste, Datenbanken oder Runtime-/Produkt-AI.
+- Beispiel-lokale Parser oder wiederverwendbare Mouse-Helper als Frameworkersatz.
+
+### Decision and Follow-up Model
+
+- `UseExistingFramework`: Bestehende Komponente erfüllt den Vertrag.
+- `SmallFrameworkFix`: Kleiner Feature-bezogener Framework-Fix mit Tests.
+- `IntentionalDeviation`: Bewusste, dokumentierte Abweichung.
+- `FollowUpHardening`: Reales Problem außerhalb des akzeptierten 020-Scopes.
diff --git a/specs/020-mouse-support-interaction/tasks.md b/specs/020-mouse-support-interaction/tasks.md
new file mode 100644
index 00000000..5c5907f8
--- /dev/null
+++ b/specs/020-mouse-support-interaction/tasks.md
@@ -0,0 +1,197 @@
+# Tasks: Mouse Support and Interaction Hardening
+
+**Input**: All accepted artifacts under `specs/020-mouse-support-interaction/`
+**Delivery mode**: `MergeAndSync`
+**Acceptance ledger**: `specs/020-mouse-support-interaction/pr-evidence.md`
+
+Tasks are sequential because most slices share Driver, Controls, evidence,
+version, documentation, statistics, agent, or delivery files. No `[P]` markers
+are used where ownership could overlap.
+
+## Phase 1: Preflight and Evidence Foundation
+
+- [X] T001 Create `specs/020-mouse-support-interaction/pr-evidence.md` from the autonomous evidence template before any later task records or runtime edits
+- [X] T002 Verify branch `020-mouse-support-interaction`, clean ancestry from synchronized `main`, and `.specify/feature.json`; record in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T003 Run `specify check` and the PowerShell prerequisites check with tasks included; record exact results in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T004 Verify all 020 checklists have zero incomplete items and record counts in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T005 Read `AGENTS.md`, Constitution, binding Lastenheft, and every 020 artifact; record material conflicts or `None` in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T006 Verify the six installed preset names, versions, and priorities; record them in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T007 Add ingress-observation, interaction, host, framework-decision, and comment-decision matrices to `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T008 Add governance rows with complete owner, reviewer, date, result, residual-risk, follow-up, and trigger fields to `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T009 Add requirements/SC coverage, validation, generated-output hygiene, remote delivery, and retrospective tables to `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T010 Record `speckit-constitution` as unchanged and `speckit-taskstoissues` as `N/A` because no constitutional conflict exists and one dependency-ordered PR is the delivery unit
+- [X] T011 Record Specify, four accepted Clarify decisions, second-pass convergence, checklist convergence, Plan, Plan Review, Tasks, and pending Analyze gates in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T012 Verify no placeholders or clarification markers remain outside normative checklist text and record the scan in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T013 Record hard scope boundaries and the explicit native-Windows/wheel/hover/touch/protocol follow-up boundary in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T014 Record the `MergeAndSync` authority source and narrow admin-bypass rule in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T015 Prove `git diff -- tv203s/` is empty before implementation and record the boundary in `specs/020-mouse-support-interaction/pr-evidence.md`
+
+## Phase 2: Compile Surface, Historical Intent, and Architecture Gate
+
+- [X] T016 Review project references for Driver -> Core and Controls -> Driver and record absence of dependency cycles in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T017 Review imports and planned public XML documentation for new Driver/Core/Controls APIs before the first red test
+- [X] T018 Review test-harness helpers, app-loop injection points, focus/owner assertions, and linked-source assembly identity before the first red test
+- [X] T019 Review `tv203s/contrib/tvision/classes/tevent.cc` and `tmouse.cc` plus `include/tv/event.h` read-only; record retained event/double-click intent
+- [X] T020 Review `tv203s/contrib/tvision/classes/unix/xtermmouse.cc` plus `include/tv/unix/xtmouse.h` and `mouse.h` read-only; record retained host intent and protocol deviation
+- [X] T021 Review `tv203s/contrib/tvision/classes/tview.cc`, `twindow.cc` plus `include/tv/view.h` and `window.h` read-only; record hit/focus/drag intent
+- [X] T022 Classify ingress, focus, activation, double-click, title drag, fallback, and host support with exactly one framework decision each in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T023 Record the component ownership gate: Driver protocol/state, Core canonical event, Controls interaction, no example-local reusable logic
+- [X] T024 Record compile-surface findings and any required pre-red corrections in `specs/020-mouse-support-interaction/pr-evidence.md`
+- [X] T025 Re-prove `tv203s/` remains unchanged after historical review
+
+## Phase 3: User Story 1 - Failing Driver Vertical Slice
+
+- [X] T026 Add `tests/TuiVision.Drivers.Tests/ConsoleMouseIngressTests.cs` with failing complete SGR left press/move/release mapping tests
+- [X] T027 Extend the Driver matrix with failing one-based-to-zero-based coordinate and current-buffer boundary tests
+- [X] T028 Extend the Driver matrix with explicit malformed, truncated, oversized, non-numeric, unknown-button, wheel, and trailing-input rejection cases
+- [X] T029 Extend the Driver matrix with invalid move-before-press, duplicate press, release-without-press, capability-disabled, and capability-unsupported cases
+- [X] T030 Add failing stream-recovery proof showing a rejected observation does not consume the next independent valid observation
+- [X] T031 Add failing double-click proof for same left button, exact cell, exact target, and `<= 500 ms`
+- [X] T032 Add failing non-double proof for `501 ms`, different cell, different target, clock regression, and capability reset
+- [X] T033 Increment the manual build counter, run the focused Driver matrix expecting the documented red compile/contract boundary, and record version/failures
+- [X] T034 Verify every grouped red case has an explicit expected failure and local file ownership; record the red matrix result
+
+## Phase 4: User Story 1 - Driver Ingress Implementation
+
+- [X] T035 Add fully XML-documented capability, host-family, protocol, and rejection enums/records in `src/TuiVision.Drivers.Console/ConsoleMouseIngress.cs`
+- [X] T036 Implement bounded complete SGR-1006 framing and numeric parsing in `ConsoleMouseIngress.cs`
+- [X] T037 Implement syntax, size, coordinate, button, capability, and phase validation before publication
+- [X] T038 Implement zero-or-one mapping to existing `TEvent` with no parallel UI event abstraction
+- [X] T039 Implement pressed-button and position state with atomic reset on capability loss or shutdown
+- [X] T040 Implement injected monotonic double-click classification with the exact 500 ms/cell/target contract using a point-to-target-key delegate and no Driver-to-Controls reference
+- [X] T041 Add controlled observation queue and ingress ownership to `src/TuiVision.Drivers.Console/TConsoleDriver.cs`
+- [X] T042 Implement supported/disabled/unsupported host classification for interactive macOS/Linux, WSL, native Windows Console, and headless input
+- [X] T043 Update `src/TuiVision.Drivers.Console/DriverCapabilityMap.cs` to describe the delivered managed mouse ingress and honest unsupported boundaries
+- [X] T044 Review Driver changes for didactic comment value and add only concise why/trade-off/proof-boundary comments
+- [X] T045 Increment the manual build counter, run the focused Driver matrix to green, and record exact count/version/result
+
+## Phase 5: User Story 2 - Focus, Activation, and Coordinates
+
+- [X] T046 Add failing recursive global/local coordinate tests for nested owner trees in `tests/TuiVision.Controls.Tests/TViewMouseInteractionTests.cs`
+- [X] T047 Add failing `TGroup` tests for topmost visible hit selection and focus transfer before handling
+- [X] T048 Add failing negative focus tests for covered, hidden, disabled, non-selectable, and outside targets
+- [X] T049 Add failing exactly-once activation tests for a focused button through group dispatch
+- [X] T050 Add failing no-activation tests for rejected/duplicate/outside observations
+- [X] T051 Increment the manual build counter, run focused Controls mouse tests expecting the documented red boundary, and record it
+- [X] T052 Update `TView.MakeGlobal` and `TView.MakeLocal` to traverse the owner chain while preserving root behavior
+- [X] T053 Update `TGroup.HandleEvent` to identify one topmost visible hit target for mouse events
+- [X] T054 Transfer focus through `TGroup.SetFocus` only for eligible selectable mouse-down targets before normal handling
+- [X] T055 Ensure one target receives the mouse event and existing control activation remains the only command path
+- [X] T056 Preserve keyboard/command pre-process, focused, and post-process behavior unchanged
+- [X] T057 Review coordinate/focus/dispatch changes for didactic comments and historical deviations
+- [X] T058 Increment the manual build counter, run focused Core/Controls interaction tests to green, and record result
+- [X] T059 Run existing button, dialog, list, group, and program mouse/focus regression tests in the same targeted Release batch where possible
+- [X] T060 Record `SmallFrameworkFix` evidence for coordinate and focus routing with red/green test names
+- [X] T061 Confirm no example file contains a raw parser or competing mouse abstraction and record the scan
+
+## Phase 6: User Story 3 - Single Window Title Drag
+
+- [X] T062 Add failing `TWindow` tests for left press on a movable title row, multiple moves, and release commit
+- [X] T063 Add failing owner-boundary clamp tests on all four desktop edges
+- [X] T064 Add failing non-drag tests for body press, non-movable window, right/unknown button, and move without press
+- [X] T065 Add failing cancellation tests for Escape, capability loss, disabled target, removal, and shutdown
+- [X] T066 Add failing keyboard regression proof for `Ctrl+F5`, arrows, Enter commit, and Escape restore
+- [X] T067 Increment the manual build counter, run focused window tests expecting the documented red boundary, and record it
+- [X] T068 Add bounded title-drag session state to `src/TuiVision.Controls/TWindow.cs`
+- [X] T069 Start drag only for valid left press on the top title row of a movable, visible, owned window
+- [X] T070 Apply pointer delta and clamp the complete window bounds inside the owner extent
+- [X] T071 Commit on valid release and clear all transient drag state
+- [X] T072 Cancel and clear drag on Escape, capability loss, disable/removal, and shutdown without a hanging state
+- [X] T073 Preserve existing keyboard move-mode behavior as the complete fallback
+- [X] T074 Review drag logic for concise historical/trade-off/cancellation comments
+- [X] T075 Increment the manual build counter, run focused window and keyboard regression tests to green, and record result
+
+## Phase 7: User Stories 1, 2, and 4 - Runtime and App-Loop Proof
+
+- [X] T076 Add failing `TProgram` tests for controlled Driver observations reaching the real `GetEvent` and `HandleEvent` route
+- [X] T077 Add failing runtime lifecycle tests for supported enable, disabled, unsupported, and cleanup/reset states
+- [X] T078 Add a bounded app-loop harness with two focusable controls, one movable window, status text, and rendered-cell capture
+- [X] T079 Add failing primary proof for click focus and exactly-once activation through `app.Run()`
+- [X] T080 Add failing primary proof for qualifying/non-qualifying double click through `app.Run()`
+- [X] T081 Add failing primary proof for title drag, clamped bounds, release, and cancellation through `app.Run()`
+- [X] T082 Add failing primary proof that keyboard focus, activation, and move remain complete when mouse is disabled/unsupported
+- [X] T083 Implement `TProgram` SGR enable/read/disable lifecycle, point-to-target-key resolution, and queue-before-keyboard event retrieval without changing existing key semantics
+- [X] T084 Implement visible bilingual capability/focus/activation/double-click/drag/fallback status in the integration harness
+- [X] T085 Increment the manual build counter, run the complete app-loop matrix to green, and record state/view/status/cell proof plus limits
+
+## Phase 8: Host, Security, Documentation, and Routing
+
+- [X] T086 Complete macOS, Linux, WSL, native Windows Console, and headless host rows with evidence class, result, risk, and re-evaluation trigger
+- [X] T087 Run any locally available physical macOS terminal spot-check without overstating non-executed Linux/WSL/Windows evidence
+- [X] T088 Complete parser/interaction comment-decision rows and confirm no trivial what-comments were added
+- [X] T089 Populate NIST SSDF, CWE Top 25, secure-input, fail-safe, and STRIDE/CIA/CAPEC governance rows
+- [X] T090 Populate trigger-based `N/A` rows for ASVS, new SBOM/VEX/SLSA/OpenSSF, AI-SBOM, NIS2, CRA, EU AI Act, and DORA
+- [X] T091 Populate iSAQB/component-boundary rows and trigger-based `N/A` for S-ADR, arc42 changes, Zero Trust, SAMM, BSI C3A, and BSI C5
+- [X] T092 Populate A11Y, didactic-comment, cross-platform host, script-governance `N/A`, agent-parity, and `.specify/templates/` `N/A` rows
+- [X] T093 Review existing security and architecture evidence and update only triggered host-input/threat rows; record unchanged rationale elsewhere
+- [X] T094 Create `docs/guides/mouse-support.md` with DE-first/EN-second protocol, hosts, capability, interactions, keyboard fallback, security, A11Y, and proof boundaries
+- [X] T095 Add the mouse-support guide to `docs/toc.yml` and update relevant README/index links
+- [X] T096 Review changed Markdown for CEFR-B2, umlauts/ß, semantic structure, fenced-language tags, and text-first accessibility
+- [X] T097 Update active Feature-020 and next Feature-021 context in all five maintained agent files
+- [X] T098 Verify the five agent context blocks are synchronized and record hashes/results
+- [X] T099 Update `Pflichtenheft.md` completion and next-intake marker to `Lastenheft_05_TerminalCharsetAndEmulation.md`
+- [X] T100 Update `docs/project-statistics.md` with 020 scope, lines, work window, 80/125-line baselines, validation, and next intake
+
+## Phase 9: Validation, Archive, and Local Completion
+
+- [X] T101 Run `git diff --check`, placeholder/TODO, scope, local-parser, generated-output, and `tv203s/` scans; record results
+- [X] T102 Run `dotnet format --verify-no-changes --no-restore` and record result
+- [X] T103 Increment the manual build counter and run all targeted Driver/Core/Controls Release tests; record per-project counts and version
+- [X] T104 Increment the manual build counter and run the full Release suite; record per-project and total counts
+- [X] T105 Validate `coverlet.runsettings`, increment the manual build counter, run canonical coverage, and record all five required assembly percentages
+- [X] T106 Run `docfx docfx.json`, then Playwright/axe DocFX smoke; record warnings/errors/tests
+- [X] T107 Review the generated mouse guide through UTF-8 `lynx` when available and record the text-first result
+- [X] T108 Run repository diff/tracked-secret scans and record results
+- [X] T109 Remove generated DocFX/API/test artifacts and prove they are absent from Git
+- [X] T110 Verify every FR, CR, and SC has an exact evidence link and every framework/host/governance row is complete
+- [X] T111 Verify exactly one drag target, no executable out-of-scope change, and no remaining local blocker
+- [X] T112 Archive `Lastenheft_04_MouseSupportAndInteraction.md` through the PowerShell rename workflow with suffix `020-mouse-support-interaction`
+- [X] T113 Record final local task count, changed files, validation, conditional gates, follow-ups, and retrospective observations
+- [X] T114 Re-run `specify check`, prerequisite/task checks, checklist counts, and final Analyze consistency after implementation evidence updates
+- [X] T115 Mark all local tasks through T115 complete only after their acceptance results are present in `pr-evidence.md`
+
+## Phase 10: Authorized GitHub Delivery
+
+- [X] T116 Align `Directory.Build.props` to `1.20..` without incrementing build, stage intentional files, and record scope
+- [X] T117 Commit the complete 020 implementation and capture commit/version in `pr-evidence.md`
+- [X] T118 Recalculate branch commit count, align version/evidence, and create a bounded follow-up commit only if required
+- [X] T119 Push `020-mouse-support-interaction`, record observed branch/commit, align and commit truthful evidence if needed, and push again
+- [X] T120 Create a ready feature PR from `pr-evidence.md`, record its URL, and push the bounded PR-reference evidence update
+- [X] T121 Monitor required CI, Claude/Copilot availability, review comments, and GraphQL threads to convergence; record each state
+- [X] T122 Remediate every actionable remote finding with focused validation and record response/thread resolution
+- [ ] T123 Use the authorized narrow admin bypass only after green required checks, zero actionable threads, and a sole human-approval block; record exact boundary
+- [ ] T124 Merge with a merge commit, delete the remote feature branch, switch to local `main`, fetch/prune/pull fast-forward, and prove clean `HEAD == origin/main`
+- [ ] T125 Record post-merge facts through a non-empty evidence-only closeout PR only when causally impossible before merge; otherwise document why none is needed
+- [ ] T126 Finish with synchronized clean `main` and a complete Feature-020 evidence record, ready for the separate retrospective decision
+
+## Dependencies and Execution Order
+
+- T001-T025 gate all implementation.
+- T026-T034 establish the Driver red slice; T035-T045 must pass before Controls work.
+- T046-T061 gate T062-T075; both gate app-loop tasks T076-T085.
+- Host/docs/governance T086-T100 precede validation/archive T101-T115.
+- Delivery T116-T126 starts only after every local acceptance gate passes.
+- `pr-evidence.md`, `Directory.Build.props`, `TProgram.cs`, `TGroup.cs`,
+ `TWindow.cs`, documentation, statistics, and agent files remain single-writer.
+- No delivery task is accepted without its exact 020 evidence entry.
+
+## Requirement Coverage
+
+| Requirement | Task coverage |
+|---|---|
+| FR-001 to FR-007 | T016-T045, T076-T087 |
+| FR-008 to FR-010 | T046-T061, T076-T085 |
+| FR-011 to FR-013 | T062-T075, T081-T085 |
+| FR-014 to FR-017 | T073, T078-T085, T094-T096 |
+| FR-018 to FR-020 | T026-T085, T061, T086-T087 |
+| FR-021 to FR-023 | T019-T025, T044, T057, T074, T088 |
+| FR-024 to FR-026 | T094-T100, T112-T113 |
+| CR-001 to CR-003 | T004-T005, T016-T024, T089, T101-T105 |
+| CR-004 to CR-008 | T089-T093 |
+| CR-009 to CR-012 | T086-T100, T106-T107 |
+| CR-013 to CR-015 | T017-T018, T024, T026-T034, T116-T126 |
+| SC-001 to SC-004 | T026-T075, T079-T085 |
+| SC-005 to SC-006 | T078-T085, T094-T096 |
+| SC-007 to SC-009 | T061, T086-T087, T101-T105 |
+| SC-010 to SC-012 | T022, T088-T093, T110-T113 |
+| SC-013 | T097-T100, T112-T126 |
diff --git a/src/TuiVision.Controls/IMouseInteractionSession.cs b/src/TuiVision.Controls/IMouseInteractionSession.cs
new file mode 100644
index 00000000..338e7734
--- /dev/null
+++ b/src/TuiVision.Controls/IMouseInteractionSession.cs
@@ -0,0 +1,6 @@
+namespace TuiVision.Controls;
+
+internal interface IMouseInteractionSession
+{
+ void CancelMouseInteraction();
+}
diff --git a/src/TuiVision.Controls/ShellCommandIds.cs b/src/TuiVision.Controls/ShellCommandIds.cs
index 247db11b..4c11bd64 100644
--- a/src/TuiVision.Controls/ShellCommandIds.cs
+++ b/src/TuiVision.Controls/ShellCommandIds.cs
@@ -89,4 +89,12 @@ public static class ShellCommandIds
/// Oeffnet die Hilfe. / Opens help.
///
public const ushort cmHelp = 112;
+
+ ///
+ /// Meldet eine Änderung der Runtime-Maus-Capability, damit aktive
+ /// Interaktionen fail-safe beendet werden.
+ ///
+ /// Reports a runtime mouse-capability change so active interactions can end fail-safe.
+ ///
+ public const ushort cmMouseCapabilityChanged = 113;
}
diff --git a/src/TuiVision.Controls/TGroup.cs b/src/TuiVision.Controls/TGroup.cs
index e59e935f..1467795e 100644
--- a/src/TuiVision.Controls/TGroup.cs
+++ b/src/TuiVision.Controls/TGroup.cs
@@ -235,6 +235,11 @@ public void Remove(TView view)
throw new ArgumentException("The view does not belong to this group.", nameof(view));
}
+ if (view is IMouseInteractionSession mouseInteraction)
+ {
+ mouseInteraction.CancelMouseInteraction();
+ }
+
// Vorgänger suchen (O(n)) / Find predecessor (O(n))
TView prev = FindPrev(view);
@@ -528,7 +533,18 @@ public override void HandleEvent(TEvent @event)
{
// Vor- und Nachverarbeitung rahmen den fokussierten Empfänger ein; ein geleertes Ereignis beendet die Kette.
// Pre- and post-processing surround the focused receiver; a cleared event stops the chain.
- base.HandleEvent(@event);
+ if (GetState(TViewState.Disabled))
+ {
+ return;
+ }
+
+ // Der Owner fokussiert die Gruppe; innerhalb des Containers muss derselbe MouseDown bis zum Kind weiterlaufen.
+ // The owner focuses the group; inside the container the same mouse down must continue to the child.
+ if ((@event.What & TEventKind.Mouse) == 0)
+ {
+ base.HandleEvent(@event);
+ }
+
if (@event.What == TEventKind.Nothing)
{
return;
@@ -549,7 +565,24 @@ public override void HandleEvent(TEvent @event)
{
Phase = DrawPhase.Focused;
- if ((@event.What & (TEventKind.KeyDown | TEventKind.Command)) != 0)
+ if ((@event.What & TEventKind.Mouse) != 0)
+ {
+ TView? target = @event.What == TEventKind.MouseDown
+ ? FindTopmostMouseTarget(@event.Mouse.Where)
+ : Current;
+
+ if (@event.What == TEventKind.MouseDown
+ && target is not null
+ && target.Options.HasFlag(TViewOptions.Selectable))
+ {
+ // Fokus folgt demselben Topmost-Ziel wie der Dispatch; so kann keine verdeckte View Commands auslösen.
+ // Focus follows the same topmost target as dispatch, so a covered view cannot invoke commands.
+ SetFocus(target);
+ }
+
+ target?.HandleEvent(@event);
+ }
+ else if ((@event.What & (TEventKind.KeyDown | TEventKind.Command)) != 0)
{
Current?.HandleEvent(@event);
}
@@ -611,6 +644,29 @@ protected override void OnBoundsChanged()
/// Das erste Kind oder null. / The first child or null.
protected internal TView? GetFirstChild() => First();
+ ///
+ /// Liefert einen stabilen laufzeitlokalen Schlüssel für das tiefste sichtbare
+ /// Mausziel an einer globalen Position.
+ ///
+ /// Returns a stable process-local key for the deepest visible mouse target
+ /// at a global position.
+ ///
+ /// Globale Position. / Global position.
+ /// Zielschlüssel oder null. / Target key or null.
+ protected internal string? ResolveMouseTargetKey(TPoint where)
+ {
+ TView? target = FindTopmostMouseTarget(where);
+ if (target is TGroup group)
+ {
+ return group.ResolveMouseTargetKey(where)
+ ?? $"{target.GetType().FullName}:{System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(target)}";
+ }
+
+ return target is null
+ ? null
+ : $"{target.GetType().FullName}:{System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(target)}";
+ }
+
// -------------------------------------------------------------------------
// Private Hilfsmethoden / Private helper methods
// -------------------------------------------------------------------------
@@ -630,6 +686,23 @@ protected override void OnBoundsChanged()
///
private TView? First() => _last?.Next;
+ private TView? FindTopmostMouseTarget(TPoint where)
+ {
+ TView? target = null;
+ ForEach(view =>
+ {
+ // Die Zeichenreihenfolge macht den letzten sichtbaren Treffer zum obersten Empfänger.
+ // Draw order makes the last visible hit the topmost receiver.
+ if (view.GetState(TViewState.Visible)
+ && !view.GetState(TViewState.Disabled)
+ && view.MouseInView(where))
+ {
+ target = view;
+ }
+ });
+ return target;
+ }
+
///
/// Iteriert über alle Kind-Views und ruft auf.
///
diff --git a/src/TuiVision.Controls/TProgram.cs b/src/TuiVision.Controls/TProgram.cs
index 5d510362..eadb4383 100644
--- a/src/TuiVision.Controls/TProgram.cs
+++ b/src/TuiVision.Controls/TProgram.cs
@@ -15,6 +15,8 @@ namespace TuiVision.Controls;
///
public class TProgram : TGroup
{
+ private const string EnableMouseReporting = "\x1b[?1000h\x1b[?1006h";
+ private const string DisableMouseReporting = "\x1b[?1006l\x1b[?1000l";
///
/// Der Standard-Treiber für die Konsolenausgabe.
///
@@ -39,6 +41,8 @@ public class TProgram : TGroup
private bool _shouldQuit;
private readonly TuiVision.Drivers.Console.SystemConsolePresenter _presenter = new();
+ private readonly Queue _pendingConsoleKeys = new();
+ private bool _mouseCapabilityConfigured;
///
/// Initialisiert eine neue Instanz der -Klasse.
@@ -76,6 +80,7 @@ public virtual void Run()
// Clear screen before first draw so no residual prompt content shows through.
try { Console.Clear(); } catch { }
try { Console.CursorVisible = false; } catch { }
+ StartMouseInput();
// Initialer Zeichenvorgang und Ausgabe / Initial draw and present
Draw();
@@ -116,8 +121,9 @@ public virtual void Run()
/// Clears the TUI content and restores the default console state so that the
/// shell prompt appears after the application exits.
///
- private static void CleanupConsole()
+ private void CleanupConsole()
{
+ StopMouseInput();
try { Console.TreatControlCAsInput = false; } catch { }
try { Console.Clear(); } catch { }
try { Console.ResetColor(); } catch { }
@@ -189,7 +195,19 @@ public virtual void GetEvent(out TEvent @event)
{
try
{
- ConsoleKeyInfo key = Console.ReadKey(intercept: true);
+ if (Driver.HasPendingMouseObservation)
+ {
+ Driver.TryGetMouseEvent(ResolveMouseTargetKey, out @event, out _);
+ return;
+ }
+
+ ConsoleKeyInfo key = ReadConsoleKey();
+ if (key.KeyChar == '\x1b' && TryCollectSgrObservation(key, out string sequence))
+ {
+ Driver.QueueMouseObservation(new ConsoleMouseObservation(sequence, Environment.TickCount64));
+ Driver.TryGetMouseEvent(ResolveMouseTargetKey, out @event, out _);
+ return;
+ }
bool isAlt = (key.Modifiers & ConsoleModifiers.Alt) != 0;
bool isCtrl = (key.Modifiers & ConsoleModifiers.Control) != 0;
@@ -230,6 +248,20 @@ public virtual void GetEvent(out TEvent @event)
}
}
+ ///
+ /// Setzt einen kontrollierten Capability-Zustand für deterministische
+ /// Integrationstests oder einen expliziten Host-Adapter.
+ ///
+ /// Sets a controlled capability state for deterministic integration tests
+ /// or an explicit host adapter.
+ ///
+ /// Zu verwendender Zustand. / Capability state to use.
+ public void ConfigureMouseCapability(ConsoleMouseCapability capability)
+ {
+ _mouseCapabilityConfigured = true;
+ ApplyMouseCapability(capability);
+ }
+
///
/// Verarbeitet ein Ereignis und prüft auf den Quit-Befehl sowie allgemeines Routing.
///
@@ -312,4 +344,163 @@ protected override void CurrentChanged()
base.CurrentChanged();
HandleEvent(TEvent.CreateBroadcast(ShellCommandIds.cmFocusChanged, Current));
}
+
+ private void StartMouseInput()
+ {
+ if (_mouseCapabilityConfigured)
+ {
+ return;
+ }
+
+ ConsoleMouseCapability detected = ConsoleMouseCapabilityDetector.DetectCurrent();
+ if (detected.State == ConsoleMouseCapabilityState.Disabled
+ && detected.Protocol == ConsoleMouseProtocol.Sgr1006)
+ {
+ try
+ {
+ Console.Out.Write(EnableMouseReporting);
+ Console.Out.Flush();
+ detected = detected with
+ {
+ State = ConsoleMouseCapabilityState.Enabled,
+ Reason = "SGR 1006 mouse reporting is enabled for this interactive terminal."
+ };
+ }
+ catch
+ {
+ detected = detected with
+ {
+ State = ConsoleMouseCapabilityState.Disabled,
+ Reason = "SGR 1006 activation failed; keyboard input remains available."
+ };
+ }
+ }
+
+ ApplyMouseCapability(detected);
+ }
+
+ private void StopMouseInput()
+ {
+ ConsoleMouseCapability current = Driver.MouseIngress.Capability;
+ if (current.State == ConsoleMouseCapabilityState.Enabled && !_mouseCapabilityConfigured)
+ {
+ try
+ {
+ Console.Out.Write(DisableMouseReporting);
+ Console.Out.Flush();
+ }
+ catch
+ {
+ // Cleanup is best effort; state reset below still prevents a stale interaction.
+ }
+ }
+
+ if (current.Protocol == ConsoleMouseProtocol.Sgr1006)
+ {
+ ApplyMouseCapability(current with
+ {
+ State = ConsoleMouseCapabilityState.Disabled,
+ Reason = "Mouse reporting is disabled after application shutdown."
+ });
+ }
+ else
+ {
+ ApplyMouseCapability(current);
+ }
+ }
+
+ private void ApplyMouseCapability(ConsoleMouseCapability capability)
+ {
+ Driver.MouseIngress.SetCapability(capability);
+ HandleEvent(TEvent.CreateBroadcast(ShellCommandIds.cmMouseCapabilityChanged, capability.State));
+ }
+
+ private ConsoleKeyInfo ReadConsoleKey() =>
+ _pendingConsoleKeys.Count > 0
+ ? _pendingConsoleKeys.Dequeue()
+ : Console.ReadKey(intercept: true);
+
+ private bool TryCollectSgrObservation(ConsoleKeyInfo escape, out string sequence)
+ {
+ sequence = string.Empty;
+ List consumed = new();
+ if (!TryReadSequenceKey(out ConsoleKeyInfo bracket) || bracket.KeyChar != '[')
+ {
+ if (bracket.KeyChar != '\0')
+ {
+ _pendingConsoleKeys.Enqueue(bracket);
+ }
+
+ return false;
+ }
+
+ consumed.Add(bracket);
+ if (!TryReadSequenceKey(out ConsoleKeyInfo marker) || marker.KeyChar != '<')
+ {
+ if (marker.KeyChar != '\0')
+ {
+ consumed.Add(marker);
+ }
+
+ RestoreConsumedKeys(consumed);
+ return false;
+ }
+
+ consumed.Add(marker);
+ System.Text.StringBuilder builder = new(ConsoleMouseIngress.MaximumSequenceLength);
+ builder.Append(escape.KeyChar);
+ builder.Append(bracket.KeyChar);
+ builder.Append(marker.KeyChar);
+
+ while (builder.Length < ConsoleMouseIngress.MaximumSequenceLength
+ && TryReadSequenceKey(out ConsoleKeyInfo next))
+ {
+ consumed.Add(next);
+ builder.Append(next.KeyChar);
+ if (next.KeyChar is 'M' or 'm')
+ {
+ sequence = builder.ToString();
+ return true;
+ }
+ }
+
+ RestoreConsumedKeys(consumed);
+ return false;
+ }
+
+ private static bool TryReadSequenceKey(out ConsoleKeyInfo key)
+ {
+ long deadline = Environment.TickCount64 + 20;
+ do
+ {
+ try
+ {
+ if (Console.KeyAvailable)
+ {
+ key = Console.ReadKey(intercept: true);
+ return true;
+ }
+ }
+ catch
+ {
+ break;
+ }
+
+ Thread.SpinWait(64);
+ }
+ while (Environment.TickCount64 <= deadline);
+
+ key = default;
+ return false;
+ }
+
+ private void RestoreConsumedKeys(IEnumerable consumed)
+ {
+ // Nicht als Maus erkannte Bytes bleiben in Reihenfolge als Tastatureingabe erhalten.
+ // Bytes not recognized as mouse input remain available as keyboard input in order.
+ foreach (ConsoleKeyInfo key in consumed)
+ {
+ _pendingConsoleKeys.Enqueue(key);
+ }
+ }
}
diff --git a/src/TuiVision.Controls/TView.cs b/src/TuiVision.Controls/TView.cs
index de35188b..0172d74a 100644
--- a/src/TuiVision.Controls/TView.cs
+++ b/src/TuiVision.Controls/TView.cs
@@ -374,7 +374,8 @@ public void GrowTo(int x, int y)
/// Der Punkt in globalen Koordinaten.
/// The point in global coordinates.
///
- public TPoint MakeGlobal(TPoint source) => source + Origin;
+ public TPoint MakeGlobal(TPoint source) =>
+ Owner is null ? source + Origin : Owner.MakeGlobal(source + Origin);
///
/// Wandelt einen globalen Punkt in lokale Ansichtskoordinaten um.
@@ -386,7 +387,7 @@ public void GrowTo(int x, int y)
/// Der Punkt in lokalen Koordinaten.
/// The point in local coordinates.
///
- public TPoint MakeLocal(TPoint source) => source - Origin;
+ public TPoint MakeLocal(TPoint source) => source - MakeGlobal(new TPoint(0, 0));
///
/// Prüft, ob eine globale Mausposition innerhalb dieser Ansicht liegt.
diff --git a/src/TuiVision.Controls/TWindow.cs b/src/TuiVision.Controls/TWindow.cs
index 0682c158..8ad3ca39 100644
--- a/src/TuiVision.Controls/TWindow.cs
+++ b/src/TuiVision.Controls/TWindow.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT Licence. See LICENSE file in the project root for full licence information.
using TuiVision.Core;
+using TuiVision.Drivers.Console;
namespace TuiVision.Controls;
@@ -14,7 +15,7 @@ namespace TuiVision.Controls;
/// Supports optional close affordance (×), Ctrl+W, guarded Escape, and a reversible
/// move mode via Ctrl+F5.
///
-public class TWindow : TGroup
+public class TWindow : TGroup, IMouseInteractionSession
{
// Scan-Codes / Scan codes
private const byte ScanEscape = 0x01;
@@ -33,6 +34,9 @@ public class TWindow : TGroup
// Verschiebe-Modus / Move mode
private bool _moveMode;
private TRect _moveModeOriginalBounds;
+ private bool _mouseDragging;
+ private TPoint _mouseDragStart;
+ private TRect _mouseDragOriginalBounds;
///
/// Initialisiert ein Fenster mit Titel und Begrenzungsrahmen.
@@ -69,6 +73,7 @@ public TWindow(string title, int x, int y, int width, int height, WindowFlags fl
ArgumentNullException.ThrowIfNull(title);
Title = title;
Flags = flags;
+ EventMask |= TEventKind.MouseMove | TEventKind.MouseUp;
}
///
@@ -99,6 +104,15 @@ public TWindow(string title, int x, int y, int width, int height, WindowFlags fl
///
public bool IsInMoveMode => _moveMode;
+ ///
+ /// Gibt an, ob dieses Fenster gerade durch den begrenzten Titelzeilen-Pfad
+ /// mit der Maus verschoben wird.
+ ///
+ /// Indicates whether this window is currently being moved through the
+ /// bounded title-row mouse path.
+ ///
+ public bool IsMouseDragging => _mouseDragging;
+
///
/// Zeichnet den Fensterrahmen mit Titel und füllt den Innenbereich mit der Hintergrundfarbe.
/// Wenn gesetzt ist, wird das ×-Symbol in der oberen linken Ecke angezeigt.
@@ -175,6 +189,20 @@ public override void Draw()
/// Das zu verarbeitende Ereignis. / The event to process.
public override void HandleEvent(TEvent @event)
{
+ if (@event.What == TEventKind.Broadcast
+ && @event.Message.Command == ShellCommandIds.cmMouseCapabilityChanged
+ && @event.Message.Info is ConsoleMouseCapabilityState state
+ && state != ConsoleMouseCapabilityState.Enabled)
+ {
+ CancelMouseDrag(restore: true);
+ return;
+ }
+
+ if ((@event.What & TEventKind.Mouse) != 0 && HandleMouseDrag(@event))
+ {
+ return;
+ }
+
if (@event.What == TEventKind.KeyDown)
{
byte scan = @event.KeyDown.ScanCode;
@@ -182,6 +210,13 @@ public override void HandleEvent(TEvent @event)
ushort shift = @event.KeyDown.ShiftState;
bool isCtrl = (shift & ShiftCtrl) != 0;
+ if (_mouseDragging && (scan == ScanEscape || ch == '\x1b'))
+ {
+ CancelMouseDrag(restore: true);
+ @event.Clear(this);
+ return;
+ }
+
// ---- Verschiebe-Modus aktiv / Move mode active ----
if (_moveMode)
{
@@ -253,4 +288,105 @@ public override void HandleEvent(TEvent @event)
@event.Clear();
}
}
+
+ ///
+ /// Beendet einen aktiven Maus-Drag, bevor der Disabled-Zustand propagiert wird.
+ ///
+ /// Ends an active mouse drag before the disabled state is propagated.
+ ///
+ /// Zu ändernder Zustand. / State to change.
+ /// Zustand setzen oder löschen. / Whether to set or clear the state.
+ public override void SetState(TViewState state, bool enable)
+ {
+ if (enable && (state & TViewState.Disabled) != 0)
+ {
+ CancelMouseDrag(restore: true);
+ }
+
+ base.SetState(state, enable);
+ }
+
+ ///
+ /// Beendet transiente Mausinteraktion vor dem normalen Gruppen-Shutdown.
+ ///
+ /// Ends transient mouse interaction before normal group shutdown.
+ ///
+ public override void ShutDown()
+ {
+ CancelMouseDrag(restore: true);
+ base.ShutDown();
+ }
+
+ void IMouseInteractionSession.CancelMouseInteraction() => CancelMouseDrag(restore: true);
+
+ private bool HandleMouseDrag(TEvent @event)
+ {
+ if (@event.What == TEventKind.MouseDown)
+ {
+ TPoint local = MakeLocal(@event.Mouse.Where);
+ if (!_mouseDragging
+ && Flags.HasFlag(WindowFlags.Move)
+ && @event.Mouse.Buttons == TMouseButtons.Left
+ && local.Y == 0
+ && local.X >= 0
+ && local.X < Size.X)
+ {
+ _mouseDragging = true;
+ _mouseDragStart = @event.Mouse.Where;
+ _mouseDragOriginalBounds = GetBounds();
+ @event.Clear(this);
+ return true;
+ }
+
+ return false;
+ }
+
+ if (!_mouseDragging)
+ {
+ return false;
+ }
+
+ if (@event.What == TEventKind.MouseMove)
+ {
+ if (Owner is null)
+ {
+ CancelMouseDrag(restore: true);
+ return true;
+ }
+
+ TPoint delta = @event.Mouse.Where - _mouseDragStart;
+ TPoint size = _mouseDragOriginalBounds.B - _mouseDragOriginalBounds.A;
+ TRect ownerExtent = Owner.GetExtent();
+ int x = Math.Clamp(_mouseDragOriginalBounds.A.X + delta.X, ownerExtent.A.X, Math.Max(ownerExtent.A.X, ownerExtent.B.X - size.X));
+ int y = Math.Clamp(_mouseDragOriginalBounds.A.Y + delta.Y, ownerExtent.A.Y, Math.Max(ownerExtent.A.Y, ownerExtent.B.Y - size.Y));
+ Locate(new TRect(x, y, x + size.X, y + size.Y));
+ @event.Clear(this);
+ return true;
+ }
+
+ if (@event.What == TEventKind.MouseUp)
+ {
+ // Release schreibt die bereits begrenzte Vorschau fest; alle anderen Abbrüche stellen den Start wieder her.
+ // Release commits the already clamped preview; every other cancellation restores the start.
+ _mouseDragging = false;
+ @event.Clear(this);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void CancelMouseDrag(bool restore)
+ {
+ if (!_mouseDragging)
+ {
+ return;
+ }
+
+ _mouseDragging = false;
+ if (restore)
+ {
+ Locate(_mouseDragOriginalBounds);
+ }
+ }
}
diff --git a/src/TuiVision.Drivers.Console/ConsoleMouseIngress.cs b/src/TuiVision.Drivers.Console/ConsoleMouseIngress.cs
new file mode 100644
index 00000000..177a67a4
--- /dev/null
+++ b/src/TuiVision.Drivers.Console/ConsoleMouseIngress.cs
@@ -0,0 +1,492 @@
+// Copyright (c) 2026 Thorsten Hindermann / TuiVision Contributors.
+// Licensed under the MIT Licence. See LICENSE file in the project root for full licence information.
+
+using System.Globalization;
+using TuiVision.Core;
+
+namespace TuiVision.Drivers.Console;
+
+///
+/// Beschreibt den Aktivierungszustand des Konsolen-Mauseingangs.
+///
+/// Describes the activation state of the console mouse ingress.
+///
+public enum ConsoleMouseCapabilityState
+{
+ /// Der Host unterstützt den Vertrag, er ist aber deaktiviert. / The host supports the contract, but it is disabled.
+ Disabled,
+
+ /// Der Vertrag ist unterstützt und aktiviert. / The contract is supported and enabled.
+ Enabled,
+
+ /// Der Host bietet keinen belegten Vertrag. / The host has no proven contract.
+ Unsupported
+}
+
+///
+/// Benennt die für Maussupport relevanten Hostfamilien.
+///
+/// Names the host families relevant to mouse support.
+///
+public enum ConsoleMouseHostFamily
+{
+ /// Unbekannter Host. / Unknown host.
+ Unknown,
+
+ /// macOS-Terminal. / macOS terminal.
+ MacOS,
+
+ /// Linux-Terminal. / Linux terminal.
+ Linux,
+
+ /// Windows Subsystem for Linux. / Windows Subsystem for Linux.
+ Wsl,
+
+ /// Native Windows-Konsole. / Native Windows console.
+ WindowsConsole,
+
+ /// Umgeleitete oder nicht interaktive Ein-/Ausgabe. / Redirected or non-interactive input/output.
+ Headless
+}
+
+///
+/// Benennt den aktivierten Terminal-Mausprotokollvertrag.
+///
+/// Names the enabled terminal mouse protocol contract.
+///
+public enum ConsoleMouseProtocol
+{
+ /// Kein Protokoll. / No protocol.
+ None,
+
+ /// SGR-1006-Zellkoordinaten. / SGR 1006 cell coordinates.
+ Sgr1006
+}
+
+///
+/// Benennt den Grund, aus dem eine Host-Beobachtung nicht veröffentlicht wurde.
+///
+/// Names the reason why a host observation was not published.
+///
+public enum ConsoleMouseRejectionReason
+{
+ /// Keine Ablehnung. / No rejection.
+ None,
+
+ /// Capability ist deaktiviert oder nicht unterstützt. / The capability is disabled or unsupported.
+ CapabilityUnavailable,
+
+ /// Die Syntax ist unvollständig oder unzulässig. / The syntax is incomplete or invalid.
+ InvalidSyntax,
+
+ /// Die Sequenz überschreitet die feste Grenze. / The sequence exceeds the fixed limit.
+ SequenceTooLong,
+
+ /// Ein Zahlenfeld ist ungültig. / A numeric field is invalid.
+ InvalidNumber,
+
+ /// Koordinaten liegen außerhalb des aktuellen Puffers. / Coordinates lie outside the current buffer.
+ CoordinatesOutOfRange,
+
+ /// Button oder Interaktion gehören nicht zum Feature-Vertrag. / The button or interaction is outside the feature contract.
+ UnsupportedButton,
+
+ /// Die Phase passt nicht zum aktuellen Press-Zustand. / The phase does not match the current press state.
+ InvalidTransition
+}
+
+///
+/// Beschreibt Host, Protokoll und Begründung eines Mauseingangs-Zustands.
+///
+/// Describes the host, protocol, and rationale of a mouse ingress state.
+///
+/// Aktivierungszustand. / Activation state.
+/// Hostfamilie. / Host family.
+/// Protokollvertrag. / Protocol contract.
+/// Textorientierte Begründung. / Text-first rationale.
+public readonly record struct ConsoleMouseCapability(
+ ConsoleMouseCapabilityState State,
+ ConsoleMouseHostFamily HostFamily,
+ ConsoleMouseProtocol Protocol,
+ string Reason);
+
+///
+/// Klassifiziert den aktuellen oder einen kontrolliert beschriebenen Host für
+/// den begrenzten SGR-1006-Vertrag.
+///
+/// Classifies the current host or a controlled host description for the bounded
+/// SGR 1006 contract.
+///
+public static class ConsoleMouseCapabilityDetector
+{
+ ///
+ /// Erkennt den aktuellen Prozess- und Terminalzustand.
+ ///
+ /// Detects the current process and terminal state.
+ ///
+ /// Erkannte Capability. / Detected capability.
+ public static ConsoleMouseCapability DetectCurrent()
+ {
+ bool isWsl = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WSL_DISTRO_NAME"));
+ return Detect(
+ System.Console.IsInputRedirected,
+ System.Console.IsOutputRedirected,
+ Environment.GetEnvironmentVariable("TERM"),
+ OperatingSystem.IsWindows(),
+ isWsl,
+ OperatingSystem.IsMacOS(),
+ OperatingSystem.IsLinux());
+ }
+
+ ///
+ /// Klassifiziert kontrollierte Hostmerkmale für deterministische Tests und
+ /// die echte Laufzeiterkennung.
+ ///
+ /// Classifies controlled host traits for deterministic tests and real runtime detection.
+ ///
+ /// Eingabe ist umgeleitet. / Input is redirected.
+ /// Ausgabe ist umgeleitet. / Output is redirected.
+ /// Wert der `TERM`-Variable. / Value of the `TERM` variable.
+ /// Windows-Laufzeit. / Windows runtime.
+ /// WSL-Umgebung. / WSL environment.
+ /// macOS-Laufzeit. / macOS runtime.
+ /// Linux-Laufzeit. / Linux runtime.
+ /// Klassifizierte Capability. / Classified capability.
+ public static ConsoleMouseCapability Detect(
+ bool inputRedirected,
+ bool outputRedirected,
+ string? term,
+ bool isWindows,
+ bool isWsl,
+ bool isMacOS,
+ bool isLinux)
+ {
+ if (inputRedirected || outputRedirected)
+ {
+ return new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Unsupported,
+ ConsoleMouseHostFamily.Headless,
+ ConsoleMouseProtocol.None,
+ "Redirected or non-interactive input/output does not provide the SGR mouse contract.");
+ }
+
+ ConsoleMouseHostFamily host = isWsl
+ ? ConsoleMouseHostFamily.Wsl
+ : isMacOS
+ ? ConsoleMouseHostFamily.MacOS
+ : isLinux
+ ? ConsoleMouseHostFamily.Linux
+ : isWindows
+ ? ConsoleMouseHostFamily.WindowsConsole
+ : ConsoleMouseHostFamily.Unknown;
+
+ if (host == ConsoleMouseHostFamily.WindowsConsole)
+ {
+ return new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Unsupported,
+ host,
+ ConsoleMouseProtocol.None,
+ "Native Windows Console mouse records require a separate backend contract.");
+ }
+
+ if (string.IsNullOrWhiteSpace(term) || string.Equals(term, "dumb", StringComparison.OrdinalIgnoreCase))
+ {
+ return new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Unsupported,
+ host,
+ ConsoleMouseProtocol.None,
+ "The terminal does not advertise a usable interactive protocol.");
+ }
+
+ if (host is ConsoleMouseHostFamily.MacOS or ConsoleMouseHostFamily.Linux or ConsoleMouseHostFamily.Wsl)
+ {
+ return new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Disabled,
+ host,
+ ConsoleMouseProtocol.Sgr1006,
+ "SGR 1006 is available and must be enabled explicitly by the runtime lifecycle.");
+ }
+
+ return new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Unsupported,
+ ConsoleMouseHostFamily.Unknown,
+ ConsoleMouseProtocol.None,
+ "The current host family has no proven mouse ingress contract.");
+ }
+}
+
+///
+/// Wandelt vollständige, begrenzte SGR-1006-Beobachtungen in kanonische
+/// -Instanzen um.
+///
+/// Converts complete, bounded SGR 1006 observations into canonical
+/// instances.
+///
+public sealed class ConsoleMouseIngress
+{
+ /// Maximal akzeptierte Sequenzlänge. / Maximum accepted sequence length.
+ public const int MaximumSequenceLength = 64;
+
+ /// Inklusive Doppelklickgrenze in Millisekunden. / Inclusive double-click limit in milliseconds.
+ public const long DoubleClickMilliseconds = 500;
+
+ private readonly object _sync = new();
+ private bool _leftPressed;
+ private long? _lastClickTimestamp;
+ private TPoint? _lastClickPosition;
+ private string? _lastClickTarget;
+
+ ///
+ /// Erstellt einen Eingang mit dem angegebenen Capability-Zustand.
+ ///
+ /// Creates an ingress with the specified capability state.
+ ///
+ /// Anfänglicher Capability-Zustand. / Initial capability state.
+ public ConsoleMouseIngress(ConsoleMouseCapability capability)
+ {
+ ValidateCapability(capability);
+ Capability = capability;
+ }
+
+ ///
+ /// Liefert den aktuellen Capability-Zustand.
+ ///
+ /// Gets the current capability state.
+ ///
+ public ConsoleMouseCapability Capability { get; private set; }
+
+ ///
+ /// Setzt den Capability-Zustand und verwirft jeden transienten Press- oder
+ /// Klickzustand, damit Reaktivierung keine alte Interaktion fortsetzt.
+ ///
+ /// Sets the capability state and discards transient press or click state so
+ /// reactivation cannot continue an old interaction.
+ ///
+ /// Neuer Zustand. / New state.
+ public void SetCapability(ConsoleMouseCapability capability)
+ {
+ ValidateCapability(capability);
+ lock (_sync)
+ {
+ Capability = capability;
+ ResetTransientState();
+ }
+ }
+
+ ///
+ /// Prüft eine vollständige Host-Beobachtung und veröffentlicht bei Erfolg
+ /// genau ein kanonisches Mausereignis.
+ ///
+ /// Validates one complete host observation and publishes exactly one
+ /// canonical mouse event on success.
+ ///
+ /// Vollständige SGR-1006-Sequenz. / Complete SGR 1006 sequence.
+ /// Monotone Zeit in Millisekunden. / Monotonic time in milliseconds.
+ /// Aktuelle Pufferbreite. / Current buffer width.
+ /// Aktuelle Pufferhöhe. / Current buffer height.
+ /// Optionaler Zielschlüssel-Auflöser ohne Controls-Abhängigkeit. / Optional target-key resolver without a Controls dependency.
+ /// Kanonisches Ereignis oder `Nothing`. / Canonical event or `Nothing`.
+ /// Ablehnungsgrund oder `None`. / Rejection reason or `None`.
+ /// true genau dann, wenn ein Ereignis veröffentlicht wurde. / true exactly when an event was published.
+ public bool TryAccept(
+ string sequence,
+ long timestampMilliseconds,
+ int bufferWidth,
+ int bufferHeight,
+ Func? targetResolver,
+ out TEvent @event,
+ out ConsoleMouseRejectionReason rejectionReason)
+ {
+ @event = TEvent.CreateNone();
+ rejectionReason = ConsoleMouseRejectionReason.None;
+
+ lock (_sync)
+ {
+ if (Capability.State != ConsoleMouseCapabilityState.Enabled
+ || Capability.Protocol != ConsoleMouseProtocol.Sgr1006)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.CapabilityUnavailable;
+ return false;
+ }
+
+ if (sequence is null || sequence.Length == 0)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.InvalidSyntax;
+ return false;
+ }
+
+ if (sequence.Length > MaximumSequenceLength)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.SequenceTooLong;
+ return false;
+ }
+
+ if (!TryParse(sequence.AsSpan(), out int buttonCode, out int hostX, out int hostY, out bool release, out rejectionReason))
+ {
+ return false;
+ }
+
+ if (bufferWidth <= 0 || bufferHeight <= 0
+ || hostX < 1 || hostY < 1 || hostX > bufferWidth || hostY > bufferHeight)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.CoordinatesOutOfRange;
+ return false;
+ }
+
+ TPoint where = new((short)(hostX - 1), (short)(hostY - 1));
+ if (release)
+ {
+ if (!_leftPressed || (buttonCode != 0 && buttonCode != 3))
+ {
+ rejectionReason = buttonCode is 0 or 3
+ ? ConsoleMouseRejectionReason.InvalidTransition
+ : ConsoleMouseRejectionReason.UnsupportedButton;
+ return false;
+ }
+
+ _leftPressed = false;
+ @event = TEvent.CreateMouse(TEventKind.MouseUp, TMouseButtons.None, false, where);
+ return true;
+ }
+
+ if (buttonCode == 32)
+ {
+ if (!_leftPressed)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.InvalidTransition;
+ return false;
+ }
+
+ @event = TEvent.CreateMouse(TEventKind.MouseMove, TMouseButtons.Left, false, where);
+ return true;
+ }
+
+ if (buttonCode != 0)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.UnsupportedButton;
+ return false;
+ }
+
+ if (_leftPressed)
+ {
+ rejectionReason = ConsoleMouseRejectionReason.InvalidTransition;
+ return false;
+ }
+
+ string? target = targetResolver?.Invoke(where);
+ bool doubleClick = IsDoubleClick(timestampMilliseconds, where, target);
+ _leftPressed = true;
+ _lastClickTimestamp = timestampMilliseconds;
+ _lastClickPosition = where;
+ _lastClickTarget = target;
+ @event = TEvent.CreateMouse(TEventKind.MouseDown, TMouseButtons.Left, doubleClick, where);
+ return true;
+ }
+ }
+
+ private static bool TryParse(
+ ReadOnlySpan sequence,
+ out int buttonCode,
+ out int x,
+ out int y,
+ out bool release,
+ out ConsoleMouseRejectionReason rejectionReason)
+ {
+ buttonCode = 0;
+ x = 0;
+ y = 0;
+ release = false;
+ rejectionReason = ConsoleMouseRejectionReason.InvalidSyntax;
+
+ if (sequence.Length < 9 || !sequence.StartsWith("\x1b[<".AsSpan()))
+ {
+ return false;
+ }
+
+ char terminator = sequence[^1];
+ if (terminator != 'M' && terminator != 'm')
+ {
+ return false;
+ }
+
+ ReadOnlySpan body = sequence[3..^1];
+ int firstSeparator = body.IndexOf(';');
+ int secondSeparator = firstSeparator < 0 ? -1 : body[(firstSeparator + 1)..].IndexOf(';');
+ if (firstSeparator <= 0 || secondSeparator < 1)
+ {
+ return false;
+ }
+
+ secondSeparator += firstSeparator + 1;
+ if (body[(secondSeparator + 1)..].IndexOf(';') >= 0)
+ {
+ return false;
+ }
+
+ if (!TryParseNumber(body[..firstSeparator], out buttonCode)
+ || !TryParseNumber(body[(firstSeparator + 1)..secondSeparator], out x)
+ || !TryParseNumber(body[(secondSeparator + 1)..], out y))
+ {
+ rejectionReason = ConsoleMouseRejectionReason.InvalidNumber;
+ return false;
+ }
+
+ release = terminator == 'm';
+ rejectionReason = ConsoleMouseRejectionReason.None;
+ return true;
+ }
+
+ private static bool TryParseNumber(ReadOnlySpan value, out int result)
+ {
+ result = 0;
+ return value.Length is > 0 and <= 9
+ && int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
+ }
+
+ private bool IsDoubleClick(long timestampMilliseconds, TPoint where, string? target)
+ {
+ // Eine rückläufige Zeitquelle beendet die Vergleichskette, statt alte Klicks fälschlich zusammenzuführen.
+ // A regressing time source ends the comparison chain instead of combining stale clicks incorrectly.
+ if (_lastClickTimestamp is long previous && timestampMilliseconds < previous)
+ {
+ _lastClickTimestamp = null;
+ _lastClickPosition = null;
+ _lastClickTarget = null;
+ return false;
+ }
+
+ return target is not null
+ && _lastClickTarget is not null
+ && _lastClickTimestamp is long prior
+ && timestampMilliseconds - prior <= DoubleClickMilliseconds
+ && _lastClickPosition == where
+ && string.Equals(_lastClickTarget, target, StringComparison.Ordinal);
+ }
+
+ private void ResetTransientState()
+ {
+ _leftPressed = false;
+ _lastClickTimestamp = null;
+ _lastClickPosition = null;
+ _lastClickTarget = null;
+ }
+
+ private static void ValidateCapability(ConsoleMouseCapability capability)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(capability.Reason);
+ if (capability.State == ConsoleMouseCapabilityState.Enabled
+ && capability.Protocol == ConsoleMouseProtocol.None)
+ {
+ throw new ArgumentException("An enabled mouse capability requires a protocol.", nameof(capability));
+ }
+ }
+}
+
+///
+/// Eine kontrollierte vollständige Host-Beobachtung für die Driver-Queue.
+///
+/// A controlled complete host observation for the Driver queue.
+///
+/// SGR-1006-Sequenz. / SGR 1006 sequence.
+/// Monotone Zeit. / Monotonic time.
+public readonly record struct ConsoleMouseObservation(string Sequence, long TimestampMilliseconds);
diff --git a/src/TuiVision.Drivers.Console/DriverCapabilityMap.cs b/src/TuiVision.Drivers.Console/DriverCapabilityMap.cs
index 2cbb0691..5cdfc965 100644
--- a/src/TuiVision.Drivers.Console/DriverCapabilityMap.cs
+++ b/src/TuiVision.Drivers.Console/DriverCapabilityMap.cs
@@ -87,11 +87,11 @@ public static class DriverCapabilityMap
"Win32 ReadConsoleInput).",
DriverCapabilityBucket.MouseInput =>
- "The managed .NET 10 console baseline does not recreate a dedicated cross-platform " +
- "raw mouse-driver layer. Mouse payloads remain represented by TuiVision.Core.TEvent " +
- "and TMouseEvent for UI-level handling, while the historical per-platform *mouse.cc " +
- "drivers (INT 33h, GPM, Win32 input records, Xterm mouse protocols) are consciously " +
- "not reproduced one-to-one.",
+ "ConsoleMouseIngress and TConsoleDriver provide a bounded managed SGR 1006 path for " +
+ "interactive macOS/Linux terminals and WSL. Mouse payloads remain represented by " +
+ "TuiVision.Core.TEvent and TMouseEvent. Native Windows Console, X10, wheel, hover, " +
+ "touch, and the historical per-platform *mouse.cc matrix are not reproduced one-to-one; " +
+ "they remain explicit unsupported or follow-up boundaries.",
DriverCapabilityBucket.DisplayAdaptation =>
"Managed Unicode and System.Text.Encoding APIs on .NET 10. Codepage and font selection " +
diff --git a/src/TuiVision.Drivers.Console/TConsoleDriver.cs b/src/TuiVision.Drivers.Console/TConsoleDriver.cs
index 041b5a0a..d0349e63 100644
--- a/src/TuiVision.Drivers.Console/TConsoleDriver.cs
+++ b/src/TuiVision.Drivers.Console/TConsoleDriver.cs
@@ -37,6 +37,8 @@ public interface IConsolePresenter
///
public sealed class TConsoleDriver
{
+ private readonly Queue _mouseObservations = new();
+
///
/// Erstellt einen neuen Treiber mit einem Hinterpuffer der angegebenen Größe.
///
@@ -53,6 +55,11 @@ public sealed class TConsoleDriver
public TConsoleDriver(int width, int height)
{
BackBuffer = new TConsoleBuffer(width, height);
+ MouseIngress = new ConsoleMouseIngress(new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Disabled,
+ ConsoleMouseHostFamily.Unknown,
+ ConsoleMouseProtocol.None,
+ "Mouse ingress has not been enabled."));
}
///
@@ -62,6 +69,66 @@ public TConsoleDriver(int width, int height)
///
public TConsoleBuffer BackBuffer { get; private set; }
+ ///
+ /// Der begrenzte Host-Mauseingang dieses Treibers.
+ ///
+ /// The bounded host mouse ingress owned by this driver.
+ ///
+ public ConsoleMouseIngress MouseIngress { get; }
+
+ ///
+ /// Gibt an, ob eine kontrollierte Host-Beobachtung auf Verarbeitung wartet.
+ ///
+ /// Indicates whether a controlled host observation is waiting to be processed.
+ ///
+ public bool HasPendingMouseObservation => _mouseObservations.Count > 0;
+
+ ///
+ /// Reiht eine vollständige kontrollierte Host-Beobachtung für den nächsten
+ /// Ereignisabruf ein.
+ ///
+ /// Queues one complete controlled host observation for the next event retrieval.
+ ///
+ /// Einzureihende Beobachtung. / Observation to queue.
+ public void QueueMouseObservation(ConsoleMouseObservation observation)
+ {
+ ArgumentNullException.ThrowIfNull(observation.Sequence);
+ _mouseObservations.Enqueue(observation);
+ }
+
+ ///
+ /// Verarbeitet die nächste kontrollierte Beobachtung gegen die aktuelle
+ /// Puffergröße.
+ ///
+ /// Processes the next controlled observation against the current buffer size.
+ ///
+ /// Optionaler Zielschlüssel-Auflöser. / Optional target-key resolver.
+ /// Kanonisches Ereignis oder `Nothing`. / Canonical event or `Nothing`.
+ /// Ablehnungsgrund. / Rejection reason.
+ /// true, wenn ein Ereignis veröffentlicht wurde. / true when an event was published.
+ public bool TryGetMouseEvent(
+ Func? targetResolver,
+ out TEvent @event,
+ out ConsoleMouseRejectionReason rejectionReason)
+ {
+ if (_mouseObservations.Count == 0)
+ {
+ @event = TEvent.CreateNone();
+ rejectionReason = ConsoleMouseRejectionReason.None;
+ return false;
+ }
+
+ ConsoleMouseObservation observation = _mouseObservations.Dequeue();
+ return MouseIngress.TryAccept(
+ observation.Sequence,
+ observation.TimestampMilliseconds,
+ BackBuffer.Width,
+ BackBuffer.Height,
+ targetResolver,
+ out @event,
+ out rejectionReason);
+ }
+
///
/// Ändert die Größe des Hinterpuffers und kopiert den sichtbaren Inhalt des alten Puffers.
/// Zellen außerhalb des neuen Bereichs werden verworfen; neue Zellen sind leer.
diff --git a/tests/TuiVision.Controls.Tests/TProgramMouseIntegrationTests.cs b/tests/TuiVision.Controls.Tests/TProgramMouseIntegrationTests.cs
new file mode 100644
index 00000000..42735c4b
--- /dev/null
+++ b/tests/TuiVision.Controls.Tests/TProgramMouseIntegrationTests.cs
@@ -0,0 +1,303 @@
+using TuiVision.Core;
+using TuiVision.Drivers.Console;
+
+namespace TuiVision.Controls.Tests;
+
+///
+/// Prüft den echten `TProgram`-Ereignispfad und einen vollständigen App-Loop
+/// für Fokus, Aktivierung, Doppelklick, Drag und Tastaturfallback.
+///
+/// Verifies the real `TProgram` event path and a complete app loop for focus,
+/// activation, double click, drag, and keyboard fallback.
+///
+[TestClass]
+public sealed class TProgramMouseIntegrationTests
+{
+ ///
+ /// Prüft, dass eine Driver-Beobachtung über `TProgram.GetEvent` als
+ /// kanonisches Ereignis ankommt.
+ ///
+ /// Verifies that a Driver observation reaches the canonical contract through
+ /// `TProgram.GetEvent`.
+ ///
+ [TestMethod]
+ public void GetEvent_QueuedMouseObservation_UsesRealProgramIngress()
+ {
+ MouseProofProgram app = new();
+ app.ConfigureMouseCapability(EnabledCapability());
+ app.Driver.QueueMouseObservation(new ConsoleMouseObservation("\x1b[<0;18;3M", 100));
+
+ app.GetEvent(out TEvent @event);
+
+ Assert.AreEqual(TEventKind.MouseDown, @event.What);
+ Assert.AreEqual(new TPoint(17, 2), @event.Mouse.Where);
+ Assert.AreEqual(TMouseButtons.Left, @event.Mouse.Buttons);
+ }
+
+ ///
+ /// Prüft Fokus, genau eine Aktivierung, sichtbaren Status und gerenderte Cells
+ /// im echten App-Loop.
+ ///
+ /// Verifies focus, exactly one activation, visible status, and rendered cells
+ /// in the real app loop.
+ ///
+ [TestMethod]
+ public void AppLoop_ClickFocusAndActivation_ProducesStateViewAndCellProof()
+ {
+ MouseProofProgram app = new();
+ app.ConfigureMouseCapability(EnabledCapability());
+ app.QueueMouse("\x1b[<0;18;3M", 100, "\x1b[<0;18;3m", 110);
+
+ app.Run();
+
+ Assert.AreEqual(1, app.ActivationCount);
+ Assert.AreEqual("Second", app.ActivatedButtonText);
+ Assert.AreEqual("TButton", app.FocusedKindAtActivation);
+ Assert.AreEqual(ConsoleMouseCapabilityState.Disabled, app.Driver.MouseIngress.Capability.State);
+ Assert.IsTrue(BufferText(app.Driver.BackBuffer).Contains("Mouse activated Second", StringComparison.Ordinal));
+ }
+
+ ///
+ /// Prüft die Doppelklickklassifizierung über Driver, Zielauflösung, Program
+ /// und konkrete View-Behandlung.
+ ///
+ /// Verifies double-click classification through Driver, target resolution,
+ /// Program, and concrete view handling.
+ ///
+ [TestMethod]
+ public void AppLoop_DoubleClick_ReachesSameConcreteViewOnce()
+ {
+ MouseProofProgram app = new();
+ app.ConfigureMouseCapability(EnabledCapability());
+ app.QueueMouse(
+ "\x1b[<0;32;3M", 100,
+ "\x1b[<0;32;3m", 110,
+ "\x1b[<0;32;3M", 600);
+
+ app.Run();
+
+ Assert.AreEqual(2, app.DoubleView.MouseDownCount);
+ Assert.AreEqual(1, app.DoubleView.DoubleClickCount);
+ Assert.IsTrue(BufferText(app.Driver.BackBuffer).Contains("Double click accepted", StringComparison.Ordinal));
+ }
+
+ ///
+ /// Prüft Titel-Drag und gerenderte Endposition im echten App-Loop.
+ ///
+ /// Verifies title dragging and the rendered final position in the real app loop.
+ ///
+ [TestMethod]
+ public void AppLoop_TitleDrag_ChangesWindowAndRenderedRegion()
+ {
+ MouseProofProgram app = new();
+ app.ConfigureMouseCapability(EnabledCapability());
+ app.QueueMouse(
+ "\x1b[<0;7;9M", 100,
+ "\x1b[<32;12;12M", 110,
+ "\x1b[<0;12;12m", 120);
+
+ app.Run();
+
+ Assert.AreEqual(new TRect(10, 11, 20, 16), app.Window.Bounds);
+ Assert.IsFalse(app.Window.IsMouseDragging);
+ Assert.AreEqual('┌', app.Driver.BackBuffer.GetCell(10, 11).Glyph);
+ }
+
+ ///
+ /// Prüft, dass deaktivierter und nicht unterstützter Maussupport keine
+ /// Tastaturaktivierung verhindert und nach Run kein aktiver Zustand bleibt.
+ ///
+ /// Verifies that disabled and unsupported mouse support does not prevent
+ /// keyboard activation and leaves no active state after Run.
+ ///
+ [TestMethod]
+ public void AppLoop_DisabledOrUnsupportedMouse_PreservesKeyboardFallback()
+ {
+ foreach (ConsoleMouseCapability capability in new[]
+ {
+ new ConsoleMouseCapability(ConsoleMouseCapabilityState.Disabled, ConsoleMouseHostFamily.MacOS, ConsoleMouseProtocol.Sgr1006, "Disabled test"),
+ new ConsoleMouseCapability(ConsoleMouseCapabilityState.Unsupported, ConsoleMouseHostFamily.Headless, ConsoleMouseProtocol.None, "Unsupported test")
+ })
+ {
+ MouseProofProgram app = new();
+ app.ConfigureMouseCapability(capability);
+ app.FocusFirstButton();
+ app.QueueKeyboard(ControlEventFactory.CreateKeyDown(charCode: '\r', scanCode: 0x1C));
+
+ app.Run();
+
+ Assert.AreEqual(1, app.ActivationCount);
+ Assert.AreEqual("First", app.ActivatedButtonText);
+ Assert.AreNotEqual(ConsoleMouseCapabilityState.Enabled, app.Driver.MouseIngress.Capability.State);
+ Assert.IsTrue(BufferText(app.Driver.BackBuffer).Contains("Keyboard activated First", StringComparison.Ordinal));
+ }
+ }
+
+ private static ConsoleMouseCapability EnabledCapability() => new(
+ ConsoleMouseCapabilityState.Enabled,
+ ConsoleMouseHostFamily.MacOS,
+ ConsoleMouseProtocol.Sgr1006,
+ "Controlled app-loop proof");
+
+ private static string BufferText(TConsoleBuffer buffer)
+ {
+ System.Text.StringBuilder text = new();
+ for (int y = 0; y < buffer.Height; y++)
+ {
+ for (int x = 0; x < buffer.Width; x++)
+ {
+ char glyph = buffer.GetCell(x, y).Glyph;
+ text.Append(glyph == '\0' ? ' ' : glyph);
+ }
+
+ text.Append('\n');
+ }
+
+ return text.ToString();
+ }
+
+ private sealed class MouseProofProgram : TProgram
+ {
+ private const ushort FirstCommand = 2001;
+ private const ushort SecondCommand = 2002;
+ private readonly Queue _keyboardEvents = new();
+ private readonly TButton _firstButton;
+ private readonly TButton _secondButton;
+ private readonly ProofStatusView _status;
+
+ public MouseProofProgram() : base(new TRect(0, 0, 80, 25))
+ {
+ _firstButton = new TButton(new TRect(2, 2, 14, 5), "First", FirstCommand, TButtonFlags.bfNormal);
+ _secondButton = new TButton(new TRect(16, 2, 28, 5), "Second", SecondCommand, TButtonFlags.bfNormal);
+ DoubleView = new DoubleClickProofView(new TRect(30, 2, 46, 5), SetStatus);
+ Window = new TWindow("Drag", 5, 8, 10, 5, WindowFlags.Move);
+ _status = new ProofStatusView(new TRect(0, 23, 80, 24));
+ Insert(_firstButton);
+ Insert(_secondButton);
+ Insert(DoubleView);
+ Insert(Window);
+ Insert(_status);
+ }
+
+ public int ActivationCount { get; private set; }
+
+ public string ActivatedButtonText { get; private set; } = string.Empty;
+
+ public string FocusedKindAtActivation { get; private set; } = string.Empty;
+
+ public DoubleClickProofView DoubleView { get; }
+
+ public TWindow Window { get; }
+
+ public void QueueMouse(params object[] sequenceTimePairs)
+ {
+ for (int index = 0; index < sequenceTimePairs.Length; index += 2)
+ {
+ Driver.QueueMouseObservation(new ConsoleMouseObservation(
+ (string)sequenceTimePairs[index],
+ Convert.ToInt64(sequenceTimePairs[index + 1], System.Globalization.CultureInfo.InvariantCulture)));
+ }
+ }
+
+ public void QueueKeyboard(params TEvent[] events)
+ {
+ foreach (TEvent @event in events)
+ {
+ _keyboardEvents.Enqueue(@event);
+ }
+ }
+
+ public void FocusFirstButton() => SetFocus(_firstButton);
+
+ public override void GetEvent(out TEvent @event)
+ {
+ if (Driver.HasPendingMouseObservation)
+ {
+ base.GetEvent(out @event);
+ return;
+ }
+
+ @event = _keyboardEvents.Count > 0
+ ? _keyboardEvents.Dequeue()
+ : TEvent.CreateCommand(ShellCommandIds.cmQuit);
+ }
+
+ public override void HandleEvent(TEvent @event)
+ {
+ if (@event.What == TEventKind.Command
+ && @event.Message.Command is FirstCommand or SecondCommand)
+ {
+ ActivationCount++;
+ ActivatedButtonText = @event.Message.Command == FirstCommand ? "First" : "Second";
+ FocusedKindAtActivation = Current?.GetType().Name ?? "None";
+ string input = Driver.MouseIngress.Capability.State == ConsoleMouseCapabilityState.Enabled ? "Mouse" : "Keyboard";
+ SetStatus($"{input} activated {ActivatedButtonText}");
+ @event.Clear(this);
+ return;
+ }
+
+ base.HandleEvent(@event);
+ }
+
+ private void SetStatus(string text) => _status.Text = text;
+ }
+
+ private sealed class DoubleClickProofView : TView
+ {
+ private readonly Action _setStatus;
+
+ public DoubleClickProofView(TRect bounds, Action setStatus) : base(bounds)
+ {
+ _setStatus = setStatus;
+ Options |= TViewOptions.Selectable;
+ }
+
+ public int MouseDownCount { get; private set; }
+
+ public int DoubleClickCount { get; private set; }
+
+ public override void HandleEvent(TEvent @event)
+ {
+ if (@event.What == TEventKind.MouseDown)
+ {
+ MouseDownCount++;
+ if (@event.Mouse.DoubleClick)
+ {
+ DoubleClickCount++;
+ _setStatus("Double click accepted");
+ }
+
+ @event.Clear(this);
+ return;
+ }
+
+ base.HandleEvent(@event);
+ }
+
+ public override void Draw()
+ {
+ GetDrawBuffer()?.WriteText(Origin.X, Origin.Y, "Double target".AsSpan());
+ }
+ }
+
+ private sealed class ProofStatusView : TView
+ {
+ public ProofStatusView(TRect bounds) : base(bounds)
+ {
+ }
+
+ public string Text { get; set; } = "Mouse proof ready";
+
+ public override void Draw()
+ {
+ TConsoleBuffer? buffer = GetDrawBuffer();
+ if (buffer is null)
+ {
+ return;
+ }
+
+ string value = Text.PadRight(Size.X);
+ buffer.WriteText(Origin.X, Origin.Y, value.AsSpan(0, Size.X));
+ }
+ }
+}
diff --git a/tests/TuiVision.Controls.Tests/TViewMouseInteractionTests.cs b/tests/TuiVision.Controls.Tests/TViewMouseInteractionTests.cs
new file mode 100644
index 00000000..55bcaa8b
--- /dev/null
+++ b/tests/TuiVision.Controls.Tests/TViewMouseInteractionTests.cs
@@ -0,0 +1,209 @@
+using TuiVision.Core;
+
+namespace TuiVision.Controls.Tests;
+
+///
+/// Prüft globale Mauskoordinaten, Topmost-Hit-Routing, Fokusbesitz und
+/// Exactly-once-Aktivierung in verschachtelten View-Bäumen.
+///
+/// Verifies global mouse coordinates, topmost hit routing, focus ownership,
+/// and exactly-once activation in nested view trees.
+///
+[TestClass]
+public sealed class TViewMouseInteractionTests
+{
+ ///
+ /// Prüft die vollständige Owner-Kette für globale und lokale Koordinaten.
+ ///
+ /// Verifies the complete owner chain for global and local coordinates.
+ ///
+ [TestMethod]
+ public void MakeGlobalAndLocal_NestedOwners_UseScreenCoordinates()
+ {
+ TGroup root = new(new TRect(0, 0, 80, 25));
+ TGroup desktop = new(new TRect(0, 1, 80, 24));
+ TGroup window = new(new TRect(3, 2, 23, 12));
+ TView child = new(new TRect(2, 1, 8, 4));
+ root.Insert(desktop);
+ desktop.Insert(window);
+ window.Insert(child);
+
+ Assert.AreEqual(new TPoint(5, 4), child.MakeGlobal(new TPoint(0, 0)));
+ Assert.AreEqual(new TPoint(0, 0), child.MakeLocal(new TPoint(5, 4)));
+ Assert.IsTrue(child.MouseInView(new TPoint(6, 5)));
+ Assert.IsFalse(child.MouseInView(new TPoint(4, 3)));
+ }
+
+ ///
+ /// Prüft, dass nur der zuletzt eingefügte sichtbare Treffer das Ereignis
+ /// erhält und vor der Behandlung fokussiert ist.
+ ///
+ /// Verifies that only the last inserted visible hit receives the event and
+ /// is focused before handling.
+ ///
+ [TestMethod]
+ public void HandleEvent_OverlappingViews_FocusesAndDispatchesOnlyTopmost()
+ {
+ TGroup group = new(new TRect(0, 0, 30, 10));
+ RecordingView bottom = new(new TRect(1, 1, 12, 6));
+ RecordingView top = new(new TRect(1, 1, 12, 6));
+ group.Insert(bottom);
+ group.Insert(top);
+
+ group.HandleEvent(MouseDown(3, 3));
+
+ Assert.AreSame(top, group.Current);
+ Assert.IsTrue(top.WasFocusedWhenHandled);
+ Assert.AreEqual(1, top.MouseCount);
+ Assert.AreEqual(0, bottom.MouseCount);
+ }
+
+ ///
+ /// Prüft, dass ungeeignete obere Views den passenden darunterliegenden
+ /// Treffer weder verdecken noch selbst fokussiert werden.
+ ///
+ /// Verifies that ineligible upper views neither hide an eligible lower hit
+ /// nor receive focus themselves.
+ ///
+ [TestMethod]
+ public void HandleEvent_IneligibleTopViews_SelectsEligibleVisibleTarget()
+ {
+ foreach (Action makeIneligible in new Action[]
+ {
+ view => view.SetState(TViewState.Visible, false),
+ view => view.SetState(TViewState.Disabled, true)
+ })
+ {
+ TGroup group = new(new TRect(0, 0, 30, 10));
+ RecordingView eligible = new(new TRect(1, 1, 12, 6));
+ RecordingView ineligible = new(new TRect(1, 1, 12, 6));
+ makeIneligible(ineligible);
+ group.Insert(eligible);
+ group.Insert(ineligible);
+
+ group.HandleEvent(MouseDown(3, 3));
+
+ Assert.AreSame(eligible, group.Current);
+ Assert.AreEqual(1, eligible.MouseCount);
+ Assert.AreEqual(0, ineligible.MouseCount);
+ }
+ }
+
+ ///
+ /// Prüft, dass eine sichtbare nicht selektierbare obere View das Ereignis
+ /// erhält, ohne Fokus zu beanspruchen oder die darunterliegende View zu aktivieren.
+ ///
+ /// Verifies that a visible non-selectable upper view receives the event
+ /// without claiming focus or activating the lower view.
+ ///
+ [TestMethod]
+ public void HandleEvent_NonSelectableTopView_ReceivesMouseWithoutFocus()
+ {
+ TGroup group = new(new TRect(0, 0, 30, 10));
+ RecordingView lower = new(new TRect(1, 1, 12, 6));
+ RecordingView upper = new(new TRect(1, 1, 12, 6));
+ upper.Options &= ~TViewOptions.Selectable;
+ group.Insert(lower);
+ group.Insert(upper);
+
+ group.HandleEvent(MouseDown(3, 3));
+
+ Assert.IsNull(group.Current);
+ Assert.AreEqual(1, upper.MouseCount);
+ Assert.IsFalse(upper.WasFocusedWhenHandled);
+ Assert.AreEqual(0, lower.MouseCount);
+ }
+
+ ///
+ /// Prüft, dass ein äußerer Klick keinen Fokus oder Command erzeugt.
+ ///
+ /// Verifies that an outside click produces neither focus nor a command.
+ ///
+ [TestMethod]
+ public void HandleEvent_ClickOutsideTargets_DoesNotFocusOrActivate()
+ {
+ CommandRecordingGroup group = new(new TRect(0, 0, 30, 10));
+ TButton button = new(new TRect(1, 1, 10, 3), "OK", 4242, TButtonFlags.bfNormal);
+ group.Insert(button);
+
+ group.HandleEvent(MouseDown(20, 8));
+
+ Assert.IsNull(group.Current);
+ Assert.AreEqual(0, group.CommandCount);
+ }
+
+ ///
+ /// Prüft, dass derselbe MouseDown-Pfad den bestehenden Button-Command genau
+ /// einmal auslöst.
+ ///
+ /// Verifies that the same mouse-down path invokes the existing button
+ /// command exactly once.
+ ///
+ [TestMethod]
+ public void HandleEvent_ButtonHit_FocusesAndActivatesExactlyOnce()
+ {
+ CommandRecordingGroup group = new(new TRect(0, 0, 30, 10));
+ TButton button = new(new TRect(1, 1, 10, 3), "OK", 4242, TButtonFlags.bfNormal);
+ group.Insert(button);
+
+ TEvent @event = MouseDown(3, 2);
+ group.HandleEvent(@event);
+
+ Assert.AreSame(button, group.Current);
+ Assert.AreEqual(1, group.CommandCount);
+ Assert.AreEqual((ushort)4242, group.LastCommand);
+ Assert.AreEqual(TEventKind.Nothing, @event.What);
+ }
+
+ private static TEvent MouseDown(int x, int y) =>
+ TEvent.CreateMouse(TEventKind.MouseDown, TMouseButtons.Left, false, new TPoint((short)x, (short)y));
+
+ private sealed class RecordingView : TView
+ {
+ public RecordingView(TRect bounds) : base(bounds)
+ {
+ Options |= TViewOptions.Selectable;
+ }
+
+ public int MouseCount { get; private set; }
+
+ public bool WasFocusedWhenHandled { get; private set; }
+
+ public override void HandleEvent(TEvent @event)
+ {
+ if ((@event.What & TEventKind.Mouse) != 0)
+ {
+ MouseCount++;
+ WasFocusedWhenHandled = GetState(TViewState.Focused);
+ @event.Clear(this);
+ return;
+ }
+
+ base.HandleEvent(@event);
+ }
+ }
+
+ private sealed class CommandRecordingGroup : TGroup
+ {
+ public CommandRecordingGroup(TRect bounds) : base(bounds)
+ {
+ }
+
+ public int CommandCount { get; private set; }
+
+ public ushort LastCommand { get; private set; }
+
+ public override void HandleEvent(TEvent @event)
+ {
+ if (@event.What == TEventKind.Command)
+ {
+ CommandCount++;
+ LastCommand = @event.Message.Command;
+ @event.Clear(this);
+ return;
+ }
+
+ base.HandleEvent(@event);
+ }
+ }
+}
diff --git a/tests/TuiVision.Controls.Tests/TWindowMouseDragTests.cs b/tests/TuiVision.Controls.Tests/TWindowMouseDragTests.cs
new file mode 100644
index 00000000..87581a2a
--- /dev/null
+++ b/tests/TuiVision.Controls.Tests/TWindowMouseDragTests.cs
@@ -0,0 +1,167 @@
+using TuiVision.Core;
+using TuiVision.Drivers.Console;
+
+namespace TuiVision.Controls.Tests;
+
+///
+/// Prüft den einzigen Feature-020-Drag-Vertrag: ein bewegliches Fenster wird
+/// am Titelrahmen gezogen, begrenzt und fail-safe beendet.
+///
+/// Verifies the sole Feature 020 drag contract: a movable window is dragged by
+/// its title row, clamped, and ended fail-safe.
+///
+[TestClass]
+public sealed class TWindowMouseDragTests
+{
+ ///
+ /// Prüft Press, mehrere Moves und Release mit festgeschriebener Endposition.
+ ///
+ /// Verifies press, multiple moves, and release with a committed final position.
+ ///
+ [TestMethod]
+ public void TitleDrag_PressMovesRelease_CommitsPosition()
+ {
+ (TGroup owner, TWindow window) = CreateWindow();
+
+ owner.HandleEvent(Mouse(TEventKind.MouseDown, 6, 3, TMouseButtons.Left));
+ owner.HandleEvent(Mouse(TEventKind.MouseMove, 8, 4, TMouseButtons.Left));
+ owner.HandleEvent(Mouse(TEventKind.MouseMove, 10, 6, TMouseButtons.Left));
+ owner.HandleEvent(Mouse(TEventKind.MouseUp, 10, 6, TMouseButtons.None));
+
+ Assert.AreEqual(new TRect(9, 6, 19, 11), window.Bounds);
+ Assert.IsFalse(window.IsMouseDragging);
+ }
+
+ ///
+ /// Prüft das Clamping der vollständigen Fenstergrenzen an allen Owner-Kanten.
+ ///
+ /// Verifies clamping of the complete window bounds at all owner edges.
+ ///
+ [TestMethod]
+ public void TitleDrag_OutsideOwner_ClampsCompleteWindow()
+ {
+ (TGroup owner, TWindow window) = CreateWindow();
+ owner.HandleEvent(Mouse(TEventKind.MouseDown, 6, 3, TMouseButtons.Left));
+ owner.HandleEvent(Mouse(TEventKind.MouseMove, short.MaxValue, short.MaxValue, TMouseButtons.Left));
+ Assert.AreEqual(new TRect(30, 10, 40, 15), window.Bounds);
+
+ owner.HandleEvent(Mouse(TEventKind.MouseMove, short.MinValue, short.MinValue, TMouseButtons.Left));
+ Assert.AreEqual(new TRect(0, 0, 10, 5), window.Bounds);
+ owner.HandleEvent(Mouse(TEventKind.MouseUp, 0, 0, TMouseButtons.None));
+ }
+
+ ///
+ /// Prüft, dass Körperklick, nicht bewegliche Fenster, falsche Buttons und
+ /// Move ohne Press keinen Drag starten.
+ ///
+ /// Verifies that body clicks, non-movable windows, wrong buttons, and moves
+ /// without a press do not start a drag.
+ ///
+ [TestMethod]
+ public void InvalidDragStarts_DoNotChangeWindow()
+ {
+ (TGroup owner, TWindow window) = CreateWindow();
+ TRect original = window.Bounds;
+ owner.HandleEvent(Mouse(TEventKind.MouseDown, 6, 4, TMouseButtons.Left));
+ owner.HandleEvent(Mouse(TEventKind.MouseMove, 12, 8, TMouseButtons.Left));
+ Assert.AreEqual(original, window.Bounds);
+
+ (TGroup fixedOwner, TWindow fixedWindow) = CreateWindow(WindowFlags.None);
+ fixedOwner.HandleEvent(Mouse(TEventKind.MouseDown, 6, 3, TMouseButtons.Left));
+ fixedOwner.HandleEvent(Mouse(TEventKind.MouseMove, 12, 8, TMouseButtons.Left));
+ Assert.AreEqual(original, fixedWindow.Bounds);
+
+ window.HandleEvent(Mouse(TEventKind.MouseDown, 6, 3, TMouseButtons.Right));
+ window.HandleEvent(Mouse(TEventKind.MouseMove, 12, 8, TMouseButtons.Left));
+ Assert.AreEqual(original, window.Bounds);
+ }
+
+ ///
+ /// Prüft Escape und Capability-Verlust als explizite Abbruchpfade.
+ ///
+ /// Verifies Escape and capability loss as explicit cancellation paths.
+ ///
+ [TestMethod]
+ public void TitleDrag_EscapeOrCapabilityLoss_RestoresAndClearsState()
+ {
+ (TGroup owner, TWindow window) = CreateWindow();
+ TRect original = window.Bounds;
+ StartAndMove(owner);
+ owner.HandleEvent(ControlEventFactory.CreateKeyDown(charCode: '\x1b', scanCode: 0x01));
+ Assert.AreEqual(original, window.Bounds);
+ Assert.IsFalse(window.IsMouseDragging);
+
+ StartAndMove(owner);
+ owner.HandleEvent(TEvent.CreateBroadcast(
+ ShellCommandIds.cmMouseCapabilityChanged,
+ ConsoleMouseCapabilityState.Disabled));
+ Assert.AreEqual(original, window.Bounds);
+ Assert.IsFalse(window.IsMouseDragging);
+ }
+
+ ///
+ /// Prüft Disable, Removal und Shutdown als sofortige Abbruchgrenzen.
+ ///
+ /// Verifies disable, removal, and shutdown as immediate cancellation boundaries.
+ ///
+ [TestMethod]
+ public void TitleDrag_DisableRemovalOrShutdown_ClearsState()
+ {
+ (TGroup owner, TWindow disabledWindow) = CreateWindow();
+ StartAndMove(owner);
+ disabledWindow.SetState(TViewState.Disabled, true);
+ Assert.IsFalse(disabledWindow.IsMouseDragging);
+
+ (TGroup removalOwner, TWindow removedWindow) = CreateWindow();
+ StartAndMove(removalOwner);
+ removalOwner.Remove(removedWindow);
+ Assert.IsFalse(removedWindow.IsMouseDragging);
+
+ (TGroup shutdownOwner, TWindow shutdownWindow) = CreateWindow();
+ StartAndMove(shutdownOwner);
+ shutdownWindow.ShutDown();
+ Assert.IsFalse(shutdownWindow.IsMouseDragging);
+ }
+
+ ///
+ /// Prüft, dass der bestehende Tastaturmodus neben dem Maus-Drag unverändert
+ /// Enter-Commit und Escape-Restore unterstützt.
+ ///
+ /// Verifies that the existing keyboard mode still supports Enter commit and
+ /// Escape restore alongside mouse drag.
+ ///
+ [TestMethod]
+ public void KeyboardMoveMode_RemainsCompleteFallback()
+ {
+ (_, TWindow window) = CreateWindow();
+ TRect original = window.Bounds;
+ window.HandleEvent(ControlEventFactory.CreateKeyDown(scanCode: 0x3F, shiftState: 0x0004));
+ window.HandleEvent(ControlEventFactory.CreateKeyDown(scanCode: 0x4D));
+ window.HandleEvent(ControlEventFactory.CreateKeyDown(charCode: '\r', scanCode: 0x1C));
+ Assert.IsFalse(window.IsInMoveMode);
+ Assert.AreNotEqual(original, window.Bounds);
+
+ TRect committed = window.Bounds;
+ window.HandleEvent(ControlEventFactory.CreateKeyDown(scanCode: 0x3F, shiftState: 0x0004));
+ window.HandleEvent(ControlEventFactory.CreateKeyDown(scanCode: 0x4D));
+ window.HandleEvent(ControlEventFactory.CreateKeyDown(charCode: '\x1b', scanCode: 0x01));
+ Assert.AreEqual(committed, window.Bounds);
+ }
+
+ private static (TGroup Owner, TWindow Window) CreateWindow(WindowFlags flags = WindowFlags.Move)
+ {
+ TGroup owner = new(new TRect(0, 0, 40, 15));
+ TWindow window = new("Move", 5, 3, 10, 5, flags);
+ owner.Insert(window);
+ return (owner, window);
+ }
+
+ private static void StartAndMove(TGroup owner)
+ {
+ owner.HandleEvent(Mouse(TEventKind.MouseDown, 6, 3, TMouseButtons.Left));
+ owner.HandleEvent(Mouse(TEventKind.MouseMove, 10, 6, TMouseButtons.Left));
+ }
+
+ private static TEvent Mouse(TEventKind kind, int x, int y, TMouseButtons buttons) =>
+ TEvent.CreateMouse(kind, buttons, false, new TPoint(x, y));
+}
diff --git a/tests/TuiVision.Drivers.Tests/ConsoleMouseIngressTests.cs b/tests/TuiVision.Drivers.Tests/ConsoleMouseIngressTests.cs
new file mode 100644
index 00000000..acc67bd2
--- /dev/null
+++ b/tests/TuiVision.Drivers.Tests/ConsoleMouseIngressTests.cs
@@ -0,0 +1,254 @@
+using TuiVision.Core;
+using TuiVision.Drivers.Console;
+
+namespace TuiVision.Drivers.Tests;
+
+///
+/// Prüft den begrenzten SGR-1006-Mauseingang einschließlich atomarer Ablehnung,
+/// Zustandsfolgen und deterministischer Doppelklickgrenzen.
+///
+/// Verifies the bounded SGR 1006 mouse ingress, including atomic rejection,
+/// state transitions, and deterministic double-click boundaries.
+///
+[TestClass]
+public sealed class ConsoleMouseIngressTests
+{
+ private const int Width = 80;
+ private const int Height = 25;
+
+ ///
+ /// Prüft die unterstützten SGR-Hostfamilien und die ehrlichen Unsupported-Grenzen.
+ ///
+ /// Verifies supported SGR host families and honest unsupported boundaries.
+ ///
+ [TestMethod]
+ public void CapabilityDetector_ClassifiesSupportedAndUnsupportedHosts()
+ {
+ ConsoleMouseCapability mac = ConsoleMouseCapabilityDetector.Detect(false, false, "xterm-256color", false, false, true, false);
+ ConsoleMouseCapability linux = ConsoleMouseCapabilityDetector.Detect(false, false, "xterm", false, false, false, true);
+ ConsoleMouseCapability wsl = ConsoleMouseCapabilityDetector.Detect(false, false, "xterm-256color", true, true, false, true);
+ ConsoleMouseCapability windows = ConsoleMouseCapabilityDetector.Detect(false, false, null, true, false, false, false);
+ ConsoleMouseCapability headless = ConsoleMouseCapabilityDetector.Detect(true, false, "xterm", false, false, true, false);
+ ConsoleMouseCapability dumb = ConsoleMouseCapabilityDetector.Detect(false, false, "dumb", false, false, false, true);
+
+ Assert.AreEqual((ConsoleMouseCapabilityState.Disabled, ConsoleMouseHostFamily.MacOS, ConsoleMouseProtocol.Sgr1006), (mac.State, mac.HostFamily, mac.Protocol));
+ Assert.AreEqual((ConsoleMouseCapabilityState.Disabled, ConsoleMouseHostFamily.Linux, ConsoleMouseProtocol.Sgr1006), (linux.State, linux.HostFamily, linux.Protocol));
+ Assert.AreEqual((ConsoleMouseCapabilityState.Disabled, ConsoleMouseHostFamily.Wsl, ConsoleMouseProtocol.Sgr1006), (wsl.State, wsl.HostFamily, wsl.Protocol));
+ Assert.AreEqual((ConsoleMouseCapabilityState.Unsupported, ConsoleMouseHostFamily.WindowsConsole, ConsoleMouseProtocol.None), (windows.State, windows.HostFamily, windows.Protocol));
+ Assert.AreEqual((ConsoleMouseCapabilityState.Unsupported, ConsoleMouseHostFamily.Headless, ConsoleMouseProtocol.None), (headless.State, headless.HostFamily, headless.Protocol));
+ Assert.AreEqual((ConsoleMouseCapabilityState.Unsupported, ConsoleMouseHostFamily.Linux, ConsoleMouseProtocol.None), (dumb.State, dumb.HostFamily, dumb.Protocol));
+ }
+
+ ///
+ /// Prüft eine vollständige Press-Move-Release-Folge und die Umrechnung auf
+ /// nullbasierte Framework-Koordinaten.
+ ///
+ /// Verifies a complete press-move-release sequence and conversion to
+ /// zero-based framework coordinates.
+ ///
+ [TestMethod]
+ public void TryAccept_ValidLeftSequence_PublishesCanonicalEventsExactlyOnce()
+ {
+ ConsoleMouseIngress ingress = CreateEnabledIngress();
+
+ AssertAccepted(ingress, "\x1b[<0;1;1M", 100, TEventKind.MouseDown, TMouseButtons.Left, new TPoint(0, 0));
+ AssertAccepted(ingress, "\x1b[<32;80;25M", 110, TEventKind.MouseMove, TMouseButtons.Left, new TPoint(79, 24));
+ AssertAccepted(ingress, "\x1b[<0;80;25m", 120, TEventKind.MouseUp, TMouseButtons.None, new TPoint(79, 24));
+ }
+
+ ///
+ /// Prüft, dass Koordinaten außerhalb des aktuellen Puffers vollständig
+ /// abgelehnt und nicht still geklemmt werden.
+ ///
+ /// Verifies that coordinates outside the current buffer are rejected
+ /// completely instead of being silently clamped.
+ ///
+ [TestMethod]
+ [DataRow("\x1b[<0;0;1M")]
+ [DataRow("\x1b[<0;1;0M")]
+ [DataRow("\x1b[<0;81;1M")]
+ [DataRow("\x1b[<0;1;26M")]
+ public void TryAccept_OutOfRangeCoordinates_RejectsAtomically(string sequence)
+ {
+ AssertRejected(CreateEnabledIngress(), sequence, ConsoleMouseRejectionReason.CoordinatesOutOfRange);
+ }
+
+ ///
+ /// Prüft die projektlokale Negativmatrix für Syntax-, Größen-, Zahlen- und
+ /// Button-Grenzen.
+ ///
+ /// Verifies the project-local negative matrix for syntax, size, numeric,
+ /// and button boundaries.
+ ///
+ [TestMethod]
+ [DataRow("", ConsoleMouseRejectionReason.InvalidSyntax)]
+ [DataRow("\x1b[<0;1;1", ConsoleMouseRejectionReason.InvalidSyntax)]
+ [DataRow("\x1b[
+ /// Prüft die feste Sequenzlängengrenze unabhängig von Zahlenüberläufen.
+ ///
+ /// Verifies the fixed sequence length boundary independently of numeric overflow.
+ ///
+ [TestMethod]
+ public void TryAccept_OversizedObservation_RejectsBeforeParsing()
+ {
+ AssertRejected(CreateEnabledIngress(), "\x1b[<0;" + new string('9', 80) + ";1M", ConsoleMouseRejectionReason.SequenceTooLong);
+ }
+
+ ///
+ /// Prüft ungültige Phasenfolgen, Capability-Grenzen und den Reset transienter Zustände.
+ ///
+ /// Verifies invalid phase sequences, capability boundaries, and transient-state reset.
+ ///
+ [TestMethod]
+ public void TryAccept_InvalidPhaseOrCapability_RejectsFailSafe()
+ {
+ ConsoleMouseIngress ingress = CreateEnabledIngress();
+ AssertRejected(ingress, "\x1b[<32;2;2M", ConsoleMouseRejectionReason.InvalidTransition);
+ AssertRejected(ingress, "\x1b[<0;2;2m", ConsoleMouseRejectionReason.InvalidTransition);
+ AssertAccepted(ingress, "\x1b[<0;2;2M", 100, TEventKind.MouseDown, TMouseButtons.Left, new TPoint(1, 1));
+ AssertRejected(ingress, "\x1b[<0;3;3M", ConsoleMouseRejectionReason.InvalidTransition);
+
+ ingress.SetCapability(new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Disabled,
+ ConsoleMouseHostFamily.MacOS,
+ ConsoleMouseProtocol.Sgr1006,
+ "Disabled for test"));
+ AssertRejected(ingress, "\x1b[<32;3;3M", ConsoleMouseRejectionReason.CapabilityUnavailable);
+
+ ingress.SetCapability(new ConsoleMouseCapability(
+ ConsoleMouseCapabilityState.Unsupported,
+ ConsoleMouseHostFamily.Headless,
+ ConsoleMouseProtocol.None,
+ "Headless"));
+ AssertRejected(ingress, "\x1b[<0;1;1M", ConsoleMouseRejectionReason.CapabilityUnavailable);
+ }
+
+ ///
+ /// Prüft, dass eine fehlerhafte Beobachtung die nächste eigenständige gültige
+ /// Beobachtung nicht beschädigt.
+ ///
+ /// Verifies that a rejected observation does not damage the next independent
+ /// valid observation.
+ ///
+ [TestMethod]
+ public void TryAccept_RejectedObservation_PreservesNextIndependentObservation()
+ {
+ ConsoleMouseIngress ingress = CreateEnabledIngress();
+ AssertRejected(ingress, "\x1b[
+ /// Prüft die inklusive 500-ms-Grenze bei identischem Button, Ziel und Zelle.
+ ///
+ /// Verifies the inclusive 500 ms boundary for the same button, target, and cell.
+ ///
+ [TestMethod]
+ public void TryAccept_SecondMatchingPressWithin500Milliseconds_IsDoubleClick()
+ {
+ ConsoleMouseIngress ingress = CreateEnabledIngress();
+ Func target = _ => "button-a";
+
+ AssertAccepted(ingress, "\x1b[<0;4;5M", 100, TEventKind.MouseDown, TMouseButtons.Left, new TPoint(3, 4), false, target);
+ AssertAccepted(ingress, "\x1b[<0;4;5m", 110, TEventKind.MouseUp, TMouseButtons.None, new TPoint(3, 4), false, target);
+ AssertAccepted(ingress, "\x1b[<0;4;5M", 600, TEventKind.MouseDown, TMouseButtons.Left, new TPoint(3, 4), true, target);
+ }
+
+ ///
+ /// Prüft Zeit-, Positions-, Ziel-, Uhrsprung- und Capability-Reset-Grenzen
+ /// unabhängig voneinander.
+ ///
+ /// Verifies time, position, target, clock-regression, and capability-reset
+ /// boundaries independently.
+ ///
+ [TestMethod]
+ public void TryAccept_NonMatchingPresses_RemainSingleClicks()
+ {
+ AssertSecondPressIsSingle(100, 601, new TPoint(3, 4), new TPoint(3, 4), "a", "a");
+ AssertSecondPressIsSingle(100, 600, new TPoint(3, 4), new TPoint(4, 4), "a", "a");
+ AssertSecondPressIsSingle(100, 600, new TPoint(3, 4), new TPoint(3, 4), "a", "b");
+ AssertSecondPressIsSingle(600, 100, new TPoint(3, 4), new TPoint(3, 4), "a", "a");
+
+ ConsoleMouseIngress resetIngress = CreateEnabledIngress();
+ Func target = _ => "a";
+ AssertAccepted(resetIngress, "\x1b[<0;4;5M", 100, TEventKind.MouseDown, TMouseButtons.Left, new TPoint(3, 4), false, target);
+ AssertAccepted(resetIngress, "\x1b[<0;4;5m", 110, TEventKind.MouseUp, TMouseButtons.None, new TPoint(3, 4), false, target);
+ resetIngress.SetCapability(new ConsoleMouseCapability(ConsoleMouseCapabilityState.Disabled, ConsoleMouseHostFamily.MacOS, ConsoleMouseProtocol.Sgr1006, "Reset"));
+ resetIngress.SetCapability(EnabledCapability());
+ AssertAccepted(resetIngress, "\x1b[<0;4;5M", 200, TEventKind.MouseDown, TMouseButtons.Left, new TPoint(3, 4), false, target);
+ }
+
+ private static void AssertSecondPressIsSingle(
+ long firstTime,
+ long secondTime,
+ TPoint firstPoint,
+ TPoint secondPoint,
+ string firstTarget,
+ string secondTarget)
+ {
+ ConsoleMouseIngress ingress = CreateEnabledIngress();
+ AssertAccepted(ingress, Sequence(firstPoint, false), firstTime, TEventKind.MouseDown, TMouseButtons.Left, firstPoint, false, _ => firstTarget);
+ AssertAccepted(ingress, Sequence(firstPoint, true), firstTime + 1, TEventKind.MouseUp, TMouseButtons.None, firstPoint, false, _ => firstTarget);
+ AssertAccepted(ingress, Sequence(secondPoint, false), secondTime, TEventKind.MouseDown, TMouseButtons.Left, secondPoint, false, _ => secondTarget);
+ }
+
+ private static string Sequence(TPoint point, bool release) =>
+ $"\x1b[<0;{point.X + 1};{point.Y + 1}{(release ? 'm' : 'M')}";
+
+ private static ConsoleMouseIngress CreateEnabledIngress() => new(EnabledCapability());
+
+ private static ConsoleMouseCapability EnabledCapability() => new(
+ ConsoleMouseCapabilityState.Enabled,
+ ConsoleMouseHostFamily.MacOS,
+ ConsoleMouseProtocol.Sgr1006,
+ "Interactive SGR test host");
+
+ private static void AssertAccepted(
+ ConsoleMouseIngress ingress,
+ string sequence,
+ long timestamp,
+ TEventKind expectedKind,
+ TMouseButtons expectedButtons,
+ TPoint expectedPoint,
+ bool expectedDoubleClick = false,
+ Func? targetResolver = null)
+ {
+ bool accepted = ingress.TryAccept(
+ sequence,
+ timestamp,
+ Width,
+ Height,
+ targetResolver,
+ out TEvent @event,
+ out ConsoleMouseRejectionReason rejection);
+
+ Assert.IsTrue(accepted, $"Expected accepted event, rejection was {rejection}.");
+ Assert.AreEqual(ConsoleMouseRejectionReason.None, rejection);
+ Assert.AreEqual(expectedKind, @event.What);
+ Assert.AreEqual(expectedButtons, @event.Mouse.Buttons);
+ Assert.AreEqual(expectedPoint, @event.Mouse.Where);
+ Assert.AreEqual(expectedDoubleClick, @event.Mouse.DoubleClick);
+ }
+
+ private static void AssertRejected(
+ ConsoleMouseIngress ingress,
+ string sequence,
+ ConsoleMouseRejectionReason expectedReason)
+ {
+ bool accepted = ingress.TryAccept(sequence, 100, Width, Height, null, out TEvent @event, out ConsoleMouseRejectionReason rejection);
+ Assert.IsFalse(accepted);
+ Assert.AreEqual(TEventKind.Nothing, @event.What);
+ Assert.AreEqual(expectedReason, rejection);
+ }
+}