feat(files): a real Files backend and a rebuilt interface #10

Open
nonos-sync wants to merge 32 commits from nonos-sync/gh-478 into main AGit
Member

The Files capsule gets a real backend and a rebuilt interface. It previously
listed names and little else: no search, no tags, no history, no counts, and a
UI drawn on the retired 1-bit glyph grid.

Backend, in the filesystem server

New verbs, each carrying its own opcode and no new capability:

  • Dirstat aggregates a subtree recursively and matches on a path boundary,
    so /doc never swallows /documents.
  • Search walks the store by name and by content, returning the first
    matching line per file with its line number. It matches directories too.
  • Journal records file access in a bounded ring and serves it back, which
    is what Recents and Continue Working read.
  • Usage reports the file count, the real summed byte total, and the store's
    file ceiling.

Metadata rides a reserved sidecar prefix through the existing read and write
paths, so tags, favorites, view preferences and the open-with table persist
with no new opcode and no new capability. Blobs are validated on load rather
than trusted.

Interface

Rebuilt across the whole capsule: a stroked line-art icon set, a shared chrome
vocabulary of plates, pills and glows, a sidebar rail with a live drive card, an
icon toolbar with a breadcrumb capsule and a responsive search control, a Home
screen with real category counts, a browse list with sortable column headers,
checkboxes and tag chips, a selection action band, a working info panel, date
grouping in Recents, and highlighted matches in Search.

Two rules held throughout. Every control shares one geometry pass between the
painter and the hit-test, so a click always lands where the pixel was drawn.
And every figure on screen is measured from real data. Where a feature has no
backend, meaning sharing, archives and thumbnails, the control draws dimmed and
says so on click rather than silently doing nothing.

Verification

  • cargo check for the user target is clean at every commit.
  • The host proof harness includes the real capsule source and exercises the
    sidecar codec, the tag reconciliation, and the dirstat, journal and search
    verbs.
  • Booted under QEMU. The staged image was confirmed to carry strings unique to
    this work before the result was believed, with zero attestation failures,
    zero faults and no panic.

Known gaps, stated rather than hidden

  • Forward has no visit history behind it and draws dimmed.
  • A deep breadcrumb elides from the wrong end, hiding the current directory.
  • Tag from the selection band acts on the cursor entry, not the whole selection.
  • Permissions knows one bit, a readonly toggle, and shows only that.
  • A search query spanning the filename and its parent path draws no highlight.
  • The sidebar drive card is withheld rather than overlapped when the rail is
    crowded at small window heights.

Opened on GitHub by senseix21 as pull request 478. Review and merge happen there while this repository is kept in step from GitHub; this copy follows it, and is marked merged or closed when it is.

