Skip to content

Commit c1e6269

Browse files
authored
Merge pull request #6 from oposs/integrate/2026-07
Batch: paste/password fixes, MaskedInput adoption (tvision 0.14), form scroll, container classification
2 parents bdf2f82 + c998366 commit c1e6269

19 files changed

Lines changed: 1590 additions & 381 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ docs/book/
1111
.claude
1212
*~
1313
examples/ds-carbo-feh.toml
14+
15+
# SDD / superpowers scratch artifacts
16+
.superpowers/

CHANGES.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,50 @@ All notable changes to eDAPtor are documented here. The format follows
1010

1111
### Changed
1212

13+
- The password editor's New/Confirm fields now use tvision-rs's native masked
14+
field instead of eDAPtor's own hand-rolled masking. Behaviour for the operator
15+
is the same or better: typing and pasting a password are robust (the fixes
16+
below are now guaranteed by the framework, not a local mirror), and each field
17+
gains a **reveal eye** in its last column — hold Space or press-and-hold the
18+
eye with the mouse to peek at the password while editing.
19+
- **The DIT browsing model now classifies containers by `objectClass`, not by
20+
whether they happen to have children.** Previously a container was defined as
21+
"an entry with ≥1 child", so an *empty* `organizationalUnit` vanished from the
22+
**DIT** tree (it showed up misfiled in the **Entries** pane instead), and a
23+
sub-OU that itself had children never appeared under its parent in the
24+
**Entries** pane (the one-level find skipped it too). eDAPtor now recognizes
25+
`organizationalUnit`, `organization`, `dcObject`, `domain` and `container`
26+
(case-insensitive) as containers regardless of child count, in addition to
27+
"has children". The **DIT** tree lists every container, including empty ones;
28+
the **Entries** pane lists all direct children — sub-containers alongside leaf
29+
entries — with sub-containers marked with a `` prefix so they read distinct
30+
from plain entries. Selecting a sub-container row opens its own entry in the
31+
form for editing, exactly like the existing `‹self›` row.
32+
1333
### Fixed
1434

35+
- Pasting into a multi-value field (e.g. `givenName`) now works. The inline
36+
multi-value editor is eDAPtor's own view rather than a plain input line, and it
37+
was silently dropping the terminal's bracketed-paste event; it now inserts the
38+
pasted text at the cursor, with each pasted line starting a new value.
39+
- **Security:** pasting into a password field no longer shows the pasted text in
40+
the clear. The masked field forwarded the terminal's bracketed paste straight
41+
to its inner input, which rendered the cleartext (and, worse, staged nothing
42+
because the masked mirror was never updated). Paste is now masked exactly like
43+
typed input — bullets on screen, the real value mirrored for staging — and
44+
control characters (a trailing newline, tabs) are stripped.
45+
- The Set-password dialog now **verifies that the two fields match** before
46+
accepting OK. Previously OK always closed the dialog, silently staging nothing
47+
when New and Confirm differed (or were empty); it now refuses to close and
48+
shows "The two passwords do not match." / "Enter a password." until they agree.
49+
- Fixed a crash (`removal index … should be < len …`) when editing a password
50+
field whose masked mirror had desynced (triggered by the paste bug above); the
51+
backspace path is now bounds-safe as a belt-and-braces guard.
52+
- Read-only multi-value fields edited via the modal picker (`objectClass`,
53+
membership, choice, password, …) now **scroll line-by-line** when their value
54+
block is taller than the form's viewport, instead of jumping straight to the
55+
next field and leaving the block's lower lines unreachable below the frame.
56+
1557
## 1.3.0 - 2026-07-23
1658

1759
### New

CLAUDE.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
A schema-driven TUI LDAP editor (Rust + tvision-rs). Read this before touching the repo.
44

5-
## Start here: read the handover
5+
## Start here: read the controller handoff
66

