From cfdd0bf9502f365a71a2daf0c08011e868799aaf Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 12 Jul 2026 04:09:15 +0200 Subject: [PATCH 1/6] chore: rename Lastenheft to Lastenheft_04_MouseSupportAndInteraction.020-mouse-support-interaction.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...04_MouseSupportAndInteraction.020-mouse-support-interaction.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Lastenheft_04_MouseSupportAndInteraction.md => Lastenheft_04_MouseSupportAndInteraction.020-mouse-support-interaction.md (100%) 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 From f76a0bfefaa7edc6e27721f117cb735e068ca52f Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 12 Jul 2026 04:12:39 +0200 Subject: [PATCH 2/6] feat: harden mouse support and interaction --- .github/agents/copilot-instructions.md | 10 + .github/copilot-instructions.md | 10 + .specify/feature.json | 2 +- AGENTS.md | 10 + CLAUDE.md | 10 + Directory.Build.props | 6 +- GEMINI.md | 10 + Pflichtenheft.md | 9 +- README.md | 2 + docs/architecture/runtime-view.md | 34 ++ docs/guides/mouse-support.md | 125 +++++ docs/project-statistics.md | 110 ++-- docs/security/threat-model.md | 3 +- docs/toc.yml | 2 + .../checklists/domain-acceptance.md | 20 + .../checklists/plan-quality.md | 22 + .../checklists/plan-review.md | 17 + .../checklists/requirements.md | 39 ++ .../contracts/mouse-interaction-acceptance.md | 61 +++ .../data-model.md | 119 +++++ specs/020-mouse-support-interaction/plan.md | 214 ++++++++ .../pr-evidence.md | 258 +++++++++ .../quickstart.md | 58 +++ .../020-mouse-support-interaction/research.md | 112 ++++ specs/020-mouse-support-interaction/spec.md | 356 +++++++++++++ specs/020-mouse-support-interaction/tasks.md | 197 +++++++ .../IMouseInteractionSession.cs | 6 + src/TuiVision.Controls/ShellCommandIds.cs | 8 + src/TuiVision.Controls/TGroup.cs | 77 ++- src/TuiVision.Controls/TProgram.cs | 195 ++++++- src/TuiVision.Controls/TView.cs | 5 +- src/TuiVision.Controls/TWindow.cs | 138 ++++- .../ConsoleMouseIngress.cs | 492 ++++++++++++++++++ .../DriverCapabilityMap.cs | 10 +- .../TConsoleDriver.cs | 67 +++ .../TProgramMouseIntegrationTests.cs | 303 +++++++++++ .../TViewMouseInteractionTests.cs | 209 ++++++++ .../TWindowMouseDragTests.cs | 167 ++++++ .../ConsoleMouseIngressTests.cs | 254 +++++++++ 39 files changed, 3678 insertions(+), 69 deletions(-) create mode 100644 docs/guides/mouse-support.md create mode 100644 specs/020-mouse-support-interaction/checklists/domain-acceptance.md create mode 100644 specs/020-mouse-support-interaction/checklists/plan-quality.md create mode 100644 specs/020-mouse-support-interaction/checklists/plan-review.md create mode 100644 specs/020-mouse-support-interaction/checklists/requirements.md create mode 100644 specs/020-mouse-support-interaction/contracts/mouse-interaction-acceptance.md create mode 100644 specs/020-mouse-support-interaction/data-model.md create mode 100644 specs/020-mouse-support-interaction/plan.md create mode 100644 specs/020-mouse-support-interaction/pr-evidence.md create mode 100644 specs/020-mouse-support-interaction/quickstart.md create mode 100644 specs/020-mouse-support-interaction/research.md create mode 100644 specs/020-mouse-support-interaction/spec.md create mode 100644 specs/020-mouse-support-interaction/tasks.md create mode 100644 src/TuiVision.Controls/IMouseInteractionSession.cs create mode 100644 src/TuiVision.Drivers.Console/ConsoleMouseIngress.cs create mode 100644 tests/TuiVision.Controls.Tests/TProgramMouseIntegrationTests.cs create mode 100644 tests/TuiVision.Controls.Tests/TViewMouseInteractionTests.cs create mode 100644 tests/TuiVision.Controls.Tests/TWindowMouseDragTests.cs create mode 100644 tests/TuiVision.Drivers.Tests/ConsoleMouseIngressTests.cs 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..6ef2efb3 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.2.143 + 1.20.2.143 + 1.20.2.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/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..6fa9d55e --- /dev/null +++ b/specs/020-mouse-support-interaction/pr-evidence.md @@ -0,0 +1,258 @@ +# 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-T116 are complete; only authorized GitHub delivery T117-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` | +| Push | Open | Pending branch/commit | +| Pull request | Open | Pending URL | +| Required checks | Open | Pending check summary | +| Review threads | Open | Pending GraphQL count | +| Unavailable reviews | None observed yet | Record provider/quota facts if present | +| 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**: Pending implementation observation. +- **Recurring blocker**: Pending implementation/remote observation. +- **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..cd15d80e --- /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 +- [ ] T117 Commit the complete 020 implementation and capture commit/version in `pr-evidence.md` +- [ ] T118 Recalculate branch commit count, align version/evidence, and create a bounded follow-up commit only if required +- [ ] T119 Push `020-mouse-support-interaction`, record observed branch/commit, align and commit truthful evidence if needed, and push again +- [ ] T120 Create a ready feature PR from `pr-evidence.md`, record its URL, and push the bounded PR-reference evidence update +- [ ] T121 Monitor required CI, Claude/Copilot availability, review comments, and GraphQL threads to convergence; record each state +- [ ] 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); + } +} From 0d99df0b37ef66fe2af2a077cc24a25e6a7183b4 Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 12 Jul 2026 04:13:02 +0200 Subject: [PATCH 3/6] docs: record mouse implementation evidence --- Directory.Build.props | 6 +++--- specs/020-mouse-support-interaction/pr-evidence.md | 4 +++- specs/020-mouse-support-interaction/tasks.md | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6ef2efb3..76ab96f4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,9 +5,9 @@ enable enable true - 1.20.2.143 - 1.20.2.143 - 1.20.2.143 + 1.20.3.143 + 1.20.3.143 + 1.20.3.143 CS1591 diff --git a/specs/020-mouse-support-interaction/pr-evidence.md b/specs/020-mouse-support-interaction/pr-evidence.md index 6fa9d55e..359d7d97 100644 --- a/specs/020-mouse-support-interaction/pr-evidence.md +++ b/specs/020-mouse-support-interaction/pr-evidence.md @@ -227,7 +227,7 @@ test file. No cross-project ownership or hidden aggregate assertion was used. ## Local Completion -- Tasks T001-T116 are complete; only authorized GitHub delivery T117-T126 remains. +- Tasks T001-T118 are complete; only authorized GitHub delivery T119-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, @@ -242,6 +242,8 @@ test file. No cross-project ownership or hidden aggregate assertion was used. | 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 | Open | Pending branch/commit | | Pull request | Open | Pending URL | | Required checks | Open | Pending check summary | diff --git a/specs/020-mouse-support-interaction/tasks.md b/specs/020-mouse-support-interaction/tasks.md index cd15d80e..f563549d 100644 --- a/specs/020-mouse-support-interaction/tasks.md +++ b/specs/020-mouse-support-interaction/tasks.md @@ -153,8 +153,8 @@ are used where ownership could overlap. ## Phase 10: Authorized GitHub Delivery - [X] T116 Align `Directory.Build.props` to `1.20..` without incrementing build, stage intentional files, and record scope -- [ ] T117 Commit the complete 020 implementation and capture commit/version in `pr-evidence.md` -- [ ] T118 Recalculate branch commit count, align version/evidence, and create a bounded follow-up commit only if required +- [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 - [ ] T119 Push `020-mouse-support-interaction`, record observed branch/commit, align and commit truthful evidence if needed, and push again - [ ] T120 Create a ready feature PR from `pr-evidence.md`, record its URL, and push the bounded PR-reference evidence update - [ ] T121 Monitor required CI, Claude/Copilot availability, review comments, and GraphQL threads to convergence; record each state From e98bcfc001535c125171079fba02edbfaa5f732c Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 12 Jul 2026 04:13:25 +0200 Subject: [PATCH 4/6] docs: record mouse branch publication --- Directory.Build.props | 6 +++--- specs/020-mouse-support-interaction/pr-evidence.md | 4 ++-- specs/020-mouse-support-interaction/tasks.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 76ab96f4..60c6938f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,9 +5,9 @@ enable enable true - 1.20.3.143 - 1.20.3.143 - 1.20.3.143 + 1.20.4.143 + 1.20.4.143 + 1.20.4.143 CS1591 diff --git a/specs/020-mouse-support-interaction/pr-evidence.md b/specs/020-mouse-support-interaction/pr-evidence.md index 359d7d97..dbb7b002 100644 --- a/specs/020-mouse-support-interaction/pr-evidence.md +++ b/specs/020-mouse-support-interaction/pr-evidence.md @@ -227,7 +227,7 @@ test file. No cross-project ownership or hidden aggregate assertion was used. ## Local Completion -- Tasks T001-T118 are complete; only authorized GitHub delivery T119-T126 remains. +- Tasks T001-T119 are complete; only authorized GitHub delivery T120-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, @@ -244,7 +244,7 @@ test file. No cross-project ownership or hidden aggregate assertion was used. | 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 | Open | Pending branch/commit | +| 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 | Open | Pending URL | | Required checks | Open | Pending check summary | | Review threads | Open | Pending GraphQL count | diff --git a/specs/020-mouse-support-interaction/tasks.md b/specs/020-mouse-support-interaction/tasks.md index f563549d..d8f59599 100644 --- a/specs/020-mouse-support-interaction/tasks.md +++ b/specs/020-mouse-support-interaction/tasks.md @@ -155,7 +155,7 @@ are used where ownership could overlap. - [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 -- [ ] T119 Push `020-mouse-support-interaction`, record observed branch/commit, align and commit truthful evidence if needed, and push again +- [X] T119 Push `020-mouse-support-interaction`, record observed branch/commit, align and commit truthful evidence if needed, and push again - [ ] T120 Create a ready feature PR from `pr-evidence.md`, record its URL, and push the bounded PR-reference evidence update - [ ] T121 Monitor required CI, Claude/Copilot availability, review comments, and GraphQL threads to convergence; record each state - [ ] T122 Remediate every actionable remote finding with focused validation and record response/thread resolution From c72c5b42b9f9e2de45987775711faaf47a603e4d Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 12 Jul 2026 04:14:00 +0200 Subject: [PATCH 5/6] docs: link mouse support pull request --- Directory.Build.props | 6 +++--- specs/020-mouse-support-interaction/pr-evidence.md | 4 ++-- specs/020-mouse-support-interaction/tasks.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 60c6938f..e4d9b030 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,9 +5,9 @@ enable enable true - 1.20.4.143 - 1.20.4.143 - 1.20.4.143 + 1.20.5.143 + 1.20.5.143 + 1.20.5.143 CS1591 diff --git a/specs/020-mouse-support-interaction/pr-evidence.md b/specs/020-mouse-support-interaction/pr-evidence.md index dbb7b002..620c7d5d 100644 --- a/specs/020-mouse-support-interaction/pr-evidence.md +++ b/specs/020-mouse-support-interaction/pr-evidence.md @@ -227,7 +227,7 @@ test file. No cross-project ownership or hidden aggregate assertion was used. ## Local Completion -- Tasks T001-T119 are complete; only authorized GitHub delivery T120-T126 remains. +- Tasks T001-T120 are complete; only authorized GitHub delivery T121-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, @@ -245,7 +245,7 @@ test file. No cross-project ownership or hidden aggregate assertion was used. | 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 | Open | Pending URL | +| Pull request | Pass | Ready PR [#48](https://github.com/hindermath/TuiVision/pull/48), created from the evidence-derived scope and validation summary | | Required checks | Open | Pending check summary | | Review threads | Open | Pending GraphQL count | | Unavailable reviews | None observed yet | Record provider/quota facts if present | diff --git a/specs/020-mouse-support-interaction/tasks.md b/specs/020-mouse-support-interaction/tasks.md index d8f59599..2f059235 100644 --- a/specs/020-mouse-support-interaction/tasks.md +++ b/specs/020-mouse-support-interaction/tasks.md @@ -156,7 +156,7 @@ are used where ownership could overlap. - [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 -- [ ] T120 Create a ready feature PR from `pr-evidence.md`, record its URL, and push the bounded PR-reference evidence update +- [X] T120 Create a ready feature PR from `pr-evidence.md`, record its URL, and push the bounded PR-reference evidence update - [ ] T121 Monitor required CI, Claude/Copilot availability, review comments, and GraphQL threads to convergence; record each state - [ ] 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 From 6944719709013536480f37fc954690969fb227c9 Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 12 Jul 2026 04:23:36 +0200 Subject: [PATCH 6/6] docs: record mouse review convergence --- Directory.Build.props | 6 +++--- specs/020-mouse-support-interaction/pr-evidence.md | 12 ++++++------ specs/020-mouse-support-interaction/tasks.md | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e4d9b030..72e7f634 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,9 +5,9 @@ enable enable true - 1.20.5.143 - 1.20.5.143 - 1.20.5.143 + 1.20.6.143 + 1.20.6.143 + 1.20.6.143 CS1591 diff --git a/specs/020-mouse-support-interaction/pr-evidence.md b/specs/020-mouse-support-interaction/pr-evidence.md index 620c7d5d..2db3bbaa 100644 --- a/specs/020-mouse-support-interaction/pr-evidence.md +++ b/specs/020-mouse-support-interaction/pr-evidence.md @@ -227,7 +227,7 @@ test file. No cross-project ownership or hidden aggregate assertion was used. ## Local Completion -- Tasks T001-T120 are complete; only authorized GitHub delivery T121-T126 remains. +- 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, @@ -246,15 +246,15 @@ test file. No cross-project ownership or hidden aggregate assertion was used. | 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 | Open | Pending check summary | -| Review threads | Open | Pending GraphQL count | -| Unavailable reviews | None observed yet | Record provider/quota facts if present | +| 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**: Pending implementation observation. -- **Recurring blocker**: Pending implementation/remote observation. +- **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/tasks.md b/specs/020-mouse-support-interaction/tasks.md index 2f059235..5c5907f8 100644 --- a/specs/020-mouse-support-interaction/tasks.md +++ b/specs/020-mouse-support-interaction/tasks.md @@ -157,8 +157,8 @@ are used where ownership could overlap. - [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 -- [ ] T121 Monitor required CI, Claude/Copilot availability, review comments, and GraphQL threads to convergence; record each state -- [ ] T122 Remediate every actionable remote finding with focused validation and record response/thread resolution +- [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