From f178aab641dca85abbaacf97797110a5931633a1 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 12:51:34 +0200 Subject: [PATCH] Make every xterm resource a config value with today's value as default; tools read the rendered resources (issue 1656) --- modules/xterm/README.md | 17 +- modules/xterm/cmd/xterm-tools/config_test.go | 274 +++++++++++++++++++ modules/xterm/cmd/xterm-tools/main.go | 5 +- modules/xterm/cmd/xterm-tools/tools.go | 103 ++++--- modules/xterm/cmd/xterm-tools/xterm_test.go | 37 ++- modules/xterm/files/xresources-normal | 56 ++-- modules/xterm/module.json | 169 ++++++++++++ 7 files changed, 571 insertions(+), 90 deletions(-) create mode 100644 modules/xterm/cmd/xterm-tools/config_test.go diff --git a/modules/xterm/README.md b/modules/xterm/README.md index 9bf7feff..61aff57c 100644 --- a/modules/xterm/README.md +++ b/modules/xterm/README.md @@ -21,17 +21,24 @@ They are those both workstations ran, with three changes: - **scrollback:** `saveLines` 10000 instead of 2000. The palette (background, foreground and the sixteen colours) is today's, unchanged. So are the -clipboard keys: Ctrl+Shift+C copies to the clipboard and Ctrl+Shift+V pastes from it. The bundle's -`Resources()` is the one source of these values. The manifest test holds the manifest to it, and -`xterm_colours` compares against it. +clipboard keys: Ctrl+Shift+C copies to the clipboard and Ctrl+Shift+V pastes from it. + +Every one of these values is a **config value** (ADR 0348; issue 1656) with today's value as its default: +`face`, `size` (the predefined `font-size`, 6 to 72), `save-lines`, `term-name`, `render-font`, `bold-mode`, +`always-bold-mode`, `allow-bold-fonts`, `background`, `foreground`, `color0` to `color15` (each `#rrggbb`), +and the two clipboard bindings `copy-binding` and `paste-binding`. `files/xresources-normal` holds only +`${config:}` placeholders, so a machine changes its font size through mesh-config. The manifest test holds +the file to the keys, and the file filled with the defaults to the text every machine had before. +The tools keep no copy of the values: `xterm_font` and `xterm_colours` read the `XTerm*` lines of the rendered +resources file `xorg` writes (`~/.config/xorg/xresources`) as the module's value, and say so when it is not there. ## Tools | tool | | what | |---|---|---| | `node-terminal-emulator.open` | d | a terminal in the session: the login shell, or a command run through it (`$SHELL -lc`), in a directory (the home by default), with an optional title, and `hold` to keep it open. It starts as a transient unit of the account's service manager (`systemd-run --user`), so it outlives the call and every runtime restart | -| `xterm_font` | r/a | the face and size in force and the installed font they resolve to (`fc-match`); set face or size for terminals opened from now on. Only installed faces are accepted | -| `xterm_colours` | r | background, foreground, cursor and the sixteen colours as `#rrggbb`, each beside the module's default | +| `xterm_font` | r/a | the face and size in force beside the module's value (from the rendered resources), and the installed font they resolve to (`fc-match`); set face or size for terminals opened from now on. Only installed faces are accepted | +| `xterm_colours` | r | background, foreground, cursor and the sixteen colours as `#rrggbb`, each beside the module's value from the rendered resources | ## What it leaves found diff --git a/modules/xterm/cmd/xterm-tools/config_test.go b/modules/xterm/cmd/xterm-tools/config_test.go new file mode 100644 index 00000000..a08bae77 --- /dev/null +++ b/modules/xterm/cmd/xterm-tools/config_test.go @@ -0,0 +1,274 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + + "xterm/internal/desktop" +) + +// todaysResources is the file every machine had before the module took config values (novox/hq issue 1656). +var todaysResources = strings.Join([]string{ + "! xterm: the face, the palette and the clipboard keys. Scoped to the XTerm class, so they", + "! outrank any `*name` resource of yours and touch no other X program.", + "XTerm*termName: xterm-256color", + "XTerm*saveLines: 10000", + "XTerm*renderFont: true", + "XTerm*faceName: JetBrainsMono Nerd Font", + "XTerm*faceSize: 12", + "XTerm*boldMode: false", + "XTerm*alwaysBoldMode: false", + "XTerm*allowBoldFonts: true", + "XTerm*background: #000000", + "XTerm*foreground: #a8a8a8", + "XTerm*color0: #000000", + "XTerm*color8: #666666", + "XTerm*color1: #9e1828", + "XTerm*color9: #bc5766", + "XTerm*color2: #008800", + "XTerm*color10: #61a171", + "XTerm*color3: #d2bb4b", + "XTerm*color11: #e7db52", + "XTerm*color4: #414171", + "XTerm*color12: #5085af", + "XTerm*color5: #963c59", + "XTerm*color13: #a97a99", + "XTerm*color6: #418179", + "XTerm*color14: #6ba4a4", + "XTerm*color7: #bebebe", + "XTerm*color15: #ffffff", + "XTerm*VT100.Translations: #override \\n\\", + " Ctrl Shift ~Alt C: copy-selection(CLIPBOARD) \\n\\", + " Ctrl Shift ~Alt V: insert-selection(CLIPBOARD)", + "", +}, "\n") + +var placeholder = regexp.MustCompile(`\$\{config:([a-z0-9][a-z0-9_.-]*)\}`) + +// asText is a default as the controller writes a value into a file: a string as it is, a number without a +// needless fraction, a boolean as true or false. +func asText(t *testing.T, v any) string { + t.Helper() + switch v := v.(type) { + case string: + return v + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case bool: + return strconv.FormatBool(v) + } + t.Fatalf("a default of no type: %v", v) + return "" +} + +func template(t *testing.T) string { + t.Helper() + m := readManifest(t) + if len(m.Shell) != 1 { + t.Fatalf("%+v", m.Shell) + } + return m.Shell[0].Code +} + +// 1656/AC1: every value the resources set is a config value with today's value as its default. +func TestEveryResourceValueIsAConfigValueWithTodaysDefault(t *testing.T) { + m := readManifest(t) + want := map[string]any{ + "face": "JetBrainsMono Nerd Font", "size": 12.0, "save-lines": 10000.0, "term-name": "xterm-256color", + "render-font": true, "bold-mode": false, "always-bold-mode": false, "allow-bold-fonts": true, + "background": "#000000", "foreground": "#a8a8a8", + "color0": "#000000", "color1": "#9e1828", "color2": "#008800", "color3": "#d2bb4b", + "color4": "#414171", "color5": "#963c59", "color6": "#418179", "color7": "#bebebe", + "color8": "#666666", "color9": "#bc5766", "color10": "#61a171", "color11": "#e7db52", + "color12": "#5085af", "color13": "#a97a99", "color14": "#6ba4a4", "color15": "#ffffff", + "copy-binding": "Ctrl Shift ~Alt C", "paste-binding": "Ctrl Shift ~Alt V", + } + if len(m.Config) != len(want) { + t.Errorf("declares %d config values, want %d", len(m.Config), len(want)) + } + for key, def := range want { + c, ok := m.Config[key] + if !ok { + t.Errorf("%s: not declared", key) + continue + } + if c.Default != def || c.Required || strings.TrimSpace(c.Description) == "" { + t.Errorf("%s: %+v, want default %v and a description", key, c, def) + } + wantType := map[bool]string{true: "string"}[true] + switch def.(type) { + case float64: + wantType = "number" + case bool: + wantType = "boolean" + } + if c.Type != wantType { + t.Errorf("%s: type %q, want %q", key, c.Type, wantType) + } + } + size := m.Config["size"] + if size.Predefined != "font-size" || size.Minimum == nil || *size.Minimum != 6 || size.Maximum == nil || *size.Maximum != 72 { + t.Errorf("size: %+v, want the predefined font-size from 6 to 72", size) + } + names := []string{"background", "foreground"} + for i := 0; i < 16; i++ { + names = append(names, "color"+strconv.Itoa(i)) + } + for _, key := range names { + c := m.Config[key] + re, err := regexp.Compile("^(?:" + c.Pattern + ")$") + if err != nil || c.Pattern == "" { + t.Errorf("%s: pattern %q", key, c.Pattern) + continue + } + if !re.MatchString("#0a1B2c") || re.MatchString("red") || re.MatchString("#12345") || re.MatchString("#1234567") { + t.Errorf("%s: pattern %q is not #rrggbb", key, c.Pattern) + } + } + for _, key := range []string{"copy-binding", "paste-binding"} { + re := regexp.MustCompile("^(?:" + m.Config[key].Pattern + ")$") + if !re.MatchString("Ctrl Shift ~Alt C") || re.MatchString("a: b") || re.MatchString("a\nb") || re.MatchString(`a\`) { + t.Errorf("%s: pattern %q lets a value end the resource", key, m.Config[key].Pattern) + } + } +} + +// 1656/AC2: no literal value is left in the resources, and every declared key is used. +func TestEveryResourceValueIsAPlaceholderOfADeclaredKeyAndEveryKeyIsUsed(t *testing.T) { + m := readManifest(t) + text := template(t) + used := map[string]bool{} + for _, l := range strings.Split(text, "\n") { + switch { + case l == "", strings.HasPrefix(l, "!"): + continue + case strings.HasPrefix(l, "XTerm*VT100.Translations: #override"): + continue + } + _, value, ok := strings.Cut(l, ": ") + if strings.HasPrefix(l, " ") { + // a translation line: `: \n\` + value, _, _ = strings.Cut(strings.TrimSpace(l), ": ") + ok = true + } + if !ok { + t.Errorf("not a resource: %q", l) + continue + } + f := placeholder.FindStringSubmatch(value) + if f == nil || f[0] != value { + t.Errorf("a literal value, not a ${config:}: %q", l) + continue + } + if _, declared := m.Config[f[1]]; !declared { + t.Errorf("%q names a key the module does not declare", l) + } + used[f[1]] = true + } + for key := range m.Config { + if !used[key] { + t.Errorf("%s is declared and no resource uses it", key) + } + } +} + +// 1656/AC3: filled with the defaults, the resources are the file every machine had. +func TestFilledWithTheDefaultsTheResourcesAreTodaysFileByteForByte(t *testing.T) { + m := readManifest(t) + got := placeholder.ReplaceAllStringFunc(template(t), func(ref string) string { + c, ok := m.Config[placeholder.FindStringSubmatch(ref)[1]] + if !ok { + t.Fatalf("%s: no such key", ref) + } + return asText(t, c.Default) + }) + if got != todaysResources { + t.Fatalf("filled with the defaults:\n%s\nwant:\n%s", got, todaysResources) + } +} + +// renderedWith is the resources file xorg writes with this module's piece filled from the defaults, and +// the given values changed. +func renderedWith(t *testing.T, changed map[string]string) string { + t.Helper() + m := readManifest(t) + text := placeholder.ReplaceAllStringFunc(template(t), func(ref string) string { + key := placeholder.FindStringSubmatch(ref)[1] + if v, ok := changed[key]; ok { + return v + } + return asText(t, m.Config[key].Default) + }) + return "! The mesh's X resources (module xorg)\nXft.dpi: 96\n# xterm\n" + text + "Xft.rgba: rgb\n" +} + +func deskWithResources(t *testing.T, rendered string, f *fake) xterm { + t.Helper() + path := filepath.Join(t.TempDir(), "xresources") + if rendered != "" { + if err := os.WriteFile(path, []byte(rendered), 0o644); err != nil { + t.Fatal(err) + } + } + return xterm{d: f.desk(true), resources: path} +} + +// 1656/AC4: the tools' defaults are what the module's resources were rendered with, not constants. +func TestFontReportsTheDefaultTheModuleRenderedNotAConstant(t *testing.T) { + f := &fake{query: "XTerm*faceName:\tInter\nXTerm*faceSize:\t13\n"} + x := deskWithResources(t, renderedWith(t, map[string]string{"size": "10", "face": "Fira Code"}), f) + got, err := x.font(context.Background(), desktop.Args{}) + if err != nil { + t.Fatal(err) + } + if j := asJSON(got); !strings.Contains(j, `"default":{"face":"Fira Code","size":10}`) { + t.Fatalf("%s", j) + } +} + +// 1656/AC4 +func TestColoursReportTheDefaultTheModuleRenderedNotAConstant(t *testing.T) { + f := &fake{query: "XTerm*color1:\t#aa0000\nXTerm*foreground:\t#112233\nXTerm*color2:\trgb:00/88/01\n"} + x := deskWithResources(t, renderedWith(t, map[string]string{"color1": "#aa0000", "foreground": "#112233"}), f) + got, err := x.colours(context.Background(), desktop.Args{}) + if err != nil { + t.Fatal(err) + } + j := asJSON(got) + for _, want := range []string{`{"name":"color1","value":"#aa0000","default":"#aa0000"}`, + `{"name":"foreground","value":"#112233","default":"#112233"}`, + `{"name":"color2","value":"#008801","default":"#008800","differs":true}`} { + if !strings.Contains(j, want) { + t.Errorf("lacks %s in %s", want, j) + } + } +} + +// 1656/AC4: without the rendered file there is no default to report, said loudly. +func TestWithoutTheRenderedResourcesTheToolsSayWhy(t *testing.T) { + x := deskWithResources(t, "", &fake{}) + if _, err := x.font(context.Background(), desktop.Args{}); err == nil || !strings.Contains(err.Error(), "xresources") { + t.Fatalf("font: %v", err) + } + if _, err := x.colours(context.Background(), desktop.Args{}); err == nil || !strings.Contains(err.Error(), "xresources") { + t.Fatalf("colours: %v", err) + } +} + +// 1656/AC4: tools.go holds no copy of the defaults. +func TestTheToolsHoldNoCopyOfTheDefaults(t *testing.T) { + src, err := os.ReadFile("tools.go") + if err != nil { + t.Fatal(err) + } + for _, lit := range []string{"JetBrainsMono", "#a8a8a8", "#9e1828", "xterm-256color", "Face =", "Size =", "func Resources"} { + if strings.Contains(string(src), lit) { + t.Errorf("tools.go holds %q", lit) + } + } +} diff --git a/modules/xterm/cmd/xterm-tools/main.go b/modules/xterm/cmd/xterm-tools/main.go index f4cadab7..6b71d2dc 100644 --- a/modules/xterm/cmd/xterm-tools/main.go +++ b/modules/xterm/cmd/xterm-tools/main.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "os" + "path/filepath" "time" stdio "git.novox.be/novox/mesh-sdk/go" @@ -17,7 +18,7 @@ import ( ) func main() { - if err := stdio.Serve("", tools(xterm{d: desktop.Machine("i3", "sway")})); err != nil { + if err := stdio.Serve("", tools(xterm{d: desktop.Machine("i3", "sway"), resources: filepath.Join(desktop.Home(), ResourcesFile)})); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } @@ -53,7 +54,7 @@ func tools(x xterm) []stdio.Tool { "resources set the default again at the next login.", Input: desktop.Schema(map[string]any{ "face": desktop.Str("a font family, as fc-list names it"), - "size": desktop.Num("points, 6 to 48"), + "size": desktop.Num("points, 6 to 72"), }), Run: call(x.font), }, diff --git a/modules/xterm/cmd/xterm-tools/tools.go b/modules/xterm/cmd/xterm-tools/tools.go index b2f27ca4..d5029b6f 100644 --- a/modules/xterm/cmd/xterm-tools/tools.go +++ b/modules/xterm/cmd/xterm-tools/tools.go @@ -11,47 +11,50 @@ import ( "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"}, +type xterm struct { + d desktop.Desk + resources string } -// 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]) +// 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) + } } - for _, kv := range Palette { - fmt.Fprintf(&b, "XTerm*%s: %s\n", kv[0], kv[1]) + 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) } - 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() + 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 @@ -124,6 +127,10 @@ func (x xterm) font(ctx context.Context, a desktop.Args) (any, error) { 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") @@ -142,8 +149,8 @@ func (x xterm) font(ctx context.Context, a desktop.Args) (any, error) { res = append(res, "XTerm*faceName: "+face) } if sized { - if size < 6 || size > 48 { - return nil, fmt.Errorf("size is from 6 to 48 points") + 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)) } @@ -156,9 +163,13 @@ func (x xterm) font(ctx context.Context, a desktop.Args) (any, error) { 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": Face, "size": Size}, + "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) @@ -175,7 +186,7 @@ func (x xterm) font(ctx context.Context, a desktop.Args) (any, error) { 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. +// 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 { @@ -191,6 +202,10 @@ func Hex(v string) string { } 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 @@ -205,9 +220,9 @@ func (x xterm) colours(ctx context.Context, a desktop.Args) (any, error) { 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]}) + 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 diff --git a/modules/xterm/cmd/xterm-tools/xterm_test.go b/modules/xterm/cmd/xterm-tools/xterm_test.go index 224e09f8..a331fbb6 100644 --- a/modules/xterm/cmd/xterm-tools/xterm_test.go +++ b/modules/xterm/cmd/xterm-tools/xterm_test.go @@ -19,7 +19,8 @@ type manifest struct { Name string `json:"name"` Serves []string `json:"serves"` } `json:"claims"` - Tools []string `json:"tools"` + Tools []string `json:"tools"` + Config map[string]configKey `json:"config"` Environment struct { Variables map[string]string `json:"variables"` } `json:"environment"` @@ -37,6 +38,18 @@ type manifest struct { } `json:"build"` } +type configKey struct { + Type string `json:"type"` + Description string `json:"description"` + Pattern string `json:"pattern"` + Minimum *float64 `json:"minimum"` + Maximum *float64 `json:"maximum"` + Predefined string `json:"predefined"` + Default any `json:"default"` + Required bool `json:"required"` + Trusted bool `json:"trusted"` +} + func readManifest(t *testing.T) manifest { raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) if err != nil { @@ -99,10 +112,8 @@ func TestItsResourcesAreOneContributionInTheNormalSlotScopedToXTerm(t *testing.T if len(m.Shell) != 1 || m.Shell[0].For != "xresources" || m.Shell[0].Slot != "normal" { t.Fatalf("%+v", m.Shell) } - if m.Shell[0].Code != Resources() { - t.Fatal("the manifest's resources are not the ones the bundle describes: regenerate the manifest from Resources()") - } - for _, l := range strings.Split(strings.TrimSpace(Resources()), "\n") { + code := m.Shell[0].Code + for _, l := range strings.Split(strings.TrimSpace(code), "\n") { switch { case strings.HasPrefix(l, "!"), strings.HasPrefix(l, " "): case strings.HasPrefix(l, "#"): @@ -111,8 +122,8 @@ func TestItsResourcesAreOneContributionInTheNormalSlotScopedToXTerm(t *testing.T t.Errorf("a resource of another program, or of every one: %q", l) } } - if !strings.Contains(Resources(), "XTerm*faceName: JetBrainsMono Nerd Font") || !strings.Contains(Resources(), "copy-selection(CLIPBOARD)") { - t.Fatal("the chosen face and the clipboard keys") + if !strings.Contains(code, "XTerm*faceName: ${config:face}") || !strings.Contains(code, "copy-selection(CLIPBOARD)") { + t.Fatal("the face and the clipboard keys") } } @@ -202,7 +213,11 @@ func TestFontAndColoursAreReadAsAnXTermSeesThem(t *testing.T) { } } f := &fake{query: q} - got, err := xterm{d: f.desk(true)}.colours(context.Background(), desktop.Args{}) + rendered := filepath.Join(t.TempDir(), "xresources") + if err := os.WriteFile(rendered, []byte(renderedWith(t, nil)), 0o644); err != nil { + t.Fatal(err) + } + got, err := xterm{d: f.desk(true), resources: rendered}.colours(context.Background(), desktop.Args{}) if err != nil { t.Fatal(err) } @@ -210,14 +225,14 @@ func TestFontAndColoursAreReadAsAnXTermSeesThem(t *testing.T) { if !strings.Contains(j, `{"name":"color1","value":"#ff0000","default":"#9e1828","differs":true}`) || !strings.Contains(j, `{"name":"foreground","value":"#a8a8a8","default":"#a8a8a8"}`) { t.Fatalf("%s", j) } - if _, err := (xterm{d: f.desk(true)}).font(context.Background(), desktop.Args{"face": "Missing Font"}); err == nil { + if _, err := (xterm{d: f.desk(true), resources: rendered}).font(context.Background(), desktop.Args{"face": "Missing Font"}); err == nil { t.Fatal("a face that is not installed") } - if _, err := (xterm{d: f.desk(true)}).font(context.Background(), desktop.Args{"size": 99.0}); err == nil { + if _, err := (xterm{d: f.desk(true), resources: rendered}).font(context.Background(), desktop.Args{"size": 99.0}); err == nil { t.Fatal("size out of range") } f.ran, f.stdin = nil, nil - got, err = xterm{d: f.desk(true)}.font(context.Background(), desktop.Args{"face": "Inter", "size": 13.0}) + got, err = xterm{d: f.desk(true), resources: rendered}.font(context.Background(), desktop.Args{"face": "Inter", "size": 13.0}) if err != nil { t.Fatal(err) } diff --git a/modules/xterm/files/xresources-normal b/modules/xterm/files/xresources-normal index d95ee3b9..8867c313 100644 --- a/modules/xterm/files/xresources-normal +++ b/modules/xterm/files/xresources-normal @@ -1,31 +1,31 @@ ! xterm: the face, the palette and the clipboard keys. Scoped to the XTerm class, so they ! outrank any `*name` resource of yours and touch no other X program. -XTerm*termName: xterm-256color -XTerm*saveLines: 10000 -XTerm*renderFont: true -XTerm*faceName: JetBrainsMono Nerd Font -XTerm*faceSize: 12 -XTerm*boldMode: false -XTerm*alwaysBoldMode: false -XTerm*allowBoldFonts: true -XTerm*background: #000000 -XTerm*foreground: #a8a8a8 -XTerm*color0: #000000 -XTerm*color8: #666666 -XTerm*color1: #9e1828 -XTerm*color9: #bc5766 -XTerm*color2: #008800 -XTerm*color10: #61a171 -XTerm*color3: #d2bb4b -XTerm*color11: #e7db52 -XTerm*color4: #414171 -XTerm*color12: #5085af -XTerm*color5: #963c59 -XTerm*color13: #a97a99 -XTerm*color6: #418179 -XTerm*color14: #6ba4a4 -XTerm*color7: #bebebe -XTerm*color15: #ffffff +XTerm*termName: ${config:term-name} +XTerm*saveLines: ${config:save-lines} +XTerm*renderFont: ${config:render-font} +XTerm*faceName: ${config:face} +XTerm*faceSize: ${config:size} +XTerm*boldMode: ${config:bold-mode} +XTerm*alwaysBoldMode: ${config:always-bold-mode} +XTerm*allowBoldFonts: ${config:allow-bold-fonts} +XTerm*background: ${config:background} +XTerm*foreground: ${config:foreground} +XTerm*color0: ${config:color0} +XTerm*color8: ${config:color8} +XTerm*color1: ${config:color1} +XTerm*color9: ${config:color9} +XTerm*color2: ${config:color2} +XTerm*color10: ${config:color10} +XTerm*color3: ${config:color3} +XTerm*color11: ${config:color11} +XTerm*color4: ${config:color4} +XTerm*color12: ${config:color12} +XTerm*color5: ${config:color5} +XTerm*color13: ${config:color13} +XTerm*color6: ${config:color6} +XTerm*color14: ${config:color14} +XTerm*color7: ${config:color7} +XTerm*color15: ${config:color15} XTerm*VT100.Translations: #override \n\ - Ctrl Shift ~Alt C: copy-selection(CLIPBOARD) \n\ - Ctrl Shift ~Alt V: insert-selection(CLIPBOARD) + ${config:copy-binding}: copy-selection(CLIPBOARD) \n\ + ${config:paste-binding}: insert-selection(CLIPBOARD) diff --git a/modules/xterm/module.json b/modules/xterm/module.json index e6a565ac..b65be8eb 100644 --- a/modules/xterm/module.json +++ b/modules/xterm/module.json @@ -21,6 +21,175 @@ "xterm_font", "xterm_colours" ], + "config": { + "face": { + "type": "string", + "description": "The font family xterm draws with, as fc-list names it: JetBrainsMono Nerd Font, because research 026/04 chose it for its glyphs the prompt uses. A family the machine does not have falls back to the nearest installed face (xterm_font says which).", + "pattern": "[A-Za-z0-9 ._-]{1,64}", + "default": "JetBrainsMono Nerd Font" + }, + "size": { + "type": "number", + "description": "The size of xterm's text, in points: 12 reads well on a screen of about 100 DPI, and a denser screen needs a larger size.", + "minimum": 6, + "maximum": 72, + "predefined": "font-size", + "default": 12 + }, + "save-lines": { + "type": "number", + "description": "How many lines of scrollback xterm keeps per terminal: 10000 holds a long build log, and each line costs memory in every open terminal.", + "minimum": 0, + "maximum": 1000000, + "default": 10000 + }, + "term-name": { + "type": "string", + "description": "The TERM value xterm sets for the programs it runs: xterm-256color, because the palette has 256 colours and every program knows the name.", + "pattern": "[A-Za-z0-9._-]{1,64}", + "default": "xterm-256color" + }, + "render-font": { + "type": "boolean", + "description": "Whether xterm draws with fontconfig faces (true) or the old X core fonts (false): true, because the face is a fontconfig family and core fonts cannot show it.", + "default": true + }, + "bold-mode": { + "type": "boolean", + "description": "Whether xterm draws bold text by overstriking the normal face (true): false, because the face has a real bold style and overstriking blurs it.", + "default": false + }, + "always-bold-mode": { + "type": "boolean", + "description": "Whether xterm overstrikes bold even where the face has a bold style (true): false, for the same reason as bold-mode.", + "default": false + }, + "allow-bold-fonts": { + "type": "boolean", + "description": "Whether xterm may use the face's bold style for bold text (true): true, so bold text is drawn with the real bold weight.", + "default": true + }, + "background": { + "type": "string", + "description": "The terminal's background colour, as #rrggbb: black, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#000000" + }, + "foreground": { + "type": "string", + "description": "The terminal's text colour, as #rrggbb: a light grey, easier on the eyes than white on black.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#a8a8a8" + }, + "color0": { + "type": "string", + "description": "Palette entry 0, the normal black, as #rrggbb: #000000, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#000000" + }, + "color1": { + "type": "string", + "description": "Palette entry 1, the normal red, as #rrggbb: #9e1828, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#9e1828" + }, + "color2": { + "type": "string", + "description": "Palette entry 2, the normal green, as #rrggbb: #008800, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#008800" + }, + "color3": { + "type": "string", + "description": "Palette entry 3, the normal yellow, as #rrggbb: #d2bb4b, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#d2bb4b" + }, + "color4": { + "type": "string", + "description": "Palette entry 4, the normal blue, as #rrggbb: #414171, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#414171" + }, + "color5": { + "type": "string", + "description": "Palette entry 5, the normal magenta, as #rrggbb: #963c59, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#963c59" + }, + "color6": { + "type": "string", + "description": "Palette entry 6, the normal cyan, as #rrggbb: #418179, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#418179" + }, + "color7": { + "type": "string", + "description": "Palette entry 7, the normal white, as #rrggbb: #bebebe, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#bebebe" + }, + "color8": { + "type": "string", + "description": "Palette entry 8, the bright black, as #rrggbb: #666666, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#666666" + }, + "color9": { + "type": "string", + "description": "Palette entry 9, the bright red, as #rrggbb: #bc5766, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#bc5766" + }, + "color10": { + "type": "string", + "description": "Palette entry 10, the bright green, as #rrggbb: #61a171, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#61a171" + }, + "color11": { + "type": "string", + "description": "Palette entry 11, the bright yellow, as #rrggbb: #e7db52, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#e7db52" + }, + "color12": { + "type": "string", + "description": "Palette entry 12, the bright blue, as #rrggbb: #5085af, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#5085af" + }, + "color13": { + "type": "string", + "description": "Palette entry 13, the bright magenta, as #rrggbb: #a97a99, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#a97a99" + }, + "color14": { + "type": "string", + "description": "Palette entry 14, the bright cyan, as #rrggbb: #6ba4a4, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#6ba4a4" + }, + "color15": { + "type": "string", + "description": "Palette entry 15, the bright white, as #rrggbb: #ffffff, as both workstations ran it.", + "pattern": "#[0-9a-fA-F]{6}", + "default": "#ffffff" + }, + "copy-binding": { + "type": "string", + "description": "The key combination that copies the selection to the clipboard, in X translation syntax: Ctrl Shift ~Alt C, so a plain Ctrl+C still interrupts the program. It holds no colon, backslash or line break, which would end the resource.", + "pattern": "[^:\\\\\\n]+", + "default": "Ctrl Shift ~Alt C" + }, + "paste-binding": { + "type": "string", + "description": "The key combination that pastes the clipboard, in X translation syntax: Ctrl Shift ~Alt V, the pair of copy-binding. It holds no colon, backslash or line break, which would end the resource.", + "pattern": "[^:\\\\\\n]+", + "default": "Ctrl Shift ~Alt V" + } + }, "items": [ { "collection": "environment",