Files
mesh-catalog/modules/xterm/cmd/xterm-tools/tools.go
T
jschoubben f178aab641
mesh/delivery-group group fix/1656-xterm-config rejected: a member's own check failed
mesh/merge-gate waits for its group's composed check
mesh/repo-check head f178aab641dc merged into trunk 3259765d46bc beside mesh-controller#420@69a67876 — fail: its merge-check.sh failed: files asking for a …
mesh/delivery superseded: a newer head of the same pull request
Make every xterm resource a config value with today's value as default; tools read the rendered resources (issue 1656)
2026-10-21 12:51:40 +02:00

230 lines
7.2 KiB
Go

package main
import (
"context"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"xterm/internal/desktop"
)
type xterm struct {
d desktop.Desk
resources string
}
// PaletteNames are the colour resources xterm_colours reports, in the order the resources write them.
// Their values are the module's config values, read from the rendered resources (Rendered), never held here.
var PaletteNames = []string{
"background", "foreground",
"color0", "color8", "color1", "color9", "color2", "color10", "color3", "color11",
"color4", "color12", "color5", "color13", "color6", "color14", "color7", "color15",
}
// ResourcesFile is where xorg writes the X resources, under the account's home: this module's resources are
// its contribution to the `normal` slot, filled with the module's config values.
const ResourcesFile = ".config/xorg/xresources"
// Rendered is the XTerm* resources of a rendered resources file, by name: `XTerm*faceSize: 12` is faceSize 12.
// The translations, which span lines, and every resource of another program are left out.
func Rendered(text string) map[string]string {
out := map[string]string{}
for _, l := range strings.Split(text, "\n") {
k, v, ok := strings.Cut(l, ":")
name, scoped := strings.CutPrefix(strings.TrimSpace(k), "XTerm*")
if ok && scoped && !strings.Contains(name, ".") {
out[name] = strings.TrimSpace(v)
}
}
return out
}
// defaults are the resources the module's config values rendered, as xorg placed them: what the module sets, so
// what a terminal has unless the session's own resources say otherwise.
func (x xterm) defaults() (map[string]string, error) {
text, err := os.ReadFile(x.resources)
if err != nil {
return nil, fmt.Errorf("the module's resources are not where xorg places them (%s): is xorg assigned to this machine, "+
"and has its xresources file been written? %w", x.resources, err)
}
got := Rendered(string(text))
if len(got) == 0 {
return nil, fmt.Errorf("%s holds no XTerm* resource: the xterm module's resources are not in it", x.resources)
}
return got, nil
}
// openScript runs in the new terminal: into the directory, then the login shell, or a command
// through it so the account's environment and aliases are there.
const openScript = `cd -- "$1" || exit 1
if [ -n "$2" ]; then exec "${SHELL:-/bin/sh}" -lc "$2"; fi
exec "${SHELL:-/bin/sh}" -l`
// OpenArgs is the xterm command line open starts.
func OpenArgs(dir, command, title string, hold bool) []string {
args := []string{"xterm"}
if title != "" {
args = append(args, "-title", title)
}
if hold {
args = append(args, "-hold")
}
return append(args, "-e", "/bin/sh", "-c", openScript, "sh", dir, command)
}
func (x xterm) open(ctx context.Context, a desktop.Args) (any, error) {
s, err := x.d.Find()
if err != nil {
return nil, err
}
dir := a.Opt("directory", desktop.Home())
if strings.HasPrefix(dir, "~") {
if dir, err = desktop.InHome(dir); err != nil {
return nil, err
}
}
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
return nil, fmt.Errorf("%s is not a directory here", dir)
}
hold, _, err := a.Bool("hold")
if err != nil {
return nil, err
}
argv := OpenArgs(dir, a.Opt("command", ""), a.Opt("title", ""), hold)
launched, err := x.d.Launch(ctx, s, "xterm", argv...)
if err != nil {
return nil, err
}
return map[string]any{"session": s.Display, "directory": dir, "command": a.Opt("command", "(the login shell)"), "launched": launched}, nil
}
// Effective is a resource's value as an XTerm sees it in `xrdb -query`: its XTerm*-scoped value, else
// the global *-scoped one.
func Effective(query, name string) string {
var global string
for _, l := range strings.Split(query, "\n") {
k, v, ok := strings.Cut(l, ":")
if !ok {
continue
}
switch strings.TrimSpace(k) {
case "XTerm*" + name, "XTerm." + name, "XTerm*vt100." + name, "XTerm*VT100." + name:
return strings.TrimSpace(v)
case "*" + name:
global = strings.TrimSpace(v)
}
}
return global
}
var faceName = regexp.MustCompile(`^[A-Za-z0-9 ._-]{1,64}$`)
func (x xterm) font(ctx context.Context, a desktop.Args) (any, error) {
s, err := x.d.Find()
if err != nil {
return nil, err
}
def, err := x.defaults()
if err != nil {
return nil, err
}
env := s.Env(x.d.Base)
face := a.Opt("face", "")
size, sized, err := a.Number("size")
if err != nil {
return nil, err
}
if face != "" || sized {
var res []string
if face != "" {
if !faceName.MatchString(face) {
return nil, fmt.Errorf("a face is a family name, as fc-list names it")
}
if fam := x.d.Run(ctx, env, nil, "fc-list", face, "family"); strings.TrimSpace(fam.Stdout) == "" {
return nil, fmt.Errorf("no installed font is %q: the fonts module installs the mesh's faces", face)
}
res = append(res, "XTerm*faceName: "+face)
}
if sized {
if size < 6 || size > 72 {
return nil, fmt.Errorf("size is from 6 to 72 points")
}
res = append(res, "XTerm*faceSize: "+strconv.FormatFloat(size, 'f', -1, 64))
}
if r := x.d.Run(ctx, env, []byte(strings.Join(res, "\n")+"\n"), "xrdb", "-nocpp", "-merge", "-"); !r.OK() {
return nil, r.Err()
}
}
q := x.d.Run(ctx, env, nil, "xrdb", "-query")
if !q.OK() {
return nil, q.Err()
}
inForce := Effective(q.Stdout, "faceName")
var points any = def["faceSize"]
if n, err := strconv.ParseFloat(def["faceSize"], 64); err == nil {
points = n
}
answer := map[string]any{
"session": s.Display, "face": inForce, "size": Effective(q.Stdout, "faceSize"),
"default": map[string]any{"face": def["faceName"], "size": points},
}
if inForce != "" {
m := x.d.Run(ctx, env, nil, "fc-match", "-f", "%{family[0]}|%{style[0]}|%{file}", inForce)
if parts := strings.SplitN(m.Stdout, "|", 3); m.OK() && len(parts) == 3 {
answer["resolves_to"] = map[string]any{"family": parts[0], "style": parts[1], "file": parts[2],
"exact": strings.EqualFold(parts[0], inForce)}
}
}
if face != "" || sized {
answer["lasts"] = "for terminals opened from now on, until the next login"
}
return answer, nil
}
var rgbSpec = regexp.MustCompile(`^rgb:([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})$`)
// Hex writes an X colour (`rgb:9e/18/28`, `#rrggbb`) as #rrggbb; anything else (a colour name) as it is.
func Hex(v string) string {
v = strings.TrimSpace(v)
if m := rgbSpec.FindStringSubmatch(v); m != nil {
out := "#"
for _, c := range m[1:] {
n, _ := strconv.ParseUint(c, 16, 32)
max := uint64(1)<<(4*len(c)) - 1
out += fmt.Sprintf("%02x", (n*255+max/2)/max)
}
return out
}
return strings.ToLower(v)
}
func (x xterm) colours(ctx context.Context, a desktop.Args) (any, error) {
def, err := x.defaults()
if err != nil {
return nil, err
}
res, s, err := x.d.InSession(ctx, "xrdb", "-query")
if err != nil {
return nil, err
}
if !res.OK() {
return nil, res.Err()
}
type colour struct {
Name string `json:"name"`
Value string `json:"value"`
Default string `json:"default"`
Differs bool `json:"differs,omitempty"`
}
var out []colour
for _, name := range PaletteNames {
v, d := Hex(Effective(res.Stdout, name)), Hex(def[name])
out = append(out, colour{name, v, d, v != d})
}
cursor := Hex(Effective(res.Stdout, "cursorColor"))
return map[string]any{"session": s.Display, "colours": out, "cursor": cursor}, nil
}