The Files capsule gets a real backend and a rebuilt interface. It previously listed names and little else: no search, no tags, no history, no counts, and a UI drawn on the retired 1-bit glyph grid. ## Backend, in the filesystem server New verbs, each carrying its own opcode and no new capability: - **Dirstat** aggregates a subtree recursively and matches on a path boundary, so `/doc` never swallows `/documents`. - **Search** walks the store by name and by content, returning the first matching line per file with its line number. It matches directories too. - **Journal** records file access in a bounded ring and serves it back, which is what Recents and Continue Working read. - **Usage** reports the file count, the real summed byte total, and the store's file ceiling. Metadata rides a reserved sidecar prefix through the existing read and write paths, so tags, favorites, view preferences and the open-with table persist with no new opcode and no new capability. Blobs are validated on load rather than trusted. ## Interface Rebuilt across the whole capsule: a stroked line-art icon set, a shared chrome vocabulary of plates, pills and glows, a sidebar rail with a live drive card, an icon toolbar with a breadcrumb capsule and a responsive search control, a Home screen with real category counts, a browse list with sortable column headers, checkboxes and tag chips, a selection action band, a working info panel, date grouping in Recents, and highlighted matches in Search. Two rules held throughout. Every control shares one geometry pass between the painter and the hit-test, so a click always lands where the pixel was drawn. And every figure on screen is measured from real data. Where a feature has no backend, meaning sharing, archives and thumbnails, the control draws dimmed and says so on click rather than silently doing nothing. ## Verification - `cargo check` for the user target is clean at every commit. - The host proof harness includes the real capsule source and exercises the sidecar codec, the tag reconciliation, and the dirstat, journal and search verbs. - Booted under QEMU. The staged image was confirmed to carry strings unique to this work before the result was believed, with zero attestation failures, zero faults and no panic. ## Known gaps, stated rather than hidden - Forward has no visit history behind it and draws dimmed. - A deep breadcrumb elides from the wrong end, hiding the current directory. - Tag from the selection band acts on the cursor entry, not the whole selection. - Permissions knows one bit, a readonly toggle, and shows only that. - A search query spanning the filename and its parent path draws no highlight. - The sidebar drive card is withheld rather than overlapped when the rail is crowded at small window heights. --- Opened on GitHub by senseix21 as [pull request 478](https://github.com/NON-OS/nonos-micro-kernel/pull/478). Review and merge happen there while this repository is kept in step from GitHub; this copy follows it, and is marked merged or closed when it is.
The Files redesign shows per-folder item counts and sizes, which the
existing usage() cannot answer because it is store-wide and immediate
children are all build_entries can see. Reports truncation so a capped
walk renders a lower bound instead of a wrong total.
Recents orders by last-opened, an axis stat_full cannot report because
mtime only moves on write. Dedupe-on-touch keeps a repeatedly opened
file from filling the list.
The Search screen queries the whole store, which state.filter cannot do
because it only matches the directory already listed. Binary and
oversized files are name-matched only so one blob cannot stall the
synchronous reply.
Opcode 22 is the next free value; 7 is OP_HEALTHCHECK despite being
absent from the client table. Folder cards need a recursive aggregate
that no existing op can answer.
Touching inside the open handler is what makes Recents system-wide: a
file opened by the terminal or the audio player appears there too. Both
sides clamp to 200 entries because the full ring can overrun
MAX_PAYLOAD_BYTES at a 255-byte path.
Both sides clamp to 200 hits so a wide query cannot build a reply past
MAX_PAYLOAD_BYTES, which mk_ipc_call would silently truncate.
starts_with(prefix) alone let a sibling whose name merely begins with
the prefix string count toward it (e.g. prefix "/docs" matched
"/docs2/x"), silently inflating file counts and byte totals. Require
the byte right after the prefix to be a path separator, with root
("/") matching every path below it.
Tags, favorites and preferences all need a small persistent map and no
new capability; one codec over ordinary vfs files serves all three.
Decode degrades to empty on any bad or forward-version input because
metadata must never be able to fail app startup.
Tag filtering extends the existing view filter rather than becoming a
SortMode, which would need the three-file sort lockstep for no gain.
Reconcile is prefix-scoped because the live listing only covers the
directory in view. TagMap's impl is split across tags.rs/tags_mutate.rs/
tags_query.rs to keep each new file under the 75-line cap given the
mandatory AGPL header; entries/valid_tag/the two limit consts are
pub(crate) so the sibling impl blocks can share them.
Favorites store a zero-padded ordinal as the sidecar key so the codec's
sort-by-key preserves pin order instead of sorting paths. Prefs fall
back to the built-in defaults on any bad value.
Undo stores the already-computed inverse so it never re-derives one, and
a delete clears the stack: the vfs has no trash, so offering undo after
a delete would be a lie.
TagMap::from_blob and Favorites::from_blob loaded sidecar records
straight off disk with none of the limits their mutation paths (add)
enforce. A hand-edited or corrupted blob could load past
TAG_MAX_PER_PATH/TAG_MAX_ASSIGNMENTS, carry invalid or unnormalized
tags, or (for favorites) exceed FAVORITES_MAX and duplicate a path
under two ordinals. Both loaders now re-derive the same invariants
add would have enforced: cap, per-tag validation and normalization,
dedup, and dropping empty entries.

