package catalogue import ( "bytes" "encoding/json" "fmt" "math" "regexp" "sort" "strings" "text/template" ) // A kind a seat receives as data, rendered by its holder (novox/hq ADR 0255). // // ADR 0212 made a contribution text in the tool's own grammar, which the controller places and never // reads. That is right where the tool is the seat's for good — a hotkey daemon's trigger lines — and // wrong where the seat's whole point is that its tool can be replaced: a module adding a battery to the // bar would write i3status-rust's TOML, and a second bar would have to read it. So a seat may define a // kind as data instead. The seat says the data's shape; a contributor gives data in that shape; the // holder gives a template that renders one piece of it into its tool's grammar, as a module's facts // template renders the roster (`facts`): the data is the mesh's, the format is the holder's. // // What the holder places is narrowed by the shape's placing fields — the bar's `bar` and `place` — in // the placeholder, `${contribution:::=,…}`, and every combination of them is // placed by the holder exactly once, so nothing a module contributes is dropped by a holder that forgot // a corner of the shape. // Shape is what a kind received as data looks like. type Shape struct { // Places are the fields a holder narrows its placeholders by, each with every value it takes. Places []PlaceField // Order is the whole-number field that orders the pieces within one placeholder (lowest first, // then module order); absent in a piece means OrderDefault. Order string // Check is what else is wrong with one piece's data, beyond its placing fields and its order. Check func(data map[string]any) []string `json:"-"` // Examples are pieces every holder's template must render, one for each variant the shape has, // so a holder that cannot render one is refused at registration and not found on a machine. Examples []map[string]any // Reads is the state on the bus a piece shows, each a state and the one key of it, or nothing // (novox/hq ADR 0260): a contributor offers only state it keeps itself, and the holder on the same // machine is granted to read that key, so the holder's manifest names no contributor. Reads func(data map[string]any) []StateRead `json:"-"` // Text says every piece of this kind is, rendered, text that a collector may place as a collection of items // (design 57 L3): a collector then names the kind as `${collected:contribution:::=,…}`. Text bool } // collectableAsText is whether a collector may name a kind received as data as a collection. func collectableAsText(r Receivable) bool { return r.Shape != nil && r.Shape.Text } // PlaceField is one field a holder places by, and the values it takes. type PlaceField struct { Field string Values []string } // OrderDefault is a piece's order when it gives none: the middle of 0–99. const OrderDefault = 50 // KnownCapabilities is every capability the node-engine detects (mesh-host internal/profile), the // names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its // contribution out on every machine, silently, so it is refused. A detector added there is added here. var KnownCapabilities = []string{ "battery", "container-groups", "container-runtime", "file-references", "firewall", "graphical-session", "overlay", "package-manager", "power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager", "uplink-systemd-networkd", "user-namespaces", "virtualisation", } // renderFuncs are the functions a holder's template may call. `quote` writes a value as a JSON // string with DEL escaped as well — JSON leaves it bare, and TOML refuses it — which makes it a valid // basic string in TOML and in most tools' grammars, so a contributed command with quotes or control // characters in it cannot break out of the holder's line. var renderFuncs = template.FuncMap{"quote": quote} func quote(v any) (string, error) { b, err := json.Marshal(fmt.Sprint(v)) return strings.ReplaceAll(string(b), "\x7f", `\u007f`), err } // StateRead is one key of another module's state that a holder reads: Read is `.`, // and Key the one key, empty for the holder's own machine until ReadsGranted names it. type StateRead struct { Read, Key string } // stateKeyName is a key a piece may name: a machine's name is one. var stateKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // ReadsGranted is the state a holder reads because modules on its machine offer it in pieces of a // kind its seat receives as data (novox/hq ADR 0260), each with the one key the piece shows — the // machine's own when it names none — sorted and once each. A module's items of such a kind (design 57 L3) grant // what they show too, always at the machine's own key; an item whose capability the machine lacks grants a key // that is never written, which is no wider than its own machine's. The bus grants the holder these keys and // no others: the holder names no contributor, and reads only what is offered to it where it runs. func ReadsGranted(holder Manifest, onMachine []Manifest, machine string) []StateRead { seen := map[StateRead]bool{} for _, claim := range holder.Claims { s, known := SeatNamed(claim.Name) if !known { continue } for _, r := range s.Receives { if r.Shape == nil || r.Shape.Reads == nil { continue } for _, m := range onMachine { for _, c := range m.allContributions() { if cs, ok := SeatNamed(c.Seat); !ok || cs.Name != s.Name || c.Kind != r.Kind { continue } for _, sr := range r.Shape.Reads(c.Data) { if sr.Key == "" { sr.Key = machine } if module, _, err := ReadState(sr.Read); err == nil && module == m.Module && module != holder.Module && stateKeyName.MatchString(sr.Key) { seen[sr] = true } } } // A moved contributor's items of the kind (design 57 L3): what each shows, read from its attributes as a // contribution's data, and always this machine's own key (ADR 0260), never another's (issue 774: // power's draw block became an item and i3status-rust was issued nothing). for _, it := range m.Items { if it.Collection != "contribution:"+s.Name+":"+r.Kind { continue } for _, sr := range r.Shape.Reads(itemData(it)) { sr.Key = machine if module, _, err := ReadState(sr.Read); err == nil && module == m.Module && module != holder.Module && stateKeyName.MatchString(sr.Key) { seen[sr] = true } } } } } } out := make([]StateRead, 0, len(seen)) for sr := range seen { out = append(out, sr) } sort.Slice(out, func(i, j int) bool { if out[i].Read != out[j].Read { return out[i].Read < out[j].Read } return out[i].Key < out[j].Key }) return out } // itemData is an item's attributes in the shape of a contribution's data: each at the top and among the options, where // a shape's Reads looks for what a piece shows (barBlockReads: shows, options.state). func itemData(it Item) map[string]any { data, options := map[string]any{}, map[string]any{} for k, v := range it.Attributes { data[k], options[k] = v, v } data["options"] = options return data } // wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float. func wholeIn(v any, lo, hi int) (int, bool) { f, ok := v.(float64) if !ok { if i, isInt := v.(int); isInt { f, ok = float64(i), true } } if !ok || f != math.Trunc(f) || f < float64(lo) || f > float64(hi) { return 0, false } return int(f), true } // placeholderWhere parses the narrowing part of a placeholder, `bar=bottom,place=status`, against the // kind's shape. Empty narrows nothing. func placeholderWhere(r Receivable, where string) (map[string]string, error) { out := map[string]string{} if where == "" { return out, nil } if r.Shape == nil { return nil, fmt.Errorf("%s is text, which a placeholder does not narrow", r.Kind) } for _, pair := range strings.Split(where, ",") { field, value, ok := strings.Cut(pair, "=") var p *PlaceField for i := range r.Shape.Places { if r.Shape.Places[i].Field == field { p = &r.Shape.Places[i] } } switch { case !ok || p == nil: return nil, fmt.Errorf("%q narrows by %s, and %s is placed by %s", where, field, r.Kind, placeFields(r)) case !oneOf(p.Values, value): return nil, fmt.Errorf("%q says %s=%s; %s is one of %s", where, field, value, field, strings.Join(p.Values, ", ")) case out[field] != "": return nil, fmt.Errorf("%q names %s twice", where, field) } out[field] = value } return out, nil } func placeFields(r Receivable) string { var names []string for _, p := range r.Shape.Places { names = append(names, p.Field) } return strings.Join(names, ", ") } // holderTemplate is the holder's template for a kind, parsed. func holderTemplate(holder Manifest, seat Seat, kind string) (*template.Template, error) { for _, c := range holder.Claims { if s, known := SeatNamed(c.Name); !known || s.Name != seat.Name { continue } text, has := c.Renders[kind] if !has || strings.TrimSpace(text) == "" { break } t, err := template.New(seat.Name + ":" + kind).Funcs(renderFuncs).Parse(text) if err != nil { return nil, fmt.Errorf("%s's template for %s:%s does not parse: %v", holder.Module, seat.Name, kind, err) } return t, nil } return nil, fmt.Errorf("%s places %s:%s, which is data, and its claim gives no template for it in `renders` "+ "(novox/hq ADR 0255)", holder.Module, seat.Name, kind) } // renderPiece is one piece of data in the holder's grammar. The template sees the piece's fields, // `module`, the contributor, and `machine`, the node it is composed for. A value the piece does not have reads as nothing in a `with` or an `if`; // printed bare it would write `` into the tool's file, which is refused here instead. func renderPiece(t *template.Template, module, machine string, data map[string]any) (string, error) { view := map[string]any{"module": module, "machine": machine} for k, v := range data { view[k] = v } var b bytes.Buffer if err := t.Execute(&b, view); err != nil { return "", fmt.Errorf("rendering %s's piece: %v", module, err) } out := b.String() if strings.Contains(out, "") { return "", fmt.Errorf("rendering %s's piece printed a value it does not have: %q", module, out) } return out, nil } // placedPiece is one contributed piece on its way into a holder's file. type placedPiece struct { module string data map[string]any order int // text is a moved contributor's item, rendered (collections.go heldItems): placed as it is, not through the // holder's template. text string item bool } // shapedContributions is every module's data of one kind to one seat, narrowed by where and by the // machine's capabilities, ordered, and rendered through the holder's template — each piece under a // comment line naming its module. // // A piece the template renders to nothing — a `shows` the holder does not know yet — or fails on is // left out and answered as unplaced, naming its module: the other pieces and the rest of the machine's // declaration go on (novox/hq ADR 0255). func shapedContributions(modules []Manifest, holder Manifest, machine string, facts map[string]string, s Seat, r Receivable, where map[string]string, caps map[string]bool) (string, []string, error) { var pieces []placedPiece for _, m := range inModuleOrder(modules) { for _, c := range m.allContributions() { if c.Kind != r.Kind || !capable(c, caps) { continue } if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name { continue } match := true for field, value := range where { if v, _ := c.Data[field].(string); v != value { match = false } } if !match { continue } order := OrderDefault if v, ok := wholeIn(c.Data[r.Shape.Order], 0, 99); ok { order = v } pieces = append(pieces, placedPiece{module: m.Module, data: c.Data, order: order}) } // A moved contributor's items of the kind, by their attributes (design 57 L3, issue 777): each in its // module's place, by its order, then its name. if !r.Dirs { for _, h := range heldItems(m, "contribution:"+s.Name+":"+r.Kind, where, facts, caps) { pieces = append(pieces, placedPiece{module: m.Module, order: h.item.order(), text: h.text, item: true}) } } } if len(pieces) == 0 { return "", nil, nil } var t *template.Template for _, p := range pieces { if p.item { continue } var err error if t, err = holderTemplate(holder, s, r.Kind); err != nil { return "", nil, err } break } sort.SliceStable(pieces, func(a, b int) bool { return pieces[a].order < pieces[b].order }) var b strings.Builder var unplaced []string for _, p := range pieces { if p.item { fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module) b.WriteString(p.text) continue } out, err := renderPiece(t, p.module, machine, p.data) if err == nil && strings.TrimSpace(out) == "" { err = fmt.Errorf("%s's template renders nothing for it", holder.Module) } if err != nil { unplaced = append(unplaced, fmt.Sprintf("%s's %s:%s %s is not placed by %s: %v", p.module, s.Name, r.Kind, describePiece(p.data), holder.Module, err)) continue } fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module) b.WriteString(out) if !strings.HasSuffix(out, "\n") { b.WriteString("\n") } } return b.String(), unplaced, nil } // describePiece is a piece in one line, its fields in order, for a person reading why it was left out. func describePiece(data map[string]any) string { var parts []string for _, k := range sortedKeys(data) { if _, nested := data[k].(map[string]any); nested { continue } parts = append(parts, fmt.Sprintf("%s=%v", k, data[k])) } return "(" + strings.Join(parts, " ") + ")" } // capable is whether a contribution applies on a machine with these capabilities: always, unless it // names one with `if-capability` the machine did not report (novox/hq ADR 0255). A machine whose // capabilities are not known has none, so a conditional piece is left out rather than guessed in. func capable(c SeatContribution, caps map[string]bool) bool { return c.IfCapability == "" || caps[c.IfCapability] }