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.
215 lines
6.7 KiB
Go
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
|
|
}
|