GTK 3/4 settings, qt6ct, portals.conf and the default cursor as owned files. GTK_THEME, GTK2_RC_FILES, the Qt words and XCURSOR_* as environment contributions. The GSettings keys the portal serves go in the xinitrc slot, replacing the predecessor's appearance script, and the cursor in the xresources slot. Qt is drawn by Fusion with qt6ct's darker palette instead of the user-repository adwaita-qt, which is no longer developed. qt5ct is dropped. The fonts are research 026's Inter and JetBrains Mono, and portals.conf routes the Secret interface to gnome-keyring, which nothing answered. The tools are appearance (dark or light per audience, switched for the session), cursor, icons, and portal-check (which backend answers which interface, and why).
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}
|
|
}
|