from_blob's new body moved to a new sibling file, tags_load.rs,
since tags.rs was already near the 75-line cap.
entries, valid_tag, TAG_MAX_PER_PATH and TAG_MAX_ASSIGNMENTS were
widened to pub(crate) so the impl could be split across tags.rs,
tags_load.rs, tags_mutate.rs and tags_query.rs. pub(in crate::fm) is
the true minimum the split needs.
p.starts_with(prefix) matched a sibling directory too (prefix "/p"
matched "/p2/x"), the same bug just fixed in dirstat.rs. Mirror
dirstat's in_subtree boundary check: the byte after the prefix must
be a path separator, and exact equality to the prefix is not "under"
it.
The narrower pub(in crate::fm) does not resolve when fs_proofs pulls this
source in via #[path], because the host harness assembles the modules
under fm_logic and has no crate::fm to name. Host-testing the real source
outranks the tighter visibility, so this reverts to pub(crate).
The tags, favorites and prefs modules import nonos_app_skeleton::sidecar,
which fs_proofs has no dependency on; aliasing the crate to itself and
carrying the sidecar codec as a #[path] module makes the real capsule
source resolve unchanged. state.rs is a shim because the real one pulls
preview.rs, which calls the vfs syscall client and cannot build on the
host.
State::prefix always ends in a slash (entries.rs builds child paths as
"{prefix}{name}" and event_parent truncates to keep the separator), so
in_subtree counted the separator twice and returned false for every path
under the directory being viewed. reconcile was therefore a silent
no-op: tags for deleted files were never dropped.
Covers the store logic directly and the three reply-body layouts by
walking them back the way each client does, since the handlers resolve
super::util and super::path and cannot be host-included.
Pins the blob format against malformed, truncated and forward-version
input, and the tag, favorite, preference, open-with and undo bounds
that keep a corrupted sidecar from growing the in-memory maps.
sidecar::decode returns records verbatim, so a corrupt blob carrying one
path under many records loaded an entry per record. tags_for and remove
only ever see the first, leaving the rest unreachable and undeletable
while to_blob kept re-serializing them. add can never produce that state
because it finds the existing slot first.
The Search screen could never find a folder, because name matching
skipped every directory entry. Directories now match by name and carry
kind 2, since a store path does not otherwise say which it is and the
UI needs to pick an icon. Content search still skips them.
The Files capsule had a backend with no surface: the redesigned sidebar,
header, Home/Recents/Search/Tags/Shared screens and info panel are added
here, and every clickable thing on them is laid out by one function that
the painter draws from and the hit-test searches. That pattern is the
point of the change -- the previous grid hit-test assumed its own left
edge and knew nothing of the info panel's reserved strip, so it sat a
padding step out of register with the cells actually on screen.

Three consequences worth calling out:

Tags were unreachable. TagMap::add, remove and reconcile had no caller,
so the tag axis could never be populated and the Tags screen could never
show anything. A tag prompt on 't' toggles a tag on the cursor path, and
a refresh reconciles the map against the listing so a tag cannot outlive
the path it names.

The info panel walked the store on the paint path. dirstat is a blocking
IPC round-trip and this tree has documented 16ms reply timeouts, so the
walk moves to a cache keyed by the cursor path, refreshed before paint
rather than inside it.

Sidecar metadata was RAM-only. The vfs write handler never reaches the
block layer, so tags, favourites and prefs reset on every boot; they are
now committed on window close, which is coarse on purpose because each
changed blob costs a payload extent the store cannot reclaim.

Text is measured throughout -- no glyph-count arithmetic -- and every
colour comes from a theme token.
The status line and hint strip were the last surface still on the 1-bit
glyph grid, so they rendered as bitmap monospace under chrome that is
otherwise proportional AA text, and the prompt caret was placed by glyph
count rather than by what was drawn. Both now measure through the same
facade, and the band takes its colours from the palette tokens.
The icon module drew every glyph as axis-aligned rectangles and carried
only two shapes, so Home, Recents, Shared and Tags all rendered as the
same grey block; it also cut shapes by filling with a background colour,
which on a transparent overlay punches through to the wallpaper rather
than hollowing anything.

Icons are now unit-space polyline and ring tables stroked with line_aa,
which blends, and the sidebar and row painters select a real glyph per
kind. Alongside them a plate, glow and pill vocabulary gives the window
shell its rounded card, hairline and glow, and gives later phases the
controls to build on.
The rail was a flat label list: no brand, no grouping, one grey block
for every destination and no sense of where the store stands. It now
opens with a brand row, groups navigation above a hairline from the
pinned places below, and closes with a drive card at the foot.

The active row is the shared Active pill, so its tint, hairline and
glow are the same ones every other accented control uses. Icons rise
to 22px because the stroked glyphs lose their detail below that.

Storage numbers are real. vfs::usage reports no byte ceiling, so the
bar is files against max_files and the byte figure is stated on its
own line rather than given an invented denominator. It is fetched in
refresh alongside the journal and cached, never called from paint.

The card is a row in the same list side_rows returns, so the hit-test
covers the band it was drawn in. side_hit now measures each row by
its own height instead of a fixed step, which is what lets an 88px
card and a 19px rule share one list.
The header was flat text buttons over two hit bands, and the crumb
band was starved to the point that only the store root ever drew. It
is now one band of icon controls: a nav plate, a breadcrumb capsule
with chevron separators, a search control, a grid/list segment, sort
and an accented New pill.

Every control comes from one producer, header_slots, so the painter
and the hit-test cannot disagree about where anything sits. The clear
box is derived from the search slot rather than measured twice, and
is pushed first so it wins inside the field it lives in.

