0eb9845cd9d1d4e4e45d22d545a41cb997ae09f7
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6fc4e7afc9 |
The community has a page, and the page is its invite
A fourth About page, `Join Discord`, sitting between the author and the ask: what is this, who made it, where is everyone, how do I help. Like its three siblings it is written markdown under assets/pages, reads nothing and transmits nothing, and keeps a CLI face — `meshterm discord`. It is short on purpose. The invite link, and the code for it in a `qr` fence, because on a PicoCalc there is no clipboard and the code is how a link leaves the screen at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
424c33ac93 |
The purge is an edit, and archiving is not a deletion
JP's pass over the sweep, and one cut corner it found: the gallery specimen hand-stamped its percentiles, so three contacts rendered as the 2nd, 7th and 11th of a field of three. The number cannot be written down and still be true — it is a rank against the others in the same list — so the specimen now scores through rank_contacts like everything else and reads 10/30/50. The computation was never wrong; the picture of it was. The ladder answers in the number the reader is choosing: how many contacts you *keep*, the one that has to fit the device, with what goes as the muted remainder. It also stops rounding with round(), whose banker's habit sent 4.5 and 3.75 to the same 4 and made "keep 90%" and "keep 75%" identical rungs on a five-contact table; ceil is both distinct and the safe direction on a keep. The preview is editable. Delete lifts one contact out of the sweep and the list refreshes through replace_items, so sparing the one node you recognised no longer means backing out to a shallower rung that also spares forty you didn't care about. Rows h-scroll with the percentile lane pinned out of the shift — it is your place in a list ordered by it. Neither the scroll atom nor the keep atom is written into the base hint; the select list splices each in where its key would act, which is also what keeps all three inside 72 cells. It is a hub now, held by session.stay: re-running run_screen on a screen that had already resolved spun forever the first time a row was spared. Opening a candidate's page from in there offered to delete it, which was two answers to one question. open_node_detail takes manage=False: the rule is that management verbs belong to the list a contact lives in, never to a page opened out of a list about to act on it wholesale. The confirm is amber and button-only. Two caution tiers exist for two costs, and a red typed gate here spent the app's loudest warning on its most reversible bulk action. Red is left to the deletion, which now says what it is — the key forgotten by both sides, no restore, only being heard again — and points at the archive as the reversible way to free the same slot. The page carries all three verbs: archive a live contact, restore an archived one, delete either for good. Archived contacts get their own screen rather than a section nailed to the foot of the Contacts list, reached by a row under the purge carrying the tally. That needed the shared contact list to stop believing every list has the same columns: the middle lanes are now a declared tuple of ContactLane (NAME and KEY stay fixtures), read by the header, the row builder, the sort metric and the width arithmetic alike, replacing a show_traced flag that had already made three of those four say "and if it's the trace picker…". The archived list declares NAME · ARCHIVED · KEY and sorts on exactly those. ARCHIVED draws as HEARD does — a relative age under recency heat — because it is the same question about a different event. One simplification falls out: TRACED sorted on a raw timestamp and opened descending to mean newest-first, alone among the age columns. Every age lane now sorts by age with never at +inf, so traced opens ascending like heard, and reads the same either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
99cd616ae5 |
The purge keeps the contacts worth a slot, and archives the rest
A device's contact table is finite, and a busy mesh fills it with whatever adverts arrived — mostly nodes heard once, in passing, four hops out — until there is no room left to discover anyone new. The age ladder could only ask how long ago; it had nothing to say about which of two equally quiet contacts was the one you would miss. So contacts are now rated. Six weighted terms, each normalised and summed: direct correspondence heaviest at 30, then how lately heard (25), how often (15), channel posting (12), hops (10) and distance (8). Additive rather than multiplicative, because a product lets one zero annihilate a contact that is strong everywhere else — a node advertising no position would have scored nothing however much you talk to it. The rule the whole thing rests on is that an unknown scores *neutral*. Half a real mesh advertises no location, plenty of nodes are never overheard as a relayed frame so carry no hop count, and a channel poster whose name two contacts share can be attributed to neither — the wire gives a channel frame no sender key at all. A term that cannot be computed takes the population median of the contacts that could, so the decision falls to the axes that are known. Scoring those as zero would have swept by how much metadata a node happens to broadcast. It is also why the two least reliable terms carry the two smallest weights, and why distance calibrates against this mesh's own median spread rather than a constant: 50 km is far downtown and unremarkable in a valley. A newcomer gets 25 points decaying to nothing over a fortnight, because every other term rewards accumulated evidence and a node first heard this morning is otherwise arithmetically identical to one quiet for a year. Four things are never candidates at all: a watched node, anyone you have sent a message to, a repeater whose admin login is stored, and a contact MeshTerm has never heard — that last one predates this history, and sweeping it would be purging by how long the app has been running. They still rank, since a percentile that dropped them would not be a rank against your contacts; they just consume no kept slot, so starring a node never deepens the next sweep. Only the percentile is ever displayed. A raw 47.3 means nothing without the distribution it came from and would need a legend the moment the weights were retuned. The victims are archived, not deleted: off the device, which is the scarce resource, and kept here with a stamp. Everything else was already independent of the contact row — reception history, map position, transcripts — and the store keeps the full key, so a restore is one write, which the chat screen already performs on demand when a send is rejected for an unknown recipient. merge_contacts learns to withhold them, or the union would put them straight back and the sweep would look like it did nothing. The old age ladder survives as the second section of the one picker, because "everything I haven't heard in a year" is a plain predictable operation a ranking cannot express. Both routes converge on the same preview — weakest first, with each row's percentile and the two or three facts that put it there — so an age rung reaching into contacts that rank perfectly well says so before the typed confirm rather than after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
972c0b6f03 |
Screens: Esc is the way out, so no row spends two lines saying so
The app-wide Back row existed because it duplicated Esc — two of its call sites said as much in a comment. On both platforms Esc is a labelled key and every footer hint names it, so the row bought nothing for the blank separator and line it took out of every screen; on the PicoCalc's 26 rows that is a row in thirteen. back_rows() is gone, and with it the four hand-drawn copies: ReorderScreen's clean-state row, the live feed's pinned foot stop (the cursor now clamps on the oldest packet), node detail's tail action, and the action-list rows on the trace and TX-sweep screens. Kept, because there leaving is a choice rather than an exit — the same rule that keeps a dialog's Cancel button: exit_rows()' staged pair, where Apply has no key of its own and ✗ Back names what the other way out costs; ReorderScreen's dirty-state pair; the two Quit rows, which initiate the app's terminal action behind a confirm; and the new-record dialog's Close, which is its default, so dropping it would let a reflex Enter navigate. With no Back row there is no Back value: the six spellings callers used to test three at a time (_BACK, "__back__", _CANCEL, two tuple shapes, bare None) collapse to `is CANCEL` / `is None`. The gallery now asserts the absence of an exit row on every screen, on both platforms. Resolves findings E and §10 of docs/survey-navigation.md. Finding D — Esc peeling a filter on three screens and leaving on two — still stands, and is now the only thing left telling a user what Esc does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
80a2a9bd0b |
Contacts: a contact is dropped one at a time, from its own page
The list could only sweep contacts by age — a purge or nothing. Removing one named contact now lives where the reader is already looking at it: the last action on its Node detail page, behind a red Cancel/Remove confirm, and the list it returns to rebuilds without the row. The deletion lands in both halves of what the list shows — the device's table and the cross-session store — because forgetting only the radio's half merges the contact straight back on the next read. A refusal is put in a dialog and nothing is forgotten: a contact the radio still holds must not vanish from the list. Only the contact goes; its reception history and chat messages stay, and a later send offers to write it back. An action row's tint now survives the icon lane being dropped — it lands on the label, as marked_label already does for menu rows — so the delete still reads as a delete on the PicoCalc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f5d0fb3677 |
Map: silence about a tile is not the source saying it's empty
A tile fetch that gets no answer was being remembered as one that did. BasemapSource is careful about the difference — a timeout is never written to disk, precisely so the next pan can ask again — and both callers then threw it away, filing every None as "fetched, empty/absent" and never asking again for the life of the screen. The same latch sat one level up: _resolve() marked itself done whether or not it had resolved anything, so one failed TileJSON round-trip made `available` False, and _ensure_tiles skipped every fetch, for the whole session. That is a desktop no-op and a PicoCalc bug. The device brings its Wi-Fi up some forty seconds into the boot, well after an app started alongside it reaches the menu and warms the tile source — so the map answers "offline" before the answer can be true and draws the entire session from whatever the cache already held. JP, 2026-08-18: black tiles at the two highest zooms, where one z14 tile is the whole screen and a hole is the picture. VE2RAD_FH_50m's tile (14/4842/5861) was simply absent from the device cache; nothing there had been written in ten days of using the map, and it fetched in 1.4s the moment it was asked for. So: `available` reports what is known and never itself goes to the network (it is read on the paint path), a failed resolve stands for 30s rather than for ever, `answered_empty` lets a caller tell the two Nones apart, and a tile nobody answered about waits out a 20s cooldown instead of a restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ea9b7c1cb1 |
Main menu: Device config, Support MeshTerm
Two rows renamed, and the rename carried into the screens they open — the config editor's table title and its staged-count header, the support page's own title (it comes from the tool, so it followed on its own). "Support this project" never said which project; the menu is MeshTerm's and the section above it is already About MeshTerm, so the name is the useful half of that phrase. "Device configuration" was the longest label in the list and set the width of the whole name column: at 19 cells instead of 23, every description now gets 51 and the widest row is 68. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3ccffd13c9 |
About MeshTerm: a sixth section, for the questions the five doings can't hold
The menu asks "What would you like to do?" and its five sections each name a doing. "What is this thing, and who made it" is not one, and had nowhere to go — so it gets the sixth section, the one that names a subject, sorted last where a reader who has run out of things to do finds it. Three pages, in the order a stranger asks: About MeshTerm what this is, where it came from, its terms About the author the person behind it, on the mesh and off it Support this project what keeps it going, and the ways to help Every page is scaffolding: the headings are the settled part, and each section carries one lowercase muted "placeholder — …" line where its prose will go. That is the app's own empty-state voice, so an unwritten page reads as deliberately unwritten rather than as a screen that failed to load, and filling one in means replacing its placeholder — the screen, the menu rows and the CLI faces all follow unchanged. What is not a placeholder is the version, the author and the copyright span: they come from the package itself, so the About page can never describe a build it isn't running inside. The pages act on nothing — no radio, no database, no device — so each opens straight to its screen with no prompt first, and each keeps a CLI face (meshterm about / about-author / support) printing the same page to a shell. Two new icons, both funnelled for the console: 📖 read/about becomes ¶, the page-of-prose mark, and 💰 support/donate becomes $. ScrollScreen earns a derived footer hint on the way through. A read-only body has only two keys to name, the pager and Esc, so passing "↑↓ PgUp/ PgDn scroll · Esc close" at every call site was both repetition and a lie on a body that fits — the standing rule is that a footer never advertises a key that would do nothing. Left unset, it now names the pager only while the content actually overflows, and takes its Esc verb from the surface: a floating view closes, a full screen goes back. Every existing caller passes its hint explicitly and is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8a80d47815 |
F-key lane: a jump rides the Shift half of the page it follows
Home and End are on the PicoCalc's keyboard. Paging is not. So the
jump-to-either-end pair was never the unreachable thing that earned it
F1/F2 -- it only ever wanted a chip for consistency with the pager it
belongs to. Now it rides that pager: Shift+F5 is Top because F5 is
Page up, Shift+F4 is Bottom because F4 is Page down. One axis, one pair
of keys, the second-grade verb sitting behind the first.
That frees F1-F3 on every screen, and the lane is the only place the
device can advertise anything -- there is no hint line to read a chord
off. So the freed slots went to the affordances that were invisible
there:
select lists Sect down/up, the section jumps. Ctrl+PgUp on a
keyboard with no PgUp is the one binding in the app
that was physically unreachable. Plus Delete, lit on
exactly the rows the key acts on.
map Region (Home, renamed -- it frames the ground the mesh
covers, it does not "reset" anything), You (new: Ctrl+L,
recentre on our own node at the zoom you chose), and
Frame for the find matches.
node page the tab it would switch you to.
time machine the window span it would take you to -- w was
undiscoverable on the device.
The map is the one exception to the new rule: Region answers "where
should I be looking", not "how far down the body am I", so it keeps F1
beside the other two destinations rather than hiding behind a pager it
has nothing to do with.
Screens that never scroll (every dialog, the busy splash, the reorder
list) now say so with an empty lane instead of offering a pager that
does nothing. Screens whose Home/End mean something else say what:
Latest/Oldest in a chat, Newest/Oldest in the packet viewer, You on the
walk (where End is bound to nothing, so its slot stays blank).
The map's hint line finally fits 72: its basemap status was never a key
hint, so it moved to the title's status chain where the standards put
those -- which is what took the map off the gallery's pre-existing
overflow list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d79d85b7ee |
An unknown node is light grey, and bold stops recolouring it
Two reports on the PicoCalc's message paths, one root cause each. The graph drew an unnamed hop's hash — and the sender's name — as white, the "you" colour. MapCanvas emits its own bold for markers and labels, and its colours arrive already quantized to a palette slot; the kernel VT then draws bold as *brightness*, so anything that landed in the dim bank was promoted to its bright partner. Light grey became white. A purple repeater would have become pink the same way. The canvas now drops emphasis where bold is brightness — the colour is the load-bearing part. The same trap reaches the theme through inheritance: Rich merges a row's base style into every span, so a dim-slot span left silent about bold changes colour inside a selected row. Every dim-slot style now says which it means — `not bold` to keep its colour, `bold` where the promotion is the point (only title.muted) — and a test forbids staying silent. The pathline drew the same hop dark grey, because `muted` collapses onto the console's dark grey while the desktop's is light. That conflated two things: `muted` is chrome and may recede a step, but a node we can't identify is content you can still act on. It gets its own name, `node.unknown` — light grey, identical to today's desktop muted, so nothing moves there — and it is what `name_style` returns keyless, what an unlit `highlighted_hash` wears, and what the `○` ring is. Also fixes a crash from the previous commit: the graph's label colour resolved a marker through `parse_hex`, which the type marks' theme names are not. A repeater relay we couldn't name would have raised. The gallery's node-detail header spelled a marker hex by hand; it goes through the app's own styles now, so a palette bug can't hide behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f21817b816 |
P6: the 53-column worklist graduates - every picocalc case a hard gate
The P1 xfail list (contacts, map, walk, livefeed, remote_cli, path_composer) turned out to be footer_hint overflows plus one real body bug - and picocalc never draws the hint strings at all since P4's F-key lane replaced them. So: - The gallery asserts the footer actually drawn per platform: the hint string on regular, the screen's F-lane (both banks) on picocalc. - path_composer's wrapped empty-state note emits one entry per wrapped line instead of smuggling a newline into a single windowed row - the one genuine 53-column body overflow. - _KNOWN_WIDE is empty: all 32 picocalc gallery cases (16 screens x two geometries) are now hard gates. Only the pre-existing regular map footer xfail remains, documented as not this seam's to fix. - First per-screen F-lane verbs, proving the override pattern: the contact list puts sort cycling on F3/F8, chat puts paths/retry there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
84caae53f8 |
P3 rework: the PicoCalc visual language, done properly
The first P3 (a84abf3) ran on the wrong model and its palette never reached the TUI: MESH_THEME_16 was written in 256-colour cube indices instead of the 16 VT slots, and the platform theme pick was wired into the scripted-CLI console while the real rasterizer stayed hardcoded to truecolor. Replace it end to end: - theme._VT_SLOTS: the canonical slot->RGB design (conventional hue families kept; slots 5/6 become the track/faint slates), MESH_THEME_16 rewritten against color(0..15) with the VT's bold-as-brightness and dim-bank-background rules pinned by tests. /etc/vtrgb + a boot oneshot install it (console-font script), OSC fallback when kbd is absent. - platforms.on_platform: set_platform now re-runs registered bindings, so theme, rasterizer and widgets swap implementations once per switch and hot paths read module globals - no per-call get_platform(). - The render-boundary fold (theme.fold_text): one str.translate pass (NFKD accent folds - the Terminus base has no accented Latin - plus symbol stand-ins and the width-padded emoji map), an embedded-SGR quantizer for canvas truecolor, and a ?-at-own-width catch-all that makes the no-wide-glyph short-circuit true by construction. Applied at the only two places every visible byte passes: render_to_ansi and MapCanvas.to_ansi_lines. - ui/fontset.py freezes the installed font's inventory (dumped live); the gallery now asserts rendered picocalc ANSI carries no truecolor/ 256-colour SGR and no character outside it - the parity gate the first attempt faked by asserting a dataclass field. - Font build: nine new 6x12 marks (ellipsis, warn, backspace, shift, gear, refresh, clock, crosshair, padlock) drawn into donor slots, with donor misses now fatal and a keeper canary (donating pi once erased Cyrillic pe - shared slot). Table re-dumped on-device: identical to fontset.py, 519 = 519. - Type-based name colouring: registry moved to core/nodetypes (services no longer import ui), fed from monitor observations, heard_nodes and merge_contacts. Heat gradient quantizes to heat.* steps on picocalc. - ui/marks.py: the P2-deferred constant relocation - widgets no longer drags map_render/mapcanvas/pathgraph for tuples and aliases. - meshterm specimen: the visual language on one card through the real funnels - the on-device acceptance screen (JP's request). - tools/chat: delivered/no-ack state uses the standard check/query marks, not the banned emoji pair. Verified on the Lyra: font+palette installed, specimen framebuffer captures in .claude/plans/. 1259 tests green, 13 xfailed (P6 list). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c94a242812 |
P3: PicoCalc visual language — 16-slot palette and compact glyphs
Add the PicoCalc visual system: - MESH_THEME_16: 16-slot palette with deliberate RGB values for truecolor=False - make_console(): picks theme based on platform.truecolor - glyph(): central emoji→single-BMP-char mapping for the 512-glyph font - Type-based name_style() and register_node_type() for PicoCalc colour (key→type) - Text folding: fold_text() with NFKD accent stripping and emoji→placeholder - Platform-aware icon rendering in packet_viewer, dashboard_screen, courier_screen - Reception layer registers node types for colour lookup - Gallery tests check truecolor constraint on PICOCALC All 1239 tests pass; 13 xfail (P6 width worklist). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> |
||
|
|
8f1fba135e |
Platform seam: meshterm/platforms.py, --platform flag, dual-platform gallery
One frozen Platform spec (REGULAR/PICOCALC), resolved once at boot (--platform flag > MESHTERM_PLATFORM env > /proc/device-tree/model auto-detect > REGULAR) and read via get_platform()/set_platform() - never a stale `from platforms import PLATFORM`, which the module docstring calls out. `meshterm platform` reports the resolution and everything it looked at. Wires the two correctness bindings that matter before any flavour work lands: width_reclaim off on PicoCalc's exact-width console (session._reclaim_last_column, env override still wins), and the emoji-width calibration probe skipped outright when a platform never draws emoji. tests/test_gallery.py renders all 16 screens the interactive menu opens against REGULAR (72x24) and PICOCALC (53x26 live floor, 53x40 lux case), asserting no line overflows its platform - 35 passing cases, 13 xfailed as a real P6 worklist (contacts, map, walk, livefeed, remote_cli, path_composer). Also caught and left alone: the map screen's footer hint already overflowed the *existing* 72-col standard before this seam existed, unrelated to picocalc - xfailed rather than fixed as a drive-by. tests/test_platforms.py unit-tests the module directly; conftest.py gained an autouse fixture resetting the platform between tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |