ADR 0255: the bar takes blocks as data from the module that knows the hardware
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/the-bar-takes-blocks rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request

The operator chose a generic boundary for the bar: a block is said in terms no bar
owns and rendered by the seat's holder, placed only where the machine reports the
hardware, and generic hardware comes from the generic seat's module, not a vendor's.
To-be 42 records it; ADR 0208 points to it.
This commit is contained in:
jochen
2026-10-08 13:45:27 +02:00
parent df0097245d
commit 5852b66988
3 changed files with 205 additions and 1 deletions
@@ -93,6 +93,10 @@ reports, `graphical-session`, still gates the display server itself.
> included, is a dependency on the seat that receives it. What a contribution contains still follows
> the tool's grain.
> **Extended — 2026-10-08, by [ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md).** A seat may receive a kind as data in a shape the seat
> defines, rendered by its holder's template: `node-bar` receives blocks that way. A contribution may
> also be placed only where the machine reports a capability.
**5. The display server's module writes the session's start.** It writes a block at the start of
`~/.xinitrc`, in this order:
@@ -0,0 +1,183 @@
---
topic: what runs on it
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
---
# 255. The bar takes blocks as data from the module that knows the hardware, placed where the machine has it
## Context
The bar's module claims `node-bar` ([ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)) and
writes both bars' files whole. When it was adopted from the workstations on 2026-10-04, three kinds of
block were left out because they were not the bar's to know:
- the battery, which only the laptop has;
- the GPU's load, an AMD block on the laptop, commented out for NVIDIA on the workstation;
- three Bluetooth headsets by hardware address.
The bar's own documentation said that a block which follows a machine's hardware belongs to the module
that knows that hardware. It also said such a module had no way in. ADR 0208 §4 lets a module
contribute to a holder's file "in the tool's own grain", and
[ADR 0212](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md) made that one
general form: a seat, a kind the seat receives, and text in the tool's own grammar, which the controller
places and never reads. `node-bar` received nothing. Since then the laptop's bar has shown no battery.
Measured on the laptop on 2026-10-08, the bar's memory block read 96 %. It counted the cache the kernel
gives back when asked as used. The `memory-pressure` module, which judges the same machine, read 73 %.
Three facts shaped the decision:
- **The text form would tie every contributor to one bar.** A module giving the bar a battery under
ADR 0212 would write i3status-rust's own grammar. A second bar module, waybar or polybar, could not
read it, and the seat exists so that its holder can be replaced.
- **The mesh already separates data from format once.** A module's `facts` give a path and a template.
The controller renders the mesh's roster through the template. The data is the mesh's and the format
is the module's.
- **What a machine has is already reported.** The node-engine reports detected capabilities, each with
what it saw and how. The controller uses them to refuse a module a machine cannot run. A battery is
not among them yet.
## Considered Options
1. **The seat receives blocks as data, and its holder renders them.** A contribution says what the block
shows in terms no bar owns. The holder turns it into its own grammar. Chosen by the operator on
2026-10-08: "option 1 is exactly how it should work, other modules can 'provide' block information in
some form or another (use a generic boundary layer)".
2. **Blocks that hide themselves where they do not apply.** i3status-rust's common `if_command` option
(a test for the battery's directory, say) hides a block at run time on a machine without the
hardware. Rejected: the knowledge stays in the bar, every bar asks the same question again at run
time, and another bar module would have to carry the same tests in its own grammar.
3. **A text contribution in the bar's grammar, under ADR 0212 as it stands.** Rejected for the reason
above: the contributor would write one bar's grammar, and the seat could not change hands.
4. **A provision the bar requires.** Rejected: a provision is a service one module serves another
([ADR 0040](0040-what-a-module-is.md)) and is resolved to one provider. A bar takes pieces from any
number of modules on its own machine, and that is what a seat's contributions already are.
On the source of the battery, the operator ruled on the same day that generic information must not come
from a vendor's module. A battery is any laptop's. The laptop model's module was the first candidate
and is not the source.
## Decision
**1. A seat may receive a kind as data.** The seat defines the kind's shape: its fields, their values,
and which fields place a piece. A contribution of that kind gives `data` in that shape instead of
`content`. The holder's claim gives a template for the kind in `renders`: a template over one piece,
written in its tool's grammar. The controller renders each piece through it and places the result.
Each piece is preceded by a comment naming its module, as in ADR 0212 §3. A kind given as text stays
as ADR 0212 decided.
**2. The holder places a data kind by its placing fields.** The placeholder is
`${contribution:<seat>:<kind>:<field>=<value>,…}`. Within one placeholder, pieces go in the order the
shape names, then in module order. Across all of its files, the holder places every combination of
the placing fields exactly once. Its template must render every example the seat defines. If a
combination is missing, a contribution is lost. If it is placed twice, the contribution is written
twice. Both are refused at registration. A holder that places none of the kind is not refused. It was
written before the kind existed, and its contributions wait until it places them.
**3. A contribution may name a capability the machine must report: `if-capability`.** The controller
composes it only for a machine whose node-engine reported that capability. Where nothing is known,
there is nothing. This holds for both forms, text and data. The machine's facts decide at composition.
The tool does not decide at run time.
**4. A kind may be offered.** A contribution of an offered kind does not depend on the seat
(an exception to ADR 0212 §4). Where nobody holds the seat, nothing is placed, and the contributor
runs there all the same.
**5. `node-bar` receives `block`, offered, as data.** A block has these fields:
- `bar`: `bottom` or `top`;
- `place`: `resources`, beside the processor, memory and disks, or `status`, beside the sound and
before the clock;
- `order`: a whole number from 0 to 99, 50 when absent;
- `shows`: `battery` (option `device`) or `command` (options `command` and `interval`, one line the
command prints, every interval seconds);
- `options`: the options of what it shows.
Nothing in a block belongs to one bar. A new kind of block is added to the seat's shape, and every
holder's template must render it before the shape is accepted.
**6. Generic hardware is shown by the module that holds the generic seat, conditioned on the machine's
facts.** A vendor's module contributes only what is that vendor's alone. The power module holds
`node-power` on every machine, so it contributes:
- the battery block, `if-capability: battery`;
- the power-draw block, `if-capability: power-meter`.
The laptop model's module contributes no block. Something only its vendor's daemon knows would be its
block, such as the charge limit asusd holds. None is contributed yet.
**7. The node-engine reports two more capabilities, read from the kernel:**
- `battery`: a battery that powers the machine. A device's battery, such as a mouse's or a headset's,
is not one, and neither is an empty bay.
- `power-meter`: something here measures the power drawn. That is a system battery, a processor
package's powercap energy counter (RAPL, on Intel and AMD), or a graphics device that reports its
own power.
**8. The power drawn is measured honestly, and the block says what was measured.**
- **On battery:** the battery's discharge rate. That is the whole machine, and the block says
`machine`.
- **On mains, or on a machine without a battery:** the processor package's power, as a rate of
change of its energy counter. A discrete graphics device's own power is added where the device
reports it. The block says `CPU` or `CPU+GPU`, never the machine.
- An APU's integrated graphics report the whole package, so they are not counted a second time.
- A sleeping GPU is not woken to be asked.
- **The machine at the wall** can only be measured by a metered plug or a UPS. It is not measured.
**9. The energy counter stays root's.** Read often, it leaks what the processor is doing (the
PLATYPUS attack, CVE-2020-8694), which is why the kernel makes it root's. No permission is loosened.
- The power module runs a sampler as root. It reads the counter every five seconds and publishes
only the averaged rate, as one line anyone may read.
- The bar's command prints that line. It says the power is not measured when the line is missing
or older than thirty seconds.
**10. The memory block shows what programs hold,** not counting the cache. That is the figure
`memory-pressure` judges the same machine by.
## Consequences
- The laptop's bar gains the battery and the power draw. A workstation's bar gains the power draw, and its
processor's part is labelled as such. A server has no bar, and nothing is placed there.
- Replacing the bar's module costs one template. No contributor changes.
- `node-bar` is the first seat to receive data. `node-hotkeys`, `node-display-session` and
`node-backup` keep text.
- **Version skew:** an older controller refuses a manifest with `data`, `if-capability` or `renders`.
The controller is delivered before the catalogue, as a delivery group's order rules already require
([ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) §4).
The node-engine is delivered first of all. Until a machine's node-engine reports `battery` and
`power-meter`, that machine's bar shows neither block, and nothing fails.
- **What got harder:**
- The controller now runs a holder's template, and a template can be wrong. Its examples are rendered
at registration, and a value it prints but does not have is refused, not written as `<no value>`.
- The kind's shape is code in the controller's seat table, as the seats' verbs are. A new block kind
is a change to the controller and to every holder's template.
- **Left open:**
- the machine's draw at the wall, from a metered plug or a UPS;
- a GPU-load block, which belongs to a generic module that does not exist;
- a vendor-only block, such as the laptop's charge limit;
- the headsets, which are one person's devices.
## How it is checked
| Rule | Checked by |
|---|---|
| A data contribution fits its seat's shape (fields, values, required options, order 0–99), and text and data are not mixed | the catalogue check; the controller's `TestABlockOutsideTheShapeIsRefused` |
| A holder places every combination of the placing fields once, with a template that renders every example | the catalogue check; `TestAHolderPlacesEveryCombinationOnceWithATemplateThatRendersEveryExample` |
| Pieces are rendered by the holder's template, in place and order, each named by module | `TestABlockIsRenderedByTheHolderInItsPlaceAndOrderNamedByModule` |
| `if-capability` places a contribution only where the machine reported the capability, for text and data alike | `TestAContributionIfACapabilityIsPlacedOnlyWhereTheMachineReportsIt` |
| An offered kind makes no dependency on its seat | `TestAnOfferedBlockMakesNoDependencyOnTheBar` |
| `battery` and `power-meter` are read from the kernel, and a device's battery is not the machine's | the node-engine's `power_test.go` |
| The power draw says `machine` only on battery, never counts an APU's graphics twice, and never wakes a sleeping GPU | the power module's `draw_test.go` |
| Only a rate leaves root, readable by anyone | the power module's `TestOnlyTheRateIsPublishedReadableByAnyone` |
| The bar's template renders a battery and a quoted command, each bar places both places once, and the memory block uses what programs hold | the i3status-rust module's `blocks_test.go` |
| On the machines | the merge gate's composition of every machine, and after rollout the bar's `i3status_rust_blocks` tool on the laptop and a workstation |
## References
- [ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md) §4, which this extends
- [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md),
[ADR 0212](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md) §1, §3 and §4, whose
general form this widens
- [ADR 0211](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md), the power seat
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-10-04
updated: 2026-10-08
decisions:
- 02-DECISIONS/0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
@@ -19,6 +19,7 @@ decisions:
- 02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md
- 02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md
- 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
- 02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md
---
# 42. The machines' modules, in order
@@ -118,6 +119,22 @@ laptop model's vendor keys become its contribution. The window-manager fragments
clipboard, the wallpaper, the bar and the laptop model become `config` contributions to
`node-display-session`.
**Added 2026-10-08.** `node-bar` receives blocks as data ([ADR 0255](../../02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md)). A block
says which bar, which place (`resources` or `status`), an order, and what it shows (`battery`, or one
line a `command` prints). `i3status-rust` renders it with the template its claim gives and places
both places of both bars once. The bar writes no block that depends on the hardware:
- `power` contributes the battery where the node-engine reports `battery`, and the power draw where it
reports `power-meter`. On battery the draw is the machine's discharge; otherwise it is the processor
package's power from its energy counter, plus a discrete GPU's own, and says so. A root sampler
publishes only the rate, so the counter stays root's.
- The laptop model's module contributes no block. A vendor-only figure, such as its charge limit, would
be its block.
- The bar's memory block shows what programs hold, the figure `memory-pressure` judges by.
The node-engine is delivered first, so the machines report the two capabilities. The controller comes
next, because it parses `data`, `if-capability` and `renders`. The catalogue comes last. Left open: the
draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets.
## Phase 3 — one machine model
The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)