Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,551 changes: 2,551 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-0-core-cpp.md

Large diffs are not rendered by default.

4,352 changes: 4,352 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-1-reactive.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

6,607 changes: 6,607 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-2-ui.md

Large diffs are not rendered by default.

8,858 changes: 8,858 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-3-tui.md

Large diffs are not rendered by default.

7,199 changes: 7,199 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-4-qt-quick.md

Large diffs are not rendered by default.

11,987 changes: 11,987 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-5-forms-engine.md

Large diffs are not rendered by default.

5,690 changes: 5,690 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-6-examples-foundation.md

Large diffs are not rendered by default.

6,909 changes: 6,909 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-7-examples-bank.md

Large diffs are not rendered by default.

9,438 changes: 9,438 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-8-examples-ladder-a.md

Large diffs are not rendered by default.

13,618 changes: 13,618 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-9-examples-ladder-b.md

Large diffs are not rendered by default.

442 changes: 442 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui-interfaces.md

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions docs/superpowers/plans/2026-10-04-declarative-ui-tui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Declarative UI Program — Master Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or
> superpowers:executing-plans to implement the part plans task-by-task. Read this file first, then
> `2026-10-04-declarative-ui-tui-interfaces.md`: together they fix the branch, the commit layout, the conventions
> every part follows, and the names parts share.

**Goal:** Every example application under `examples/` describes its UI once — declarative controllers plus a
toolkit-free view tree — and one binary per app picks a terminal UI or Qt Quick at runtime by dependency injection;
the shipped QML forms renderer is replaced by a C++ forms engine. Delivered on **one branch, as one pull request,
with one commit per semantic unit**.

**Specs** (`docs/superpowers/specs/`): `2026-10-04-core-cpp-mouse-design.md` (spec 0),
`2026-10-04-declarative-ui-tui-design.md` (spec 1), `2026-10-04-forms-engine-design.md` (spec 2),
`2026-10-04-qtquick-frontend-design.md` (spec 3), `2026-10-04-examples-migration-design.md` (spec 4).

## The parts

Execute in this order; each part assumes every earlier part has landed on the branch.

| Part | Plan (`docs/superpowers/plans/2026-10-04-declarative-ui-tui-…`) | Spec | `wip` key(s) | Ends as commit(s) |
|---|---|---|---|---|
| 0 | `0-core-cpp.md` | 0 | (core-cpp repo), `corecpp` | `core: build against core-cpp 0.7` |
| 1 | `1-reactive.md` | 1 §3–4b | `reactive` | `reactive: signal graph, view state and declarative control` |
| 2 | `2-ui.md` | 1 §5–5b | `ui` | `ui: toolkit-agnostic view tree, mount, frontend seam and RecordingBackend` |
| 3 | `3-tui.md` | 1 §2, §6 | `ioloop`, then `tui` | `core: IoLoopDriver::Caller, the I/O loop on the calling thread`, then `tui: morph::tui terminal frontend` |
| 4 | `4-qt-quick.md` | 3 | `qtquick` | `qt_quick: morph::qt_quick frontend` |
| 5 | `5-forms-engine.md` | 2 | `forms` | `forms: the C++ forms engine, rendered through the view tree` |
| 6 | `6-examples-foundation.md` | 4 §2–4, 1 §8 | `examples-common` | `examples/common: Qt-free app environment, transport, poller and test waits; gallery and workout` |
| 7 | `7-examples-bank.md` | 4 §5 | `bank` | `examples/bank: one app, any frontend` |
| 8 | `8-examples-ladder-a.md` | 4 §5 | `pastebin`, `bookmarks`, `polls` | one commit per app |
| 9 | `9-examples-ladder-b.md` | 4 §5 | `kanban`, `ledger`, `lims` | one commit per app |
| 10 | `10-examples-forms-and-retire.md` | 4 §5, §8; 2 §11 | `forms-demo`, `retire` | `examples/forms: …`, then `forms, examples: retire the QML renderer and the Qt client stack` |

## The pull request's history

Branch `feature/declarative-ui`, cut from `master`. When every part has landed, `git log --oneline master..HEAD`
shows exactly, in order:

1. `docs: declarative UI program — design and implementation plans`
2. `core: build against core-cpp 0.7`
3. `reactive: …`
4. `ui: …`
5. `core: IoLoopDriver::Caller …`
6. `tui: …`
7. `qt_quick: …`
8. `forms: …`
9. `examples/common: …`
10. `examples/bank: …`
11. `examples/pastebin: …`
12. `examples/bookmarks: …`
13. `examples/polls: …`
14. `examples/kanban: …`
15. `examples/ledger: …`
16. `examples/lims: …`
17. `examples/forms: …`
18. `forms, examples: retire the QML renderer and the Qt client stack`

Each commit builds, passes its tests and carries its own `docs/spec/` files, map entries, `CHANGELOG.md` lines and
pinned facts. Example-only commits get no CHANGELOG entry (CONTRIBUTING), except where they add a framework seam;
commit 18 gets a "Removed" entry.

## Conventions every part follows

### `wip` commits, one per task

Every task ends with a commit whose subject starts `wip(<key>): `, `<key>` from the table. They exist for review (a
reviewer reads `git show` of one task) and for bisecting inside a part; they never survive the part.

### Squashing a part

The last task of every part — and of each group in parts 3, 8 and 9 — runs:

```bash
key=reactive # the group's key
pre=$(git rev-parse HEAD)
first=$(git log --reverse --format=%H --grep="^wip($key):" master..HEAD | head -1)
test -n "$first" || { echo "no wip($key) commits -- wrong key?"; exit 1; }
git log --format=%s "$first^..HEAD" | grep -v "^wip($key): " && { echo "foreign commit inside the part"; exit 1; }
git reset --soft "$first^"
git commit -F /path/to/message.txt # the message the part plan gives
git diff --exit-code "$pre" HEAD && echo "squash preserved the tree"
git log --oneline master..HEAD
```

`git diff --exit-code` proves the squash changed history and nothing else; the last line must show the expected
list so far.

### The docs commit

Before execution starts, the spec and plan commits already on the branch are squashed into commit 1 with the same
procedure over `master..HEAD` (all of them are `docs:` commits). Plans are not edited during execution: when a plan
turns out wrong, the code and its `docs/spec/` file are made right and the deviation is stated in the body of that
part's squashed commit — what the plan said, what was done instead, and why. AGENTS.md's filing rule applies to a
defect found in `include/morph` or `src` outside a part's scope.

### Before execution starts: file the morph finding — ask the user first

Planning found, by reading only, a defect in `include/morph` outside every part's scope; AGENTS.md files it. Confirm
with the user, then:

```bash
cd /Users/christianparpart/projects/morph
sed -n '/static ::core::async::Task<void> acceptFlow/,/^ }$/p' include/morph/net/socket_server.hpp
git -C ~/projects/core-cpp show v0.5.1:CHANGELOG.md | sed -n '/A POSIX accept that failed/,/accepted again at once/p'
```

and `gh issue create --repo LASTRADA-Software/morph --title "net: SocketServer's accept flow retries a dead listener
forever" --label bug --label "area: net"` with a body that states: **what** — `SocketServer::Core::acceptFlow`
returns only on `NetErrorCode::Cancelled` and retries every other failed `accept` after 50 ms; since core-cpp 0.5.1
(`EINVAL` from accept → `BadHandle`) a listener that is no longer listening reports `BadHandle` on every attempt,
so the flow wakes every 50 ms forever instead of ending; **evidence** — the two outputs above, pasted; **verification
status** — inferred from reading the code, not reproduced; observed in no test or deployment (weak evidence, labelled
as such); **what would close it** — the flow ends on `BadHandle` (and the server reports the listener as dead), with a
test that closes the listening socket underneath it and asserts the flow exits; reopen if a `BadHandle` is found that
a retry can recover from. Record the issue number for the PR body.

### Outward-facing steps are confirmed first

Pushing core-cpp's branch, merging it, tagging `v0.7.0`, filing the core-cpp Shift+Tab issue, pushing this branch
and opening the pull request are each confirmed with the user immediately before they are taken.

### Commit message trailer

Every commit ends with `Signed-off-by: Christian Parpart <christian@parpart.family>`.

## Build configurations used by the plans

