package main import ( "context" "fmt" "os" "regexp" "strconv" "strings" "xterm/internal/desktop" ) type xterm struct{ d desktop.Desk } // The module's defaults: the face research 026/04 chose and both workstations' palette. const ( Face = "JetBrainsMono Nerd Font" Size = 12 ) // Palette is the colours both workstations ran, by resource name, in the order they are written. var Palette = [][2]string{ {"background", "#000000"}, {"foreground", "#a8a8a8"}, {"color0", "#000000"}, {"color8", "#666666"}, {"color1", "#9e1828"}, {"color9", "#bc5766"}, {"color2", "#008800"}, {"color10", "#61a171"}, {"color3", "#d2bb4b"}, {"color11", "#e7db52"}, {"color4", "#414171"}, {"color12", "#5085af"}, {"color5", "#963c59"}, {"color13", "#a97a99"}, {"color6", "#418179"}, {"color14", "#6ba4a4"}, {"color7", "#bebebe"}, {"color15", "#ffffff"}, } // Resources is the module's contribution to the X resources (the xresources slot `normal`), as the // manifest carries it; the manifest test holds the two to one text. func Resources() string { var b strings.Builder b.WriteString("! xterm: the face, the palette and the clipboard keys. Scoped to the XTerm class, so they\n") b.WriteString("! outrank any `*name` resource of yours and touch no other X program.\n") for _, kv := range [][2]string{ {"termName", "xterm-256color"}, {"saveLines", "10000"}, {"renderFont", "true"}, {"faceName", Face}, {"faceSize", strconv.Itoa(Size)}, {"boldMode", "false"}, {"alwaysBoldMode", "false"}, {"allowBoldFonts", "true"}, } { fmt.Fprintf(&b, "XTerm*%s: %s\n", kv[0], kv[1]) } for _, kv := range Palette { fmt.Fprintf(&b, "XTerm*%s: %s\n", kv[0], kv[1]) } b.WriteString("XTerm*VT100.Translations: #override \\n\\\n") b.WriteString(" Ctrl Shift ~Alt C: copy-selection(CLIPBOARD) \\n\\\n") b.WriteString(" Ctrl Shift ~Alt V: insert-selection(CLIPBOARD)\n") return b.String() } // 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 } 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 > 48 { return nil, fmt.Errorf("size is from 6 to 48 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") answer := map[string]any{ "session": s.Display, "face": inForce, "size": Effective(q.Stdout, "faceSize"), "default": map[string]any{"face": Face, "size": Size}, } 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`, `#9e1828`) 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) { 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 _, kv := range Palette { v := Hex(Effective(res.Stdout, kv[0])) out = append(out, colour{kv[0], v, kv[1], v != kv[1]}) } cursor := Hex(Effective(res.Stdout, "cursorColor")) return map[string]any{"session": s.Display, "colours": out, "cursor": cursor}, nil }