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
230 lines
7.2 KiB
Go
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
|
|
}
|