| Directory | Configure |
|---|---|
| `build/reactive` | `cmake -S . -B build/reactive -G Ninja -DCMAKE_BUILD_TYPE=Debug -DMORPH_BUILD_EXAMPLES=OFF` |
| `build/tui` | `build/reactive`'s flags plus `-DMORPH_BUILD_TUI=ON` |
| `build/qt` | `build/reactive`'s flags plus `-DMORPH_BUILD_QT=ON -DMORPH_BUILD_QT_QUICK=ON` |
| `build/all` | `-DCMAKE_BUILD_TYPE=Debug -DMORPH_BUILD_TUI=ON -DMORPH_BUILD_QT=ON -DMORPH_BUILD_QT_QUICK=ON -DMORPH_BUILD_LADDER=ON -DMORPH_LADDER_RUNGS=all -DMORPH_BUILD_BANK_EXAMPLE=ON -DMORPH_BUILD_NET=ON` (plus `-DMORPH_BUILD_FORMS_QML=ON` until part 10 removes it) |

Configuring prints `morph: warnings: ... strict=ON`; if it does not, stop and say so (CONTRIBUTING).

## Finishing the branch

After part 10's squash:

- [ ] **Whole-branch gates** on `build/all` and on Linux CI's configurations: build; `ctest`; the sanitizer
presets with `scripts/check_sanitizer_instrumentation.sh`; the Docs build; clang-tidy-diff against
`origin/master...HEAD` with the file count asserted; `scripts/check_install_export.sh`. Then a build with
`MORPH_BUILD_TUI=OFF MORPH_BUILD_QT_QUICK=OFF`, proving both frontends are optional and the app libraries and
their tests still build.
- [ ] **WebAssembly:** the `wasm-ladder.yml` and `wasm-demo.yml` configurations build.
- [ ] **Every commit builds:** `git rebase --exec "cmake --build build/all && ctest --test-dir build/all" master`
(no `-i`). A failure names the commit that does not stand alone; fix it in that commit.
- [ ] **History check:** `git log --oneline master..HEAD` shows exactly the 18 commits above.
- [ ] **Push and open the PR — ask the user first.** Then `git push -u origin feature/declarative-ui` and
`gh pr create`, the body listing the commits, linking the specs, and stating what was verified on which
configurations.
- [ ] **Delete core-cpp's abandoned `feature/reactive-ui` — ask the user first.** Then
`git -C ~/projects/core-cpp branch -D feature/reactive-ui` and
`git -C ~/projects/core-cpp push origin --delete feature/reactive-ui`.
102 changes: 102 additions & 0 deletions docs/superpowers/specs/2026-10-04-core-cpp-mouse-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# core-cpp: standard mouse tracking and pointer capture — design (spec 0)

core-cpp's `core::tui` decodes SGR mouse reports, including motion with a button held, but never
asks a standard terminal to send them: the only mouse mode it enables is Contour's private passive
mode 2029. xterm, kitty, WezTerm, iTerm2 and Windows Terminal therefore report no mouse events at
all. Its `Screen` also hit-tests every mouse event, so a drag that leaves the component it started
on loses its moves and its release. This spec closes both gaps so a `core::tui` application can
implement drag-and-drop, and morph's TUI frontend can let kanban's board move tasks by dragging.

The work lands in the core-cpp repository as release **0.7.0**, which is the version morph pins.

## Contents

