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" ) // graphicalSessionSeats are the eleven, in the order ADR 0208's table reads, each with the verbs // research 026/05 starts it with. Three have none yet: the bar (which receives blocks instead, // ADR 0255), the compositor and the secret // service are roles a second holder competes for, and nothing has needed to ask them anything. func graphicalSessionSeats() []Seat { const decided = "novox/hq ADR 0208" return []Seat{ {Name: LoginManagerSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "sessions", Description: "The sessions the login manager offers on this machine, and which " + "one the operator account starts by default.", Input: schema(map[string]string{}, nil)}, }}, {Name: DisplayServerSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "displays", Description: "The monitors connected now, each with its identity, its modes and " + "where it is placed; and the layout profile in force, if one matches.", Input: schema(map[string]string{}, nil)}, // Profiles are keyed by the monitors' identities and are the operator's data (ADR 0208 §6). {Name: "layout", Description: "The monitor layout profiles, keyed by the connected monitors' " + "identities: list them, save the current arrangement under a name, or apply one.", Input: withEnum(schema(map[string]string{ "action": "list, save or apply", "name": "the profile to save or apply (save and apply only)", }, []string{"action"}), "action", "list", "save", "apply")}, }}, // It receives window-manager configuration lines from every other module (novox/hq ADR 0212): // bindings, start-up commands, rules — placed by the session's holder, never written into // its directory by the contributor. {Name: DisplaySessionSeat, Scope: ScopeNode, Decision: decided, Receives: []Receivable{{Kind: "config", Comment: "#"}}, Serves: []Verb{ {Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.", Input: schema(map[string]string{}, nil)}, {Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether " + "it is visible or focused.", Input: schema(map[string]string{}, nil)}, {Name: "windows", Description: "The session's windows: each one's title, class, workspace and " + "whether it has focus; narrowed to one workspace when named.", Input: schema(map[string]string{"workspace": "one workspace (optional)"}, nil)}, }}, {Name: TerminalEmulatorSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "open", Description: "Open a terminal window in the operator's session, running a command " + "or the login shell, in a directory or the account's home.", Input: schema(map[string]string{ "command": "what to run in it (optional; the login shell when absent)", "directory": "where it starts (optional; the account's home when absent)", }, nil)}, }}, {Name: LauncherSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "menu", Description: "Put a menu of choices in front of the operator and answer with the " + "one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", Input: map[string]any{"type": "object", "required": []string{"choices"}, "properties": map[string]any{ "choices": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the lines to choose between, in order"}, "prompt": map[string]any{"type": "string", "description": "what the menu asks (optional)"}, }}}, }}, {Name: NotifierSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "send", Description: "Show the operator a notification.", Input: withEnum(schema(map[string]string{ "title": "the notification's summary", "body": "its text (optional)", "urgency": "low, normal (the default) or critical", }, []string{"title"}), "urgency", "low", "normal", "critical")}, {Name: "history", Description: "The notifications shown lately, newest first.", Input: schema(map[string]string{"limit": "how many (optional, default 20)"}, nil)}, }}, {Name: LockScreenSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "lock", Description: "Lock the operator's session now.", Input: schema(map[string]string{}, nil)}, }}, {Name: ClipboardSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: "history", Description: "What the clipboard held lately, newest first.", Input: schema(map[string]string{"limit": "how many (optional, default 20)"}, nil)}, {Name: "copy", Description: "Put text on the operator's clipboard.", Input: schema(map[string]string{"text": "the text"}, []string{"text"})}, }}, // It receives blocks as data (novox/hq ADR 0255): what a block shows, said for any bar, which // the holder renders in its own grammar. A module that knows something about the machine — the // power module its battery — gives the block; the bar knows no machine's hardware. {Name: BarSeat, Scope: ScopeNode, Decision: decided, Receives: []Receivable{{Kind: BarKindBlock, Comment: "#", Offered: true, Shape: barBlockShape()}}}, {Name: CompositorSeat, Scope: ScopeNode, Decision: decided}, {Name: SecretServiceSeat, Scope: ScopeNode, Decision: decided}, } } // withEnum narrows one string property of a schema to the values it may take, so a caller is told // the choices by the schema rather than by a refusal. func withEnum(s map[string]any, property string, values ...string) map[string]any { props := s["properties"].(map[string]any) p := props[property].(map[string]any) p["enum"] = values return s } // 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 `.` 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 a block shows, if any. func barBlockReads(data map[string]any) []string { if data["shows"] != "state" { return nil } options, _ := data["options"].(map[string]any) if s, ok := options["state"].(string); ok && s != "" { return []string{s} } return nil } type barOption struct { kind string // text, or seconds (a whole number from 1 to 86400) required bool } // barBlockShape is the shape of a block (novox/hq ADR 0255): which bar, where on it, in what order, // what it shows, and that kind's options. // // - bar: top or bottom; // - place: resources (beside the processor, memory and disks) or status (beside the sound, before the // clock) — the two places every holder keeps for contributed blocks; // - order: 0–99 within the place, 50 when absent; // - shows: battery or command; options: what that kind takes. func barBlockShape() *Shape { return &Shape{ Places: []PlaceField{{Field: "bar", Values: []string{"bottom", "top"}}, {Field: "place", Values: []string{"resources", "status"}}}, Order: "order", Check: barBlockProblems, Reads: barBlockReads, Examples: []map[string]any{ {"bar": "bottom", "place": "status", "shows": "battery"}, {"bar": "bottom", "place": "status", "shows": "battery", "options": map[string]any{"device": "BAT0"}}, {"bar": "bottom", "place": "status", "shows": "command", "options": map[string]any{"command": "echo \"a line\"", "interval": float64(5)}}, {"bar": "bottom", "place": "status", "shows": "state", "options": map[string]any{"state": "power.draw"}}, {"bar": "bottom", "place": "status", "shows": "state", "options": map[string]any{"state": "power.draw", "key": "a-machine"}}, }, } } 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 }