It requires x11-display, names itself in TERMINAL, and contributes its X resources to the xresources slot normal: today's palette and clipboard keys, JetBrainsMono Nerd Font, 10000 lines of scrollback, all scoped to XTerm* instead of every Xt program. It owns no file. The tools are the seat's open, which starts a terminal through the account's service manager so it outlives the runtime's restarts, and font (in force, what fontconfig resolves it to, set for new terminals) and colours.
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}
|
|
}
|