Skip to content

Entry form: audit block, every field selectable, sideways scrolling - #11

Merged
oetiker merged 9 commits into
mainfrom
feat/entry-meta-block
Jul 28, 2026
Merged

Entry form: audit block, every field selectable, sideways scrolling#11
oetiker merged 9 commits into
mainfrom
feat/entry-meta-block

Conversation

@oetiker

@oetiker oetiker commented Jul 28, 2026

Copy link
Copy Markdown
Member

Started as "show the entry's timestamps"; the review of that surfaced a deeper
problem — values you cannot reach are values you cannot read — which the rest of
the branch fixes.

Audit block

Below the form, separated by a blank row, the entry's server-maintained
createTimestamp, creatorsName, modifyTimestamp and modifiersName,
labelled created / created by / modified / modified by:

                  created  2026-07-28 11:34:02
               created by  cn=admin,dc=example,dc=org
                 modified  2026-07-28 12:01:09
              modified by  cn=admin,dc=example,dc=org

Timestamps render in local time (the offset is captured once at the top of
main, because time refuses to read it in a multi-threaded process). Entry
reads request these operational attributes explicitly — * is user attributes
only, which is why they were absent. The save path already re-reads the entry, so
modified refreshes itself.

The trap this had to avoid: EditForm::to_edit_entry feeds validate(), so
had these attributes leaked in, every save would have failed with "attribute
not allowed by objectClass" — and the orphan sweep in sync_schema_fields would
have emitted a Delete against a server-maintained attribute on any objectClass
change. Both are covered by tests. Classification is by attribute name
(form_model::is_meta_attr), not a per-field flag: these are operational, so
they can never be in an objectClass's MUST∪MAY, and one predicate cannot desync
the way a flag threaded through 53 struct literals could.

Every field is selectable