No action was lost. Sort folds into the sliders button with its mode
still named, and Undo keeps its own control. Forward has no history
to walk, so it draws dimmed rather than pretending.

Search is responsive: a field on a wide window, a magnifier button
below 1000px, both from one width function. That is what gives the
breadcrumb room to show more than a single segment at the default
window size.
Home was a bare line list. It now opens with a greeting and a real
date, then a row of category cards, then what you were last working
on, then the storage row.

Every figure on the screen is measured, not decorated. Card counts
come from a dirstat walk cached on the state and refreshed with the
rest, never from a client call inside paint. Recents counts the real
access journal, and an empty journal says so rather than drawing an
empty frame. The storage bars divide files by the store's own file
ceiling, because the store reports no byte ceiling; the byte figures
stand on their own line rather than being given a made-up
denominator.

Shared has no identity backend, so its card is dimmed and says so,
and still opens the surface that explains why.

Cards and storage tiles are laid out once and both painted and
hit-tested from that one pass, so a click lands where the card was
drawn.
The list was a flat run of rows with no way to see or change what it
was sorted by, and the grid was bare glyphs. The list now carries a
column strip for name, type, size and modified, checkboxes that
reflect and drive the existing selection, inline tag chips, and a
per-row menu affordance. The grid draws cards on the shared plate.

Clicking a column sorts by it through the same machinery the sort
control already uses, so the two cannot disagree about the mode. The
active column is lit rather than carrying a direction caret: the
sort model has no ascending axis, and drawing one would be a lie.

There is no row-menu machinery, so both menu affordances draw dimmed
rather than silently doing nothing. There is no image decoder, so a
grid card shows its typed glyph and never pretends to a thumbnail.

Every list x-coordinate comes from one column function that the
header, the rows and the click path all call with the same
arguments. Rows moved down by the strip, so the visible-row count is
re-derived and the keyboard scroll follows it; otherwise arrowing to
the last entry would scroll to a row that was never drawn.
Selecting files told you nothing and offered nothing. A band now
appears with the count, the byte total and the actions, and the
footer carries the count beside the status line.

Five of the seven actions run real handlers: move, duplicate, tag,
delete and clear. Share and Compress have no backend at all, so they
draw dimmed and say so when clicked rather than pretending to work.

The info panel's tag chips remove for real and persist, the quick
actions all run, and the permissions block shows the one bit this
capsule actually knows instead of an invented rwx grid.

Byte totals are stated as a lower bound when the selection holds a
directory, because a directory carries no size and a total that
quietly omitted it would be a number the manager cannot stand behind.

The band and the panel are each laid out once and painted and
hit-tested from that one pass, including the clip at the footer, so
nothing is clickable that was not drawn.
Recents was an undifferentiated run of rows and Search gave no sign
of why a file matched. Recents now groups under real date headings
and Search marks the matched text in place.

Both surfaces carry a filter chip row across one shared filetype
axis. A chip is emitted only when its count is non-zero, so a filter
can never empty the list it filters, and the counts are taken over
the unfiltered set so the axis does not collapse to whatever is
already selected. Search classifies from the store's own hit kind
rather than the path, because a directory hit carries no trailing
separator and would otherwise be filed under its extension.

The headings share the row list the hit-test walks, so a click below
a heading lands on the row that was drawn, and a click on a heading
is inert rather than opening the row beneath it.

The match band measures the prefix for its x and the match for its
width at the same size the row draws, and refuses a byte index that
is not a char boundary. Row text offsets moved into one function the
row painter itself calls, so the band cannot drift off the glyphs.

A zero clock now yields one section that says the clock is
unavailable, rather than stamping every row as today.
Grouping stopped owning the reserved-prefix filter when that moved
into the journal view, and it now takes borrowed rows. The host proof
suite still called it the old way and no longer compiled.

The two filtering tests now run the view and the grouping together,
which is the pair the capsule actually calls, so they prove the
reserved namespace is dropped where it is really dropped.
The host proof gate builds the included capsule sources with warnings
denied, and four membership tests spelled as a predicate scan plus the
palette tripped it.

The scans become contains, which is what they meant. The palette is
marked allowed in the harness rather than trimmed: it is one shared
table, the harness includes no painter to consume it, and the kernel
build uses every token in it.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin +refs/pull/10/head:nonos-sync/gh-478
git switch nonos-sync/gh-478
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
NON-OS/nonos-micro-kernel!10
No description provided.