xterm: every resource a config value, today's values as defaults (issue 1656) #416
+12
-5
@@ -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:<key>}` 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
|
||||
|
||||
|
||||
@@ -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 <Key> C: copy-selection(CLIPBOARD) \\n\\",
|
||||
" Ctrl Shift ~Alt <Key> 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 <Key> C", "paste-binding": "Ctrl Shift ~Alt <Key> 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 <Key> 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: `<binding>: <action> \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:<key>}: %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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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 <Key> C: copy-selection(CLIPBOARD) \\n\\\n")
|
||||
b.WriteString(" Ctrl Shift ~Alt <Key> 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 <Key> C: copy-selection(CLIPBOARD) \n\
|
||||
Ctrl Shift ~Alt <Key> V: insert-selection(CLIPBOARD)
|
||||
${config:copy-binding}: copy-selection(CLIPBOARD) \n\
|
||||
${config:paste-binding}: insert-selection(CLIPBOARD)
|
||||
|
||||
@@ -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 <Key> 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 <Key> C"
|
||||
},
|
||||
"paste-binding": {
|
||||
"type": "string",
|
||||
"description": "The key combination that pastes the clipboard, in X translation syntax: Ctrl Shift ~Alt <Key> 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 <Key> V"
|
||||
}
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"collection": "environment",
|
||||
|
||||
Reference in New Issue
Block a user