7-
`docs/HANDOVER.md` carries the **current concern** into each session — what is
8-
in-flight, what to do next, and the load-bearing facts for it. Read it right after
9-
pulling. Right now it covers the `feat/shuttle-widget` buttons + resizable-dialog
10-
work (Tasks 1–4 committed; Task 4 review + manual resize check + Task 5 docs
11-
remain), driven with the `superpowers:subagent-driven-development` skill against the
12-
plan in `docs/superpowers/plans/2026-07-03-shuttle-buttons-resize.md` and the SDD
13-
ledger `.superpowers/sdd/progress.md`.
7+
`docs/controller-handoff.md` is the starter pack carried into each session — the
8+
mission, where things stand, what to do next, and the judgment that git can't
9+
reconstruct. Read it first, then `git log <handoff-commit>..HEAD` (the commit is
10+
named in its header) for everything that changed since. Write a fresh one via the
11+
`controller-handoff` skill before rolling over. (This supersedes the old
12+
`docs/HANDOVER.md` convention.)
1413

1514
## At the start of every session: pull first
1615

@@ -54,6 +53,16 @@ When in doubt about where something goes: details → mdBook, orientation → RE
5453
- `examples/config.toml` — the annotated reference config that
5554
`docs/src/configuration/full-example.md` embeds. Keep the two consistent.
5655

56+
## Upstream framework: tvision-rs
57+
58+
edaptor is built on **tvision-rs** and regularly upstreams reusable widgets to it
59+
(the Shuttle view, the ListViewer find, InputLine bracketed paste). The working
60+
checkout of that crate lives at **`../rstv`** (sibling of this repo,
61+
`/home/oetiker/checkouts/rstv`). When a fix or feature belongs in the framework
62+
rather than in edaptor, develop it there — iterate from edaptor via a `path`
63+
dependency on `../rstv`, then release a tagged version and bump the `tvision-rs`
64+
requirement in `Cargo.toml` back to the published crate.
65+
5766
## Build, test, lint
5867

5968
`cargo`/`make` from the repo root. **Cap parallelism at 4 cores** (shared machine):

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ chumsky = "0.12"
2222
oid = "0.3"
2323
unicode-width = "0.2"
2424
# tvision-rs is the TUI framework driving the edaptor browser (tree / leaf list / entry form).
25-
# 0.13.1: InputLine handles Event::Paste, so terminal pastes reach form fields.
26-
tvision-rs = "0.13.1"
25+
# 0.14.0: native InputLine masking + RevealEye/MaskedInput (replaces the local password mirror).
26+
tvision-rs = "0.14.0"
2727

2828
[dev-dependencies]
2929
tempfile = "3"

