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 }