[1 Mouse tracking modes](#1-mouse-tracking-modes) · [2 Pointer capture](#2-pointer-capture) ·
[3 Hit testing](#3-hit-testing) · [4 Tests](#4-tests) · [5 Release and pin](#5-release-and-pin) ·
[6 Out of scope](#6-out-of-scope)

## 1. Mouse tracking modes

```cpp
namespace core::tui {
/// How much mouse input the terminal is asked to report.
enum class MouseTracking : std::uint8_t {
Off, ///< No reports (default).
Buttons, ///< Press and release (DEC 1000).
Drag, ///< Press, release and motion while a button is held (DEC 1002).
AnyMotion, ///< Press, release and all motion (DEC 1003).
};
}
```

- **Opt-in, default `Off`.** Mouse reporting changes what a click-drag does in the user's terminal
(it stops selecting text; most terminals keep Shift+drag for selection), so no existing
`core::tui` program changes behaviour. An application asks for a mode.
- **API.** `TerminalInput::setMouseTracking(MouseTracking)` and `Terminal::setMouseTracking(…)`
(forwarding), effective at the next `enableProtocols()` — before `initialize()` for the common
case, and immediately when the protocols are already enabled.
- **Sequences.** For any mode but `Off`, `enableProtocols()` writes the mode's DEC private set
(`CSI ? 1000 h`, `1002 h` or `1003 h`) followed by SGR encoding `CSI ? 1006 h`;
`disableProtocols()` writes the matching resets in reverse order. Contour's passive mode 2029 is
still enabled as today; on Contour it carries the `uiHandled` flag and is harmless beside the
standard modes.
- **The existing hover path.** Today 1003 is enabled when the terminal reports mode 2029 as
changeable, for hover tooltips. That stays, and is expressed as "the requested mode is raised to
`AnyMotion`".
- **Windows.** The console input mode already includes `ENABLE_VIRTUAL_TERMINAL_INPUT`, under
which Windows Terminal delivers VT mouse reports; the same sequences apply.

The parser needs no change: `CSI < b ; x ; y M` with bit 32 already becomes
`MouseEvent{Move, button = b & 3}` (button 3 means "none held", which only `AnyMotion` produces).

## 2. Pointer capture

`Screen::dispatchMouseEvent` gains implicit capture:

- A `Press` delivered to component `C` (after bubbling, the component that returned non-`Ignored`)
makes `C` the **capture target**.
- While a capture target exists, every `Move` and the next `Release` go to it directly — no hit
test, no bubbling — with coordinates relative to `C`'s screen bounds, so they may be negative or
beyond its size.
- The `Release` ends the capture. So does the capture target's destruction or removal from the
tree (the component's teardown clears it) and `Screen::releasePointer()`.
- Scroll events are never captured.

Without capture, a drag's motion lands on whatever is under the pointer, so a component cannot
follow its own drag; with it, the component that started the drag sees the whole gesture.

## 3. Hit testing

`Component* Screen::componentAt(int row, int col) const` becomes public: viewport-relative,
0-based, returning the top-most visible component at that cell — overlays first (last shown wins),
then the main tree in reverse z-order, deepest descendant first — or null. A drag source asks it
which component lies under the pointer when the button is released, to find the drop target.

## 4. Tests

In core-cpp's Catch2 suites:

- **Parser:** `CSI <32;10;5M` → `Move`, button 0, x 10, y 5; `CSI <35;…M` → `Move`, button 3.
- **Protocols:** for each `MouseTracking` value, the bytes `enableProtocols()` and
`disableProtocols()` write (through a mock output), including `Off` writing no 1000/1002/1003.
- **Capture:** press on component A, move outside A, release elsewhere — A receives all three with
A-relative coordinates, B under the pointer receives nothing; destroying A mid-drag clears the
capture and the next move hit-tests normally; scroll is not captured.
- **`componentAt`:** overlay above tree, z-order, nested child, empty cell → null.

## 5. Release and pin

- core-cpp: CHANGELOG entry, version 0.7.0, tag `v0.7.0`. Pushing the branch, merging and tagging
in core-cpp are outward-facing steps, each confirmed with the user before it is taken.
- morph: `MORPH_CORE_CPP_VERSION 0.7`, `GIT_TAG v0.7.0` in the root `CMakeLists.txt`; the config
template follows (`find_dependency(core-cpp 0.7 CONFIG)`). The plan reviews core-cpp's 0.6.0 and
0.7.0 changelogs for anything morph's existing code depends on and fixes it in the same commit.
- An issue is filed against core-cpp for decoding legacy Shift+Tab (`CSI Z`), which no release
decodes; until then Shift+Tab reaches an application only on terminals speaking the Kitty
keyboard protocol. Filing it is confirmed with the user first.

## 6. Out of scope

- Drag-and-drop semantics (payloads, drop targets, drag visuals) — those are morph's TUI frontend
(spec 1 §6) built on capture and `componentAt`.
- Mouse cursor shape changes, pixel-precise (SGR-Pixels, 1016) reporting, multi-button chords.
Loading
Loading