docs/controller-handoff.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Controller Handoff — edaptor batch: MaskedInput adoption + form/browser fixes
2+
3+
> Starter pack for the next controller session. This handoff lives in ONE
4+
> worktree — run `git worktree list` first and confirm this is the workstream
5+
> you're resuming. Read this first, then `git log <handoff-commit>..HEAD` for
6+
> everything that changed since. Detail is NOT here — it's in git + the reports
7+
> (§6). Before you rewrite this file at your own handoff: read the previous
8+
> version (`git show HEAD:docs/controller-handoff.md`) and carry forward any
9+
> lesson in §4/§5 that is still true. Fresh synthesis, not blank page. On merge
10+
> into another branch, rewrite that branch's handoff to the merged reality.
11+
12+
Handoff commit: 9589a21 (branch `integrate/2026-07`) Date: 2026-07-24 Reason: milestone before rollover
13+
Worktree / branch: main checkout `/home/oetiker/checkouts/edaptor` @ integrate/2026-07
14+
Sibling worktrees: none. rstv (tvision-rs source) sits at `/home/oetiker/checkouts/rstv` (sibling repo, not a worktree).
15+
16+
## 1. Mission
17+
edaptor is a schema-driven TUI LDAP editor on **tvision-rs**. This session did two
18+
things: (A) shipped **tvision-rs 0.14.0** — native `InputLine` password masking +
19+
a `RevealEye` toggle + a `MaskedInput` composite (the structural cure for a class
20+
of password bugs), now **released to crates.io**; and (B) a batch of edaptor fixes
21+
on top of Track A (the carried-over paste/password bugfixes), all combined into
22+
**`integrate/2026-07`** and opened as **edaptor PR #6**.
23+
24+
## 2. Where we are now
25+
- **tvision-rs 0.14.0 — DONE & RELEASED.** PR #21 merged, tag v0.14.0 published.
26+
edaptor pins `tvision-rs = "0.14.0"`.
27+
- **edaptor `integrate/2026-07` — PR #6 open, green, NOT merged.** It merges Track A
28+
+ three reviewed fixes (all green together: 890 lib tests, clippy `-D warnings`,
29+
fmt clean):
30+
1. Track A (`fix/multivalue-paste`): multi-value paste + password paste/confirm/
31+
backspace fixes.
32+
2. `feat/adopt-masked-input`: delete the hand-rolled password mirror, adopt
33+
native `MaskedInput` (−377 lines). Supersedes Track A's password-mirror fixes.
34+
3. `fix/launch-value-scroll`: read-only multi-value blocks now scroll line-by-line.
35+
4. `fix/container-classification`: containers classified by objectClass, empty OUs
36+
in the tree, sub-containers in the entry pane.
37+
- `main` was clean/synced with origin at handoff. The four sibling branches are all
38+
merged into `integrate/2026-07`.
39+
- **carbo still runs plain v1.3.0 — the live panic + silent-password bugs are STILL
40+
in production.** Redeploy is the last pending action (needs explicit go-ahead).
41+
42+
## 3. Do this next
43+
1. **Get PR #6 merged** (oposs/edaptor#6) — user reviews/merges to `main`.
44+
2. **Redeploy carbo** from the merged binary (recipe in §4) — it has live bugs.
45+
Production + ssh: confirm before running.
46+
3. Optional follow-ups (not blocking): the two tangential shuttle bugs (§7), and
47+
sub-container navigate-on-Enter (§7).
48+
49+
## 4. Lessons & traps ← the irreplaceable part
50+
- **The whole password-bug cluster was ONE root cause: an unhandled `Event::Paste`
51+
in a custom view.** tvision delivers terminal bracketed paste as `Event::Paste`
52+
(routed like a KeyDown), distinct from `Event::Command(Command::PASTE)` (Ctrl+V).
53+
A `match` on only `KeyDown` silently drops it. Now moot in the password field
54+
(native `MaskedInput`), but the pattern recurs — when "paste doesn't work", look
55+
for a view that doesn't handle `Event::Paste`.
56+
- **Rust method-resolution trap in `MaskedInput` (carry forward for ANY tvision
57+
composite):** an inherent `&mut self value()` is silently shadowed by the
58+
`View::value(&self)` default (which returns `None`) at the dot-call site → your
59+
getter returns `None` and no error. Fix: make the inherent getter `&self`, and
60+
**explicitly override `value`/`set_value`/`set_value_ctx` in `impl View`** so the
61+
`#[delegate]` macro doesn't forward to `Group`'s no-op defaults. A dedicated
62+
round-trip-through-`Box<dyn View>` test guards it.
63+
- **tvision facts (0.14):** timers are **synchronous** via `Context::set_timer`/
64+
`kill_timer` over an injected `TimerQueue` — there is **no** `Deferred::SetTimer`.
65+
`InputLine::SURFACE_ROLES` is **not pub**. **Theme glyphs live in `Glyphs`**
66+
(`reveal_eye_hidden`/`reveal_eye_revealed`); widgets read `ctx.glyphs()` — do NOT
67+
bake glyphs into a widget config (that was a review finding). Masked clipboard
68+
guard keys on `mask.is_some()` (blocks copy even when *revealed* — a deliberate
69+
security choice), not on `masking()`.
70+
- **rstv release hygiene:** `.github/workflows/release.yml` OWNS version bumps +
71+
CHANGELOG rollover. A feature branch must ONLY add notes under `## Unreleased`
72+
and must NOT bump `Cargo.toml`/tag. (We tripped this and had to revert.)
73+
- **The "form scroll" bug was NOT the shuttle** (the shuttle works). It was the
74+
form's read-only **`LaunchValueView`** launch block: single focus stop, passes
75+
Up/Down straight through, no internal scroll → tall blocks unreachable. Fix is
76+
**geometry-driven in the form** (`ScrollGroup::scroll_block_edge`) — the block
77+
exposes **no cursor** (a caret would wrongly imply editability). Don't add a caret
78+
to a read-only block.
79+
- **Container classification:** a node is a container if its objectClass ∈
80+
{organizationalUnit, organization, dcObject, domain, container} (case-insensitive)
81+
OR it has children. **`is_branch` (pure has-children) is STILL load-bearing for the
82+
promote/demote child-count reflow — do NOT replace it.** Only tree membership
83+
(`branch_dns`), panel-2 exclusion, AND the `upsert` **tree-dirty signal** moved to
84+
container terms. The upsert-signal gap (still keyed on `is_branch` after the rest
85+
moved) was a real review catch: a live-created/relabelled empty OU didn't refresh
86+
panel 1 until a rescan. Snapshot `was_container` before mutating, like `was_branch`.
87+
- **`.superpowers/` is now gitignored in edaptor** (it was NOT — a subagent report
88+
file leaked into a commit; cleaned in the integration merge). Reports are scratch.
89+
- **carbo deploy recipe:** cargo target dir is redirected to
90+
`/home/oetiker/scratch/cargo-target` (NOT `./target`) — use `cargo metadata` to
91+
find the binary. edaptor has **no `--version`**. Build release locally (this box
92+
glibc 2.39 → carbo Ubuntu 26.04 glibc 2.43; older→newer is safe), `scp` to
93+
`ds-carbo-feh-adm:/tmp`, `ssh -t ds-carbo-feh-adm` (passwordless sudo) → back up
94+
`/usr/local/bin/edaptor` to a dated `.bak`, then `install -m0755`. Config:
95+
`/etc/edaptor/ds-carbo-feh.toml`.
96+
97+
## 5. Don'ts & constraints
98+
- **≤ 4 cores** for all compiles/tests (shared 128-core box). Never two building
99+
subagents at once. `podman` not docker; `pnpm` not npm.
100+
- **Confirm before ssh / carbo redeploy** — production, outward-facing; back up the
101+
old binary first.
102+
- **Don't push/merge/redeploy without the user's explicit go-ahead.**
103+
- **Settled — do not relitigate:** container rule = objectClass set OR has-children;
104+
panel 2 shows sub-containers (▸) + leaves; a sub-container row **opens its entry**
105+
(navigate-on-Enter is a *future option*, not a bug); `MaskedInput` reveal is
106+
**non-sticky** (Space = 1 s peek); eye glyphs are **theme-controlled**; the masked
107+
clipboard block covers the revealed state too.
108+
- Don't reintroduce the removed `[profile.picker.<attr>]` / `[profile.password]`
109+
config layers.
110+
111+
## 6. Where the detail lives
112+
- Change history: `git log 70eb910..HEAD` on `integrate/2026-07` (the whole batch).
113+
- **edaptor PR #6** (oposs/edaptor#6); **tvision PR #21** (oposs... `oetiker/tvision-rs#21`, MERGED).
114+
- Subagent reports (gitignored scratch): `.superpowers/adopt-masked-input-report.md`,
115+
`.superpowers/launch-scroll-report.md`, `.superpowers/container-classification-report.md`.
116+
- Key files: `src/ui/pw_editor.rs` (MaskedInput adoption), `src/ui/panes/form.rs` +
117+
`src/ui/scroll_group.rs` (`scroll_block_edge` launch-block scroll),
118+
`src/workflows/structure.rs` (`is_container`, `upsert` tree-dirty signal),
119+
`src/ui/panes/leaf.rs` + `src/workflows/labels.rs` + `leaf_search.rs` (panel-2
120+
children incl. sub-containers).
121+
122+
## 7. Open questions / pending decisions
123+
- Merge PR #6 + redeploy carbo — pending user.
124+
- **Sub-container navigate-on-Enter**: selecting a sub-container row currently opens
125+
its entry (like `‹self›`). Navigate-the-tree-into-it was considered and left out
126+
(ambiguous UX, not requested). Revisit if the user wants folder-style nav.
127+
- **Two tangential shuttle bugs** found during investigation, deliberately NOT fixed:
128+
(1) list scrollbar step goes stale after a dialog resize (framework: `Group`/
129+
`Window::on_bounds_changed` doesn't cascade to children); (2) the Available list
130+
resets to the top on every refresh/move (`new_list` always `reset_focus=true`).
131+
132+
## 8. Staleness watch
133+
- PR #6 may already be merged — check `gh pr view 6`; if merged, `integrate/2026-07`
134+
and the four sibling branches may be deleted.
135+
- carbo may already be redeployed — verify (no `--version`; check the `.bak` date
136+
on `ds-carbo-feh-adm`).
137+
- `main` was clean at handoff; re-check `git log main` and `git status`.
138+
- This file was written on the integration branch; after PR #6 merges, `main`'s
139+
handoff IS this file — keep it honest or rewrite to the merged reality.

docs/src/usage/crud.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ currently selected branch.
8686
you type a value into a field, the default never overwrites it.
8787
3. Fill in the remaining fields (the form title reads `New entry`).
8888
4. Press **`Alt+S`**. The change is an LDIF *add*; review the preview and confirm
89-
to create the entry. The new entry appears in the **Entries** pane, and if it
90-
gives its parent its first child, the parent is promoted to a branch in the
89+
to create the entry. The new entry appears in the **Entries** pane, and if its
90+
parent was not already recognized as a container (i.e. not one of
91+
`organizationalUnit`/`organization`/`dcObject`/`domain`/`container`, and had no
92+
other children), gaining its first child now promotes the parent into the
9193
**DIT** tree.
9294

9395
### Where a new entry is created
@@ -142,9 +144,13 @@ Press **`Alt+D`** in the **Entries** pane to delete the highlighted entry.
142144

143145
1. A confirmation overlay appears (`[Y]es` / `[N]o`).
144146
2. Confirm with **`Y`** to apply the delete; **`N`** (or `Esc`) aborts.
145-
3. After a successful delete the parent's entry list is recomputed locally; if
146-
you removed the parent's last child, the parent is demoted from a branch back
147-
to a leaf in the **DIT** tree.
147+
3. After a successful delete the parent's entry list is recomputed locally. If
148+
the parent is a recognized container class (an OU, for example), it stays in
149+
the **DIT** tree even after losing its last child — now shown as an empty
150+
container, not demoted. Only a parent that was in the tree *solely* because it
151+
had children (not a recognized container class) disappears from the **DIT**
152+
tree and reverts to being listed as a plain entry in its own parent's
153+
**Entries** pane.
148154

149155
All of create, edit, rename, and delete funnel through the same
150156
diff → ChangeSet → LDIF-preview → apply pipeline; see

docs/src/usage/passwords.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ by default). The schema-generated raw field for that attribute is suppressed, so
1414
there is one clear place to set the password.
1515

1616
- Type the password once, then again to confirm — they must match.
17+
- Each field shows only bullets while you type; a small **reveal eye** sits in
18+
its last column — hold <kbd>Space</kbd>, or press and hold the eye with the
19+
mouse, to peek at what you've typed.
1720
- The cleartext is sent to the directory; the **LDIF preview masks it as
1821
`********`**, so the actual password never appears on screen.
1922
- Like any other field, the password is applied as part of the form's normal

0 commit comments

Comments
 (0)