catalogue's working set starts empty and a host gives it the seats it read (UseSeats over SeatsFromDefinitions); the node-engine's verbs and events take the seat's name from the caller. Tests take their seats from seatlisttest. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
117 lines
4.3 KiB
Go
117 lines
4.3 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// The graphical session's seats (novox/hq ADR 0208): one module per piece of software, each piece's
|
|
// role a node seat in the mesh's own set, so i3 and sway, xterm and foot, rofi and dmenu compete for
|
|
// a role rather than each inventing one — and a machine running two of one role is refused at
|
|
// assignment instead of found by two bars on one screen.
|
|
const (
|
|
LoginManagerSeat = "node-login-manager"
|
|
DisplayServerSeat = "node-display-server"
|
|
DisplaySessionSeat = "node-display-session"
|
|
TerminalEmulatorSeat = "node-terminal-emulator"
|
|
LauncherSeat = "node-launcher"
|
|
NotifierSeat = "node-notifier"
|
|
LockScreenSeat = "node-lock-screen"
|
|
ClipboardSeat = "node-clipboard"
|
|
BarSeat = "node-bar"
|
|
CompositorSeat = "node-compositor"
|
|
SecretServiceSeat = "node-secret-service"
|
|
)
|
|
|
|
// BarKindBlock is the bar's one received kind: a block (novox/hq ADR 0255).
|
|
const BarKindBlock = "block"
|
|
|
|
// barShows is what a contributed block may show, and the options each takes — the bar-neutral
|
|
// vocabulary every holder of node-bar renders. A new entry is a decision, and every holder's template
|
|
// renders it (the shape's examples) before it is accepted.
|
|
var barShows = map[string]map[string]barOption{
|
|
// The machine's battery: its charge, and how long it lasts or takes to fill.
|
|
"battery": {"device": {kind: "text"}},
|
|
// One line a command prints, run every interval seconds: what the bar cannot know itself.
|
|
"command": {"command": {kind: "text", required: true}, "interval": {kind: "seconds", required: true}},
|
|
// A value the contributor keeps on the bus (novox/hq ADR 0255): its state `<module>.<name>` and the
|
|
// key, this machine's name when absent. The value is an object whose `text` is what is shown. The
|
|
// holder is granted to read it and shows it however its bar shows a changing value; no file and
|
|
// no command passes between the two modules.
|
|
"state": {"state": {kind: "text", required: true}, "key": {kind: "text"}},
|
|
}
|
|
|
|
// barBlockReads is the state and key a block shows, if any; an empty key is the machine's own.
|
|
func barBlockReads(data map[string]any) []StateRead {
|
|
if data["shows"] != "state" {
|
|
return nil
|
|
}
|
|
options, _ := data["options"].(map[string]any)
|
|
s, ok := options["state"].(string)
|
|
if !ok || s == "" {
|
|
return nil
|
|
}
|
|
key, _ := options["key"].(string)
|
|
return []StateRead{{Read: s, Key: key}}
|
|
}
|
|
|
|
type barOption struct {
|
|
kind string // text, or seconds (a whole number from 1 to 86400)
|
|
required bool
|
|
}
|
|
|
|
func barBlockProblems(data map[string]any) []string {
|
|
var problems []string
|
|
for field := range data {
|
|
if !oneOf([]string{"bar", "place", "order", "shows", "options"}, field) {
|
|
problems = append(problems, fmt.Sprintf("a block has no field %q; it has bar, place, order, shows and options", field))
|
|
}
|
|
}
|
|
shows, _ := data["shows"].(string)
|
|
takes, known := barShows[shows]
|
|
if !known {
|
|
return append(problems, fmt.Sprintf("a block shows %v; it shows one of %s", data["shows"],
|
|
strings.Join(sortedKeys(barShows), ", ")))
|
|
}
|
|
options := map[string]any{}
|
|
if raw, has := data["options"]; has {
|
|
o, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return append(problems, "a block's options are an object")
|
|
}
|
|
options = o
|
|
}
|
|
for name, v := range options {
|
|
opt, ok := takes[name]
|
|
if !ok {
|
|
problems = append(problems, fmt.Sprintf("a %s block has no option %q; it takes %s", shows, name,
|
|
orNothing(sortedKeys(takes))))
|
|
continue
|
|
}
|
|
switch opt.kind {
|
|
case "text":
|
|
if t, isText := v.(string); !isText || strings.TrimSpace(t) == "" {
|
|
problems = append(problems, fmt.Sprintf("a %s block's %s is text", shows, name))
|
|
}
|
|
case "seconds":
|
|
if _, whole := wholeIn(v, 1, 86400); !whole {
|
|
problems = append(problems, fmt.Sprintf("a %s block's %s is a whole number of seconds from 1 to 86400", shows, name))
|
|
}
|
|
}
|
|
}
|
|
for _, name := range sortedKeys(takes) {
|
|
if _, has := options[name]; takes[name].required && !has {
|
|
problems = append(problems, fmt.Sprintf("a %s block needs the option %s", shows, name))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// withShapeCode gives a data-shaped kind of the generated seat list the checks its definition cannot carry, being code:
|
|
// the bar's block (novox/hq ADR 0255, ADR 0260).
|
|
func withShapeCode(seat, kind string, sh *Shape) {
|
|
if seat == BarSeat && kind == BarKindBlock {
|
|
sh.Check, sh.Reads = barBlockProblems, barBlockReads
|
|
}
|
|
}
|