Read-only cells were disabled, which focus skips entirely — so a value wider
than its cell could never be scrolled to its end, selected, or copied. They are
now read_only (tvision-rs 0.15.0, developed for this and upstreamed in
oetiker/tvision-rs#22): reachable, scrollable, copyable, refusing only edits.

An edit attempt raises READ_ONLY_REJECTED; the pane answers with a dialog that
names the attribute and the reason — server-maintained, schema
NO-USER-MODIFICATION, read-only session, orphaned, binary, boolean, or declared
read-only in config. PageUp/PageDown still page the viewport, so long forms
stay quick to cross.

Three existing tests encoded "read-only means unreachable". They were rewritten
to the guarantee that survived rather than deleted: the head/tail tests now
assert those rows can be reached and revealed (and that a passive re-anchor
does not yank the reveal back), and the clamp test keeps its real subject — no
wrap at either end.

Sideways scrolling

The list and launch blocks painted with put_str, which cuts silently at the
right edge, so a long member DN was unreadable even with focus. Both now carry a
horizontal offset with / marking hidden text: / and Home/End
move a read-only block, while the inline editor follows its caret.

member*  - cn=user01,ou=users,dc=example,d►     fresh
member* ◄=user01,ou=users,dc=example,dc=org     after End

Text cells use the new ValuePosition::Start, so a DN shows its head rather than
…,dc=example,dc=org.

Also

  • A truncated dn title now ends in , so a cut DN is distinguishable from a
    short one. (It already cut at the end — only the marker was missing; the
    that looked like front-truncation was Create mode's RDN placeholder.)
  • Meta labels are the bare hint, because modifiersName (modified by) would be
    the longest label in the form and the label column is sized to the longest
    label of all rows — it squeezed every value cell on a narrow pane.

Verification

make check green (fmt, clippy -D warnings, all tests); mdBook builds. Driven
live against the podman demo server throughout: the block renders with real
values, local time matches the server's Z value, an edit saves with only the
edited attribute in the LDIF and the block refreshes, read-only fields scroll and
refuse with the dialog, and member lists scroll sideways.

Design docs: docs/superpowers/specs/2026-07-28-entry-meta-block-design.md and
…-every-field-selectable-design.md.

🤖 Generated with Claude Code

oetiker and others added 9 commits July 28, 2026 11:40
Read-only createTimestamp/creatorsName/modifyTimestamp/modifiersName rows after
the form, separated by a blank line, with timestamps in local time; plus a
trailing "…" when the dn title does not fit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Panel 3 now shows the entry's server-maintained createTimestamp, creatorsName,
modifyTimestamp and modifiersName below the form, separated by a blank row and
read-only. Entry reads request those operational attributes explicitly (`*` is
user attributes only). Timestamps render in local time, captured once at the
top of main() because `time` refuses the local offset in a multi-threaded
process.

The rows are classified by attribute name (`form_model::is_meta_attr`) rather
than a per-field flag: they are operational, so they can never be in an
objectClass's MUST∪MAY and a field carrying such a label is always a meta row.
Every site that treats a field as "an attribute we own" now skips them — most
importantly `to_edit_entry`, which feeds `validate()` (leaking them there would
fail every save), and the orphan sweep in `sync_schema_fields`, where an
orphaned field emits a Delete.

Also: a DN too long for the pane title is now cut with a trailing `…`, so a
truncated DN is distinguishable from a short one. It already cut at the end —
only the marker was missing.

Verified live against the podman demo server: block renders, local time matches
the server's Z value, an edit saves (LDIF carries only the edited attribute) and
the block refreshes with the new modifyTimestamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rom the start

Two follow-ups from live review at 90 columns:

* The meta labels are now the bare hint (`modified by`, not
  `modifiersName (modified by)`). They were the longest labels in the form, and
  the label column is sized to the longest label of ALL rows — so they squeezed
  every value cell on a narrow pane.

* `InputLine::set_value` select-alls, which parks the view at the END of the
  text: any value wider than its cell showed its tail behind a `◄` marker. For a
  DN or a path the front is what identifies it, so every text cell is homed as it
  is filled. This runs on render ticks only, never mid-typing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read-only fields become navigable, horizontally scrollable and copyable via a
new read_only mode in tvision-rs InputLine (focusable, rejects mutations,
broadcasts the rejection so the pane can pop a dialog). Part 3 extends the same
fix to edaptor's list/launch blocks, where long member DNs are cut today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y, refuse

Part 2 of the read-only-navigation work. Read-only cells are built
`read_only` (the new tvision-rs InputLine mode) instead of `disabled`, and
`focusable_value_ids` returns every row. A cell focus skips cannot be scrolled,
which is why a value wider than its cell could never be read to its end.

The pane answers InputLine::READ_ONLY_REJECTED — the widget names the cell, the
pane knows the reason — with a dialog that says which attribute refused and why:
server-maintained (the audit block), schema NO-USER-MODIFICATION, read-only
session, orphaned by an objectClass change, binary, boolean, or declared
read-only in the config.

Three existing tests encoded "read-only means unreachable" and were rewritten to
the surviving guarantee rather than deleted: the head/tail scroll tests now
assert that the leading/trailing rows can be reached and revealed (and that a
passive re-anchor does not yank the reveal back), and the clamp test keeps its
real subject — no wrap at either end — with all rows as stops.

Cargo.toml points at ../rstv until the read-only mode is released upstream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Part 3 of the read-only-navigation work, and the half that fixes long member
DNs: the list and launch blocks painted their lines with put_str, which cuts
silently at the right edge, so a value wider than the cell was unreadable even
once the block had focus.

Both now carry a horizontal offset drawn with put_str_part, with ◄/► marking the
hidden text. The read-only launch block moves on ←/→ (it has no caret to follow,
and the keys fall back through to the pane once there is nothing left to reveal,
so they still leave the field). The inline editor instead follows its caret, the
same contract InputLine has — with the "- " bullet kept as left context, so Home
lands the line fully home rather than parking the bullet off screen.

`place_cursor_home` resets both to the left edge, so focus never lands on a
block mid-scroll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… block

The form's manual `home()` after every `set_value` was a workaround for a
missing property upstream. With `ValuePosition::Start` set on each cell, the
widget lands the value at its head by itself — a DN, a path or a login name is
identified by its front, not its tail.

Read-only blocks also take Home/End now, jumping straight to either edge instead
of stepping there with ←/→. A key that would not move the scroll still falls
through to the pane, so Home/End keep their pane-level meaning on a block that
already sits at that edge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The changelog entry still described the audit block as taking no Tab stop, which
the every-field-selectable change reversed two commits later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The read-only mode and ValuePosition are released upstream (oetiker/tvision-rs
#22, v0.15.0), so the temporary `path = "../rstv"` dependency goes back to a
version requirement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oetiker
oetiker merged commit 3cd16cb into main Jul 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant