Files
mesh-catalog/modules/xterm/cmd/xterm-tools/tools.go
T
jochen e4abc6eb88 xterm: the terminal holds node-terminal-emulator; its resources through xorg's slot (hq ADR 0208)
It requires x11-display, names itself in TERMINAL, and contributes its X resources
to the xresources slot normal: today's palette and clipboard keys, JetBrainsMono
Nerd Font, 10000 lines of scrollback, all scoped to XTerm* instead of every Xt
program. It owns no file.

The tools are the seat's open, which starts a terminal through the account's
service manager so it outlives the runtime's restarts, and font (in force, what
fontconfig resolves it to, set for new terminals) and colours.
2026-10-04 13:21:47 +02:00

215 lines
6.7 KiB
Go

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 <Key> C: copy-selection(CLIPBOARD) \\n\\\n")
b.WriteString(" Ctrl Shift ~Alt <Key> 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
}