It requires x11-display and contributes exec i3 to xinitrc's last slot, and XDG_CURRENT_DESKTOP/XDG_SESSION_DESKTOP to the environment. It owns ~/.config/i3/config, ending with the config.d include where other modules drop their files, and /etc/lemurs/wms/i3, which runs the session's start. Over today's identical config it drops the dead lxpolkit, the D-Bus-activated portal, the xrdb merge xorg now does, and the execs that XDG autostart already started. It sets JetBrainsMono Nerd Font, runs i3-sensible-terminal, and declares dex, which the desktop lacked. The tools speak i3's IPC: the seat's reload, workspaces and windows, and focus, move, layout save/restore, exec, kill, bindings, config check, marks and the scratchpad. A watcher in the bundle (ADR 0198) replaces the predecessor's inotify script and user unit. It reloads only a configuration i3 -C accepts, and at its start whatever i3 has not loaded.
161 lines
4.5 KiB
Go
161 lines
4.5 KiB
Go
package desktop
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Args reads a tool's arguments as JSON decoded them: strings, float64 numbers, booleans.
|
|
type Args map[string]any
|
|
|
|
// Text is a required string, trimmed.
|
|
func (a Args) Text(name string) (string, error) {
|
|
v, ok := a[name].(string)
|
|
if !ok || strings.TrimSpace(v) == "" {
|
|
return "", fmt.Errorf("%s is required, as text", name)
|
|
}
|
|
return strings.TrimSpace(v), nil
|
|
}
|
|
|
|
// Opt is an optional string, trimmed, or def.
|
|
func (a Args) Opt(name, def string) string {
|
|
if v, ok := a[name].(string); ok && strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
return def
|
|
}
|
|
|
|
// Has is whether the caller gave the argument at all.
|
|
func (a Args) Has(name string) bool {
|
|
v, ok := a[name]
|
|
return ok && v != nil
|
|
}
|
|
|
|
// Bool is an optional boolean: its value, and whether it was given.
|
|
func (a Args) Bool(name string) (bool, bool, error) {
|
|
v, ok := a[name]
|
|
if !ok || v == nil {
|
|
return false, false, nil
|
|
}
|
|
b, isBool := v.(bool)
|
|
if !isBool {
|
|
return false, false, fmt.Errorf("%s is true or false", name)
|
|
}
|
|
return b, true, nil
|
|
}
|
|
|
|
// Number is an optional number: its value, and whether it was given.
|
|
func (a Args) Number(name string) (float64, bool, error) {
|
|
v, ok := a[name]
|
|
if !ok || v == nil {
|
|
return 0, false, nil
|
|
}
|
|
f, isNum := v.(float64)
|
|
if !isNum || math.IsNaN(f) || math.IsInf(f, 0) {
|
|
return 0, false, fmt.Errorf("%s is a number", name)
|
|
}
|
|
return f, true, nil
|
|
}
|
|
|
|
// Whole is an optional whole number within [lo, hi], or def.
|
|
func (a Args) Whole(name string, def, lo, hi int) (int, error) {
|
|
f, given, err := a.Number(name)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !given {
|
|
return def, nil
|
|
}
|
|
if f != math.Trunc(f) || f < float64(lo) || f > float64(hi) {
|
|
return 0, fmt.Errorf("%s is a whole number from %d to %d", name, lo, hi)
|
|
}
|
|
return int(f), nil
|
|
}
|
|
|
|
// OneOf is an optional string that must be one of choices, or def.
|
|
func (a Args) OneOf(name, def string, choices ...string) (string, error) {
|
|
v := a.Opt(name, def)
|
|
for _, c := range choices {
|
|
if v == c {
|
|
return v, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s is one of %s", name, strings.Join(choices, ", "))
|
|
}
|
|
|
|
// Strings is an optional list of strings.
|
|
func (a Args) Strings(name string) ([]string, error) {
|
|
v, ok := a[name]
|
|
if !ok || v == nil {
|
|
return nil, nil
|
|
}
|
|
list, isList := v.([]any)
|
|
if !isList {
|
|
return nil, fmt.Errorf("%s is a list of text", name)
|
|
}
|
|
out := make([]string, 0, len(list))
|
|
for _, x := range list {
|
|
s, isText := x.(string)
|
|
if !isText {
|
|
return nil, fmt.Errorf("%s is a list of text", name)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Home is the operator account's home: the runtime's word for it, else this process's.
|
|
func Home() string {
|
|
if h := os.Getenv("MESH_OPERATOR_HOME"); h != "" {
|
|
return h
|
|
}
|
|
if h, err := os.UserHomeDir(); err == nil {
|
|
return h
|
|
}
|
|
return "/"
|
|
}
|
|
|
|
// InHome resolves a path the caller gave: `~/x` and a relative path are under the home. A path
|
|
// that leaves the home through `..` is refused, so a tool that writes never writes outside it.
|
|
func InHome(path string) (string, error) {
|
|
home := Home()
|
|
switch {
|
|
case path == "~":
|
|
path = home
|
|
case strings.HasPrefix(path, "~/"):
|
|
path = filepath.Join(home, path[2:])
|
|
case !filepath.IsAbs(path):
|
|
path = filepath.Join(home, path)
|
|
}
|
|
path = filepath.Clean(path)
|
|
if path != home && !strings.HasPrefix(path, home+string(filepath.Separator)) {
|
|
return "", fmt.Errorf("%s is outside the account's home", path)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
// Schema builds a tool's input schema from property descriptions; required names those that must
|
|
// be given. A property is a string unless its description object says otherwise.
|
|
func Schema(props map[string]any, required ...string) map[string]any {
|
|
s := map[string]any{"type": "object", "properties": props}
|
|
if len(required) > 0 {
|
|
s["required"] = required
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Str, Num, Flag, List and Enum describe one property.
|
|
func Str(desc string) map[string]any { return map[string]any{"type": "string", "description": desc} }
|
|
func Num(desc string) map[string]any { return map[string]any{"type": "number", "description": desc} }
|
|
func Int(desc string) map[string]any { return map[string]any{"type": "integer", "description": desc} }
|
|
func Flag(desc string) map[string]any { return map[string]any{"type": "boolean", "description": desc} }
|
|
func List(desc string) map[string]any {
|
|
return map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": desc}
|
|
}
|
|
func Enum(desc string, values ...string) map[string]any {
|
|
return map[string]any{"type": "string", "enum": values, "description": desc}
|
|
}
|